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>
This commit is contained in:
Jan Kleine 2026-03-01 21:16:32 +01:00 committed by GitHub
parent a700928dd5
commit 82d8f48e9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 61 additions and 5 deletions

View File

@ -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

View File

@ -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.

View File

@ -723,6 +723,7 @@ class ConsumerPlugin(
local_added,
self.filename,
self.filename,
version_label=self.metadata.version_label,
)
def _store(

View File

@ -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,
}

View File

@ -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(

View File

@ -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:

View File

@ -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")

View File

@ -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,

View File

@ -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:

View File

@ -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(