From 82d8f48e9be9f5b4eaa858cb8ed4a3729509e2f3 Mon Sep 17 00:00:00 2001 From: Jan Kleine Date: Sun, 1 Mar 2026 21:16:32 +0100 Subject: [PATCH] Enhancement: add version label filename placeholder (#12185) * Enhancement: add version label filename placeholder * fix test * add workflow placeholder * docs and missing version_label * typo * fix consume placeholder * update docs * Apply suggestion from @shamoon * fix None value --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/advanced_usage.md | 1 + docs/usage.md | 3 ++- src/documents/consumer.py | 1 + src/documents/templating/filepath.py | 7 ++++++ src/documents/templating/workflows.py | 2 ++ src/documents/tests/test_consumer.py | 11 ++++++++-- src/documents/tests/test_file_handling.py | 26 +++++++++++++++++++++++ src/documents/tests/test_workflows.py | 8 +++++-- src/documents/workflows/actions.py | 6 ++++++ src/documents/workflows/mutations.py | 1 + 10 files changed, 61 insertions(+), 5 deletions(-) diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index a293eb91d..64cd6a091 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -332,6 +332,7 @@ Paperless provides the following variables for use within filenames: - `{{ owner_username }}`: Username of document owner, if any, or "none" - `{{ original_name }}`: Document original filename, minus the extension, if any, or "none" - `{{ doc_pk }}`: The paperless identifier (primary key) for the document. +- `{{ version_label }}`: The document version label or "none" if not explicitly set. !!! warning diff --git a/docs/usage.md b/docs/usage.md index 9d08d5f02..f3a4af661 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -618,6 +618,7 @@ applied. You can use the following placeholders in the template with any trigger - `{{original_filename}}`: original file name without extension - `{{filename}}`: current file name without extension - `{{doc_title}}`: current document title (cannot be used in title assignment) +- `{{version_label}}`: the document version label (empty if not explicitly set) The following placeholders are only available for "added" or "updated" triggers @@ -626,7 +627,7 @@ The following placeholders are only available for "added" or "updated" triggers - `{{created_year_short}}`: created year - `{{created_month}}`: created month - `{{created_month_name}}`: created month name -- `{created_month_name_short}}`: created month short name +- `{{created_month_name_short}}`: created month short name - `{{created_day}}`: created day - `{{created_time}}`: created time in HH:MM format - `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set. diff --git a/src/documents/consumer.py b/src/documents/consumer.py index d8682a6d7..570538cf6 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -723,6 +723,7 @@ class ConsumerPlugin( local_added, self.filename, self.filename, + version_label=self.metadata.version_label, ) def _store( diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 59fd8e3ed..00542b3ff 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -113,6 +113,7 @@ def create_dummy_document(): archive_filename="/dummy/archive_filename.pdf", original_filename="original_file.pdf", archive_serial_number=12345, + version_label="Version #1", ) return dummy_doc @@ -189,6 +190,12 @@ def get_basic_metadata_context( if document.original_filename else no_value_default, "doc_pk": f"{document.pk:07}", + "version_label": pathvalidate.sanitize_filename( + document.version_label, + replacement_text="-", + ) + if document.version_label + else no_value_default, } diff --git a/src/documents/templating/workflows.py b/src/documents/templating/workflows.py index 66fd97e01..2d7c3f5d7 100644 --- a/src/documents/templating/workflows.py +++ b/src/documents/templating/workflows.py @@ -41,6 +41,7 @@ def parse_w_workflow_placeholders( doc_title: str | None = None, doc_url: str | None = None, doc_id: int | None = None, + version_label: str | None = None, ) -> str: """ Available title placeholders for Workflows depend on what has already been assigned, @@ -62,6 +63,7 @@ def parse_w_workflow_placeholders( "owner_username": owner_username, "original_filename": Path(original_filename).stem, "filename": Path(filename).stem, + "version_label": version_label or "", } if created is not None: formatting.update( diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index 551719807..c2a81c2e9 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -428,7 +428,11 @@ class TestConsumer( DocumentMetadataOverrides( correspondent_id=c.pk, document_type_id=dt.pk, - title="{{correspondent}}{{document_type}} {{added_month}}-{{added_year_short}}", + title=( + "{{correspondent}}{{document_type}} " + "{{added_month}}-{{added_year_short}}.{{version_label}}" + ), + version_label="v2", ), ) as consumer: consumer.run() @@ -436,7 +440,10 @@ class TestConsumer( document = Document.objects.first() now = timezone.now() - self.assertEqual(document.title, f"{c.name}{dt.name} {now.strftime('%m-%y')}") + self.assertEqual( + document.title, + f"{c.name}{dt.name} {now.strftime('%m-%y')}.v2", + ) self._assert_first_last_send_progress() def testOverrideOwner(self) -> None: diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index 1e35b96bc..6f23e9e98 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -281,6 +281,32 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase): self.assertEqual(generate_filename(d1), Path("652 - the_doc.pdf")) self.assertEqual(generate_filename(d2), Path("none - the_doc.pdf")) + @override_settings(FILENAME_FORMAT="{title}.{version_label}") + def test_version_label(self) -> None: + d1 = Document.objects.create( + title="the_doc", + mime_type="application/pdf", + checksum="A", + version_label="Version #2", + ) + d2 = Document.objects.create( + title="the_doc", + mime_type="application/pdf", + checksum="B", + ) + d3 = Document.objects.create( + title="the_doc", + mime_type="application/pdf", + checksum="C", + version_label="Super weird %@\"'<> ¯\\_(ツ)_/¯", + ) + self.assertEqual(generate_filename(d1), Path("the_doc.Version #2.pdf")) + self.assertEqual(generate_filename(d2), Path("the_doc.none.pdf")) + self.assertEqual( + generate_filename(d3), + Path("the_doc.Super weird %@-'-- ¯-_(ツ)_-¯.pdf"), + ) + @override_settings(FILENAME_FORMAT="{title} {tag_list}") def test_tag_list(self) -> None: doc = Document.objects.create(title="doc1", mime_type="application/pdf") diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 55bad6b2c..5aea28b22 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -3412,7 +3412,10 @@ class TestWorkflows( ) webhook_action = WorkflowActionWebhook.objects.create( use_params=False, - body="Test message: {{doc_url}} with id {{doc_id}}", + body=( + "Test message: {{doc_url}} with id {{doc_id}} " + "and version {{version_label}}" + ), url="http://paperless-ngx.com", include_document=False, ) @@ -3436,6 +3439,7 @@ class TestWorkflows( title="sample test", correspondent=self.c, original_filename="sample.pdf", + version_label="v3", ) run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc) @@ -3444,7 +3448,7 @@ class TestWorkflows( url="http://paperless-ngx.com", data=( f"Test message: http://localhost:8000/paperless/documents/{doc.id}/" - f" with id {doc.id}" + f" with id {doc.id} and version {doc.version_label}" ), headers={}, files=None, diff --git a/src/documents/workflows/actions.py b/src/documents/workflows/actions.py index 46d9f5c4a..4f04bc38a 100644 --- a/src/documents/workflows/actions.py +++ b/src/documents/workflows/actions.py @@ -49,6 +49,7 @@ def build_workflow_action_context( "added": timezone.localtime(document.added), "created": document.created, "id": document.pk, + "version_label": document.version_label, } correspondent_obj = ( @@ -81,6 +82,7 @@ def build_workflow_action_context( "added": timezone.localtime(timezone.now()), "created": overrides.created if overrides else None, "id": "", + "version_label": overrides.version_label if overrides else None, } @@ -116,6 +118,7 @@ def execute_email_action( context["title"], context["doc_url"], context["id"], + context["version_label"], ) if action.email.subject else "" @@ -133,6 +136,7 @@ def execute_email_action( context["title"], context["doc_url"], context["id"], + context["version_label"], ) if action.email.body else "" @@ -212,6 +216,7 @@ def execute_webhook_action( context["title"], context["doc_url"], context["id"], + context["version_label"], ) except Exception as e: logger.error( @@ -231,6 +236,7 @@ def execute_webhook_action( context["title"], context["doc_url"], context["id"], + context["version_label"], ) headers = {} if action.webhook.headers: diff --git a/src/documents/workflows/mutations.py b/src/documents/workflows/mutations.py index b93a26781..dd4045219 100644 --- a/src/documents/workflows/mutations.py +++ b/src/documents/workflows/mutations.py @@ -58,6 +58,7 @@ def apply_assignment_to_document( "", # dont pass the title to avoid recursion "", # no urls in titles document.pk, + document.version_label, ) except Exception: # pragma: no cover logger.exception(