Coverage for documents/workflows/mutations.py: 5%
142 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
3from django.utils import timezone
4from guardian.shortcuts import remove_perm
6from documents.data_models import DocumentMetadataOverrides
7from documents.models import CustomFieldInstance
8from documents.models import Document
9from documents.models import WorkflowAction
10from documents.permissions import set_permissions_for_object
11from documents.templating.workflows import parse_w_workflow_placeholders
13logger = logging.getLogger("paperless.workflows.mutations")
16def apply_assignment_to_document(
17 action: WorkflowAction,
18 document: Document,
19 logging_group,
20):
21 """
22 Apply assignment actions to a Document instance.
24 action: WorkflowAction, annotated with 'has_assign_*' boolean fields
25 """
26 if action.has_assign_tags:
27 # Apply to a freshly-fetched instance rather than the shared `document`.
28 # Document.tags.add() fires an m2m_changed signal that ultimately calls
29 # instance.refresh_from_db(), which would discard any other unsaved
30 # assignment fields (e.g. storage_path) already staged on `document`.
31 Document.objects.get(pk=document.pk).add_nested_tags(action.assign_tags.all())
33 if action.assign_correspondent:
34 document.correspondent = action.assign_correspondent
36 if action.assign_document_type:
37 document.document_type = action.assign_document_type
39 if action.assign_storage_path:
40 document.storage_path = action.assign_storage_path
42 if action.assign_owner:
43 document.owner = action.assign_owner
45 if action.assign_title:
46 try:
47 title = parse_w_workflow_placeholders(
48 action.assign_title,
49 document.correspondent.name if document.correspondent else "",
50 document.document_type.name if document.document_type else "",
51 document.owner.username if document.owner else "",
52 timezone.localtime(document.added),
53 document.original_filename or "",
54 document.filename or "",
55 document.created,
56 "", # dont pass the title to avoid recursion
57 "", # no urls in titles
58 document.pk,
59 )
60 if title:
61 document.title = title
62 except Exception: # pragma: no cover
63 logger.exception(
64 f"Error occurred parsing title assignment '{action.assign_title}', falling back to original",
65 extra={"group": logging_group},
66 )
68 if any(
69 [
70 action.has_assign_view_users,
71 action.has_assign_view_groups,
72 action.has_assign_change_users,
73 action.has_assign_change_groups,
74 ],
75 ):
76 permissions = {
77 "view": {
78 "users": action.assign_view_users.values_list("id", flat=True),
79 "groups": action.assign_view_groups.values_list("id", flat=True),
80 },
81 "change": {
82 "users": action.assign_change_users.values_list("id", flat=True),
83 "groups": action.assign_change_groups.values_list("id", flat=True),
84 },
85 }
86 set_permissions_for_object(
87 permissions=permissions,
88 object=document,
89 merge=True,
90 )
92 if action.has_assign_custom_fields:
93 for field in action.assign_custom_fields.all():
94 value_field_name = CustomFieldInstance.get_value_field_name(
95 data_type=field.data_type,
96 )
97 args = {
98 value_field_name: action.assign_custom_fields_values.get(
99 str(field.pk),
100 None,
101 ),
102 }
103 # for some reason update_or_create doesn't work here
104 instance = CustomFieldInstance.objects.filter(
105 field=field,
106 document=document,
107 ).first()
108 # empty string is indistinguishable from no value in the UI
109 if instance and args[value_field_name] not in (None, ""):
110 setattr(instance, value_field_name, args[value_field_name])
111 instance.save()
112 elif not instance:
113 CustomFieldInstance.objects.create(
114 **args,
115 field=field,
116 document=document,
117 )
120def apply_assignment_to_overrides(
121 action: WorkflowAction,
122 overrides: DocumentMetadataOverrides,
123):
124 """
125 Apply assignment actions to DocumentMetadataOverrides.
127 action: WorkflowAction, annotated with 'has_assign_*' boolean fields
128 """
129 if action.has_assign_tags:
130 if overrides.tag_ids is None:
131 overrides.tag_ids = []
132 tag_ids_to_add: set[int] = set()
133 for tag in action.assign_tags.all():
134 tag_ids_to_add.add(tag.pk)
135 tag_ids_to_add.update(int(pk) for pk in tag.get_ancestors_pks())
137 overrides.tag_ids = list(set(overrides.tag_ids) | tag_ids_to_add)
139 if action.assign_correspondent:
140 overrides.correspondent_id = action.assign_correspondent.pk
142 if action.assign_document_type:
143 overrides.document_type_id = action.assign_document_type.pk
145 if action.assign_storage_path:
146 overrides.storage_path_id = action.assign_storage_path.pk
148 if action.assign_owner:
149 overrides.owner_id = action.assign_owner.pk
151 if action.assign_title:
152 overrides.title = action.assign_title
154 if any(
155 [
156 action.has_assign_view_users,
157 action.has_assign_view_groups,
158 action.has_assign_change_users,
159 action.has_assign_change_groups,
160 ],
161 ):
162 overrides.view_users = list(
163 set(
164 (overrides.view_users or [])
165 + list(action.assign_view_users.values_list("id", flat=True)),
166 ),
167 )
168 overrides.view_groups = list(
169 set(
170 (overrides.view_groups or [])
171 + list(action.assign_view_groups.values_list("id", flat=True)),
172 ),
173 )
174 overrides.change_users = list(
175 set(
176 (overrides.change_users or [])
177 + list(action.assign_change_users.values_list("id", flat=True)),
178 ),
179 )
180 overrides.change_groups = list(
181 set(
182 (overrides.change_groups or [])
183 + list(action.assign_change_groups.values_list("id", flat=True)),
184 ),
185 )
187 if action.has_assign_custom_fields:
188 if overrides.custom_fields is None:
189 overrides.custom_fields = {}
190 overrides.custom_fields.update(
191 {
192 field.pk: action.assign_custom_fields_values.get(
193 str(field.pk),
194 None,
195 )
196 for field in action.assign_custom_fields.all()
197 },
198 )
201def apply_removal_to_document(
202 action: WorkflowAction,
203 document: Document,
204):
205 """
206 Apply removal actions to a Document instance.
208 action: WorkflowAction, annotated with 'has_remove_*' boolean fields
209 """
211 if action.remove_all_tags:
212 Document.objects.get(pk=document.pk).tags.clear()
213 else:
214 tag_ids_to_remove: set[int] = set()
215 for tag in action.remove_tags.all():
216 tag_ids_to_remove.add(tag.pk)
217 tag_ids_to_remove.update(int(pk) for pk in tag.get_descendants_pks())
219 if tag_ids_to_remove:
220 Document.objects.get(pk=document.pk).tags.remove(*tag_ids_to_remove)
222 if action.remove_all_correspondents or (
223 document.correspondent
224 and action.remove_correspondents.filter(pk=document.correspondent.pk).exists()
225 ):
226 document.correspondent = None
228 if action.remove_all_document_types or (
229 document.document_type
230 and action.remove_document_types.filter(pk=document.document_type.pk).exists()
231 ):
232 document.document_type = None
234 if action.remove_all_storage_paths or (
235 document.storage_path
236 and action.remove_storage_paths.filter(pk=document.storage_path.pk).exists()
237 ):
238 document.storage_path = None
240 if action.remove_all_owners or (
241 document.owner and action.remove_owners.filter(pk=document.owner.pk).exists()
242 ):
243 document.owner = None
245 if action.remove_all_permissions:
246 permissions = {
247 "view": {"users": [], "groups": []},
248 "change": {"users": [], "groups": []},
249 }
250 set_permissions_for_object(
251 permissions=permissions,
252 object=document,
253 merge=False,
254 )
256 if any(
257 [
258 action.has_remove_view_users,
259 action.has_remove_view_groups,
260 action.has_remove_change_users,
261 action.has_remove_change_groups,
262 ],
263 ):
264 for user in action.remove_view_users.all():
265 remove_perm("view_document", user, document)
266 for user in action.remove_change_users.all():
267 remove_perm("change_document", user, document)
268 for group in action.remove_view_groups.all():
269 remove_perm("view_document", group, document)
270 for group in action.remove_change_groups.all():
271 remove_perm("change_document", group, document)
273 if action.remove_all_custom_fields:
274 CustomFieldInstance.objects.filter(document=document).hard_delete()
275 elif action.has_remove_custom_fields:
276 CustomFieldInstance.objects.filter(
277 field__in=action.remove_custom_fields.all(),
278 document=document,
279 ).hard_delete()
282def apply_removal_to_overrides(
283 action: WorkflowAction,
284 overrides: DocumentMetadataOverrides,
285):
286 """
287 Apply removal actions to DocumentMetadataOverrides.
289 action: WorkflowAction, annotated with 'has_remove_*' boolean fields
290 """
291 if action.remove_all_tags:
292 overrides.tag_ids = None
293 elif overrides.tag_ids:
294 tag_ids_to_remove: set[int] = set()
295 for tag in action.remove_tags.all():
296 tag_ids_to_remove.add(tag.pk)
297 tag_ids_to_remove.update(int(pk) for pk in tag.get_descendants_pks())
299 overrides.tag_ids = [t for t in overrides.tag_ids if t not in tag_ids_to_remove]
301 if action.remove_all_correspondents or (
302 overrides.correspondent_id
303 and action.remove_correspondents.filter(pk=overrides.correspondent_id).exists()
304 ):
305 overrides.correspondent_id = None
307 if action.remove_all_document_types or (
308 overrides.document_type_id
309 and action.remove_document_types.filter(pk=overrides.document_type_id).exists()
310 ):
311 overrides.document_type_id = None
313 if action.remove_all_storage_paths or (
314 overrides.storage_path_id
315 and action.remove_storage_paths.filter(pk=overrides.storage_path_id).exists()
316 ):
317 overrides.storage_path_id = None
319 if action.remove_all_owners or (
320 overrides.owner_id
321 and action.remove_owners.filter(pk=overrides.owner_id).exists()
322 ):
323 overrides.owner_id = None
325 if action.remove_all_permissions:
326 overrides.view_users = None
327 overrides.view_groups = None
328 overrides.change_users = None
329 overrides.change_groups = None
330 elif any(
331 [
332 action.has_remove_view_users,
333 action.has_remove_view_groups,
334 action.has_remove_change_users,
335 action.has_remove_change_groups,
336 ],
337 ):
338 if overrides.view_users:
339 for user in action.remove_view_users.filter(pk__in=overrides.view_users):
340 overrides.view_users.remove(user.pk)
341 if overrides.change_users:
342 for user in action.remove_change_users.filter(
343 pk__in=overrides.change_users,
344 ):
345 overrides.change_users.remove(user.pk)
346 if overrides.view_groups:
347 for group in action.remove_view_groups.filter(pk__in=overrides.view_groups):
348 overrides.view_groups.remove(group.pk)
349 if overrides.change_groups:
350 for group in action.remove_change_groups.filter(
351 pk__in=overrides.change_groups,
352 ):
353 overrides.change_groups.remove(group.pk)
355 if action.remove_all_custom_fields:
356 overrides.custom_fields = None
357 elif action.has_remove_custom_fields and overrides.custom_fields:
358 for field in action.remove_custom_fields.filter(
359 pk__in=overrides.custom_fields.keys(),
360 ):
361 overrides.custom_fields.pop(field.pk, None)