diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index b4dd367fb..bbd92b463 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -79,6 +79,23 @@ class PlaceholderString(str): NO_VALUE_PLACEHOLDER = PlaceholderString("-none-") +class MatchingModelContext: + """ + Safe template context for related objects. + + Keeps legacy behavior where including the object ina template yields the related object's + name as a string, while still exposing limited attributes. + """ + + def __init__(self, *, id: int, name: str, path: str | None = None): + self.id = id + self.name = name + self.path = path + + def __str__(self) -> str: + return self.name + + _template_environment.undefined = _LogStrictUndefined _template_environment.filters["get_cf_value"] = get_cf_value @@ -221,19 +238,26 @@ def get_safe_document_context( else None, "tags": [{"name": tag.name, "id": tag.id} for tag in tags], "correspondent": ( - {"name": document.correspondent.name, "id": document.correspondent.id} + MatchingModelContext( + name=document.correspondent.name, + id=document.correspondent.id, + ) if document.correspondent else None ), "document_type": ( - {"name": document.document_type.name, "id": document.document_type.id} + MatchingModelContext( + name=document.document_type.name, + id=document.document_type.id, + ) if document.document_type else None ), - "storage_path": { - "path": document.storage_path.path, - "id": document.storage_path.id, - } + "storage_path": MatchingModelContext( + name=document.storage_path.name, + path=document.storage_path.path, + id=document.storage_path.id, + ) if document.storage_path else None, } diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index 52f06cb41..9430bf669 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -1341,6 +1341,41 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): Path("somepath/asn-201-400/asn-3xx/Does Matter.pdf"), ) + def test_template_related_context_keeps_legacy_string_coercion(self): + """ + GIVEN: + - A storage path template that uses related objects directly as strings + WHEN: + - Filepath for a document with this format is called + THEN: + - Related objects coerce to their names (legacy behavior) + - Explicit attribute access remains available for new templates + """ + sp = StoragePath.objects.create( + name="PARTNER", + path=( + "{{ document.storage_path|lower }} / " + "{{ document.correspondent|lower|replace('mi:', 'mieter/') }} / " + "{{ document_type|lower }} / " + "{{ title|lower }}" + ), + ) + doc = Document.objects.create( + title="scan_017562", + created=datetime.date(2025, 7, 2), + added=timezone.make_aware(datetime.datetime(2026, 3, 3, 11, 53, 16)), + mime_type="application/pdf", + checksum="test-checksum", + storage_path=sp, + correspondent=Correspondent.objects.create(name="mi:kochkach"), + document_type=DocumentType.objects.create(name="Mietvertrag"), + ) + + self.assertEqual( + generate_filename(doc), + Path("partner/mieter/kochkach/mietvertrag/scan_017562.pdf"), + ) + @override_settings( FILENAME_FORMAT="{{creation_date}}/{{ title_name_str }}", )