From ceee769e2603568f32113b6943a4dd045f913f8c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:46:54 -0800 Subject: [PATCH] Feature: document file versions (#12061) --- .mypy-baseline.txt | 17 +- docs/api.md | 19 +- docs/usage.md | 10 + .../document-detail.component.html | 12 +- .../document-detail.component.spec.ts | 341 +++++++- .../document-detail.component.ts | 227 ++++- .../document-version-dropdown.component.html | 134 +++ .../document-version-dropdown.component.scss | 17 + ...ocument-version-dropdown.component.spec.ts | 324 +++++++ .../document-version-dropdown.component.ts | 281 ++++++ src-ui/src/app/data/document.ts | 12 + .../services/rest/document.service.spec.ts | 100 +++ .../src/app/services/rest/document.service.ts | 113 ++- .../app/services/websocket-status.service.ts | 7 + src-ui/src/main.ts | 6 + src/documents/bulk_edit.py | 318 +++++-- src/documents/conditionals.py | 54 +- src/documents/consumer.py | 125 ++- src/documents/data_models.py | 7 + src/documents/filters.py | 35 +- src/documents/index.py | 15 +- .../migrations/0013_document_root_document.py | 37 + src/documents/models.py | 34 +- src/documents/serialisers.py | 102 ++- src/documents/signals/handlers.py | 6 + src/documents/tasks.py | 25 +- .../tests/test_api_document_versions.py | 811 ++++++++++++++++++ src/documents/tests/test_api_documents.py | 62 ++ src/documents/tests/test_bulk_edit.py | 219 +++-- src/documents/tests/test_consumer.py | 166 +++- src/documents/tests/test_document_model.py | 22 + src/documents/tests/test_task_signals.py | 38 + .../tests/test_version_conditionals.py | 91 ++ src/documents/versioning.py | 124 +++ src/documents/views.py | 522 ++++++++++- 35 files changed, 4068 insertions(+), 365 deletions(-) create mode 100644 src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html create mode 100644 src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.scss create mode 100644 src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.spec.ts create mode 100644 src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.ts create mode 100644 src/documents/migrations/0013_document_root_document.py create mode 100644 src/documents/tests/test_api_document_versions.py create mode 100644 src/documents/tests/test_version_conditionals.py create mode 100644 src/documents/versioning.py diff --git a/.mypy-baseline.txt b/.mypy-baseline.txt index 12e169efd..da962cfc2 100644 --- a/.mypy-baseline.txt +++ b/.mypy-baseline.txt @@ -96,9 +96,7 @@ src/documents/conditionals.py:0: error: Function is missing a type annotation fo src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] -src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "input_doc" [attr-defined] -src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "log" [attr-defined] src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "metadata" [attr-defined] src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "metadata" [attr-defined] src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "metadata" [attr-defined] @@ -173,7 +171,6 @@ src/documents/filters.py:0: error: Function is missing a type annotation [no-un src/documents/filters.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/filters.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/filters.py:0: error: Function is missing a type annotation [no-untyped-def] -src/documents/filters.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/filters.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/filters.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/filters.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] @@ -345,18 +342,11 @@ src/documents/migrations/0001_initial.py:0: error: Skipping analyzing "multisele src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/models.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type] -src/documents/models.py:0: error: Could not resolve manager type for "documents.models.Document.deleted_objects" [django-manager-missing] -src/documents/models.py:0: error: Could not resolve manager type for "documents.models.Document.global_objects" [django-manager-missing] -src/documents/models.py:0: error: Could not resolve manager type for "documents.models.Document.objects" [django-manager-missing] -src/documents/models.py:0: error: Couldn't resolve related manager 'custom_fields' for relation 'documents.models.CustomFieldInstance.document'. [django-manager-missing] src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.correspondent'. [django-manager-missing] src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.document_type'. [django-manager-missing] src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.storage_path'. [django-manager-missing] src/documents/models.py:0: error: Couldn't resolve related manager 'fields' for relation 'documents.models.CustomFieldInstance.field'. [django-manager-missing] -src/documents/models.py:0: error: Couldn't resolve related manager 'notes' for relation 'documents.models.Note.document'. [django-manager-missing] src/documents/models.py:0: error: Couldn't resolve related manager 'runs' for relation 'documents.models.WorkflowRun.workflow'. [django-manager-missing] -src/documents/models.py:0: error: Couldn't resolve related manager 'share_links' for relation 'documents.models.ShareLink.document'. [django-manager-missing] -src/documents/models.py:0: error: Couldn't resolve related manager 'workflow_runs' for relation 'documents.models.WorkflowRun.document'. [django-manager-missing] src/documents/models.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/models.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/models.py:0: error: Function is missing a return type annotation [no-untyped-def] @@ -559,6 +549,7 @@ src/documents/serialisers.py:0: error: Function is missing a type annotation [n src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def] +src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] @@ -975,10 +966,6 @@ src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annot src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation [no-untyped-def] -src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation [no-untyped-def] -src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] -src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] -src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/tests/test_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] @@ -1004,7 +991,6 @@ src/documents/tests/test_bulk_edit.py:0: error: Item "dict[Any, Any]" of "Group src/documents/tests/test_bulk_edit.py:0: error: Item "dict[Any, Any]" of "Group | dict[Any, Any]" has no attribute "count" [union-attr] src/documents/tests/test_bulk_edit.py:0: error: Too few arguments for "count" of "list" [call-arg] src/documents/tests/test_bulk_edit.py:0: error: Too few arguments for "count" of "list" [call-arg] -src/documents/tests/test_bulk_edit.py:0: error: Unsupported operand types for - ("None" and "int") [operator] src/documents/tests/test_caching.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] src/documents/tests/test_classifier.py:0: error: "None" has no attribute "classes_" [attr-defined] src/documents/tests/test_classifier.py:0: error: "None" has no attribute "classes_" [attr-defined] @@ -1566,6 +1552,7 @@ src/documents/views.py:0: error: Function is missing a return type annotation [ src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def] +src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def] src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def] diff --git a/docs/api.md b/docs/api.md index 7db6bbc7e..da802b819 100644 --- a/docs/api.md +++ b/docs/api.md @@ -211,6 +211,21 @@ However, querying the tasks endpoint with the returned UUID e.g. `/api/tasks/?task_id={uuid}` will provide information on the state of the consumption including the ID of a created document if consumption succeeded. +## Document Versions + +Document versions are file-level versions linked to one root document. + +- Root document metadata (title, tags, correspondent, document type, storage path, custom fields, permissions) remains shared. +- Version-specific file data (file, mime type, checksums, archive info, extracted text content) belongs to the selected/latest version. + +Version-aware endpoints: + +- `GET /api/documents/{id}/`: returns root document data; `content` resolves to latest version content by default. Use `?version={version_id}` to resolve content for a specific version. +- `PATCH /api/documents/{id}/`: content updates target the selected version (`?version={version_id}`) or latest version by default; non-content metadata updates target the root document. +- `GET /api/documents/{id}/download/`, `GET /api/documents/{id}/preview/`, `GET /api/documents/{id}/thumb/`, `GET /api/documents/{id}/metadata/`: accept `?version={version_id}`. +- `POST /api/documents/{id}/update_version/`: uploads a new version using multipart form field `document` and optional `version_label`. +- `DELETE /api/documents/{root_id}/versions/{version_id}/`: deletes a non-root version. + ## Permissions All objects (documents, tags, etc.) allow setting object-level permissions @@ -300,13 +315,13 @@ The following methods are supported: - `"doc": OUTPUT_DOCUMENT_INDEX` Optional index of the output document for split operations. - Optional `parameters`: - `"delete_original": true` to delete the original documents after editing. - - `"update_document": true` to update the existing document with the edited PDF. + - `"update_document": true` to add the edited PDF as a new version of the root document. - `"include_metadata": true` to copy metadata from the original document to the edited document. - `remove_password` - Requires `parameters`: - `"password": "PASSWORD_STRING"` The password to remove from the PDF documents. - Optional `parameters`: - - `"update_document": true` to replace the existing document with the password-less PDF. + - `"update_document": true` to add the password-less PDF as a new version of the root document. - `"delete_original": true` to delete the original document after editing. - `"include_metadata": true` to copy metadata from the original document to the new password-less document. - `merge` diff --git a/docs/usage.md b/docs/usage.md index e0d445aca..9d08d5f02 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -89,6 +89,16 @@ You can view the document, edit its metadata, assign tags, correspondents, document types, and custom fields. You can also view the document history, download the document or share it via a share link. +### Document File Versions + +Think of versions as **file history** for a document. + +- Versions track the underlying file and extracted text content (OCR/text). +- Metadata such as tags, correspondent, document type, storage path and custom fields stay on the "root" document. +- By default, search and document content use the latest version. +- In document detail, selecting a version switches the preview, file metadata and content (and download etc buttons) to that version. +- Deleting a non-root version keeps metadata and falls back to the latest remaining version. + ### Management Lists Paperless-ngx includes management lists for tags, correspondents, document types diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index fe68fea4b..7c15d513a 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -1,7 +1,7 @@ @if (archiveContentRenderType === ContentRenderType.PDF && !useNativePdfViewer) { @if (previewNumPages) { -
+
Page
of {{previewNumPages}}
@@ -24,6 +24,16 @@ Delete + +