diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index aa52d2f59..43d6e479e 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -434,6 +434,151 @@ 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 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 + +{{ custom_fields | get_cf_value('department') }} + + +{{ custom_fields | get_cf_value('phone', 'Not provided') }} + + +{{ custom_fields | get_cf_value('optional_field', 'N/A') }} +``` + +##### 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 + +{{ created_at | format_datetime('%B %d, %Y at %I:%M %p') }} + + + +{{ "2024-01-15T14:30:00" | format_datetime('%m/%d/%Y') }} + + + +{{ timestamp | format_datetime('%A, %B %d, %Y') }} + +``` + +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_date1 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 + +{{ created_date | localize_date('short', 'en_US') }} + + +{{ created_date | localize_date('medium', 'en_US') }} + + +{{ created_date | localize_date('long', 'en_US') }} + + +{{ created_date | localize_date('full', 'en_US') }} + + + +{{ created_date | localize_date('medium', 'fr_FR') }} + + +{{ created_date | localize_date('medium', 'de_DE') }} + + + +{{ created_date | localize_date('dd/MM/yyyy', 'en_GB') }} + +``` + +See the [supported locale codes]() 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") + +## 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 @@ -461,7 +606,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/pyproject.toml b/pyproject.toml index 3f3846cf8..595ff8c42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ classifiers = [ # This will allow testing to not install a webserver, mysql, etc dependencies = [ + "babel>=2.17", "bleach~=6.2.0", "celery[redis]~=5.5.1", "channels~=4.2", diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 633a85cc8..040cc325f 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -2,10 +2,12 @@ import logging import os import re from collections.abc import Iterable +from datetime import date from datetime import datetime from pathlib import PurePath import pathvalidate +from babel import dates from django.utils import timezone from django.utils.dateparse import parse_date from django.utils.text import slugify as django_slugify @@ -90,19 +92,46 @@ def get_cf_value( return None -_template_environment.filters["get_cf_value"] = get_cf_value - - def format_datetime(value: str | datetime, format: str) -> str: if isinstance(value, str): value = parse_date(value) 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. + """ + 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["slugify"] = django_slugify +_template_environment.filters["localize_date"] = localize_date + def create_dummy_document(): """ diff --git a/uv.lock b/uv.lock index 055fc32d5..39ae561e0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "sys_platform == 'darwin'", @@ -1911,6 +1911,7 @@ name = "paperless-ngx" version = "2.17.1" source = { virtual = "." } dependencies = [ + { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "celery", extra = ["redis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "channels", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -2044,6 +2045,7 @@ typing = [ [package.metadata] requires-dist = [ + { name = "babel", specifier = ">=2.17.0" }, { name = "bleach", specifier = "~=6.2.0" }, { name = "celery", extras = ["redis"], specifier = "~=5.5.1" }, { name = "channels", specifier = "~=4.2" },