From 25f0036a5071e06697980774fb932b2908310926 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 13 Apr 2024 22:37:12 -0700 Subject: [PATCH] Support custom fields, include actor for custom fields changes F --- .../audit-log/audit-log.component.html | 6 ++ src/documents/serialisers.py | 7 +- src/documents/tests/test_api_documents.py | 64 +++++++++++++++++++ src/documents/views.py | 36 +++++++++-- 4 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/components/audit-log/audit-log.component.html b/src-ui/src/app/components/audit-log/audit-log.component.html index 497049aa4..65ecaff0a 100644 --- a/src-ui/src/app/components/audit-log/audit-log.component.html +++ b/src-ui/src/app/components/audit-log/audit-log.component.html @@ -37,6 +37,12 @@ {{ change.value["objects"].join(', ') }} } + @else if (change.value["type"] === 'custom_field') { +
  • + {{ change.value["field"] }}:  + {{ change.value["value"] }} +
  • + } @else {
  • {{ change.key | titlecase }}:  diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 26930ccec..c7e86a7bf 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -5,6 +5,7 @@ import zoneinfo from decimal import Decimal import magic +from auditlog.context import set_actor from celery import states from django.conf import settings from django.contrib.auth.models import Group @@ -746,7 +747,11 @@ class DocumentSerializer( for tag in instance.tags.all() if tag not in inbox_tags_not_being_added ] - super().update(instance, validated_data) + if settings.AUDIT_LOG_ENABLED: + with set_actor(self.user): + super().update(instance, validated_data) + else: + super().update(instance, validated_data) return instance def __init__(self, *args, **kwargs): diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 1ea6f52c4..8ae67a6c0 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -339,6 +339,70 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): {"title": ["First title", "New title"]}, ) + def test_document_audit_action_w_custom_fields(self): + """ + GIVEN: + - Document with custom fields + WHEN: + - Document is updated + THEN: + - Audit log contains custom field changes + """ + doc = Document.objects.create( + title="First title", + checksum="123", + mime_type="application/pdf", + ) + custom_field = CustomField.objects.create( + name="custom field str", + data_type=CustomField.FieldDataType.STRING, + ) + self.client.force_login(user=self.user) + self.client.patch( + f"/api/documents/{doc.pk}/", + data={ + "custom_fields": [ + { + "field": custom_field.pk, + "value": "custom value", + }, + ], + }, + format="json", + ) + + response = self.client.get(f"/api/documents/{doc.pk}/audit/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[1]["actor"]["id"], self.user.id) + self.assertEqual(response.data[1]["action"], "create") + self.assertEqual( + response.data[1]["changes"], + { + "custom_fields": { + "type": "custom_field", + "field": "custom field str", + "value": "custom value", + }, + }, + ) + + @override_settings(AUDIT_LOG_ENABLED=False) + def test_document_audit_action_disabled(self): + doc = Document.objects.create( + title="First title", + checksum="123", + mime_type="application/pdf", + ) + self.client.force_login(user=self.user) + self.client.patch( + f"/api/documents/{doc.pk}/", + {"title": "New title"}, + format="json", + ) + + response = self.client.get(f"/api/documents/{doc.pk}/audit/") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_document_filters(self): doc1 = Document.objects.create( title="none1", diff --git a/src/documents/views.py b/src/documents/views.py index 48a745092..751d1428b 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -18,6 +18,7 @@ import pathvalidate from django.apps import apps from django.conf import settings from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.db import connections from django.db.migrations.loader import MigrationLoader from django.db.migrations.recorder import MigrationRecorder @@ -105,6 +106,7 @@ from documents.matching import match_storage_paths from documents.matching import match_tags from documents.models import Correspondent from documents.models import CustomField +from documents.models import CustomFieldInstance from documents.models import Document from documents.models import DocumentType from documents.models import Note @@ -743,24 +745,48 @@ class DocumentViewSet( raise Http404 if request.method == "GET": + # documents entries = [ { "id": entry.id, "timestamp": entry.timestamp, "action": entry.get_action_display(), "changes": json.loads(entry.changes), - "remote_addr": entry.remote_addr, "actor": ( {"id": entry.actor.id, "username": entry.actor.username} if entry.actor else None ), } - for entry in LogEntry.objects.filter(object_pk=doc.pk).order_by( - "-timestamp", - ) + for entry in LogEntry.objects.filter(object_pk=doc.pk) ] - return Response(entries) + + # custom fields + for entry in LogEntry.objects.filter( + object_pk__in=doc.custom_fields.values_list("id", flat=True), + content_type=ContentType.objects.get_for_model(CustomFieldInstance), + ): + entries.append( + { + "id": entry.id, + "timestamp": entry.timestamp, + "action": entry.get_action_display(), + "changes": { + "custom_fields": { + "type": "custom_field", + "field": str(entry.object_repr).split(":")[0].strip(), + "value": str(entry.object_repr).split(":")[1].strip(), + }, + }, + "actor": ( + {"id": entry.actor.id, "username": entry.actor.username} + if entry.actor + else None + ), + }, + ) + + return Response(sorted(entries, key=lambda x: x["timestamp"], reverse=True)) class SearchResultSerializer(DocumentSerializer, PassUserMixin):