diff --git a/src/documents/checks.py b/src/documents/checks.py index 69027bf21..41f836882 100644 --- a/src/documents/checks.py +++ b/src/documents/checks.py @@ -8,6 +8,7 @@ from django.db.utils import OperationalError from django.db.utils import ProgrammingError from documents.signals import document_consumer_declaration +from documents.templatetags import convert_to_django_template_format @register() @@ -69,3 +70,17 @@ def parser_check(app_configs, **kwargs): ] else: return [] + + +@register() +def filename_format_check(app_configs, **kwargs): + if settings.FILENAME_FORMAT: + converted_format = convert_to_django_template_format(settings.FILENAME_FORMAT) + if converted_format != settings.FILENAME_FORMAT: + return [ + Error( + f"Filename format {settings.FILENAME_FORMAT} is using the old style, please update to use double curly brackets", + hint=converted_format, + ), + ] + return [] diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index 86df569bf..779e1377e 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -17,6 +17,7 @@ from documents.models import Document from documents.models import DocumentType from documents.models import StoragePath from documents.models import Tag +from documents.templatetags import convert_to_django_template_format logger = logging.getLogger("paperless.filehandling") @@ -112,29 +113,6 @@ def generate_unique_filename(doc, archive_filename=False): return new_filename -def convert_to_django_template_format(old_format: str) -> str: - """ - Converts old Python string format (with {}) to Django template style (with {{ }}), - while ignoring existing {{ ... }} placeholders. - - :param old_format: The old style format string (e.g., "{title} by {author}") - :return: Converted string in Django Template style (e.g., "{{ title }} by {{ author }}") - """ - - # Step 1: Match placeholders with single curly braces but not those with double braces - pattern = r"(? dict[str, str | list[str]]: def get_custom_fields_context( custom_fields: Iterable[CustomFieldInstance], -) -> dict[str, dict[str, str]]: +) -> dict[str, dict[str, dict[str, str]]]: """ Given an Iterable of CustomFieldInstance, builds a dictionary mapping the field name to its type and value """ return { - pathvalidate.sanitize_filename( - field_instance.field.name, - replacement_text="-", - ): { - "type": pathvalidate.sanitize_filename( - field_instance.field.data_type, + "custom_fields": { + pathvalidate.sanitize_filename( + field_instance.field.name, replacement_text="-", - ), - "value": pathvalidate.sanitize_filename( - str(field_instance.value), - replacement_text="-", - ), - } - for field_instance in custom_fields + ): { + "type": pathvalidate.sanitize_filename( + field_instance.field.data_type, + replacement_text="-", + ), + "value": pathvalidate.sanitize_filename( + str(field_instance.value), + replacement_text="-", + ), + } + for field_instance in custom_fields + }, } @@ -401,14 +381,6 @@ def generate_filename( filename_format = convert_to_django_template_format( settings.FILENAME_FORMAT, ) - - # Warn the user they should update - # TODO: Move this to system check - if filename_format != settings.FILENAME_FORMAT: - logger.warning( - f"Filename format {settings.FILENAME_FORMAT} is using the old style, please update to use double curly brackets", - ) - logger.info(filename_format) else: filename_format = None diff --git a/src/documents/migrations/1054_alter_storage_path_templates.py b/src/documents/migrations/1054_alter_storage_path_templates.py index 073fb57b0..796e00b87 100644 --- a/src/documents/migrations/1054_alter_storage_path_templates.py +++ b/src/documents/migrations/1054_alter_storage_path_templates.py @@ -10,7 +10,7 @@ def convert_from_format_to_template(apps, schema_editor): StoragePath = apps.get_model("documents", "StoragePath") - from documents.file_handling import convert_to_django_template_format + from documents.templatetags import convert_to_django_template_format with transaction.atomic(): for storage_path in StoragePath.objects.all(): diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index e516719ef..fe7c7500e 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -29,8 +29,6 @@ from rest_framework import fields from rest_framework import serializers from rest_framework.fields import SerializerMethodField -from documents.file_handling import convert_to_django_template_format - if settings.AUDIT_LOG_ENABLED: from auditlog.context import set_actor @@ -56,6 +54,7 @@ from documents.models import WorkflowTrigger from documents.parsers import is_mime_type_supported from documents.permissions import get_groups_with_only_permission from documents.permissions import set_permissions_for_object +from documents.templatetags import convert_to_django_template_format from documents.validators import uri_validator logger = logging.getLogger("paperless.serializers") diff --git a/src/documents/templatetags.py b/src/documents/templatetags.py index 9a2b4b969..ff9b903d4 100644 --- a/src/documents/templatetags.py +++ b/src/documents/templatetags.py @@ -1,3 +1,4 @@ +import os import re from django import template @@ -26,7 +27,10 @@ class FilePathNode(template.Node): """ value = value.replace("\n", "").replace("\r", "") value = re.sub(r"\s*/\s*", "/", value) - return value.strip() + + # We remove trailing and leading separators, as these are always relative paths, not absolute, even if the user + # tries + return value.strip().strip(os.sep) output = self.nodelist.render(context) return clean_filepath(output) @@ -41,3 +45,26 @@ def construct_filepath(parser, token): nodelist = parser.parse(("endfilepath",)) parser.delete_first_token() return FilePathNode(nodelist) + + +def convert_to_django_template_format(old_format: str) -> str: + """ + Converts old Python string format (with {}) to Django template style (with {{ }}), + while ignoring existing {{ ... }} placeholders. + + :param old_format: The old style format string (e.g., "{title} by {author}") + :return: Converted string in Django Template style (e.g., "{{ title }} by {{ author }}") + """ + + # Step 1: Match placeholders with single curly braces but not those with double braces + pattern = r"(?