From 9d3e62ff161d8be37c8c5687cdf91e4e0883d1c3 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:27:20 -0800 Subject: [PATCH] audit log entries for version --- src/documents/bulk_edit.py | 22 ++++++++++-- src/documents/consumer.py | 44 ++++++++++++++++++++--- src/documents/data_models.py | 3 ++ src/documents/tests/test_api_documents.py | 30 ++++++++++++++++ src/documents/views.py | 29 ++++++++++++--- 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index 08673b35f..ea09d4ae5 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -414,7 +414,12 @@ def set_permissions( return "OK" -def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]: +def rotate( + doc_ids: list[int], + degrees: int, + *, + user: User | None = None, +) -> Literal["OK"]: logger.info( f"Attempting to rotate {len(doc_ids)} documents by {degrees} degrees.", ) @@ -447,6 +452,8 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]: # Preserve metadata/permissions via overrides; mark as new version overrides = DocumentMetadataOverrides().from_document(root_doc) + if user is not None: + overrides.actor_id = user.id consume_file.delay( ConsumableDocument( @@ -645,7 +652,12 @@ def split( return "OK" -def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]: +def delete_pages( + doc_ids: list[int], + pages: list[int], + *, + user: User | None = None, +) -> Literal["OK"]: logger.info( f"Attempting to delete pages {pages} from {len(doc_ids)} documents", ) @@ -675,6 +687,8 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]: pdf.save(filepath) overrides = DocumentMetadataOverrides().from_document(root_doc) + if user is not None: + overrides.actor_id = user.id consume_file.delay( ConsumableDocument( source=DocumentSource.ConsumeFolder, @@ -759,6 +773,7 @@ def edit_pdf( ) if user is not None: overrides.owner_id = user.id + overrides.actor_id = user.id consume_file.delay( ConsumableDocument( source=DocumentSource.ConsumeFolder, @@ -776,6 +791,7 @@ def edit_pdf( ) if user is not None: overrides.owner_id = user.id + overrides.actor_id = user.id if not delete_original: overrides.skip_asn_if_exists = True if delete_original and len(pdf_docs) == 1: @@ -865,6 +881,7 @@ def remove_password( ) if user is not None: overrides.owner_id = user.id + overrides.actor_id = user.id consume_file.delay( ConsumableDocument( source=DocumentSource.ConsumeFolder, @@ -882,6 +899,7 @@ def remove_password( ) if user is not None: overrides.owner_id = user.id + overrides.actor_id = user.id consume_tasks.append( consume_file.s( diff --git a/src/documents/consumer.py b/src/documents/consumer.py index e689fe0d8..4e0fae5cc 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -489,14 +489,15 @@ class ConsumerPlugin( # If this is a new version of an existing document, we need # to make sure we're not creating a new document, but updating # the existing one. + root_doc = Document.objects.get( + pk=self.input_doc.root_document_id, + ) original_document = Document.objects.get( pk=self.input_doc.root_document_id, ) self.log.debug("Saving record for updated version to database") original_document.pk = None - original_document.root_document = Document.objects.get( - pk=self.input_doc.root_document_id, - ) + original_document.root_document = root_doc file_for_checksum = ( self.unmodified_original if self.unmodified_original is not None @@ -517,7 +518,42 @@ class ConsumerPlugin( original_document.version_label = self.metadata.version_label original_document.added = timezone.now() original_document.modified = timezone.now() - original_document.save() + actor = None + + # Save the new version, potentially creating an audit log entry for the version addition if enabled. + if ( + settings.AUDIT_LOG_ENABLED + and self.metadata.actor_id is not None + ): + from auditlog.models import LogEntry + + actor = User.objects.filter(pk=self.metadata.actor_id).first() + if actor is not None: + from auditlog.context import set_actor + + with set_actor(actor): + original_document.save() + else: + original_document.save() + else: + original_document.save() + + # Create a log entry for the version addition, if enabled + if settings.AUDIT_LOG_ENABLED: + from auditlog.models import LogEntry + + LogEntry.objects.log_create( + instance=root_doc, + changes={ + "Version Added": ["None", original_document.id], + }, + action=LogEntry.Action.UPDATE, + actor=actor, + additional_data={ + "reason": "Version added", + "version_id": original_document.id, + }, + ) document = original_document else: document = self._store( diff --git a/src/documents/data_models.py b/src/documents/data_models.py index db50be171..a2452a8af 100644 --- a/src/documents/data_models.py +++ b/src/documents/data_models.py @@ -32,6 +32,7 @@ class DocumentMetadataOverrides: custom_fields: dict | None = None skip_asn_if_exists: bool = False version_label: str | None = None + actor_id: int | None = None def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides": """ @@ -51,6 +52,8 @@ class DocumentMetadataOverrides: self.storage_path_id = other.storage_path_id if other.owner_id is not None: self.owner_id = other.owner_id + if other.actor_id is not None: + self.actor_id = other.actor_id if other.skip_asn_if_exists: self.skip_asn_if_exists = True if other.version_label is not None: diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index c362f9646..124616762 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -554,6 +554,36 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertIsNone(response.data[1]["actor"]) self.assertEqual(response.data[1]["action"], "create") + def test_document_history_logs_version_deletion(self) -> None: + root_doc = Document.objects.create( + title="Root", + checksum="123", + mime_type="application/pdf", + owner=self.user, + ) + version_doc = Document.objects.create( + title="Version", + checksum="456", + mime_type="application/pdf", + root_document=root_doc, + owner=self.user, + ) + + response = self.client.delete( + f"/api/documents/{root_doc.pk}/versions/{version_doc.pk}/", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.get(f"/api/documents/{root_doc.pk}/history/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data[0]["actor"]["id"], self.user.id) + self.assertEqual(response.data[0]["action"], "update") + self.assertEqual( + response.data[0]["changes"], + {"Version Deleted": [version_doc.pk, "None"]}, + ) + @override_settings(AUDIT_LOG_ENABLED=False) def test_document_history_action_disabled(self) -> None: """ diff --git a/src/documents/views.py b/src/documents/views.py index 34a9dc637..1d2f114ce 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1545,6 +1545,8 @@ class DocumentViewSet( overrides = DocumentMetadataOverrides() if label: overrides.version_label = label.strip() + if request.user is not None: + overrides.actor_id = request.user.id async_task = consume_file.delay( input_doc, @@ -1600,7 +1602,24 @@ class DocumentViewSet( from documents import index index.remove_document_from_index(version_doc) + version_doc_id = version_doc.id version_doc.delete() + if settings.AUDIT_LOG_ENABLED: + actor = ( + request.user if request.user and request.user.is_authenticated else None + ) + LogEntry.objects.log_create( + instance=root_doc, + changes={ + "Version Deleted": [version_doc_id, "None"], + }, + action=LogEntry.Action.UPDATE, + actor=actor, + additional_data={ + "reason": "Version deleted", + "version_id": version_doc_id, + }, + ) current = ( Document.objects.filter(Q(id=root_doc.id) | Q(root_document=root_doc)) @@ -1913,13 +1932,13 @@ class BulkEditView(PassUserMixin): "modify_custom_fields": "custom_fields", "set_permissions": None, "delete": "deleted_at", - "rotate": "checksum", - "delete_pages": "checksum", + "rotate": None, + "delete_pages": None, "split": None, "merge": None, - "edit_pdf": "checksum", + "edit_pdf": None, "reprocess": "checksum", - "remove_password": "checksum", + "remove_password": None, } permission_classes = (IsAuthenticated,) @@ -1937,6 +1956,8 @@ class BulkEditView(PassUserMixin): if method in [ bulk_edit.split, bulk_edit.merge, + bulk_edit.rotate, + bulk_edit.delete_pages, bulk_edit.edit_pdf, bulk_edit.remove_password, ]: