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

1import logging 

2from datetime import date 

3from datetime import datetime 

4from pathlib import Path 

5 

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 

15 

16from documents.templating.environment import _template_environment 

17from documents.templating.filters import format_datetime 

18from documents.templating.filters import localize_date 

19 

20logger = logging.getLogger("paperless.templating") 

21 

22_LogStrictUndefined = make_logging_undefined(logger, StrictUndefined) 

23 

24 

25_template_environment.undefined = _LogStrictUndefined 

26 

27_template_environment.filters["datetime"] = format_datetime 

28 

29_template_environment.filters["slugify"] = django_slugify 

30 

31_template_environment.filters["localize_date"] = localize_date 

32 

33 

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} 

60 

61 

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 ) 

75 

76 

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 """ 

95 

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)}) 

130 

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) 

138 

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