From 1bc77546eb0b1a70b786de00f41251f3cf4e0b31 Mon Sep 17 00:00:00 2001 From: Harold Waterkeyn Date: Mon, 3 Mar 2025 17:20:04 +0100 Subject: [PATCH] Feature: Add slugify filter in templating (#9269) --- docs/advanced_usage.md | 6 +++ src/documents/templating/filepath.py | 3 ++ src/documents/tests/test_file_handling.py | 60 +++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index f7b31c919..87f397954 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -509,6 +509,12 @@ Invoice_{{ custom_fields|get_cf_value("Select Field") }}_{{ custom_fields|get_cf This will create a path like `invoices/2022/01/01/Invoice_OptionTwo_20220101.pdf` if the custom field "Date Field" is set to January 1, 2022 and "Select Field" is set to `OptionTwo`. +You can also use a custom `slugify` filter to slufigy text: + +```jinja +{{ title | slugify }} +``` + ## Automatic recovery of invalid PDFs {#pdf-recovery} Paperless will attempt to "clean" certain invalid PDFs with `qpdf` before processing if, for example, the mime_type diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index cbe621d77..45e1cad9e 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -8,6 +8,7 @@ from pathlib import PurePath import pathvalidate from django.utils import timezone from django.utils.dateparse import parse_date +from django.utils.text import slugify as django_slugify from jinja2 import StrictUndefined from jinja2 import Template from jinja2 import TemplateSyntaxError @@ -100,6 +101,8 @@ def format_datetime(value: str | datetime, format: str) -> str: _template_environment.filters["datetime"] = format_datetime +_template_environment.filters["slugify"] = django_slugify + def create_dummy_document(): """ diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index 2ec388501..6d2d396fc 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -1517,3 +1517,63 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): generate_filename(doc_a), "2024-10-01/Some Title.pdf", ) + + def test_slugify_filter(self): + """ + GIVEN: + - Filename format with slugify filter + WHEN: + - Filepath for a document with this format is called + THEN: + - The slugify filter properly converts strings to URL-friendly slugs + """ + doc = Document.objects.create( + title="Some Title! With @ Special # Characters", + created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), + added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), + mime_type="application/pdf", + pk=2, + checksum="2", + archive_serial_number=25, + ) + + with override_settings( + FILENAME_FORMAT="{{ title | slugify }}", + ): + self.assertEqual( + generate_filename(doc), + "some-title-with-special-characters.pdf", + ) + + # Test with correspondent name containing spaces and special chars + doc.correspondent = Correspondent.objects.create( + name="John's @ Office / Workplace", + ) + doc.save() + + with override_settings( + FILENAME_FORMAT="{{ correspondent | slugify }}/{{ title | slugify }}", + ): + self.assertEqual( + generate_filename(doc), + "johns-office-workplace/some-title-with-special-characters.pdf", + ) + + # Test with custom fields + cf = CustomField.objects.create( + name="Location", + data_type=CustomField.FieldDataType.STRING, + ) + CustomFieldInstance.objects.create( + document=doc, + field=cf, + value_text="Brussels @ Belgium!", + ) + + with override_settings( + FILENAME_FORMAT="{{ custom_fields | get_cf_value('Location') | slugify }}/{{ title | slugify }}", + ): + self.assertEqual( + generate_filename(doc), + "brussels-belgium/some-title-with-special-characters.pdf", + )