Coverage for documents/templating/workflows.py: 44%
60 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1import logging
2from datetime import date
3from datetime import datetime
4from pathlib import Path
6from django.utils.text import slugify as django_slugify
7from jinja2 import StrictUndefined
8from jinja2 import Template
9from jinja2 import TemplateAssertionError
10from jinja2 import TemplateSyntaxError
11from jinja2 import UndefinedError
12from jinja2 import make_logging_undefined
13from jinja2.meta import find_undeclared_variables
14from jinja2.sandbox import SecurityError
16from documents.templating.environment import _template_environment
17from documents.templating.filters import format_datetime
18from documents.templating.filters import localize_date
20logger = logging.getLogger("paperless.templating")
22_LogStrictUndefined = make_logging_undefined(logger, StrictUndefined)
25_template_environment.undefined = _LogStrictUndefined
27_template_environment.filters["datetime"] = format_datetime
29_template_environment.filters["slugify"] = django_slugify
31_template_environment.filters["localize_date"] = localize_date
34_known_placeholder_names = {
35 "correspondent",
36 "document_type",
37 "added",
38 "added_year",
39 "added_year_short",
40 "added_month",
41 "added_month_name",
42 "added_month_name_short",
43 "added_day",
44 "added_time",
45 "owner_username",
46 "original_filename",
47 "filename",
48 "created",
49 "created_year",
50 "created_year_short",
51 "created_month",
52 "created_month_name",
53 "created_month_name_short",
54 "created_day",
55 "created_time",
56 "doc_title",
57 "doc_url",
58 "doc_id",
59}
62def validate_workflow_template(text: str) -> None:
63 try:
64 ast = _template_environment.parse(text)
65 undeclared_vars = find_undeclared_variables(ast)
66 except TemplateAssertionError as e:
67 raise ValueError(f"Template assertion error: {e}")
68 except TemplateSyntaxError as e:
69 raise ValueError(f"Template syntax error: {e}")
70 unknown_vars = undeclared_vars - _known_placeholder_names
71 if unknown_vars: 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 raise KeyError(
73 f"Template references unknown placeholders: {', '.join(unknown_vars)}",
74 )
77def parse_w_workflow_placeholders(
78 text: str,
79 correspondent_name: str,
80 doc_type_name: str,
81 owner_username: str,
82 local_added: datetime,
83 original_filename: str,
84 filename: str,
85 created: date | None = None,
86 doc_title: str | None = None,
87 doc_url: str | None = None,
88 doc_id: int | None = None,
89) -> str:
90 """
91 Available title placeholders for Workflows depend on what has already been assigned,
92 e.g. for pre-consumption triggers created will not have been parsed yet, but it will
93 for added / updated triggers
94 """
96 formatting = {
97 "correspondent": correspondent_name,
98 "document_type": doc_type_name,
99 "added": local_added.isoformat(),
100 "added_year": local_added.strftime("%Y"),
101 "added_year_short": local_added.strftime("%y"),
102 "added_month": local_added.strftime("%m"),
103 "added_month_name": local_added.strftime("%B"),
104 "added_month_name_short": local_added.strftime("%b"),
105 "added_day": local_added.strftime("%d"),
106 "added_time": local_added.strftime("%H:%M"),
107 "owner_username": owner_username,
108 "original_filename": Path(original_filename).stem,
109 "filename": Path(filename).stem,
110 }
111 if created is not None:
112 formatting.update(
113 {
114 "created": created.isoformat(),
115 "created_year": created.strftime("%Y"),
116 "created_year_short": created.strftime("%y"),
117 "created_month": created.strftime("%m"),
118 "created_month_name": created.strftime("%B"),
119 "created_month_name_short": created.strftime("%b"),
120 "created_day": created.strftime("%d"),
121 "created_time": created.strftime("%H:%M"),
122 },
123 )
124 if doc_title is not None:
125 formatting.update({"doc_title": doc_title})
126 if doc_url is not None:
127 formatting.update({"doc_url": doc_url})
128 if doc_id is not None:
129 formatting.update({"doc_id": str(doc_id)})
131 logger.debug(f"Parsing Workflow Jinja template: {text}")
132 try:
133 template = _template_environment.from_string(
134 text,
135 template_class=Template,
136 )
137 rendered_template = template.render(formatting)
139 # We're good!
140 return rendered_template
141 except UndefinedError as e:
142 # The undefined class logs this already for us
143 raise e
144 except TemplateSyntaxError as e:
145 logger.warning(f"Template syntax error in title generation: {e}")
146 except SecurityError as e:
147 logger.warning(f"Template attempted restricted operation: {e}")
148 except Exception as e:
149 logger.warning(f"Unknown error in title generation: {e}")
150 logger.warning(
151 f"Invalid title format '{text}', workflow not applied: {e}",
152 )
153 raise e
154 return None