mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-12 21:35:40 -05:00
Enhancement: jinja template support for workflow title assignment (#10700)
--------- Co-authored-by: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -506,6 +506,7 @@ for the possible codes and their meanings.
|
||||
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. Since this must be used on a date or datetime object,
|
||||
you must access the field directly, i.e. `document.created`.
|
||||
An ISO string can also be provided to control the output format.
|
||||
|
||||
###### Syntax
|
||||
|
||||
@@ -516,7 +517,7 @@ you must access the field directly, i.e. `document.created`.
|
||||
|
||||
###### Parameters
|
||||
|
||||
- `value` (date | datetime): Date or datetime object to format (datetime should be timezone-aware)
|
||||
- `value` (date | datetime | str): Date, datetime object or ISO string 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')
|
||||
|
||||
|
@@ -505,35 +505,52 @@ you may want to adjust these settings to prevent abuse.
|
||||
|
||||
#### Workflow placeholders
|
||||
|
||||
Some workflow text can include placeholders but the available options differ depending on the type of
|
||||
workflow trigger. This is because at the time of consumption (when the text is to be set), no automatic tags etc. have been
|
||||
applied. You can use the following placeholders with any trigger type:
|
||||
Titles can be assigned by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
||||
This allows for complex logic to be used to generate the title, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
|
||||
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11).
|
||||
The template is provided as a string.
|
||||
|
||||
- `{correspondent}`: assigned correspondent name
|
||||
- `{document_type}`: assigned document type name
|
||||
- `{owner_username}`: assigned owner username
|
||||
- `{added}`: added datetime
|
||||
- `{added_year}`: added year
|
||||
- `{added_year_short}`: added year
|
||||
- `{added_month}`: added month
|
||||
- `{added_month_name}`: added month name
|
||||
- `{added_month_name_short}`: added month short name
|
||||
- `{added_day}`: added day
|
||||
- `{added_time}`: added time in HH:MM format
|
||||
- `{original_filename}`: original file name without extension
|
||||
- `{filename}`: current file name without extension
|
||||
Using Jinja2 Templates is also useful for [Date localization](advanced_usage.md#Date-Localization) in the title.
|
||||
|
||||
The available inputs differ depending on the type of workflow trigger.
|
||||
This is because at the time of consumption (when the text is to be set), no automatic tags etc. have been
|
||||
applied. You can use the following placeholders in the template with any trigger type:
|
||||
|
||||
- `{{correspondent}}`: assigned correspondent name
|
||||
- `{{document_type}}`: assigned document type name
|
||||
- `{{owner_username}}`: assigned owner username
|
||||
- `{{added}}`: added datetime
|
||||
- `{{added_year}}`: added year
|
||||
- `{{added_year_short}}`: added year
|
||||
- `{{added_month}}`: added month
|
||||
- `{{added_month_name}}`: added month name
|
||||
- `{{added_month_name_short}}`: added month short name
|
||||
- `{{added_day}}`: added day
|
||||
- `{{added_time}}`: added time in HH:MM format
|
||||
- `{{original_filename}}`: original file name without extension
|
||||
- `{{filename}}`: current file name without extension
|
||||
|
||||
The following placeholders are only available for "added" or "updated" triggers
|
||||
|
||||
- `{created}`: created datetime
|
||||
- `{created_year}`: created year
|
||||
- `{created_year_short}`: created year
|
||||
- `{created_month}`: created month
|
||||
- `{created_month_name}`: created month name
|
||||
- `{created_month_name_short}`: created month short name
|
||||
- `{created_day}`: created day
|
||||
- `{created_time}`: created time in HH:MM format
|
||||
- `{doc_url}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
|
||||
- `{{created}}`: created datetime
|
||||
- `{{created_year}}`: created year
|
||||
- `{{created_year_short}}`: created year
|
||||
- `{{created_month}}`: created month
|
||||
- `{{created_month_name}}`: created month name
|
||||
- `{created_month_name_short}}`: created month short name
|
||||
- `{{created_day}}`: created day
|
||||
- `{{created_time}}`: created time in HH:MM format
|
||||
- `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
|
||||
|
||||
##### Examples
|
||||
|
||||
```jinja2
|
||||
{{ created | localize_date('MMMM', 'en_US') }}
|
||||
<!-- Output: "January" -->
|
||||
|
||||
{{ added | localize_date('MMMM', 'de_DE') }}
|
||||
<!-- Output: "Juni" --> # codespell:ignore
|
||||
```
|
||||
|
||||
### Workflow permissions
|
||||
|
||||
|
@@ -0,0 +1,51 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-27 22:02
|
||||
import logging
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
from django.db import transaction
|
||||
|
||||
from documents.templating.utils import convert_format_str_to_template_format
|
||||
|
||||
logger = logging.getLogger("paperless.migrations")
|
||||
|
||||
|
||||
def convert_from_format_to_template(apps, schema_editor):
|
||||
WorkflowActions = apps.get_model("documents", "WorkflowAction")
|
||||
|
||||
with transaction.atomic():
|
||||
for WorkflowAction in WorkflowActions.objects.all():
|
||||
WorkflowAction.assign_title = convert_format_str_to_template_format(
|
||||
WorkflowAction.assign_title,
|
||||
)
|
||||
logger.debug(
|
||||
"Converted WorkflowAction id %d title to template format: %s",
|
||||
WorkflowAction.id,
|
||||
WorkflowAction.assign_title,
|
||||
)
|
||||
WorkflowAction.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1068_alter_document_created"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="WorkflowAction",
|
||||
name="assign_title",
|
||||
field=models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=(
|
||||
"Assign a document title, can be a JINJA2 template, "
|
||||
"see documentation.",
|
||||
),
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
convert_from_format_to_template,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
]
|
@@ -1207,14 +1207,12 @@ class WorkflowAction(models.Model):
|
||||
default=WorkflowActionType.ASSIGNMENT,
|
||||
)
|
||||
|
||||
assign_title = models.CharField(
|
||||
assign_title = models.TextField(
|
||||
_("assign title"),
|
||||
max_length=256,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Assign a document title, can include some placeholders, "
|
||||
"see documentation.",
|
||||
"Assign a document title, must be a Jinja2 template, see documentation.",
|
||||
),
|
||||
)
|
||||
|
||||
|
27
src/documents/templating/environment.py
Normal file
27
src/documents/templating/environment.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
|
||||
class JinjaEnvironment(SandboxedEnvironment):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.undefined_tracker = None
|
||||
|
||||
def is_safe_callable(self, obj):
|
||||
# Block access to .save() and .delete() methods
|
||||
if callable(obj) and getattr(obj, "__name__", None) in (
|
||||
"save",
|
||||
"delete",
|
||||
"update",
|
||||
):
|
||||
return False
|
||||
# Call the parent method for other cases
|
||||
return super().is_safe_callable(obj)
|
||||
|
||||
|
||||
_template_environment = JinjaEnvironment(
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
keep_trailing_newline=False,
|
||||
autoescape=False,
|
||||
extensions=["jinja2.ext.loopcontrols"],
|
||||
)
|
@@ -2,22 +2,16 @@ 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 Locale
|
||||
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
|
||||
from jinja2 import StrictUndefined
|
||||
from jinja2 import Template
|
||||
from jinja2 import TemplateSyntaxError
|
||||
from jinja2 import UndefinedError
|
||||
from jinja2 import make_logging_undefined
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from jinja2.sandbox import SecurityError
|
||||
|
||||
from documents.models import Correspondent
|
||||
@@ -27,39 +21,16 @@ from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.templating.environment import _template_environment
|
||||
from documents.templating.filters import format_datetime
|
||||
from documents.templating.filters import get_cf_value
|
||||
from documents.templating.filters import localize_date
|
||||
|
||||
logger = logging.getLogger("paperless.templating")
|
||||
|
||||
_LogStrictUndefined = make_logging_undefined(logger, StrictUndefined)
|
||||
|
||||
|
||||
class FilePathEnvironment(SandboxedEnvironment):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.undefined_tracker = None
|
||||
|
||||
def is_safe_callable(self, obj):
|
||||
# Block access to .save() and .delete() methods
|
||||
if callable(obj) and getattr(obj, "__name__", None) in (
|
||||
"save",
|
||||
"delete",
|
||||
"update",
|
||||
):
|
||||
return False
|
||||
# Call the parent method for other cases
|
||||
return super().is_safe_callable(obj)
|
||||
|
||||
|
||||
_template_environment = FilePathEnvironment(
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
keep_trailing_newline=False,
|
||||
autoescape=False,
|
||||
extensions=["jinja2.ext.loopcontrols"],
|
||||
undefined=_LogStrictUndefined,
|
||||
)
|
||||
|
||||
|
||||
class FilePathTemplate(Template):
|
||||
def render(self, *args, **kwargs) -> str:
|
||||
def clean_filepath(value: str) -> str:
|
||||
@@ -81,54 +52,7 @@ class FilePathTemplate(Template):
|
||||
return clean_filepath(original_render)
|
||||
|
||||
|
||||
def get_cf_value(
|
||||
custom_field_data: dict[str, dict[str, str]],
|
||||
name: str,
|
||||
default: str | None = None,
|
||||
) -> str | None:
|
||||
if name in custom_field_data and custom_field_data[name]["value"] is not None:
|
||||
return custom_field_data[name]["value"]
|
||||
elif default is not None:
|
||||
return default
|
||||
return None
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
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.undefined = _LogStrictUndefined
|
||||
|
||||
_template_environment.filters["get_cf_value"] = get_cf_value
|
||||
|
||||
|
60
src/documents/templating/filters.py
Normal file
60
src/documents/templating/filters.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
|
||||
from babel import Locale
|
||||
from babel import dates
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
|
||||
def localize_date(value: date | datetime | str, format: str, locale: str) -> str:
|
||||
"""
|
||||
Format a date, datetime or str object into a localized string using Babel.
|
||||
|
||||
Args:
|
||||
value (date | datetime | str): The date or datetime to format. If a datetime
|
||||
is provided, it should be timezone-aware (e.g., UTC from a Django DB object).
|
||||
if str is provided is is parsed as date.
|
||||
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, datetime or str instance.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = parse_datetime(value)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def format_datetime(value: str | datetime, format: str) -> str:
|
||||
if isinstance(value, str):
|
||||
value = parse_date(value)
|
||||
return value.strftime(format=format)
|
||||
|
||||
|
||||
def get_cf_value(
|
||||
custom_field_data: dict[str, dict[str, str]],
|
||||
name: str,
|
||||
default: str | None = None,
|
||||
) -> str | None:
|
||||
if name in custom_field_data and custom_field_data[name]["value"] is not None:
|
||||
return custom_field_data[name]["value"]
|
||||
elif default is not None:
|
||||
return default
|
||||
return None
|
@@ -1,7 +1,33 @@
|
||||
import logging
|
||||
from datetime import date
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils.text import slugify as django_slugify
|
||||
from jinja2 import StrictUndefined
|
||||
from jinja2 import Template
|
||||
from jinja2 import TemplateSyntaxError
|
||||
from jinja2 import UndefinedError
|
||||
from jinja2 import make_logging_undefined
|
||||
from jinja2.sandbox import SecurityError
|
||||
|
||||
from documents.templating.environment import _template_environment
|
||||
from documents.templating.filters import format_datetime
|
||||
from documents.templating.filters import localize_date
|
||||
|
||||
logger = logging.getLogger("paperless.templating")
|
||||
|
||||
_LogStrictUndefined = make_logging_undefined(logger, StrictUndefined)
|
||||
|
||||
|
||||
_template_environment.undefined = _LogStrictUndefined
|
||||
|
||||
_template_environment.filters["datetime"] = format_datetime
|
||||
|
||||
_template_environment.filters["slugify"] = django_slugify
|
||||
|
||||
_template_environment.filters["localize_date"] = localize_date
|
||||
|
||||
|
||||
def parse_w_workflow_placeholders(
|
||||
text: str,
|
||||
@@ -20,6 +46,7 @@ def parse_w_workflow_placeholders(
|
||||
e.g. for pre-consumption triggers created will not have been parsed yet, but it will
|
||||
for added / updated triggers
|
||||
"""
|
||||
|
||||
formatting = {
|
||||
"correspondent": correspondent_name,
|
||||
"document_type": doc_type_name,
|
||||
@@ -52,4 +79,28 @@ def parse_w_workflow_placeholders(
|
||||
formatting.update({"doc_title": doc_title})
|
||||
if doc_url is not None:
|
||||
formatting.update({"doc_url": doc_url})
|
||||
return text.format(**formatting).strip()
|
||||
|
||||
logger.debug(f"Jinja Template is : {text}")
|
||||
try:
|
||||
template = _template_environment.from_string(
|
||||
text,
|
||||
template_class=Template,
|
||||
)
|
||||
rendered_template = template.render(formatting)
|
||||
|
||||
# We're good!
|
||||
return rendered_template
|
||||
except UndefinedError as e:
|
||||
# The undefined class logs this already for us
|
||||
raise e
|
||||
except TemplateSyntaxError as e:
|
||||
logger.warning(f"Template syntax error in title generation: {e}")
|
||||
except SecurityError as e:
|
||||
logger.warning(f"Template attempted restricted operation: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Unknown error in title generation: {e}")
|
||||
logger.warning(
|
||||
f"Invalid title format '{text}', workflow not applied: {e}",
|
||||
)
|
||||
raise e
|
||||
return None
|
||||
|
@@ -304,22 +304,6 @@ class TestConsumer(
|
||||
self.assertEqual(document.title, "Override Title")
|
||||
self._assert_first_last_send_progress()
|
||||
|
||||
def testOverrideTitleInvalidPlaceholders(self):
|
||||
with self.assertLogs("paperless.consumer", level="ERROR") as cm:
|
||||
with self.get_consumer(
|
||||
self.get_test_file(),
|
||||
DocumentMetadataOverrides(title="Override {correspondent]"),
|
||||
) as consumer:
|
||||
consumer.run()
|
||||
|
||||
document = Document.objects.first()
|
||||
|
||||
self.assertIsNotNone(document)
|
||||
|
||||
self.assertEqual(document.title, "sample")
|
||||
expected_str = "Error occurred parsing title override 'Override {correspondent]', falling back to original"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
||||
def testOverrideCorrespondent(self):
|
||||
c = Correspondent.objects.create(name="test")
|
||||
|
||||
@@ -437,7 +421,7 @@ class TestConsumer(
|
||||
DocumentMetadataOverrides(
|
||||
correspondent_id=c.pk,
|
||||
document_type_id=dt.pk,
|
||||
title="{correspondent}{document_type} {added_month}-{added_year_short}",
|
||||
title="{{correspondent}}{{document_type}} {{added_month}}-{{added_year_short}}",
|
||||
),
|
||||
) as consumer:
|
||||
consumer.run()
|
||||
|
@@ -23,7 +23,6 @@ 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.factories import DocumentFactory
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from documents.tests.utils import FileSystemAssertsMixin
|
||||
@@ -1591,166 +1590,13 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestDateLocalization:
|
||||
class TestPathDateLocalization:
|
||||
"""
|
||||
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)
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"filename_format,expected_filename",
|
||||
|
296
src/documents/tests/test_filters.py
Normal file
296
src/documents/tests/test_filters.py
Normal file
@@ -0,0 +1,296 @@
|
||||
import datetime
|
||||
from typing import Any
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from documents.templating.filters import localize_date
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
TEST_DATETIME_STRING: str = "2023-10-26T14:30:05+00:00"
|
||||
|
||||
TEST_DATE_STRING: str = "2023-10-26"
|
||||
|
||||
@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",
|
||||
[
|
||||
1698330605,
|
||||
None,
|
||||
[],
|
||||
{},
|
||||
],
|
||||
)
|
||||
def test_localize_date_raises_type_error_for_invalid_input(
|
||||
self,
|
||||
invalid_value: None | list[object] | dict[Any, Any] | Literal[1698330605],
|
||||
):
|
||||
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)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, format_style, locale_str, expected_output",
|
||||
[
|
||||
pytest.param(
|
||||
TEST_DATETIME_STRING,
|
||||
"EEEE, MMM d, yyyy",
|
||||
"en_US",
|
||||
"Thursday, Oct 26, 2023",
|
||||
id="date-en_US-custom",
|
||||
),
|
||||
pytest.param(
|
||||
TEST_DATETIME_STRING,
|
||||
"dd.MM.yyyy",
|
||||
"de_DE",
|
||||
"26.10.2023",
|
||||
id="date-de_DE-custom",
|
||||
),
|
||||
# German weekday and month name translation
|
||||
pytest.param(
|
||||
TEST_DATETIME_STRING,
|
||||
"EEEE",
|
||||
"de_DE",
|
||||
"Donnerstag",
|
||||
id="weekday-de_DE",
|
||||
),
|
||||
pytest.param(
|
||||
TEST_DATETIME_STRING,
|
||||
"MMMM",
|
||||
"de_DE",
|
||||
"Oktober",
|
||||
id="month-de_DE",
|
||||
),
|
||||
# French weekday and month name translation
|
||||
pytest.param(
|
||||
TEST_DATETIME_STRING,
|
||||
"EEEE",
|
||||
"fr_FR",
|
||||
"jeudi",
|
||||
id="weekday-fr_FR",
|
||||
),
|
||||
pytest.param(
|
||||
TEST_DATETIME_STRING,
|
||||
"MMMM",
|
||||
"fr_FR",
|
||||
"octobre",
|
||||
id="month-fr_FR",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_localize_date_with_datetime_string(
|
||||
self,
|
||||
value: str,
|
||||
format_style: str,
|
||||
locale_str: str,
|
||||
expected_output: str,
|
||||
):
|
||||
"""
|
||||
Tests `localize_date` with `date` string 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_DATE_STRING,
|
||||
"EEEE, MMM d, yyyy",
|
||||
"en_US",
|
||||
"Thursday, Oct 26, 2023",
|
||||
id="date-en_US-custom",
|
||||
),
|
||||
pytest.param(
|
||||
TEST_DATE_STRING,
|
||||
"dd.MM.yyyy",
|
||||
"de_DE",
|
||||
"26.10.2023",
|
||||
id="date-de_DE-custom",
|
||||
),
|
||||
# German weekday and month name translation
|
||||
pytest.param(
|
||||
TEST_DATE_STRING,
|
||||
"EEEE",
|
||||
"de_DE",
|
||||
"Donnerstag",
|
||||
id="weekday-de_DE",
|
||||
),
|
||||
pytest.param(
|
||||
TEST_DATE_STRING,
|
||||
"MMMM",
|
||||
"de_DE",
|
||||
"Oktober",
|
||||
id="month-de_DE",
|
||||
),
|
||||
# French weekday and month name translation
|
||||
pytest.param(
|
||||
TEST_DATE_STRING,
|
||||
"EEEE",
|
||||
"fr_FR",
|
||||
"jeudi",
|
||||
id="weekday-fr_FR",
|
||||
),
|
||||
pytest.param(
|
||||
TEST_DATE_STRING,
|
||||
"MMMM",
|
||||
"fr_FR",
|
||||
"octobre",
|
||||
id="month-fr_FR",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_localize_date_with_date_string(
|
||||
self,
|
||||
value: str,
|
||||
format_style: str,
|
||||
locale_str: str,
|
||||
expected_output: str,
|
||||
):
|
||||
"""
|
||||
Tests `localize_date` with `date` string across different locales and formats.
|
||||
"""
|
||||
assert localize_date(value, format_style, locale_str) == expected_output
|
@@ -1,6 +1,8 @@
|
||||
import datetime
|
||||
import shutil
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
@@ -15,6 +17,7 @@ from guardian.shortcuts import get_users_with_perms
|
||||
from httpx import HTTPError
|
||||
from httpx import HTTPStatusError
|
||||
from pytest_httpx import HTTPXMock
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.signals.handlers import run_workflows
|
||||
@@ -22,7 +25,7 @@ from documents.signals.handlers import send_webhook
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from pytest_django.fixtures import SettingsWrapper
|
||||
|
||||
from documents import tasks
|
||||
from documents.data_models import ConsumableDocument
|
||||
@@ -122,7 +125,7 @@ class TestWorkflows(
|
||||
filter_path=f"*/{self.dirs.scratch_dir.parts[-1]}/*",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title="Doc from {correspondent}",
|
||||
assign_title="Doc from {{correspondent}}",
|
||||
assign_correspondent=self.c,
|
||||
assign_document_type=self.dt,
|
||||
assign_storage_path=self.sp,
|
||||
@@ -241,7 +244,7 @@ class TestWorkflows(
|
||||
)
|
||||
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title="Doc from {correspondent}",
|
||||
assign_title="Doc from {{correspondent}}",
|
||||
assign_correspondent=self.c,
|
||||
assign_document_type=self.dt,
|
||||
assign_storage_path=self.sp,
|
||||
@@ -892,7 +895,7 @@ class TestWorkflows(
|
||||
filter_filename="*sample*",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title="Doc created in {created_year}",
|
||||
assign_title="Doc created in {{created_year}}",
|
||||
assign_correspondent=self.c2,
|
||||
assign_document_type=self.dt,
|
||||
assign_storage_path=self.sp,
|
||||
@@ -1155,7 +1158,7 @@ class TestWorkflows(
|
||||
WHEN:
|
||||
- File that matches is added
|
||||
THEN:
|
||||
- Title is not updated, error is output
|
||||
- Title is updated but the placeholder isn't replaced
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
@@ -1181,15 +1184,12 @@ class TestWorkflows(
|
||||
created=created,
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.handlers", level="ERROR") as cm:
|
||||
document_consumption_finished.send(
|
||||
sender=self.__class__,
|
||||
document=doc,
|
||||
)
|
||||
expected_str = f"Error occurred parsing title assignment '{action.assign_title}', falling back to original"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
||||
self.assertEqual(doc.title, "sample test")
|
||||
self.assertEqual(doc.title, "Doc {created_year]")
|
||||
|
||||
def test_document_updated_workflow(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
@@ -1223,6 +1223,45 @@ class TestWorkflows(
|
||||
|
||||
self.assertEqual(doc.custom_fields.all().count(), 1)
|
||||
|
||||
def test_document_consumption_workflow_month_placeholder_addded(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
sources=f"{DocumentSource.ApiUpload}",
|
||||
filter_filename="simple*",
|
||||
)
|
||||
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title="Doc added in {{added_month_name_short}}",
|
||||
)
|
||||
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
superuser = User.objects.create_superuser("superuser")
|
||||
self.client.force_authenticate(user=superuser)
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
self.dirs.scratch_dir / "simple.pdf",
|
||||
)
|
||||
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||
tasks.consume_file(
|
||||
ConsumableDocument(
|
||||
source=DocumentSource.ApiUpload,
|
||||
original_file=test_file,
|
||||
),
|
||||
None,
|
||||
)
|
||||
document = Document.objects.first()
|
||||
self.assertRegex(
|
||||
document.title,
|
||||
r"Doc added in \w{3,}",
|
||||
) # Match any 3-letter month name
|
||||
|
||||
def test_document_updated_workflow_existing_custom_field(self):
|
||||
"""
|
||||
GIVEN:
|
||||
@@ -2035,7 +2074,7 @@ class TestWorkflows(
|
||||
filter_filename="*simple*",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title="Doc from {correspondent}",
|
||||
assign_title="Doc from {{correspondent}}",
|
||||
assign_correspondent=self.c,
|
||||
assign_document_type=self.dt,
|
||||
assign_storage_path=self.sp,
|
||||
@@ -2614,7 +2653,7 @@ class TestWorkflows(
|
||||
)
|
||||
webhook_action = WorkflowActionWebhook.objects.create(
|
||||
use_params=False,
|
||||
body="Test message: {doc_url}",
|
||||
body="Test message: {{doc_url}}",
|
||||
url="http://paperless-ngx.com",
|
||||
include_document=False,
|
||||
)
|
||||
@@ -2673,7 +2712,7 @@ class TestWorkflows(
|
||||
)
|
||||
webhook_action = WorkflowActionWebhook.objects.create(
|
||||
use_params=False,
|
||||
body="Test message: {doc_url}",
|
||||
body="Test message: {{doc_url}}",
|
||||
url="http://paperless-ngx.com",
|
||||
include_document=True,
|
||||
)
|
||||
@@ -3130,3 +3169,234 @@ class TestWebhookSecurity:
|
||||
req = httpx_mock.get_request()
|
||||
assert req.headers["Host"] == "paperless-ngx.com"
|
||||
assert "evil.test" not in req.headers.get("Host", "")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestDateWorkflowLocalization(
|
||||
SampleDirMixin,
|
||||
):
|
||||
"""Test cases for workflows that use date localization in templates."""
|
||||
|
||||
TEST_DATETIME = datetime.datetime(
|
||||
2023,
|
||||
6,
|
||||
26,
|
||||
14,
|
||||
30,
|
||||
5,
|
||||
tzinfo=datetime.timezone.utc,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title_template,expected_title",
|
||||
[
|
||||
pytest.param(
|
||||
"Created at {{ created | localize_date('MMMM', 'es_ES') }}",
|
||||
"Created at junio",
|
||||
id="spanish_month",
|
||||
),
|
||||
pytest.param(
|
||||
"Created at {{ created | localize_date('MMMM', 'de_DE') }}",
|
||||
"Created at Juni", # codespell:ignore
|
||||
id="german_month",
|
||||
),
|
||||
pytest.param(
|
||||
"Created at {{ created | localize_date('dd/MM/yyyy', 'en_GB') }}",
|
||||
"Created at 26/06/2023",
|
||||
id="british_date_format",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_document_added_workflow_localization(
|
||||
self,
|
||||
title_template: str,
|
||||
expected_title: str,
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document added workflow with title template using localize_date filter
|
||||
WHEN:
|
||||
- Document is consumed
|
||||
THEN:
|
||||
- Document title is set with localized date
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
filter_filename="*sample*",
|
||||
)
|
||||
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title=title_template,
|
||||
)
|
||||
|
||||
workflow = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
workflow.triggers.add(trigger)
|
||||
workflow.actions.add(action)
|
||||
workflow.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=None,
|
||||
original_filename="sample.pdf",
|
||||
created=self.TEST_DATETIME,
|
||||
)
|
||||
|
||||
document_consumption_finished.send(
|
||||
sender=self.__class__,
|
||||
document=doc,
|
||||
)
|
||||
|
||||
doc.refresh_from_db()
|
||||
assert doc.title == expected_title
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title_template,expected_title",
|
||||
[
|
||||
pytest.param(
|
||||
"Created at {{ created | localize_date('MMMM', 'es_ES') }}",
|
||||
"Created at junio",
|
||||
id="spanish_month",
|
||||
),
|
||||
pytest.param(
|
||||
"Created at {{ created | localize_date('MMMM', 'de_DE') }}",
|
||||
"Created at Juni", # codespell:ignore
|
||||
id="german_month",
|
||||
),
|
||||
pytest.param(
|
||||
"Created at {{ created | localize_date('dd/MM/yyyy', 'en_GB') }}",
|
||||
"Created at 26/06/2023",
|
||||
id="british_date_format",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_document_updated_workflow_localization(
|
||||
self,
|
||||
title_template: str,
|
||||
expected_title: str,
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with title template using localize_date filter
|
||||
WHEN:
|
||||
- Document is updated via API
|
||||
THEN:
|
||||
- Document title is set with localized date
|
||||
"""
|
||||
# Setup test data
|
||||
dt = DocumentType.objects.create(name="DocType Name")
|
||||
c = Correspondent.objects.create(name="Correspondent Name")
|
||||
|
||||
client = APIClient()
|
||||
superuser = User.objects.create_superuser("superuser")
|
||||
client.force_authenticate(user=superuser)
|
||||
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
filter_has_document_type=dt,
|
||||
)
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=c,
|
||||
original_filename="sample.pdf",
|
||||
created=self.TEST_DATETIME,
|
||||
)
|
||||
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title=title_template,
|
||||
)
|
||||
|
||||
workflow = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
workflow.triggers.add(trigger)
|
||||
workflow.actions.add(action)
|
||||
workflow.save()
|
||||
|
||||
client.patch(
|
||||
f"/api/documents/{doc.id}/",
|
||||
{"document_type": dt.id},
|
||||
format="json",
|
||||
)
|
||||
|
||||
doc.refresh_from_db()
|
||||
assert doc.title == expected_title
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title_template,expected_title",
|
||||
[
|
||||
pytest.param(
|
||||
"Added at {{ added | localize_date('MMMM', 'es_ES') }}",
|
||||
"Added at junio",
|
||||
id="spanish_month",
|
||||
),
|
||||
pytest.param(
|
||||
"Added at {{ added | localize_date('MMMM', 'de_DE') }}",
|
||||
"Added at Juni", # codespell:ignore
|
||||
id="german_month",
|
||||
),
|
||||
pytest.param(
|
||||
"Added at {{ added | localize_date('dd/MM/yyyy', 'en_GB') }}",
|
||||
"Added at 26/06/2023",
|
||||
id="british_date_format",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_document_consumption_workflow_localization(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
settings: SettingsWrapper,
|
||||
title_template: str,
|
||||
expected_title: str,
|
||||
):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
sources=f"{DocumentSource.ApiUpload}",
|
||||
filter_filename="simple*",
|
||||
)
|
||||
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
tmp_path / "simple.pdf",
|
||||
)
|
||||
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title=title_template,
|
||||
)
|
||||
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
settings.SCRATCH_DIR = tmp_path / "scratch"
|
||||
(tmp_path / "scratch").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Temporarily override "now" for the environment so templates using
|
||||
# added/created placeholders behave as if it's a different system date.
|
||||
with (
|
||||
mock.patch(
|
||||
"documents.tasks.ProgressManager",
|
||||
DummyProgressManager,
|
||||
),
|
||||
mock.patch(
|
||||
"django.utils.timezone.now",
|
||||
return_value=self.TEST_DATETIME,
|
||||
),
|
||||
):
|
||||
tasks.consume_file(
|
||||
ConsumableDocument(
|
||||
source=DocumentSource.ApiUpload,
|
||||
original_file=test_file,
|
||||
),
|
||||
None,
|
||||
)
|
||||
document = Document.objects.first()
|
||||
assert document.title == expected_title
|
||||
|
Reference in New Issue
Block a user