From 7b92db189fb4a216ba4a559366f1605d4b04998f Mon Sep 17 00:00:00 2001 From: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Date: Mon, 11 Aug 2025 13:20:00 -0700 Subject: [PATCH] Adds testing coverage plus a check for the locale being valid --- docs/advanced_usage.md | 21 +-- src/documents/templating/filepath.py | 6 + src/documents/tests/test_file_handling.py | 163 ++++++++++++++++++++++ 3 files changed, 172 insertions(+), 18 deletions(-) diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index 43d6e479e..763488189 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -465,9 +465,6 @@ The `get_cf_value` filter retrieves a value from custom field data with optional {{ custom_fields | get_cf_value('phone', 'Not provided') }} - - -{{ custom_fields | get_cf_value('optional_field', 'N/A') }} ``` ##### Datetime Formatting @@ -510,7 +507,7 @@ for the possible codes and their meanings. ##### Date Localization -The `localize_date1 filter formats a date or datetime object into a localized string using Babel internationalization. +The `localize_date` filter formats a date or datetime object into a localized string using Babel internationalization. This takes into account the provided locale for translation. ###### Syntax @@ -558,7 +555,7 @@ This takes into account the provided locale for translation. ``` -See the [supported locale codes]() for more options, +See the [supported format codes](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns) for more options. ### Format Presets @@ -567,18 +564,6 @@ See the [supported locale codes]() for more options, - **long**: Long format with full month name (e.g., "January 15, 2024") - **full**: Full format including day of week (e.g., "Monday, January 15, 2024") -## Usage Notes - -- All filters handle `None` values gracefully -- Date strings are automatically parsed when needed -- Timezone-aware datetime objects are recommended for `localize_date` -- Custom field data should follow the expected dictionary structure for `get_cf_value` -- Invalid format strings will raise appropriate Python exceptions - -```` - - - #### Additional Variables - `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string @@ -606,7 +591,7 @@ somepath/ {% endif %} {% endif %} /{{ title }} -```` +``` For a document with an ASN of 205, it would result in `somepath/asn-201-400/asn-2xx/Title.pdf`, but a document with an ASN of 355 would be placed in `somepath/asn-201-400/asn-3xx/Title.pdf`. diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 040cc325f..861c11cdb 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -7,6 +7,7 @@ from datetime import datetime from pathlib import PurePath import pathvalidate +from babel import Locale from babel import dates from django.utils import timezone from django.utils.dateparse import parse_date @@ -116,6 +117,11 @@ def localize_date(value: date | datetime, format: str, locale: str) -> str: Raises: TypeError: If `value` is not a date or datetime instance. """ + try: + Locale.parse(locale) + except Exception as e: + raise ValueError(f"Invalid locale identifier: {locale}") from e + if isinstance(value, datetime): return dates.format_datetime(value, format=format, locale=locale) elif isinstance(value, date): diff --git a/src/documents/tests/test_file_handling.py b/src/documents/tests/test_file_handling.py index d879137b9..67240958f 100644 --- a/src/documents/tests/test_file_handling.py +++ b/src/documents/tests/test_file_handling.py @@ -4,6 +4,7 @@ import tempfile from pathlib import Path from unittest import mock +import pytest from auditlog.context import disable_auditlog from django.conf import settings from django.contrib.auth.models import User @@ -22,6 +23,7 @@ from documents.models import Document from documents.models import DocumentType from documents.models import StoragePath from documents.tasks import empty_trash +from documents.templating.filepath import localize_date from documents.tests.utils import DirectoriesMixin from documents.tests.utils import FileSystemAssertsMixin @@ -1586,3 +1588,164 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): generate_filename(doc), Path("brussels-belgium/some-title-with-special-characters.pdf"), ) + + +class TestDateLocalization: + """ + Groups all tests related to the `localize_date` function. + """ + + TEST_DATE = datetime.date(2023, 10, 26) + + TEST_DATETIME = datetime.datetime( + 2023, + 10, + 26, + 14, + 30, + 5, + tzinfo=datetime.timezone.utc, + ) + + @pytest.mark.parametrize( + "value, format_style, locale_str, expected_output", + [ + pytest.param( + TEST_DATE, + "EEEE, MMM d, yyyy", + "en_US", + "Thursday, Oct 26, 2023", + id="date-en_US-custom", + ), + pytest.param( + TEST_DATE, + "dd.MM.yyyy", + "de_DE", + "26.10.2023", + id="date-de_DE-custom", + ), + # German weekday and month name translation + pytest.param( + TEST_DATE, + "EEEE", + "de_DE", + "Donnerstag", + id="weekday-de_DE", + ), + pytest.param( + TEST_DATE, + "MMMM", + "de_DE", + "October", + id="month-de_DE", + ), + # French weekday and month name translation + pytest.param( + TEST_DATE, + "EEEE", + "fr_FR", + "jeudi", + id="weekday-fr_FR", + ), + pytest.param( + TEST_DATE, + "MMMM", + "fr_FR", + "octobre", + id="month-fr_FR", + ), + ], + ) + def test_localize_date_with_date_objects( + self, + value: datetime.date, + format_style: str, + locale_str: str, + expected_output: str, + ): + """ + Tests `localize_date` with `date` objects across different locales and formats. + """ + assert localize_date(value, format_style, locale_str) == expected_output + + @pytest.mark.parametrize( + "value, format_style, locale_str, expected_output", + [ + pytest.param( + TEST_DATETIME, + "yyyy.MM.dd G 'at' HH:mm:ss zzz", + "en_US", + "2023.10.26 AD at 14:30:05 UTC", + id="datetime-en_US-custom", + ), + pytest.param( + TEST_DATETIME, + "dd.MM.yyyy", + "fr_FR", + "26.10.2023", + id="date-fr_FR-custom", + ), + # Spanish weekday and month translation + pytest.param( + TEST_DATETIME, + "EEEE", + "es_ES", + "jueves", + id="weekday-es_ES", + ), + pytest.param( + TEST_DATETIME, + "MMMM", + "es_ES", + "octubre", + id="month-es_ES", + ), + # Italian weekday and month translation + pytest.param( + TEST_DATETIME, + "EEEE", + "it_IT", + "giovedì", + id="weekday-it_IT", + ), + pytest.param( + TEST_DATETIME, + "MMMM", + "it_IT", + "ottobre", + id="month-it_IT", + ), + ], + ) + def test_localize_date_with_datetime_objects( + self, + value: datetime.datetime, + format_style: str, + locale_str: str, + expected_output: str, + ): + # To handle the non-breaking space in French and other locales + result = localize_date(value, format_style, locale_str) + assert result.replace("\u202f", " ") == expected_output.replace("\u202f", " ") + + @pytest.mark.parametrize( + "invalid_value", + [ + "2023-10-26", + 1698330605, + None, + [], + {}, + ], + ) + def test_localize_date_raises_type_error_for_invalid_input(self, invalid_value): + with pytest.raises(TypeError) as excinfo: + localize_date(invalid_value, "medium", "en_US") + + assert f"Unsupported type {type(invalid_value)}" in str(excinfo.value) + + def test_localize_date_raises_error_for_invalid_locale(self): + with pytest.raises(ValueError) as excinfo: + localize_date(self.TEST_DATE, "medium", "invalid_locale_code") + + assert "Invalid locale identifier" in str(excinfo.value)