From 224a873de24e0f9a836a5fd8e94b8dfac532d6a9 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:16:20 -0800 Subject: [PATCH] Version label --- .../src/app/services/rest/document.service.ts | 5 ++- src/documents/consumer.py | 5 +++ src/documents/data_models.py | 3 ++ .../migrations/0012_document_version_label.py | 24 +++++++++++ src/documents/models.py | 8 ++++ src/documents/serialisers.py | 41 ++++++++++++++++--- src/documents/views.py | 9 +++- 7 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 src/documents/migrations/0012_document_version_label.py diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 511a4017d..5ed78d771 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -191,9 +191,12 @@ export class DocumentService extends AbstractPaperlessService { return url } - uploadVersion(documentId: number, file: File) { + uploadVersion(documentId: number, file: File, label?: string) { const formData = new FormData() formData.append('document', file, file.name) + if (label) { + formData.append('label', label) + } return this.http.post( this.getResourceUrl(documentId, 'update_version'), formData, diff --git a/src/documents/consumer.py b/src/documents/consumer.py index 551c331cc..d0b45a871 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -511,6 +511,8 @@ class ConsumerPlugin( original_document.filename = None original_document.archive_filename = None original_document.archive_checksum = None + if self.metadata.version_label is not None: + original_document.version_label = self.metadata.version_label original_document.modified = timezone.now() original_document.save() document = original_document @@ -738,6 +740,9 @@ class ConsumerPlugin( if self.metadata.asn is not None: document.archive_serial_number = self.metadata.asn + if self.metadata.version_label is not None: + document.version_label = self.metadata.version_label + if self.metadata.owner_id: document.owner = User.objects.get( pk=self.metadata.owner_id, diff --git a/src/documents/data_models.py b/src/documents/data_models.py index 8958fc525..89bb9cfa8 100644 --- a/src/documents/data_models.py +++ b/src/documents/data_models.py @@ -31,6 +31,7 @@ class DocumentMetadataOverrides: change_groups: list[int] | None = None custom_fields: dict | None = None skip_asn_if_exists: bool = False + version_label: str | None = None def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides": """ @@ -52,6 +53,8 @@ class DocumentMetadataOverrides: self.owner_id = other.owner_id if other.skip_asn_if_exists: self.skip_asn_if_exists = True + if other.version_label is not None: + self.version_label = other.version_label # merge if self.tag_ids is None: diff --git a/src/documents/migrations/0012_document_version_label.py b/src/documents/migrations/0012_document_version_label.py new file mode 100644 index 000000000..99ee5c94f --- /dev/null +++ b/src/documents/migrations/0012_document_version_label.py @@ -0,0 +1,24 @@ +# Generated by Codex on 2026-02-10 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "0011_document_head_version"), + ] + + operations = [ + migrations.AddField( + model_name="document", + name="version_label", + field=models.CharField( + blank=True, + help_text="Optional short label for a document version.", + max_length=64, + null=True, + verbose_name="version label", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index c3b748656..e4d9fe097 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -317,6 +317,14 @@ class Document(SoftDeleteModel, ModelWithOwner): verbose_name=_("head version of document"), ) + version_label = models.CharField( + _("version label"), + max_length=64, + blank=True, + null=True, + help_text=_("Optional short label for a document version."), + ) + class Meta: ordering = ("-created",) verbose_name = _("document") diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 4f4df3165..ad1a90125 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1062,6 +1062,12 @@ class DuplicateDocumentSummarySerializer(serializers.Serializer): deleted_at = serializers.DateTimeField(allow_null=True) +class DocumentVersionInfoSerializer(serializers.Serializer): + id = serializers.IntegerField() + added = serializers.DateTimeField() + label = serializers.CharField(required=False, allow_null=True) + + @extend_schema_serializer( deprecate_fields=["created_date"], ) @@ -1083,7 +1089,7 @@ class DocumentSerializer( notes = NotesSerializer(many=True, required=False, read_only=True) head_version = serializers.PrimaryKeyRelatedField(read_only=True) - versions = serializers.PrimaryKeyRelatedField(many=True, read_only=True) + versions = SerializerMethodField() custom_fields = CustomFieldInstanceSerializer( many=True, @@ -1117,6 +1123,28 @@ class DocumentSerializer( duplicates = _get_viewable_duplicates(obj, user) return list(duplicates.values("id", "title", "deleted_at")) + @extend_schema_field(DocumentVersionInfoSerializer(many=True)) + def get_versions(self, obj): + head_doc = obj if obj.head_version_id is None else obj.head_version + versions_qs = Document.objects.filter(head_version=head_doc).only( + "id", + "added", + "checksum", + "version_label", + ) + versions = [*versions_qs, head_doc] + + def build_info(doc: Document) -> dict[str, object]: + return { + "id": doc.id, + "added": doc.added, + "label": doc.version_label, + } + + info = [build_info(doc) for doc in versions] + info.sort(key=lambda item: item["id"], reverse=True) + return info + def get_original_file_name(self, obj) -> str | None: return obj.original_filename @@ -1136,10 +1164,6 @@ class DocumentSerializer( request.version if request else settings.REST_FRAMEWORK["DEFAULT_VERSION"], ) - if doc.get("versions") is not None: - doc["versions"] = sorted(doc["versions"], reverse=True) - doc["versions"].append(doc["id"]) - if api_version < 9 and "created" in self.fields: # provide created as a datetime for backwards compatibility from django.utils import timezone @@ -2010,6 +2034,13 @@ class DocumentVersionSerializer(serializers.Serializer): label="Document", write_only=True, ) + label = serializers.CharField( + label="Version label", + required=False, + allow_blank=True, + allow_null=True, + max_length=64, + ) validate_document = PostDocumentSerializer().validate_document diff --git a/src/documents/views.py b/src/documents/views.py index 74524b1d4..e2f0ba4da 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1471,7 +1471,7 @@ class DocumentViewSet( "Error emailing documents, check logs for more detail.", ) - @action(methods=["post"], detail=True) + @action(methods=["post"], detail=True, parser_classes=[parsers.MultiPartParser]) def update_version(self, request, pk=None): serializer = DocumentVersionSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -1489,6 +1489,7 @@ class DocumentViewSet( try: doc_name, doc_data = serializer.validated_data.get("document") + label = serializer.validated_data.get("label") t = int(mktime(datetime.now().timetuple())) @@ -1508,9 +1509,13 @@ class DocumentViewSet( head_version_id=doc.pk, ) + overrides = DocumentMetadataOverrides() + if label: + overrides.version_label = label.strip() + async_task = consume_file.delay( input_doc, - None, + overrides, ) logger.debug( f"Updated document {doc.id} with new version",