Enhancement: record history for bulk edit operations

This commit is contained in:
shamoon 2024-10-30 14:21:55 -07:00
parent 5a74a92b74
commit 06474e2781
2 changed files with 158 additions and 2 deletions

View File

@ -1,7 +1,9 @@
import json import json
from unittest import mock from unittest import mock
from auditlog.models import LogEntry
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import override_settings
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -51,8 +53,8 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc3.tags.add(self.t2) self.doc3.tags.add(self.t2)
self.doc4.tags.add(self.t1, self.t2) self.doc4.tags.add(self.t1, self.t2)
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}") self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
self.cf1 = CustomField.objects.create(name="cf1", data_type="text") self.cf1 = CustomField.objects.create(name="cf1", data_type="string")
self.cf2 = CustomField.objects.create(name="cf2", data_type="text") self.cf2 = CustomField.objects.create(name="cf2", data_type="string")
@mock.patch("documents.bulk_edit.bulk_update_documents.delay") @mock.patch("documents.bulk_edit.bulk_update_documents.delay")
def test_api_set_correspondent(self, bulk_update_task_mock): def test_api_set_correspondent(self, bulk_update_task_mock):
@ -1254,3 +1256,87 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"pages must be a list of integers", response.content) self.assertIn(b"pages must be a list of integers", response.content)
@override_settings(AUDIT_LOG_ENABLED=True)
def test_bulk_edit_audit_log_enabled_simple_field(self):
"""
GIVEN:
- Audit log is enabled
WHEN:
- API to bulk edit documents is called
THEN:
- Audit log is created
"""
LogEntry.objects.all().delete()
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"method": "set_correspondent",
"parameters": {"correspondent": self.c2.id},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(LogEntry.objects.filter(object_pk=self.doc1.id).count(), 1)
@override_settings(AUDIT_LOG_ENABLED=True)
def test_bulk_edit_audit_log_enabled_tags(self):
"""
GIVEN:
- Audit log is enabled
WHEN:
- API to bulk edit tags is called
THEN:
- Audit log is created
"""
LogEntry.objects.all().delete()
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"method": "modify_tags",
"parameters": {
"add_tags": [self.t1.id],
"remove_tags": [self.t2.id],
},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(LogEntry.objects.filter(object_pk=self.doc1.id).count(), 1)
@override_settings(AUDIT_LOG_ENABLED=True)
def test_bulk_edit_audit_log_enabled_custom_fields(self):
"""
GIVEN:
- Audit log is enabled
WHEN:
- API to bulk edit custom fields is called
THEN:
- Audit log is created
"""
LogEntry.objects.all().delete()
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"method": "modify_custom_fields",
"parameters": {
"add_custom_fields": [self.cf1.id],
"remove_custom_fields": [],
},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(LogEntry.objects.filter(object_pk=self.doc1.id).count(), 2)

View File

@ -26,11 +26,13 @@ from django.db.models import Case
from django.db.models import Count from django.db.models import Count
from django.db.models import IntegerField from django.db.models import IntegerField
from django.db.models import Max from django.db.models import Max
from django.db.models import Model
from django.db.models import Q from django.db.models import Q
from django.db.models import Sum from django.db.models import Sum
from django.db.models import When from django.db.models import When
from django.db.models.functions import Length from django.db.models.functions import Length
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.db.models.manager import Manager
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
@ -106,6 +108,7 @@ from documents.matching import match_storage_paths
from documents.matching import match_tags from documents.matching import match_tags
from documents.models import Correspondent from documents.models import Correspondent
from documents.models import CustomField from documents.models import CustomField
from documents.models import CustomFieldInstance
from documents.models import Document from documents.models import Document
from documents.models import DocumentType from documents.models import DocumentType
from documents.models import Note from documents.models import Note
@ -961,6 +964,22 @@ class SavedViewViewSet(ModelViewSet, PassUserMixin):
class BulkEditView(PassUserMixin): class BulkEditView(PassUserMixin):
MODIFIED_FIELD_BY_METHOD = {
bulk_edit.set_correspondent: "correspondent",
bulk_edit.set_document_type: "document_type",
bulk_edit.set_storage_path: "storage_path",
bulk_edit.add_tag: "tags",
bulk_edit.remove_tag: "tags",
bulk_edit.modify_tags: "tags",
bulk_edit.modify_custom_fields: "custom_fields",
bulk_edit.set_permissions: None,
bulk_edit.delete: "deleted_at",
bulk_edit.rotate: "checksum",
bulk_edit.delete_pages: "checksum",
bulk_edit.split: None,
bulk_edit.merge: None,
}
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = BulkEditSerializer serializer_class = BulkEditSerializer
parser_classes = (parsers.JSONParser,) parser_classes = (parsers.JSONParser,)
@ -1013,8 +1032,59 @@ class BulkEditView(PassUserMixin):
return HttpResponseForbidden("Insufficient permissions") return HttpResponseForbidden("Insufficient permissions")
try: try:
modified_field = self.MODIFIED_FIELD_BY_METHOD[method]
if settings.AUDIT_LOG_ENABLED and modified_field:
old_documents = list(
Document.objects.filter(pk__in=documents).values(
"pk",
"correspondent",
"document_type",
"storage_path",
"tags",
"custom_fields",
"deleted_at",
"checksum",
),
)
# TODO: parameter validation # TODO: parameter validation
result = method(documents, **parameters) result = method(documents, **parameters)
if settings.AUDIT_LOG_ENABLED and modified_field:
new_documents = Document.objects.filter(pk__in=documents)
for doc in new_documents:
old_value = next(
item for item in old_documents if item["pk"] == doc.pk
)[modified_field]
new_value = getattr(doc, modified_field)
if isinstance(new_value, Model):
old_value = old_value.pk if old_value else None
new_value = new_value.pk if new_value else None
elif isinstance(new_value, Manager):
# old value is a list of pks already
new_value = list(new_value.values_list("pk", flat=True))
elif modified_field == "custom_fields":
new_value = list(
CustomFieldInstance.objects.filter(
document=doc,
).values_list("pk", flat=True),
)
LogEntry.objects.log_create(
instance=doc,
changes={
modified_field: [
old_value,
new_value,
],
},
action=LogEntry.Action.UPDATE,
additional_data={
"reason": f"Bulk edit: {method.__name__}",
},
)
return Response({"result": result}) return Response({"result": result})
except Exception as e: except Exception as e:
logger.warning(f"An error occurred performing bulk edit: {e!s}") logger.warning(f"An error occurred performing bulk edit: {e!s}")