Compare commits

..

1 Commits

Author SHA1 Message Date
Trenton Holmes
58fc8a6011 Small targeted upgrades to bugfix releases mostly. Unsure why dependabot isn't doing these 2025-08-11 13:54:42 -07:00
7 changed files with 664 additions and 796 deletions

3
.codespellrc Normal file
View File

@@ -0,0 +1,3 @@
[codespell]
write-changes = True
ignore-words-list = criterias,afterall,valeu,ureue,equest,ure,assertIn

View File

@@ -31,6 +31,7 @@ repos:
rev: v2.4.1 rev: v2.4.1
hooks: hooks:
- id: codespell - id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)|(^src/documents/tests/samples/)"
exclude_types: exclude_types:
- pofile - pofile
- json - json

View File

@@ -434,136 +434,6 @@ provided. The template is provided as a string, potentially multiline, and rende
In addition, the entire Document instance is available to be utilized in a more advanced way, as well as some variables which only make sense to be accessed In addition, the entire Document instance is available to be utilized in a more advanced way, as well as some variables which only make sense to be accessed
with more complex logic. with more complex logic.
#### Custom Jinja2 Filters
##### Custom Field Access
The `get_cf_value` filter retrieves a value from custom field data with optional default fallback.
###### Syntax
```jinja2
{{ custom_fields | get_cf_value('field_name') }}
{{ custom_fields | get_cf_value('field_name', 'default_value') }}
```
###### Parameters
- `custom_fields`: This _must_ be the provided custom field data
- `name` (str): Name of the custom field to retrieve
- `default` (str, optional): Default value to return if field is not found or has no value
###### Returns
- `str | None`: The field value, default value, or `None` if neither exists
###### Examples
```jinja2
<!-- Basic usage -->
{{ custom_fields | get_cf_value('department') }}
<!-- With default value -->
{{ custom_fields | get_cf_value('phone', 'Not provided') }}
```
##### Datetime Formatting
The `format_datetime`filter formats a datetime string or datetime object using Python's strftime formatting.
###### Syntax
```jinja2
{{ datetime_value | format_datetime('%Y-%m-%d %H:%M:%S') }}
```
###### Parameters
- `value` (str | datetime): Date/time value to format (strings will be parsed automatically)
- `format` (str): Python strftime format string
###### Returns
- `str`: Formatted datetime string
###### Examples
```jinja2
<!-- Format datetime object -->
{{ created_at | format_datetime('%B %d, %Y at %I:%M %p') }}
<!-- Output: "January 15, 2024 at 02:30 PM" -->
<!-- Format datetime string -->
{{ "2024-01-15T14:30:00" | format_datetime('%m/%d/%Y') }}
<!-- Output: "01/15/2024" -->
<!-- Custom formatting -->
{{ timestamp | format_datetime('%A, %B %d, %Y') }}
<!-- Output: "Monday, January 15, 2024" -->
```
See the [strftime format code documentation](https://docs.python.org/3.13/library/datetime.html#strftime-and-strptime-format-codes)
for the possible codes and their meanings.
##### Date Localization
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
```jinja2
{{ date_value | localize_date('medium', 'en_US') }}
{{ datetime_value | localize_date('short', 'fr_FR') }}
```
###### Parameters
- `value` (date | datetime): Date or datetime object to format (datetime should be timezone-aware)
- `format` (str): Format type - either a Babel preset ('short', 'medium', 'long', 'full') or custom pattern
- `locale` (str): Locale code for localization (e.g., 'en_US', 'fr_FR', 'de_DE')
###### Returns
- `str`: Localized, formatted date string
###### Examples
```jinja2
<!-- Preset formats -->
{{ created_date | localize_date('short', 'en_US') }}
<!-- Output: "1/15/24" -->
{{ created_date | localize_date('medium', 'en_US') }}
<!-- Output: "Jan 15, 2024" -->
{{ created_date | localize_date('long', 'en_US') }}
<!-- Output: "January 15, 2024" -->
{{ created_date | localize_date('full', 'en_US') }}
<!-- Output: "Monday, January 15, 2024" -->
<!-- Different locales -->
{{ created_date | localize_date('medium', 'fr_FR') }}
<!-- Output: "15 janv. 2024" -->
{{ created_date | localize_date('medium', 'de_DE') }}
<!-- Output: "15.01.2024" -->
<!-- Custom patterns -->
{{ created_date | localize_date('dd/MM/yyyy', 'en_GB') }}
<!-- Output: "15/01/2024" -->
```
See the [supported format codes](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns) for more options.
### Format Presets
- **short**: Abbreviated format (e.g., "1/15/24")
- **medium**: Medium-length format (e.g., "Jan 15, 2024")
- **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")
#### Additional Variables #### 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 - `{{ 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

View File

@@ -15,7 +15,6 @@ classifiers = [
# This will allow testing to not install a webserver, mysql, etc # This will allow testing to not install a webserver, mysql, etc
dependencies = [ dependencies = [
"babel>=2.17",
"bleach~=6.2.0", "bleach~=6.2.0",
"celery[redis]~=5.5.1", "celery[redis]~=5.5.1",
"channels~=4.2", "channels~=4.2",
@@ -36,7 +35,7 @@ dependencies = [
"django-guardian~=3.0.3", "django-guardian~=3.0.3",
"django-multiselectfield~=1.0.1", "django-multiselectfield~=1.0.1",
"django-soft-delete~=1.0.18", "django-soft-delete~=1.0.18",
"djangorestframework~=3.15", "djangorestframework~=3.16",
"djangorestframework-guardian~=0.4.0", "djangorestframework-guardian~=0.4.0",
"drf-spectacular~=0.28", "drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2025.8.1", "drf-spectacular-sidecar~=2025.8.1",
@@ -222,11 +221,6 @@ lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
] ]
lint.isort.force-single-line = true lint.isort.force-single-line = true
[tool.codespell]
write-changes = true
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober"
skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json"
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "8.0" minversion = "8.0"
pythonpath = [ pythonpath = [

View File

@@ -2,13 +2,10 @@ import logging
import os import os
import re import re
from collections.abc import Iterable from collections.abc import Iterable
from datetime import date
from datetime import datetime from datetime import datetime
from pathlib import PurePath from pathlib import PurePath
import pathvalidate import pathvalidate
from babel import Locale
from babel import dates
from django.utils import timezone from django.utils import timezone
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from django.utils.text import slugify as django_slugify from django.utils.text import slugify as django_slugify
@@ -93,51 +90,19 @@ def get_cf_value(
return None return None
_template_environment.filters["get_cf_value"] = get_cf_value
def format_datetime(value: str | datetime, format: str) -> str: def format_datetime(value: str | datetime, format: str) -> str:
if isinstance(value, str): if isinstance(value, str):
value = parse_date(value) value = parse_date(value)
return value.strftime(format=format) return value.strftime(format=format)
def localize_date(value: date | datetime, format: str, locale: str) -> str:
"""
Format a date or datetime object into a localized string using Babel.
Args:
value (date | datetime): The date or datetime to format. If a datetime
is provided, it should be timezone-aware (e.g., UTC from a Django DB object).
format (str): The format to use. Can be one of Babel's preset formats
('short', 'medium', 'long', 'full') or a custom pattern string.
locale (str): The locale code (e.g., 'en_US', 'fr_FR') to use for
localization.
Returns:
str: The localized, formatted date string.
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):
return dates.format_date(value, format=format, locale=locale)
else:
raise TypeError(f"Unsupported type {type(value)} for localize_date")
_template_environment.filters["get_cf_value"] = get_cf_value
_template_environment.filters["datetime"] = format_datetime _template_environment.filters["datetime"] = format_datetime
_template_environment.filters["slugify"] = django_slugify _template_environment.filters["slugify"] = django_slugify
_template_environment.filters["localize_date"] = localize_date
def create_dummy_document(): def create_dummy_document():
""" """

View File

@@ -4,7 +4,6 @@ import tempfile
from pathlib import Path from pathlib import Path
from unittest import mock from unittest import mock
import pytest
from auditlog.context import disable_auditlog from auditlog.context import disable_auditlog
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
@@ -23,7 +22,6 @@ from documents.models import Document
from documents.models import DocumentType from documents.models import DocumentType
from documents.models import StoragePath from documents.models import StoragePath
from documents.tasks import empty_trash from documents.tasks import empty_trash
from documents.templating.filepath import localize_date
from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin from documents.tests.utils import FileSystemAssertsMixin
@@ -1588,164 +1586,3 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
generate_filename(doc), generate_filename(doc),
Path("brussels-belgium/some-title-with-special-characters.pdf"), 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",
"Oktober",
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)

1114
uv.lock generated

File diff suppressed because it is too large Load Diff