From 6cac659824d7c3fb1c5a748c352e58a8ad7d3a76 Mon Sep 17 00:00:00 2001 From: Rebecca Graber Date: Thu, 5 Dec 2024 10:07:25 -0500 Subject: [PATCH] feat(projectHistoryLogs): add endpoints for viewing project history logs TASK-973 (#5319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### đŸ“Ŗ Summary Add two new endpoints for viewing project history logs, one for all logs, and one for logs for a given project. ### 📖 Description Adds two new endpoints: `/api/v2/project-history-logs` (all project history logs) and `/api/v2/assets//history` (logs for the specific project). The former is only available to superusers, the latter is available to anyone with the manage_asset permission for the project. Both endpoints can be searched by a all fields, most importantly `action`, `metadata__asset_uid`, and `username`. A full list of available searchable fields is in the endpoint documentation. ### 👀 Preview steps Feature/no-change template: 1. ℹī¸ have two accounts, one super and one not. Have at least two projects. 2. Change the name of both projects to generate some logs. 3. Log in as the superuser. 4. Go to `localhost/api/v2/project-history-logs` 5. đŸŸĸ You should see logs for both projects 6. Grant the non-superuser the manage_project permission for one of the projects (they will have this if they are the owner, or you can assign it from Settings > Sharing) 7. Log out 8. đŸŸĸ Go to `localhost/api/v2/project-history-logs`. You should get a 401. 9. Log in as the non-superuser. 10. đŸŸĸ Go to `localhost/api/v2/project-history-logs`. You should get a 403. 11. Go to `localhost/api/v2/assets//history` 12. đŸŸĸ You should see project history logs for just that project 13. Revoke the manage_project permission for the user. You can do this by transferring ownership or just removing the singular permission (will require you to log back in as the superuser). 14. đŸŸĸ Reload `localhost/api/v2/assets//history`. You should get a 403. --- kobo/apps/audit_log/permissions.py | 16 +- kobo/apps/audit_log/serializers.py | 38 +- .../tests/api/v2/test_api_audit_log.py | 227 +++++++- kobo/apps/audit_log/urls.py | 7 + kobo/apps/audit_log/views.py | 514 +++++++++++++++++- kpi/urls/router_api_v2.py | 22 +- 6 files changed, 810 insertions(+), 14 deletions(-) diff --git a/kobo/apps/audit_log/permissions.py b/kobo/apps/audit_log/permissions.py index 40f6ca0f19..0c073938b5 100644 --- a/kobo/apps/audit_log/permissions.py +++ b/kobo/apps/audit_log/permissions.py @@ -1,8 +1,8 @@ from rest_framework.permissions import IsAdminUser -from kpi.mixins.validation_password_permission import ( - ValidationPasswordPermissionMixin, -) +from kpi.constants import PERM_MANAGE_ASSET +from kpi.mixins.validation_password_permission import ValidationPasswordPermissionMixin +from kpi.permissions import IsAuthenticated class SuperUserPermission(ValidationPasswordPermissionMixin, IsAdminUser): @@ -10,3 +10,13 @@ class SuperUserPermission(ValidationPasswordPermissionMixin, IsAdminUser): def has_permission(self, request, view): self.validate_password(request) return bool(request.user and request.user.is_superuser) + + +class ViewProjectHistoryLogsPermission(IsAuthenticated): + + def has_permission(self, request, view): + has_asset_perm = bool( + request.user + and view.asset.has_perm(user_obj=request.user, perm=PERM_MANAGE_ASSET) + ) + return has_asset_perm and super().has_permission(request, view) diff --git a/kobo/apps/audit_log/serializers.py b/kobo/apps/audit_log/serializers.py index 55a02a4e76..5307018584 100644 --- a/kobo/apps/audit_log/serializers.py +++ b/kobo/apps/audit_log/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from kpi.fields import RelativePrefixHyperlinkedRelatedField -from .models import AuditLog +from .models import AuditLog, ProjectHistoryLog class AuditLogSerializer(serializers.ModelSerializer): @@ -64,3 +64,39 @@ class AccessLogSerializer(serializers.Serializer): def get_date_created(self, audit_log): return audit_log['date_created'].strftime('%Y-%m-%dT%H:%M:%SZ') + + +class ProjectHistoryLogSerializer(serializers.ModelSerializer): + user = serializers.HyperlinkedRelatedField( + queryset=get_user_model().objects.all(), + lookup_field='username', + view_name='user-kpi-detail', + ) + date_created = serializers.SerializerMethodField() + username = serializers.SerializerMethodField() + + class Meta: + model = ProjectHistoryLog + fields = ( + 'user', + 'user_uid', + 'username', + 'action', + 'metadata', + 'date_created', + ) + + read_only_fields = ( + 'user', + 'user_uid', + 'username', + 'action', + 'metadata', + 'date_created', + ) + + def get_date_created(self, audit_log): + return audit_log.date_created.strftime('%Y-%m-%dT%H:%M:%SZ') + + def get_username(self, audit_log): + return audit_log.user.username diff --git a/kobo/apps/audit_log/tests/api/v2/test_api_audit_log.py b/kobo/apps/audit_log/tests/api/v2/test_api_audit_log.py index 3e4a9d7654..a36d2b8037 100644 --- a/kobo/apps/audit_log/tests/api/v2/test_api_audit_log.py +++ b/kobo/apps/audit_log/tests/api/v2/test_api_audit_log.py @@ -6,13 +6,17 @@ from rest_framework.reverse import reverse from kobo.apps.audit_log.audit_actions import AuditAction -from kobo.apps.audit_log.models import AccessLog, AuditLog, AuditType +from kobo.apps.audit_log.models import AccessLog, AuditLog, AuditType, ProjectHistoryLog from kobo.apps.audit_log.tests.test_signals import skip_login_access_log from kobo.apps.kobo_auth.shortcuts import User from kpi.constants import ( ACCESS_LOG_SUBMISSION_AUTH_TYPE, ACCESS_LOG_SUBMISSION_GROUP_AUTH_TYPE, + PERM_MANAGE_ASSET, + PROJECT_HISTORY_LOG_PERMISSION_SUBTYPE, + PROJECT_HISTORY_LOG_PROJECT_SUBTYPE, ) +from kpi.models import Asset from kpi.models.import_export_task import AccessLogExportTask from kpi.tests.base_test_case import BaseTestCase from kpi.urls.router_api_v2 import URL_NAMESPACE as ROUTER_URL_NAMESPACE @@ -41,6 +45,124 @@ def force_login_user(self, user): self.client.force_login(user) +class ProjectHistoryLogTestCaseMixin: + """ + Common tests for /project-history-logs and asset//history + """ + + def test_results_have_expected_fields(self): + now = timezone.now() + metadata_dict = { + 'asset_uid': self.asset.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': 'project', + 'some': 'thing', + } + ProjectHistoryLog.objects.create( + user=self.user, + object_id=self.asset.id, + action=AuditAction.DELETE, + metadata=metadata_dict, + date_created=now, + ) + response = self.client.get(self.url) + self.assertEqual(response.data['count'], 1) + ph_log = response.data['results'][0] + self.assertListEqual( + sorted(list(ph_log.keys())), + ['action', 'date_created', 'metadata', 'user', 'user_uid', 'username'], + ) + self.assertEqual(ph_log['action'], AuditAction.DELETE), + self.assertEqual(ph_log['date_created'], now.strftime('%Y-%m-%dT%H:%M:%SZ')) + self.assertEqual( + ph_log['user'], + reverse( + 'api_v2:user-kpi-detail', + kwargs={'username': self.user.username}, + request=response.wsgi_request, + ), + ) + self.assertEqual(ph_log['user_uid'], self.user.extra_details.uid) + self.assertEqual(ph_log['username'], self.user.username) + self.assertDictEqual(ph_log['metadata'], metadata_dict) + + def test_results_are_sorted_by_date_descending(self): + now = timezone.now() + yesterday = now - timedelta(days=1) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=self.asset.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': self.asset.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': 'project', + }, + date_created=yesterday, + ) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=self.asset.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': self.asset.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': 'project', + }, + date_created=now, + ) + response = self.client.get(self.url) + self.assertEqual(response.data['count'], 2) + self.assertEqual( + response.data['results'][0]['date_created'], + now.strftime('%Y-%m-%dT%H:%M:%SZ'), + ) + self.assertEqual( + response.data['results'][1]['date_created'], + yesterday.strftime('%Y-%m-%dT%H:%M:%SZ'), + ) + + def test_results_can_be_searched_by_subtype(self): + now = timezone.now() + yesterday = now - timedelta(days=1) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=self.asset.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': self.asset.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': PROJECT_HISTORY_LOG_PROJECT_SUBTYPE, + }, + date_created=now, + ) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=self.asset.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': self.asset.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': PROJECT_HISTORY_LOG_PERMISSION_SUBTYPE, + }, + date_created=yesterday, + ) + response = self.client.get( + f'{self.url}?q=metadata__log_subtype:' + f'{PROJECT_HISTORY_LOG_PERMISSION_SUBTYPE}' + ) + self.assertEqual(response.data['count'], 1) + self.assertEqual( + response.data['results'][0]['metadata']['log_subtype'], + PROJECT_HISTORY_LOG_PERMISSION_SUBTYPE, + ) + + class ApiAuditLogTestCase(BaseAuditLogTestCase): def get_endpoint_basename(self): @@ -429,6 +551,109 @@ def test_can_search_access_logs_by_date_including_submission_groups(self): ) +class ApiProjectHistoryLogsTestCase(BaseTestCase, ProjectHistoryLogTestCaseMixin): + + fixtures = ['test_data'] + + def setUp(self): + super().setUp() + self.asset = Asset.objects.get(pk=1) + self.url = reverse( + 'api_v2:history-list', kwargs={'parent_lookup_asset': self.asset.uid} + ) + self.user = User.objects.get(username='someuser') + self.asset.assign_perm(user_obj=self.user, perm=PERM_MANAGE_ASSET) + self.client.force_login(self.user) + + def test_list_without_permissions_returns_forbidden(self): + user2 = User.objects.get(username='anotheruser') + self.client.force_login(user2) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.asset.assign_perm(user_obj=user2, perm=PERM_MANAGE_ASSET) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_show_project_history_logs_filters_to_project(self): + asset2 = Asset.objects.get(pk=2) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=self.asset.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': self.asset.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': 'project', + }, + ) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=asset2.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': asset2.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': 'project', + }, + ) + response = self.client.get(self.url) + self.assertEqual(response.data['count'], 1) + self.assertEqual( + response.data['results'][0]['metadata']['asset_uid'], self.asset.uid + ) + + +class ApiAllProjectHistoryLogsTestCase( + BaseAuditLogTestCase, ProjectHistoryLogTestCaseMixin +): + + def get_endpoint_basename(self): + return 'all-project-history-logs-list' + + def setUp(self): + super().setUp() + self.user = User.objects.get(username='admin') + self.asset = Asset.objects.get(pk=1) + self.force_login_user(self.user) + + def test_show_all_project_history_logs(self): + asset1 = Asset.objects.get(pk=1) + asset2 = Asset.objects.get(pk=2) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=asset1.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': asset1.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': 'project', + }, + ) + ProjectHistoryLog.objects.create( + user=self.user, + object_id=asset2.id, + action=AuditAction.DELETE, + metadata={ + 'asset_uid': asset2.uid, + 'ip_address': '1.2.3.4', + 'source': 'source', + 'log_subtype': 'project', + }, + ) + response = self.client.get(self.url) + self.assertEqual(response.data['count'], 2) + self.assertEqual( + response.data['results'][0]['metadata']['asset_uid'], asset2.uid + ) + self.assertEqual( + response.data['results'][1]['metadata']['asset_uid'], asset1.uid + + ) + + class ApiAccessLogsExportTestCase(BaseAuditLogTestCase): def get_endpoint_basename(self): diff --git a/kobo/apps/audit_log/urls.py b/kobo/apps/audit_log/urls.py index f2f9625b13..1fcc4947f1 100644 --- a/kobo/apps/audit_log/urls.py +++ b/kobo/apps/audit_log/urls.py @@ -5,6 +5,7 @@ AccessLogViewSet, AllAccessLogsExportViewSet, AllAccessLogViewSet, + AllProjectHistoryLogViewSet, AuditLogViewSet, ) @@ -12,6 +13,12 @@ router.register(r'audit-logs', AuditLogViewSet, basename='audit-log') router.register(r'access-logs', AllAccessLogViewSet, basename='all-access-logs') router.register(r'access-logs/me', AccessLogViewSet, basename='access-log') +# routes for PH logs for individual assets are registered in router_api_v2.py +router.register( + r'project-history-logs', + AllProjectHistoryLogViewSet, + basename='all-project-history-logs', +) router.register( r'access-logs/export', AllAccessLogsExportViewSet, basename='all-access-logs-export' ) diff --git a/kobo/apps/audit_log/views.py b/kobo/apps/audit_log/views.py index dce8deaf5a..27dc6a3d9d 100644 --- a/kobo/apps/audit_log/views.py +++ b/kobo/apps/audit_log/views.py @@ -1,15 +1,21 @@ from rest_framework import mixins, status, viewsets from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer +from rest_framework_extensions.mixins import NestedViewSetMixin from rest_framework.response import Response from kpi.filters import SearchFilter from kpi.models.import_export_task import AccessLogExportTask from kpi.permissions import IsAuthenticated +from kpi.utils.viewset_mixins import AssetNestedObjectViewsetMixin from kpi.tasks import export_task_in_background from .filters import AccessLogPermissionsFilter -from .models import AccessLog, AuditLog -from .permissions import SuperUserPermission -from .serializers import AccessLogSerializer, AuditLogSerializer +from .models import AccessLog, AuditLog, ProjectHistoryLog +from .permissions import SuperUserPermission, ViewProjectHistoryLogsPermission +from .serializers import ( + AccessLogSerializer, + AuditLogSerializer, + ProjectHistoryLogSerializer, +) class AuditLogViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): @@ -326,6 +332,507 @@ class AccessLogViewSet(AuditLogViewSet): serializer_class = AccessLogSerializer +class AllProjectHistoryLogViewSet(AuditLogViewSet): + """ + Project history logs + + Lists all project history logs for all projects. Only available to superusers. + +
+    GET /api/v2/project-history-logs/
+    
+ + > Example + > + > curl -X GET https://[kpi-url]/project-history-logs/ + + > Response 200 + + > { + > "count": 10, + > "next": null, + > "previous": null, + > "results": [ + > { + > "user": "http://localhost/api/v2/users/admin/", + > "user_uid": "u12345", + > "username": "admin", + > "action": "modify-user-permissions" + > "metadata": { + > "source": "Firefox (Ubuntu)", + > "ip_address": "172.18.0.6", + > "asset_uid": "a678910", + > "log_subtype": "permissions", + > "permissions": + > { + > "username": "user1", + > "added": ["add_submissions", "view_submissions"], + > "removed": ["change_asset"], + > } + > }, + > "date_created": "2024-08-19T16:48:58Z", + > }, + > { + > "user": "http://localhost/api/v2/users/admin/", + > "user_uid": "u56789", + > "username": "someuser", + > "action": "update-settings", + > "metadata": { + > "source": "Firefox (Ubuntu)", + > "ip_address": "172.18.0.6", + > "asset_uid": "a111213", + > "log_subtype": "project", + > "settings": + > { + > "description": + > { + > "old": "old_description", + > "new": "new_description", + > } + > "countries": + > { + > "added": ["USA"], + > "removed": ["ALB"], + > } + > } + > }, + > "date_created": "2024-08-19T16:48:58Z", + > }, + > ... + > ] + > } + + Results from this endpoint can be filtered by a Boolean query + specified in the `q` parameter. + + **Filterable fields for all project history logs:** + + 1. date_created + + 2. user_uid + + 3. user__* + + a. user__username + + b. user__email + + c. user__is_superuser + + 4. action + + available actions: + + > add-media + > allow-anonymous-submissions + > archive + > connect-project + > delete-media + > delete-service + > deploy + > disable-sharing + > disallow-anonymous-submissions + > disconnect-project + > enable-sharing + > export + > modify-imported-fields + > modify-service + > modify_sharing + > modify-user-permissions + > redeploy + > register-service + > replace-form + > share-data-publicly + > share-form-publicly + > transfer + > unarchive + > unshare-data-publicly + > unshare-form-publicly + > update_content + > update-name + > update-settings + > update-qa + + 4. metadata__* + + b. metadata__source + + c. metadata__ip_address + + d. metadata__asset_uid + + e. metadata__log_subtype + + available subtypes: + + project + permission + + **Filterable fields by action:** + + 1. add-media + + a. metadata__asset-file__uid + + b. metadata__asset-file__filename + + 2. archive + + a. metadata__latest_version_uid + + 3. clone-permissions + + a. metadata__cloned_from + + 4. connect-project + + a. metadata__paired-data__source_uid + + b. metadata__paired-data__source_name + + 5. delete-media + + a. metadata__asset-file__uid + + b. metadata__asset-file__filename + + 6. delete-service + + a. metadata__hook__uid + + b. metadata__hook__endpoint + + c. metadata__hook__active + + 7. deploy + + a. metadata__latest_version_uid + + b. metadata__latest_deployed_version_uid + + 8. disconnect-project + + a. metadata__paired-data__source_uid + + b. metadata__paired-data__source_name + + 9. modify-imported-fields + + a. metadata__paired-data__source_uid + + b. metadata__paired-data__source_name + + 10. modify-service + + a. metadata__hook__uid + + b. metadata__hook__endpoint + + c. metadata__hook__active + + 11. modify-user-permissions + + a. metadata__permissions__username + + 12. redeploy + + a. metadata__latest_version_uid + + b. metadata__latest_deployed_version_uid + + 13. register-service + + a. metadata__hook__uid + + b. metadata__hook__endpoint + + c. metadata__hook__active + + 14. transfer + + a. metadata__username + + 15. unarchive + + a. metadata__latest_version_uid + + 16. update-name + + a. metadata__name__old + + b. metadata__name__new + + 17. update-settings + + a. metadata__settings__description__old + + b. metadata__settings__description__new + + This endpoint can be paginated with 'offset' and 'limit' parameters, eg + > curl -X GET https://[kpi-url]/project-history-logs/?offset=100&limit=50 + """ + + queryset = ProjectHistoryLog.objects.all().order_by('-date_created') + serializer_class = ProjectHistoryLogSerializer + filter_backends = (SearchFilter,) + + +class ProjectHistoryLogViewSet( + AuditLogViewSet, AssetNestedObjectViewsetMixin, NestedViewSetMixin +): + """ + Project history logs + + Lists all project history logs for a single project. Only available to + those with 'manage_asset' permissions. + +
+    GET /api/v2/asset/{asset_uid}/history/
+    
+ + > Example + > + > curl -X GET https://[kpi-url]/api/v2/asset/aSAvYreNzVEkrWg5Gdcvg/history/ + + + > Response 200 + + > { + > "count": 10, + > "next": null, + > "previous": null, + > "results": [ + > { + > "user": "http://localhost/api/v2/users/admin/", + > "user_uid": "u12345", + > "username": "admin", + > "action": "modify-user-permissions" + > "metadata": { + > "source": "Firefox (Ubuntu)", + > "ip_address": "172.18.0.6", + > "asset_uid": "a678910", + > "log_subtype": "permissions", + > "permissions": + > { + > "username": "user1", + > "added": ["add_submissions", "view_submissions"], + > "removed": ["change_asset"], + > } + > }, + > "date_created": "2024-08-19T16:48:58Z", + > }, + > { + > "user": "http://localhost/api/v2/users/admin/", + > "user_uid": "u56789", + > "username": "someuser", + > "action": "update-settings", + > "metadata": { + > "source": "Firefox (Ubuntu)", + > "ip_address": "172.18.0.6", + > "asset_uid": "a111213", + > "log_subtype": "project", + > "settings": + > { + > "description": + > { + > "old": "old_description", + > "new": "new_description", + > } + > "countries": + > { + > "added": ["USA"], + > "removed": ["ALB"], + > } + > } + > }, + > "date_created": "2024-08-19T16:48:58Z", + > }, + > ... + > ] + > } + + Results from this endpoint can be filtered by a Boolean query + specified in the `q` parameter. + + **Filterable fields for all project history logs:** + + 1. date_created + + 2. user_uid + + 3. user__* + + a. user__username + + b. user__email + + c. user__is_superuser + + 4. action + + available actions: + + > add-media + > allow-anonymous-submissions + > archive + > connect-project + > delete-media + > delete-service + > deploy + > disable-sharing + > disallow-anonymous-submissions + > disconnect-project + > enable-sharing + > export + > modify-imported-fields + > modify-service + > modify_sharing + > modify-user-permissions + > redeploy + > register-service + > replace-form + > share-data-publicly + > share-form-publicly + > transfer + > unarchive + > unshare-data-publicly + > unshare-form-publicly + > update_content + > update-name + > update-settings + > update-qa + + 4. metadata__* + + b. metadata__source (browser/OS) + + c. metadata__ip_address + + d. metadata__asset_uid + + e. metadata__log_subtype + + available subtypes: + + project + permission + + **Filterable fields by action:** + + 1. add-media + + a. metadata__asset-file__uid + + b. metadata__asset-file__filename + + 2. archive + + a. metadata__latest_version_uid + + 3. clone-permissions + + a. metadata__cloned_from + + 4. connect-project + + a. metadata__paired-data__source_uid + + b. metadata__paired-data__source_name + + 5. delete-media + + a. metadata__asset-file__uid + + b. metadata__asset-file__filename + + 6. delete-service + + a. metadata__hook__uid + + b. metadata__hook__endpoint + + c. metadata__hook__active + + 7. deploy + + a. metadata__latest_version_uid + + b. metadata__latest_deployed_version_uid + + 8. disconnect-project + + a. metadata__paired-data__source_uid + + b. metadata__paired-data__source_name + + 9. modify-imported-fields + + a. metadata__paired-data__source_uid + + b. metadata__paired-data__source_name + + 10. modify-service + + a. metadata__hook__uid + + b. metadata__hook__endpoint + + c. metadata__hook__active + + 11. modify-user-permissions + + a. metadata__permissions__username + + 12. redeploy + + a. metadata__latest_version_uid + + b. metadata__latest_deployed_version_uid + + 13. register-service + + a. metadata__hook__uid + + b. metadata__hook__endpoint + + c. metadata__hook__active + + 14. transfer + + a. metadata__username + + 15. unarchive + + a. metadata__latest_version_uid + + 16. update-name + + a. metadata__name__old + + b. metadata__name__new + + 17. update-settings + + a. metadata__settings__description__old + + b. metadata__settings__description__new + + This endpoint can be paginated with 'offset' and 'limit' parameters, eg + > curl -X GET https://[kpi-url]/assets/ap732ywWxc/history/?offset=100&limit=50 + """ + + serializer_class = ProjectHistoryLogSerializer + model = ProjectHistoryLog + permission_classes = (ViewProjectHistoryLogsPermission,) + lookup_field = 'uid' + filter_backends = (SearchFilter,) + + def get_queryset(self): + return self.model.objects.filter(metadata__asset_uid=self.asset_uid).order_by( + '-date_created' + ) + + class BaseAccessLogsExportViewSet(viewsets.ViewSet): permission_classes = (IsAuthenticated,) lookup_field = 'uid' @@ -443,6 +950,7 @@ class AllAccessLogsExportViewSet(BaseAccessLogsExportViewSet): > Example > + > curl -X GET https://[kpi-url]/access-logs/export > Response 200 diff --git a/kpi/urls/router_api_v2.py b/kpi/urls/router_api_v2.py index 411d77de2b..b2c3f90fac 100644 --- a/kpi/urls/router_api_v2.py +++ b/kpi/urls/router_api_v2.py @@ -1,12 +1,14 @@ # coding: utf-8 + from django.urls import path from rest_framework_extensions.routers import ExtendedDefaultRouter from kobo.apps.audit_log.urls import router as audit_log_router +from kobo.apps.audit_log.views import ProjectHistoryLogViewSet from kobo.apps.hook.views.v2.hook import HookViewSet from kobo.apps.hook.views.v2.hook_log import HookLogViewSet from kobo.apps.languages.urls import router as language_router -from kobo.apps.organizations.views import OrganizationViewSet, OrganizationMemberViewSet +from kobo.apps.organizations.views import OrganizationMemberViewSet, OrganizationViewSet from kobo.apps.project_ownership.urls import router as project_ownership_router from kobo.apps.project_views.views import ProjectViewViewSet from kpi.views.v2.asset import AssetViewSet @@ -103,11 +105,19 @@ def get_urls(self, *args, **kwargs): parents_query_lookups=['asset'], ) -asset_routes.register(r'paired-data', - PairedDataViewset, - basename='paired-data', - parents_query_lookups=['asset'], - ) +asset_routes.register( + r'paired-data', + PairedDataViewset, + basename='paired-data', + parents_query_lookups=['asset'], +) + +asset_routes.register( + r'history', + ProjectHistoryLogViewSet, + basename='history', + parents_query_lookups=['asset'], +) data_routes = asset_routes.register(r'data', DataViewSet,