From c598275d4e2594ae417ef9091fc7e7d24bca7aac Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:19:19 -0800 Subject: [PATCH] Backend update only version label --- src/documents/serialisers.py | 16 +++ .../tests/test_api_document_versions.py | 101 +++++++++++++++ src/documents/views.py | 117 +++++++++++++++--- 3 files changed, 217 insertions(+), 17 deletions(-) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 5b591f6d3..391ac5708 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -2080,6 +2080,22 @@ class DocumentVersionSerializer(serializers.Serializer): validate_document = PostDocumentSerializer().validate_document +class DocumentVersionLabelSerializer(serializers.Serializer): + version_label = serializers.CharField( + label="Version label", + required=True, + allow_blank=True, + allow_null=True, + max_length=64, + ) + + def validate_version_label(self, value): + if value is None: + return None + normalized = value.strip() + return normalized or None + + class BulkDownloadSerializer(DocumentListSerializer): content = serializers.ChoiceField( choices=["archive", "originals", "both"], diff --git a/src/documents/tests/test_api_document_versions.py b/src/documents/tests/test_api_document_versions.py index 8f5465357..f5c1a7346 100644 --- a/src/documents/tests/test_api_document_versions.py +++ b/src/documents/tests/test_api_document_versions.py @@ -325,6 +325,107 @@ class TestDocumentVersioningApi(DirectoriesMixin, APITestCase): self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) + def test_update_version_label_updates_and_trims(self) -> None: + root = Document.objects.create( + title="root", + checksum="root", + mime_type="application/pdf", + ) + version = Document.objects.create( + title="v1", + checksum="v1", + mime_type="application/pdf", + root_document=root, + version_label="old", + ) + + resp = self.client.patch( + f"/api/documents/{root.id}/versions/{version.id}/", + {"version_label": " Label 1 "}, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_200_OK) + version.refresh_from_db() + self.assertEqual(version.version_label, "Label 1") + self.assertEqual(resp.data["version_label"], "Label 1") + self.assertEqual(resp.data["id"], version.id) + self.assertFalse(resp.data["is_root"]) + + def test_update_version_label_clears_on_blank(self) -> None: + root = Document.objects.create( + title="root", + checksum="root", + mime_type="application/pdf", + version_label="Root Label", + ) + + resp = self.client.patch( + f"/api/documents/{root.id}/versions/{root.id}/", + {"version_label": " "}, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_200_OK) + root.refresh_from_db() + self.assertIsNone(root.version_label) + self.assertIsNone(resp.data["version_label"]) + self.assertTrue(resp.data["is_root"]) + + def test_update_version_label_returns_403_without_permission(self) -> None: + owner = User.objects.create_user(username="owner") + other = User.objects.create_user(username="other") + other.user_permissions.add( + Permission.objects.get(codename="change_document"), + ) + root = Document.objects.create( + title="root", + checksum="root", + mime_type="application/pdf", + owner=owner, + ) + version = Document.objects.create( + title="v1", + checksum="v1", + mime_type="application/pdf", + root_document=root, + ) + self.client.force_authenticate(user=other) + + resp = self.client.patch( + f"/api/documents/{root.id}/versions/{version.id}/", + {"version_label": "Blocked"}, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) + + def test_update_version_label_returns_404_for_unrelated_version(self) -> None: + root = Document.objects.create( + title="root", + checksum="root", + mime_type="application/pdf", + ) + other_root = Document.objects.create( + title="other", + checksum="other", + mime_type="application/pdf", + ) + other_version = Document.objects.create( + title="other-v1", + checksum="other-v1", + mime_type="application/pdf", + root_document=other_root, + ) + + resp = self.client.patch( + f"/api/documents/{root.id}/versions/{other_version.id}/", + {"version_label": "Nope"}, + format="json", + ) + + self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) + def test_download_version_param_errors(self) -> None: root = self._create_pdf(title="root", checksum="root") diff --git a/src/documents/views.py b/src/documents/views.py index d10eff44b..b5f73f2e7 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -178,6 +178,7 @@ from documents.serialisers import CustomFieldSerializer from documents.serialisers import DocumentListSerializer from documents.serialisers import DocumentSerializer from documents.serialisers import DocumentTypeSerializer +from documents.serialisers import DocumentVersionLabelSerializer from documents.serialisers import DocumentVersionSerializer from documents.serialisers import EmailSerializer from documents.serialisers import NotesSerializer @@ -1663,6 +1664,31 @@ class DocumentViewSet( "Error updating document, check logs for more detail.", ) + def _get_root_doc_for_version_action(self, pk) -> Document: + try: + root_doc = Document.objects.select_related( + "owner", + "root_document", + ).get(pk=pk) + except Document.DoesNotExist: + raise Http404 + return get_root_document(root_doc) + + def _get_version_doc_for_root(self, root_doc: Document, version_id) -> Document: + try: + version_doc = Document.objects.select_related("owner").get( + pk=version_id, + ) + except Document.DoesNotExist: + raise Http404 + + if ( + version_doc.id != root_doc.id + and version_doc.root_document_id != root_doc.id + ): + raise Http404 + return version_doc + @extend_schema( operation_id="documents_delete_version", parameters=[ @@ -1686,14 +1712,7 @@ class DocumentViewSet( url_path=r"versions/(?P\d+)", ) def delete_version(self, request, pk=None, version_id=None): - try: - root_doc = Document.objects.select_related( - "owner", - "root_document", - ).get(pk=pk) - root_doc = get_root_document(root_doc) - except Document.DoesNotExist: - raise Http404 + root_doc = self._get_root_doc_for_version_action(pk) if request.user is not None and not has_perms_owner_aware( request.user, @@ -1702,21 +1721,13 @@ class DocumentViewSet( ): return HttpResponseForbidden("Insufficient permissions") - try: - version_doc = Document.objects.select_related("owner").get( - pk=version_id, - ) - except Document.DoesNotExist: - raise Http404 + version_doc = self._get_version_doc_for_root(root_doc, version_id) if version_doc.id == root_doc.id: return HttpResponseBadRequest( "Cannot delete the root/original version. Delete the document instead.", ) - if version_doc.root_document_id != root_doc.id: - raise Http404 - from documents import index index.remove_document_from_index(version_doc) @@ -1752,6 +1763,78 @@ class DocumentViewSet( }, ) + @extend_schema( + operation_id="documents_update_version_label", + request=DocumentVersionLabelSerializer, + parameters=[ + OpenApiParameter( + name="version_id", + type=OpenApiTypes.INT, + location=OpenApiParameter.PATH, + ), + ], + responses=inline_serializer( + name="UpdateDocumentVersionLabelResult", + fields={ + "id": serializers.IntegerField(), + "added": serializers.DateTimeField(), + "version_label": serializers.CharField( + required=False, + allow_null=True, + ), + "checksum": serializers.CharField( + required=False, + allow_null=True, + ), + "is_root": serializers.BooleanField(), + }, + ), + ) + @delete_version.mapping.patch + def update_version_label(self, request, pk=None, version_id=None): + serializer = DocumentVersionLabelSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + root_doc = self._get_root_doc_for_version_action(pk) + if request.user is not None and not has_perms_owner_aware( + request.user, + "change_document", + root_doc, + ): + return HttpResponseForbidden("Insufficient permissions") + + version_doc = self._get_version_doc_for_root(root_doc, version_id) + old_label = version_doc.version_label + version_doc.version_label = serializer.validated_data["version_label"] + version_doc.save(update_fields=["version_label"]) + + if settings.AUDIT_LOG_ENABLED and old_label != version_doc.version_label: + actor = ( + request.user if request.user and request.user.is_authenticated else None + ) + LogEntry.objects.log_create( + instance=root_doc, + changes={ + "Version Label": [old_label, version_doc.version_label], + }, + action=LogEntry.Action.UPDATE, + actor=actor, + additional_data={ + "reason": "Version label updated", + "version_id": version_doc.id, + }, + ) + + return Response( + { + "id": version_doc.id, + "added": version_doc.added, + "version_label": version_doc.version_label, + "checksum": version_doc.checksum, + "is_root": version_doc.id == root_doc.id, + }, + ) + class ChatStreamingSerializer(serializers.Serializer): q = serializers.CharField(required=True)