mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Feature: workflow removal action (#5928)
--------- Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
This commit is contained in:
		| @@ -7,6 +7,7 @@ from enum import Enum | ||||
| from pathlib import Path | ||||
| from subprocess import CompletedProcess | ||||
| from subprocess import run | ||||
| from typing import TYPE_CHECKING | ||||
| from typing import Optional | ||||
|  | ||||
| import magic | ||||
| @@ -35,6 +36,7 @@ from documents.models import FileInfo | ||||
| from documents.models import StoragePath | ||||
| from documents.models import Tag | ||||
| from documents.models import Workflow | ||||
| from documents.models import WorkflowAction | ||||
| from documents.models import WorkflowTrigger | ||||
| from documents.parsers import DocumentParser | ||||
| from documents.parsers import ParseError | ||||
| @@ -63,9 +65,26 @@ class WorkflowTriggerPlugin( | ||||
|         """ | ||||
|         Get overrides from matching workflows | ||||
|         """ | ||||
|         msg = "" | ||||
|         overrides = DocumentMetadataOverrides() | ||||
|         for workflow in Workflow.objects.filter(enabled=True).order_by("order"): | ||||
|             template_overrides = DocumentMetadataOverrides() | ||||
|         for workflow in ( | ||||
|             Workflow.objects.filter(enabled=True) | ||||
|             .prefetch_related("actions") | ||||
|             .prefetch_related("actions__assign_view_users") | ||||
|             .prefetch_related("actions__assign_view_groups") | ||||
|             .prefetch_related("actions__assign_change_users") | ||||
|             .prefetch_related("actions__assign_change_groups") | ||||
|             .prefetch_related("actions__assign_custom_fields") | ||||
|             .prefetch_related("actions__remove_tags") | ||||
|             .prefetch_related("actions__remove_correspondents") | ||||
|             .prefetch_related("actions__remove_document_types") | ||||
|             .prefetch_related("actions__remove_storage_paths") | ||||
|             .prefetch_related("actions__remove_custom_fields") | ||||
|             .prefetch_related("actions__remove_owners") | ||||
|             .prefetch_related("triggers") | ||||
|             .order_by("order") | ||||
|         ): | ||||
|             action_overrides = DocumentMetadataOverrides() | ||||
|  | ||||
|             if document_matches_workflow( | ||||
|                 self.input_doc, | ||||
| @@ -73,49 +92,137 @@ class WorkflowTriggerPlugin( | ||||
|                 WorkflowTrigger.WorkflowTriggerType.CONSUMPTION, | ||||
|             ): | ||||
|                 for action in workflow.actions.all(): | ||||
|                     if action.assign_title is not None: | ||||
|                         template_overrides.title = action.assign_title | ||||
|                     if action.assign_tags is not None: | ||||
|                         template_overrides.tag_ids = [ | ||||
|                             tag.pk for tag in action.assign_tags.all() | ||||
|                         ] | ||||
|                     if action.assign_correspondent is not None: | ||||
|                         template_overrides.correspondent_id = ( | ||||
|                             action.assign_correspondent.pk | ||||
|                         ) | ||||
|                     if action.assign_document_type is not None: | ||||
|                         template_overrides.document_type_id = ( | ||||
|                             action.assign_document_type.pk | ||||
|                         ) | ||||
|                     if action.assign_storage_path is not None: | ||||
|                         template_overrides.storage_path_id = ( | ||||
|                             action.assign_storage_path.pk | ||||
|                         ) | ||||
|                     if action.assign_owner is not None: | ||||
|                         template_overrides.owner_id = action.assign_owner.pk | ||||
|                     if action.assign_view_users is not None: | ||||
|                         template_overrides.view_users = [ | ||||
|                             user.pk for user in action.assign_view_users.all() | ||||
|                         ] | ||||
|                     if action.assign_view_groups is not None: | ||||
|                         template_overrides.view_groups = [ | ||||
|                             group.pk for group in action.assign_view_groups.all() | ||||
|                         ] | ||||
|                     if action.assign_change_users is not None: | ||||
|                         template_overrides.change_users = [ | ||||
|                             user.pk for user in action.assign_change_users.all() | ||||
|                         ] | ||||
|                     if action.assign_change_groups is not None: | ||||
|                         template_overrides.change_groups = [ | ||||
|                             group.pk for group in action.assign_change_groups.all() | ||||
|                         ] | ||||
|                     if action.assign_custom_fields is not None: | ||||
|                         template_overrides.custom_field_ids = [ | ||||
|                             field.pk for field in action.assign_custom_fields.all() | ||||
|                         ] | ||||
|                     if TYPE_CHECKING: | ||||
|                         assert isinstance(action, WorkflowAction) | ||||
|                     msg += f"Applying {action} from {workflow}\n" | ||||
|                     if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT: | ||||
|                         if action.assign_title is not None: | ||||
|                             action_overrides.title = action.assign_title | ||||
|                         if action.assign_tags is not None: | ||||
|                             action_overrides.tag_ids = list( | ||||
|                                 action.assign_tags.values_list("pk", flat=True), | ||||
|                             ) | ||||
|  | ||||
|                         if action.assign_correspondent is not None: | ||||
|                             action_overrides.correspondent_id = ( | ||||
|                                 action.assign_correspondent.pk | ||||
|                             ) | ||||
|                         if action.assign_document_type is not None: | ||||
|                             action_overrides.document_type_id = ( | ||||
|                                 action.assign_document_type.pk | ||||
|                             ) | ||||
|                         if action.assign_storage_path is not None: | ||||
|                             action_overrides.storage_path_id = ( | ||||
|                                 action.assign_storage_path.pk | ||||
|                             ) | ||||
|                         if action.assign_owner is not None: | ||||
|                             action_overrides.owner_id = action.assign_owner.pk | ||||
|                         if action.assign_view_users is not None: | ||||
|                             action_overrides.view_users = list( | ||||
|                                 action.assign_view_users.values_list("pk", flat=True), | ||||
|                             ) | ||||
|                         if action.assign_view_groups is not None: | ||||
|                             action_overrides.view_groups = list( | ||||
|                                 action.assign_view_groups.values_list("pk", flat=True), | ||||
|                             ) | ||||
|                         if action.assign_change_users is not None: | ||||
|                             action_overrides.change_users = list( | ||||
|                                 action.assign_change_users.values_list("pk", flat=True), | ||||
|                             ) | ||||
|                         if action.assign_change_groups is not None: | ||||
|                             action_overrides.change_groups = list( | ||||
|                                 action.assign_change_groups.values_list( | ||||
|                                     "pk", | ||||
|                                     flat=True, | ||||
|                                 ), | ||||
|                             ) | ||||
|                         if action.assign_custom_fields is not None: | ||||
|                             action_overrides.custom_field_ids = list( | ||||
|                                 action.assign_custom_fields.values_list( | ||||
|                                     "pk", | ||||
|                                     flat=True, | ||||
|                                 ), | ||||
|                             ) | ||||
|                         overrides.update(action_overrides) | ||||
|                     elif action.type == WorkflowAction.WorkflowActionType.REMOVAL: | ||||
|                         # Removal actions overwrite the current overrides | ||||
|                         if action.remove_all_tags: | ||||
|                             overrides.tag_ids = [] | ||||
|                         elif overrides.tag_ids: | ||||
|                             for tag in action.remove_custom_fields.filter( | ||||
|                                 pk__in=overrides.tag_ids, | ||||
|                             ): | ||||
|                                 overrides.tag_ids.remove(tag.pk) | ||||
|  | ||||
|                         if action.remove_all_correspondents or ( | ||||
|                             overrides.correspondent_id is not None | ||||
|                             and action.remove_correspondents.filter( | ||||
|                                 pk=overrides.correspondent_id, | ||||
|                             ).exists() | ||||
|                         ): | ||||
|                             overrides.correspondent_id = None | ||||
|  | ||||
|                         if action.remove_all_document_types or ( | ||||
|                             overrides.document_type_id is not None | ||||
|                             and action.remove_document_types.filter( | ||||
|                                 pk=overrides.document_type_id, | ||||
|                             ).exists() | ||||
|                         ): | ||||
|                             overrides.document_type_id = None | ||||
|  | ||||
|                         if action.remove_all_storage_paths or ( | ||||
|                             overrides.storage_path_id is not None | ||||
|                             and action.remove_storage_paths.filter( | ||||
|                                 pk=overrides.storage_path_id, | ||||
|                             ).exists() | ||||
|                         ): | ||||
|                             overrides.storage_path_id = None | ||||
|  | ||||
|                         if action.remove_all_custom_fields: | ||||
|                             overrides.custom_field_ids = [] | ||||
|                         elif overrides.custom_field_ids: | ||||
|                             for field in action.remove_custom_fields.filter( | ||||
|                                 pk__in=overrides.custom_field_ids, | ||||
|                             ): | ||||
|                                 overrides.custom_field_ids.remove(field.pk) | ||||
|  | ||||
|                         if action.remove_all_owners or ( | ||||
|                             overrides.owner_id is not None | ||||
|                             and action.remove_owners.filter( | ||||
|                                 pk=overrides.owner_id, | ||||
|                             ).exists() | ||||
|                         ): | ||||
|                             overrides.owner_id = None | ||||
|  | ||||
|                         if action.remove_all_permissions: | ||||
|                             overrides.view_users = [] | ||||
|                             overrides.view_groups = [] | ||||
|                             overrides.change_users = [] | ||||
|                             overrides.change_groups = [] | ||||
|                         else: | ||||
|                             if overrides.view_users: | ||||
|                                 for user in action.remove_view_users.filter( | ||||
|                                     pk__in=overrides.view_users, | ||||
|                                 ): | ||||
|                                     overrides.view_users.remove(user.pk) | ||||
|                             if overrides.change_users: | ||||
|                                 for user in action.remove_change_users.filter( | ||||
|                                     pk__in=overrides.change_users, | ||||
|                                 ): | ||||
|                                     overrides.change_users.remove(user.pk) | ||||
|                             if overrides.view_groups: | ||||
|                                 for user in action.remove_view_groups.filter( | ||||
|                                     pk__in=overrides.view_groups, | ||||
|                                 ): | ||||
|                                     overrides.view_groups.remove(user.pk) | ||||
|                             if overrides.change_groups: | ||||
|                                 for user in action.remove_change_groups.filter( | ||||
|                                     pk__in=overrides.change_groups, | ||||
|                                 ): | ||||
|                                     overrides.change_groups.remove(user.pk) | ||||
|  | ||||
|                     overrides.update(template_overrides) | ||||
|         self.metadata.update(overrides) | ||||
|         return msg | ||||
|  | ||||
|  | ||||
| class ConsumerError(Exception): | ||||
|   | ||||
| @@ -0,0 +1,223 @@ | ||||
| # Generated by Django 4.2.10 on 2024-02-21 21:19 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations | ||||
| from django.db import models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("auth", "0012_alter_user_first_name_max_length"), | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ("documents", "1045_alter_customfieldinstance_value_monetary"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_all_correspondents", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, | ||||
|                 verbose_name="remove all correspondents", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_all_custom_fields", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, | ||||
|                 verbose_name="remove all custom fields", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_all_document_types", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, | ||||
|                 verbose_name="remove all document types", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_all_owners", | ||||
|             field=models.BooleanField(default=False, verbose_name="remove all owners"), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_all_permissions", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, | ||||
|                 verbose_name="remove all permissions", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_all_storage_paths", | ||||
|             field=models.BooleanField( | ||||
|                 default=False, | ||||
|                 verbose_name="remove all storage paths", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_all_tags", | ||||
|             field=models.BooleanField(default=False, verbose_name="remove all tags"), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_change_groups", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="auth.group", | ||||
|                 verbose_name="remove change permissions for these groups", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_change_users", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to=settings.AUTH_USER_MODEL, | ||||
|                 verbose_name="remove change permissions for these users", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_correspondents", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="documents.correspondent", | ||||
|                 verbose_name="remove these correspondent(s)", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_custom_fields", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="documents.customfield", | ||||
|                 verbose_name="remove these custom fields", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_document_types", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="documents.documenttype", | ||||
|                 verbose_name="remove these document type(s)", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_owners", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to=settings.AUTH_USER_MODEL, | ||||
|                 verbose_name="remove these owner(s)", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_storage_paths", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="documents.storagepath", | ||||
|                 verbose_name="remove these storage path(s)", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_tags", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="documents.tag", | ||||
|                 verbose_name="remove these tag(s)", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_view_groups", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="auth.group", | ||||
|                 verbose_name="remove view permissions for these groups", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="workflowaction", | ||||
|             name="remove_view_users", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to=settings.AUTH_USER_MODEL, | ||||
|                 verbose_name="remove view permissions for these users", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="workflowaction", | ||||
|             name="assign_correspondent", | ||||
|             field=models.ForeignKey( | ||||
|                 blank=True, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 related_name="+", | ||||
|                 to="documents.correspondent", | ||||
|                 verbose_name="assign this correspondent", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="workflowaction", | ||||
|             name="assign_document_type", | ||||
|             field=models.ForeignKey( | ||||
|                 blank=True, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 related_name="+", | ||||
|                 to="documents.documenttype", | ||||
|                 verbose_name="assign this document type", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="workflowaction", | ||||
|             name="assign_storage_path", | ||||
|             field=models.ForeignKey( | ||||
|                 blank=True, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_NULL, | ||||
|                 related_name="+", | ||||
|                 to="documents.storagepath", | ||||
|                 verbose_name="assign this storage path", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="workflowaction", | ||||
|             name="assign_tags", | ||||
|             field=models.ManyToManyField( | ||||
|                 blank=True, | ||||
|                 related_name="+", | ||||
|                 to="documents.tag", | ||||
|                 verbose_name="assign this tag", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="workflowaction", | ||||
|             name="type", | ||||
|             field=models.PositiveIntegerField( | ||||
|                 choices=[(1, "Assignment"), (2, "Removal")], | ||||
|                 default=1, | ||||
|                 verbose_name="Workflow Action Type", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -997,7 +997,14 @@ class WorkflowTrigger(models.Model): | ||||
|  | ||||
| class WorkflowAction(models.Model): | ||||
|     class WorkflowActionType(models.IntegerChoices): | ||||
|         ASSIGNMENT = 1, _("Assignment") | ||||
|         ASSIGNMENT = ( | ||||
|             1, | ||||
|             _("Assignment"), | ||||
|         ) | ||||
|         REMOVAL = ( | ||||
|             2, | ||||
|             _("Removal"), | ||||
|         ) | ||||
|  | ||||
|     type = models.PositiveIntegerField( | ||||
|         _("Workflow Action Type"), | ||||
| @@ -1019,6 +1026,7 @@ class WorkflowAction(models.Model): | ||||
|     assign_tags = models.ManyToManyField( | ||||
|         Tag, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("assign this tag"), | ||||
|     ) | ||||
|  | ||||
| @@ -1027,6 +1035,7 @@ class WorkflowAction(models.Model): | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="+", | ||||
|         verbose_name=_("assign this document type"), | ||||
|     ) | ||||
|  | ||||
| @@ -1035,6 +1044,7 @@ class WorkflowAction(models.Model): | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="+", | ||||
|         verbose_name=_("assign this correspondent"), | ||||
|     ) | ||||
|  | ||||
| @@ -1043,6 +1053,7 @@ class WorkflowAction(models.Model): | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|         related_name="+", | ||||
|         verbose_name=_("assign this storage path"), | ||||
|     ) | ||||
|  | ||||
| @@ -1090,6 +1101,111 @@ class WorkflowAction(models.Model): | ||||
|         verbose_name=_("assign these custom fields"), | ||||
|     ) | ||||
|  | ||||
|     remove_tags = models.ManyToManyField( | ||||
|         Tag, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove these tag(s)"), | ||||
|     ) | ||||
|  | ||||
|     remove_all_tags = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("remove all tags"), | ||||
|     ) | ||||
|  | ||||
|     remove_document_types = models.ManyToManyField( | ||||
|         DocumentType, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove these document type(s)"), | ||||
|     ) | ||||
|  | ||||
|     remove_all_document_types = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("remove all document types"), | ||||
|     ) | ||||
|  | ||||
|     remove_correspondents = models.ManyToManyField( | ||||
|         Correspondent, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove these correspondent(s)"), | ||||
|     ) | ||||
|  | ||||
|     remove_all_correspondents = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("remove all correspondents"), | ||||
|     ) | ||||
|  | ||||
|     remove_storage_paths = models.ManyToManyField( | ||||
|         StoragePath, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove these storage path(s)"), | ||||
|     ) | ||||
|  | ||||
|     remove_all_storage_paths = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("remove all storage paths"), | ||||
|     ) | ||||
|  | ||||
|     remove_owners = models.ManyToManyField( | ||||
|         User, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove these owner(s)"), | ||||
|     ) | ||||
|  | ||||
|     remove_all_owners = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("remove all owners"), | ||||
|     ) | ||||
|  | ||||
|     remove_view_users = models.ManyToManyField( | ||||
|         User, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove view permissions for these users"), | ||||
|     ) | ||||
|  | ||||
|     remove_view_groups = models.ManyToManyField( | ||||
|         Group, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove view permissions for these groups"), | ||||
|     ) | ||||
|  | ||||
|     remove_change_users = models.ManyToManyField( | ||||
|         User, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove change permissions for these users"), | ||||
|     ) | ||||
|  | ||||
|     remove_change_groups = models.ManyToManyField( | ||||
|         Group, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove change permissions for these groups"), | ||||
|     ) | ||||
|  | ||||
|     remove_all_permissions = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("remove all permissions"), | ||||
|     ) | ||||
|  | ||||
|     remove_custom_fields = models.ManyToManyField( | ||||
|         CustomField, | ||||
|         blank=True, | ||||
|         related_name="+", | ||||
|         verbose_name=_("remove these custom fields"), | ||||
|     ) | ||||
|  | ||||
|     remove_all_custom_fields = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("remove all custom fields"), | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("workflow action") | ||||
|         verbose_name_plural = _("workflow actions") | ||||
|   | ||||
| @@ -1471,6 +1471,23 @@ class WorkflowActionSerializer(serializers.ModelSerializer): | ||||
|             "assign_change_users", | ||||
|             "assign_change_groups", | ||||
|             "assign_custom_fields", | ||||
|             "remove_all_tags", | ||||
|             "remove_tags", | ||||
|             "remove_all_correspondents", | ||||
|             "remove_correspondents", | ||||
|             "remove_all_document_types", | ||||
|             "remove_document_types", | ||||
|             "remove_all_storage_paths", | ||||
|             "remove_storage_paths", | ||||
|             "remove_custom_fields", | ||||
|             "remove_all_custom_fields", | ||||
|             "remove_all_owners", | ||||
|             "remove_owners", | ||||
|             "remove_all_permissions", | ||||
|             "remove_view_users", | ||||
|             "remove_view_groups", | ||||
|             "remove_change_users", | ||||
|             "remove_change_groups", | ||||
|         ] | ||||
|  | ||||
|     def validate(self, attrs): | ||||
| @@ -1551,10 +1568,22 @@ class WorkflowSerializer(serializers.ModelSerializer): | ||||
|                 assign_change_users = action.pop("assign_change_users", None) | ||||
|                 assign_change_groups = action.pop("assign_change_groups", None) | ||||
|                 assign_custom_fields = action.pop("assign_custom_fields", None) | ||||
|                 remove_tags = action.pop("remove_tags", None) | ||||
|                 remove_correspondents = action.pop("remove_correspondents", None) | ||||
|                 remove_document_types = action.pop("remove_document_types", None) | ||||
|                 remove_storage_paths = action.pop("remove_storage_paths", None) | ||||
|                 remove_custom_fields = action.pop("remove_custom_fields", None) | ||||
|                 remove_owners = action.pop("remove_owners", None) | ||||
|                 remove_view_users = action.pop("remove_view_users", None) | ||||
|                 remove_view_groups = action.pop("remove_view_groups", None) | ||||
|                 remove_change_users = action.pop("remove_change_users", None) | ||||
|                 remove_change_groups = action.pop("remove_change_groups", None) | ||||
|  | ||||
|                 action_instance, _ = WorkflowAction.objects.update_or_create( | ||||
|                     id=action.get("id"), | ||||
|                     defaults=action, | ||||
|                 ) | ||||
|  | ||||
|                 if assign_tags is not None: | ||||
|                     action_instance.assign_tags.set(assign_tags) | ||||
|                 if assign_view_users is not None: | ||||
| @@ -1567,6 +1596,27 @@ class WorkflowSerializer(serializers.ModelSerializer): | ||||
|                     action_instance.assign_change_groups.set(assign_change_groups) | ||||
|                 if assign_custom_fields is not None: | ||||
|                     action_instance.assign_custom_fields.set(assign_custom_fields) | ||||
|                 if remove_tags is not None: | ||||
|                     action_instance.remove_tags.set(remove_tags) | ||||
|                 if remove_correspondents is not None: | ||||
|                     action_instance.remove_correspondents.set(remove_correspondents) | ||||
|                 if remove_document_types is not None: | ||||
|                     action_instance.remove_document_types.set(remove_document_types) | ||||
|                 if remove_storage_paths is not None: | ||||
|                     action_instance.remove_storage_paths.set(remove_storage_paths) | ||||
|                 if remove_custom_fields is not None: | ||||
|                     action_instance.remove_custom_fields.set(remove_custom_fields) | ||||
|                 if remove_owners is not None: | ||||
|                     action_instance.remove_owners.set(remove_owners) | ||||
|                 if remove_view_users is not None: | ||||
|                     action_instance.remove_view_users.set(remove_view_users) | ||||
|                 if remove_view_groups is not None: | ||||
|                     action_instance.remove_view_groups.set(remove_view_groups) | ||||
|                 if remove_change_users is not None: | ||||
|                     action_instance.remove_change_users.set(remove_change_users) | ||||
|                 if remove_change_groups is not None: | ||||
|                     action_instance.remove_change_groups.set(remove_change_groups) | ||||
|  | ||||
|                 set_actions.append(action_instance) | ||||
|  | ||||
|         instance.triggers.set(set_triggers) | ||||
|   | ||||
| @@ -20,6 +20,7 @@ from django.db.models import Q | ||||
| from django.dispatch import receiver | ||||
| from django.utils import timezone | ||||
| from filelock import FileLock | ||||
| from guardian.shortcuts import remove_perm | ||||
|  | ||||
| from documents import matching | ||||
| from documents.caching import clear_metadata_cache | ||||
| @@ -34,6 +35,7 @@ from documents.models import MatchingModel | ||||
| from documents.models import PaperlessTask | ||||
| from documents.models import Tag | ||||
| from documents.models import Workflow | ||||
| from documents.models import WorkflowAction | ||||
| from documents.models import WorkflowTrigger | ||||
| from documents.permissions import get_objects_for_user_owner_aware | ||||
| from documents.permissions import set_permissions_for_object | ||||
| @@ -529,122 +531,231 @@ def run_workflow( | ||||
|     document: Document, | ||||
|     logging_group=None, | ||||
| ): | ||||
|     for workflow in Workflow.objects.filter( | ||||
|         enabled=True, | ||||
|         triggers__type=trigger_type, | ||||
|     ).order_by("order"): | ||||
|     for workflow in ( | ||||
|         Workflow.objects.filter( | ||||
|             enabled=True, | ||||
|             triggers__type=trigger_type, | ||||
|         ) | ||||
|         .prefetch_related("actions") | ||||
|         .prefetch_related("actions__assign_view_users") | ||||
|         .prefetch_related("actions__assign_view_groups") | ||||
|         .prefetch_related("actions__assign_change_users") | ||||
|         .prefetch_related("actions__assign_change_groups") | ||||
|         .prefetch_related("actions__assign_custom_fields") | ||||
|         .prefetch_related("actions__remove_tags") | ||||
|         .prefetch_related("actions__remove_correspondents") | ||||
|         .prefetch_related("actions__remove_document_types") | ||||
|         .prefetch_related("actions__remove_storage_paths") | ||||
|         .prefetch_related("actions__remove_custom_fields") | ||||
|         .prefetch_related("actions__remove_owners") | ||||
|         .prefetch_related("triggers") | ||||
|         .order_by("order") | ||||
|     ): | ||||
|         if matching.document_matches_workflow( | ||||
|             document, | ||||
|             workflow, | ||||
|             trigger_type, | ||||
|         ): | ||||
|             action: WorkflowAction | ||||
|             for action in workflow.actions.all(): | ||||
|                 logger.info( | ||||
|                     f"Applying {action} from {workflow}", | ||||
|                     extra={"group": logging_group}, | ||||
|                 ) | ||||
|                 if action.assign_tags.all().count() > 0: | ||||
|                     document.tags.add(*action.assign_tags.all()) | ||||
|  | ||||
|                 if action.assign_correspondent is not None: | ||||
|                     document.correspondent = action.assign_correspondent | ||||
|                 if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT: | ||||
|                     if action.assign_tags.all().count() > 0: | ||||
|                         document.tags.add(*action.assign_tags.all()) | ||||
|  | ||||
|                 if action.assign_document_type is not None: | ||||
|                     document.document_type = action.assign_document_type | ||||
|                     if action.assign_correspondent is not None: | ||||
|                         document.correspondent = action.assign_correspondent | ||||
|  | ||||
|                 if action.assign_storage_path is not None: | ||||
|                     document.storage_path = action.assign_storage_path | ||||
|                     if action.assign_document_type is not None: | ||||
|                         document.document_type = action.assign_document_type | ||||
|  | ||||
|                 if action.assign_owner is not None: | ||||
|                     document.owner = action.assign_owner | ||||
|                     if action.assign_storage_path is not None: | ||||
|                         document.storage_path = action.assign_storage_path | ||||
|  | ||||
|                 if action.assign_title is not None: | ||||
|                     try: | ||||
|                         document.title = parse_doc_title_w_placeholders( | ||||
|                             action.assign_title, | ||||
|                             ( | ||||
|                                 document.correspondent.name | ||||
|                                 if document.correspondent is not None | ||||
|                                 else "" | ||||
|                             ), | ||||
|                             ( | ||||
|                                 document.document_type.name | ||||
|                                 if document.document_type is not None | ||||
|                                 else "" | ||||
|                             ), | ||||
|                             ( | ||||
|                                 document.owner.username | ||||
|                                 if document.owner is not None | ||||
|                                 else "" | ||||
|                             ), | ||||
|                             timezone.localtime(document.added), | ||||
|                             ( | ||||
|                                 document.original_filename | ||||
|                                 if document.original_filename is not None | ||||
|                                 else "" | ||||
|                             ), | ||||
|                             timezone.localtime(document.created), | ||||
|                     if action.assign_owner is not None: | ||||
|                         document.owner = action.assign_owner | ||||
|  | ||||
|                     if action.assign_title is not None: | ||||
|                         try: | ||||
|                             document.title = parse_doc_title_w_placeholders( | ||||
|                                 action.assign_title, | ||||
|                                 ( | ||||
|                                     document.correspondent.name | ||||
|                                     if document.correspondent is not None | ||||
|                                     else "" | ||||
|                                 ), | ||||
|                                 ( | ||||
|                                     document.document_type.name | ||||
|                                     if document.document_type is not None | ||||
|                                     else "" | ||||
|                                 ), | ||||
|                                 ( | ||||
|                                     document.owner.username | ||||
|                                     if document.owner is not None | ||||
|                                     else "" | ||||
|                                 ), | ||||
|                                 timezone.localtime(document.added), | ||||
|                                 ( | ||||
|                                     document.original_filename | ||||
|                                     if document.original_filename is not None | ||||
|                                     else "" | ||||
|                                 ), | ||||
|                                 timezone.localtime(document.created), | ||||
|                             ) | ||||
|                         except Exception: | ||||
|                             logger.exception( | ||||
|                                 f"Error occurred parsing title assignment '{action.assign_title}', falling back to original", | ||||
|                                 extra={"group": logging_group}, | ||||
|                             ) | ||||
|  | ||||
|                     if ( | ||||
|                         ( | ||||
|                             action.assign_view_users is not None | ||||
|                             and action.assign_view_users.count() > 0 | ||||
|                         ) | ||||
|                     except Exception: | ||||
|                         logger.exception( | ||||
|                             f"Error occurred parsing title assignment '{action.assign_title}', falling back to original", | ||||
|                             extra={"group": logging_group}, | ||||
|                         or ( | ||||
|                             action.assign_view_groups is not None | ||||
|                             and action.assign_view_groups.count() > 0 | ||||
|                         ) | ||||
|                         or ( | ||||
|                             action.assign_change_users is not None | ||||
|                             and action.assign_change_users.count() > 0 | ||||
|                         ) | ||||
|                         or ( | ||||
|                             action.assign_change_groups is not None | ||||
|                             and action.assign_change_groups.count() > 0 | ||||
|                         ) | ||||
|                     ): | ||||
|                         permissions = { | ||||
|                             "view": { | ||||
|                                 "users": action.assign_view_users.all().values_list( | ||||
|                                     "id", | ||||
|                                 ) | ||||
|                                 or [], | ||||
|                                 "groups": action.assign_view_groups.all().values_list( | ||||
|                                     "id", | ||||
|                                 ) | ||||
|                                 or [], | ||||
|                             }, | ||||
|                             "change": { | ||||
|                                 "users": action.assign_change_users.all().values_list( | ||||
|                                     "id", | ||||
|                                 ) | ||||
|                                 or [], | ||||
|                                 "groups": action.assign_change_groups.all().values_list( | ||||
|                                     "id", | ||||
|                                 ) | ||||
|                                 or [], | ||||
|                             }, | ||||
|                         } | ||||
|                         set_permissions_for_object( | ||||
|                             permissions=permissions, | ||||
|                             object=document, | ||||
|                             merge=True, | ||||
|                         ) | ||||
|  | ||||
|                 if ( | ||||
|                     ( | ||||
|                         action.assign_view_users is not None | ||||
|                         and action.assign_view_users.count() > 0 | ||||
|                     ) | ||||
|                     or ( | ||||
|                         action.assign_view_groups is not None | ||||
|                         and action.assign_view_groups.count() > 0 | ||||
|                     ) | ||||
|                     or ( | ||||
|                         action.assign_change_users is not None | ||||
|                         and action.assign_change_users.count() > 0 | ||||
|                     ) | ||||
|                     or ( | ||||
|                         action.assign_change_groups is not None | ||||
|                         and action.assign_change_groups.count() > 0 | ||||
|                     ) | ||||
|                 ): | ||||
|                     permissions = { | ||||
|                         "view": { | ||||
|                             "users": action.assign_view_users.all().values_list("id") | ||||
|                             or [], | ||||
|                             "groups": action.assign_view_groups.all().values_list("id") | ||||
|                             or [], | ||||
|                         }, | ||||
|                         "change": { | ||||
|                             "users": action.assign_change_users.all().values_list("id") | ||||
|                             or [], | ||||
|                             "groups": action.assign_change_groups.all().values_list( | ||||
|                                 "id", | ||||
|                             ) | ||||
|                             or [], | ||||
|                         }, | ||||
|                     } | ||||
|                     set_permissions_for_object( | ||||
|                         permissions=permissions, | ||||
|                         object=document, | ||||
|                         merge=True, | ||||
|                     ) | ||||
|                     if action.assign_custom_fields is not None: | ||||
|                         for field in action.assign_custom_fields.all(): | ||||
|                             if ( | ||||
|                                 CustomFieldInstance.objects.filter( | ||||
|                                     field=field, | ||||
|                                     document=document, | ||||
|                                 ).count() | ||||
|                                 == 0 | ||||
|                             ): | ||||
|                                 # can be triggered on existing docs, so only add the field if it doesn't already exist | ||||
|                                 CustomFieldInstance.objects.create( | ||||
|                                     field=field, | ||||
|                                     document=document, | ||||
|                                 ) | ||||
|  | ||||
|                 if action.assign_custom_fields is not None: | ||||
|                     for field in action.assign_custom_fields.all(): | ||||
|                         if ( | ||||
|                             CustomFieldInstance.objects.filter( | ||||
|                                 field=field, | ||||
|                                 document=document, | ||||
|                             ).count() | ||||
|                             == 0 | ||||
|                         ): | ||||
|                             # can be triggered on existing docs, so only add the field if it doesn't already exist | ||||
|                             CustomFieldInstance.objects.create( | ||||
|                                 field=field, | ||||
|                                 document=document, | ||||
|                             ) | ||||
|                 elif action.type == WorkflowAction.WorkflowActionType.REMOVAL: | ||||
|                     if action.remove_all_tags: | ||||
|                         document.tags.clear() | ||||
|                     else: | ||||
|                         for tag in action.remove_tags.filter( | ||||
|                             pk__in=list(document.tags.values_list("pk", flat=True)), | ||||
|                         ).all(): | ||||
|                             document.tags.remove(tag.pk) | ||||
|  | ||||
|                     if action.remove_all_correspondents or ( | ||||
|                         document.correspondent | ||||
|                         and ( | ||||
|                             action.remove_correspondents.filter( | ||||
|                                 pk=document.correspondent.pk, | ||||
|                             ).exists() | ||||
|                         ) | ||||
|                     ): | ||||
|                         document.correspondent = None | ||||
|  | ||||
|                     if action.remove_all_document_types or ( | ||||
|                         document.document_type | ||||
|                         and ( | ||||
|                             action.remove_document_types.filter( | ||||
|                                 pk=document.document_type.pk, | ||||
|                             ).exists() | ||||
|                         ) | ||||
|                     ): | ||||
|                         document.document_type = None | ||||
|  | ||||
|                     if action.remove_all_storage_paths or ( | ||||
|                         document.storage_path | ||||
|                         and ( | ||||
|                             action.remove_storage_paths.filter( | ||||
|                                 pk=document.storage_path.pk, | ||||
|                             ).exists() | ||||
|                         ) | ||||
|                     ): | ||||
|                         document.storage_path = None | ||||
|  | ||||
|                     if action.remove_all_owners or ( | ||||
|                         document.owner | ||||
|                         and (action.remove_owners.filter(pk=document.owner.pk).exists()) | ||||
|                     ): | ||||
|                         document.owner = None | ||||
|  | ||||
|                     if action.remove_all_permissions: | ||||
|                         permissions = { | ||||
|                             "view": { | ||||
|                                 "users": [], | ||||
|                                 "groups": [], | ||||
|                             }, | ||||
|                             "change": { | ||||
|                                 "users": [], | ||||
|                                 "groups": [], | ||||
|                             }, | ||||
|                         } | ||||
|                         set_permissions_for_object( | ||||
|                             permissions=permissions, | ||||
|                             object=document, | ||||
|                             merge=False, | ||||
|                         ) | ||||
|                     elif ( | ||||
|                         (action.remove_view_users.all().count() > 0) | ||||
|                         or (action.remove_view_groups.all().count() > 0) | ||||
|                         or (action.remove_change_users.all().count() > 0) | ||||
|                         or (action.remove_change_groups.all().count() > 0) | ||||
|                     ): | ||||
|                         for user in action.remove_view_users.all(): | ||||
|                             remove_perm("view_document", user, document) | ||||
|                         for user in action.remove_change_users.all(): | ||||
|                             remove_perm("change_document", user, document) | ||||
|                         for group in action.remove_view_groups.all(): | ||||
|                             remove_perm("view_document", group, document) | ||||
|                         for group in action.remove_change_groups.all(): | ||||
|                             remove_perm("change_document", group, document) | ||||
|  | ||||
|                     if action.remove_all_custom_fields: | ||||
|                         CustomFieldInstance.objects.filter(document=document).delete() | ||||
|                     elif action.remove_custom_fields.all().count() > 0: | ||||
|                         CustomFieldInstance.objects.filter( | ||||
|                             field__in=action.remove_custom_fields.all(), | ||||
|                             document=document, | ||||
|                         ).delete() | ||||
|  | ||||
|             document.save() | ||||
|  | ||||
|   | ||||
| @@ -202,6 +202,19 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): | ||||
|                             "assign_change_groups": [self.group1.id], | ||||
|                             "assign_custom_fields": [self.cf2.id], | ||||
|                         }, | ||||
|                         { | ||||
|                             "type": WorkflowAction.WorkflowActionType.REMOVAL, | ||||
|                             "remove_tags": [self.t3.id], | ||||
|                             "remove_document_types": [self.dt.id], | ||||
|                             "remove_correspondents": [self.c.id], | ||||
|                             "remove_storage_paths": [self.sp.id], | ||||
|                             "remove_custom_fields": [self.cf1.id], | ||||
|                             "remove_owners": [self.user2.id], | ||||
|                             "remove_view_users": [self.user3.id], | ||||
|                             "remove_change_users": [self.user3.id], | ||||
|                             "remove_view_groups": [self.group1.id], | ||||
|                             "remove_change_groups": [self.group1.id], | ||||
|                         }, | ||||
|                     ], | ||||
|                 }, | ||||
|             ), | ||||
|   | ||||
| @@ -1223,3 +1223,332 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase): | ||||
|             title="test", | ||||
|         ) | ||||
|         self.assertRaises(Exception, document_matches_workflow, doc, w, 4) | ||||
|  | ||||
|     def test_removal_action_document_updated_workflow(self): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Workflow with removal action | ||||
|         WHEN: | ||||
|             - File that matches is updated | ||||
|         THEN: | ||||
|             - Action removals are applied | ||||
|         """ | ||||
|         trigger = WorkflowTrigger.objects.create( | ||||
|             type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, | ||||
|             filter_path="*", | ||||
|         ) | ||||
|         action = WorkflowAction.objects.create( | ||||
|             type=WorkflowAction.WorkflowActionType.REMOVAL, | ||||
|         ) | ||||
|         action.remove_correspondents.add(self.c) | ||||
|         action.remove_tags.add(self.t1) | ||||
|         action.remove_document_types.add(self.dt) | ||||
|         action.remove_storage_paths.add(self.sp) | ||||
|         action.remove_owners.add(self.user2) | ||||
|         action.remove_custom_fields.add(self.cf1) | ||||
|         action.remove_view_users.add(self.user3) | ||||
|         action.remove_view_groups.add(self.group1) | ||||
|         action.remove_change_users.add(self.user3) | ||||
|         action.remove_change_groups.add(self.group1) | ||||
|         action.save() | ||||
|  | ||||
|         w = Workflow.objects.create( | ||||
|             name="Workflow 1", | ||||
|             order=0, | ||||
|         ) | ||||
|         w.triggers.add(trigger) | ||||
|         w.actions.add(action) | ||||
|         w.save() | ||||
|  | ||||
|         doc = Document.objects.create( | ||||
|             title="sample test", | ||||
|             correspondent=self.c, | ||||
|             document_type=self.dt, | ||||
|             storage_path=self.sp, | ||||
|             owner=self.user2, | ||||
|             original_filename="sample.pdf", | ||||
|         ) | ||||
|         doc.tags.set([self.t1, self.t2]) | ||||
|         CustomFieldInstance.objects.create(document=doc, field=self.cf1) | ||||
|         doc.save() | ||||
|         assign_perm("documents.view_document", self.user3, doc) | ||||
|         assign_perm("documents.change_document", self.user3, doc) | ||||
|         assign_perm("documents.view_document", self.group1, doc) | ||||
|         assign_perm("documents.change_document", self.group1, doc) | ||||
|  | ||||
|         superuser = User.objects.create_superuser("superuser") | ||||
|         self.client.force_authenticate(user=superuser) | ||||
|  | ||||
|         self.client.patch( | ||||
|             f"/api/documents/{doc.id}/", | ||||
|             {"title": "new title"}, | ||||
|             format="json", | ||||
|         ) | ||||
|         doc.refresh_from_db() | ||||
|  | ||||
|         self.assertIsNone(doc.document_type) | ||||
|         self.assertIsNone(doc.correspondent) | ||||
|         self.assertIsNone(doc.storage_path) | ||||
|         self.assertEqual(doc.tags.all().count(), 1) | ||||
|         self.assertIn(self.t2, doc.tags.all()) | ||||
|         self.assertIsNone(doc.owner) | ||||
|         self.assertEqual(doc.custom_fields.all().count(), 0) | ||||
|         self.assertFalse(self.user3.has_perm("documents.view_document", doc)) | ||||
|         self.assertFalse(self.user3.has_perm("documents.change_document", doc)) | ||||
|         group_perms: QuerySet = get_groups_with_perms(doc) | ||||
|         self.assertNotIn(self.group1, group_perms) | ||||
|  | ||||
|     def test_removal_action_document_updated_removeall(self): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Workflow with removal action with remove all fields set | ||||
|         WHEN: | ||||
|             - File that matches is updated | ||||
|         THEN: | ||||
|             - Action removals are applied | ||||
|         """ | ||||
|         trigger = WorkflowTrigger.objects.create( | ||||
|             type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, | ||||
|             filter_path="*", | ||||
|         ) | ||||
|         action = WorkflowAction.objects.create( | ||||
|             type=WorkflowAction.WorkflowActionType.REMOVAL, | ||||
|             remove_all_correspondents=True, | ||||
|             remove_all_tags=True, | ||||
|             remove_all_document_types=True, | ||||
|             remove_all_storage_paths=True, | ||||
|             remove_all_custom_fields=True, | ||||
|             remove_all_owners=True, | ||||
|             remove_all_permissions=True, | ||||
|         ) | ||||
|         action.save() | ||||
|  | ||||
|         w = Workflow.objects.create( | ||||
|             name="Workflow 1", | ||||
|             order=0, | ||||
|         ) | ||||
|         w.triggers.add(trigger) | ||||
|         w.actions.add(action) | ||||
|         w.save() | ||||
|  | ||||
|         doc = Document.objects.create( | ||||
|             title="sample test", | ||||
|             correspondent=self.c, | ||||
|             document_type=self.dt, | ||||
|             storage_path=self.sp, | ||||
|             owner=self.user2, | ||||
|             original_filename="sample.pdf", | ||||
|         ) | ||||
|         doc.tags.set([self.t1, self.t2]) | ||||
|         CustomFieldInstance.objects.create(document=doc, field=self.cf1) | ||||
|         doc.save() | ||||
|         assign_perm("documents.view_document", self.user3, doc) | ||||
|         assign_perm("documents.change_document", self.user3, doc) | ||||
|         assign_perm("documents.view_document", self.group1, doc) | ||||
|         assign_perm("documents.change_document", self.group1, doc) | ||||
|  | ||||
|         superuser = User.objects.create_superuser("superuser") | ||||
|         self.client.force_authenticate(user=superuser) | ||||
|  | ||||
|         self.client.patch( | ||||
|             f"/api/documents/{doc.id}/", | ||||
|             {"title": "new title"}, | ||||
|             format="json", | ||||
|         ) | ||||
|         doc.refresh_from_db() | ||||
|  | ||||
|         self.assertIsNone(doc.document_type) | ||||
|         self.assertIsNone(doc.correspondent) | ||||
|         self.assertIsNone(doc.storage_path) | ||||
|         self.assertEqual(doc.tags.all().count(), 0) | ||||
|         self.assertEqual(doc.tags.all().count(), 0) | ||||
|         self.assertIsNone(doc.owner) | ||||
|         self.assertEqual(doc.custom_fields.all().count(), 0) | ||||
|         self.assertFalse(self.user3.has_perm("documents.view_document", doc)) | ||||
|         self.assertFalse(self.user3.has_perm("documents.change_document", doc)) | ||||
|         group_perms: QuerySet = get_groups_with_perms(doc) | ||||
|         self.assertNotIn(self.group1, group_perms) | ||||
|  | ||||
|     @mock.patch("documents.consumer.Consumer.try_consume_file") | ||||
|     def test_removal_action_document_consumed(self, m): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Workflow with assignment and removal actions | ||||
|         WHEN: | ||||
|             - File that matches is consumed | ||||
|         THEN: | ||||
|             - Action removals are applied | ||||
|         """ | ||||
|         trigger = WorkflowTrigger.objects.create( | ||||
|             type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION, | ||||
|             filter_filename="*simple*", | ||||
|         ) | ||||
|         action = WorkflowAction.objects.create( | ||||
|             assign_title="Doc from {correspondent}", | ||||
|             assign_correspondent=self.c, | ||||
|             assign_document_type=self.dt, | ||||
|             assign_storage_path=self.sp, | ||||
|             assign_owner=self.user2, | ||||
|         ) | ||||
|         action.assign_tags.add(self.t1) | ||||
|         action.assign_tags.add(self.t2) | ||||
|         action.assign_tags.add(self.t3) | ||||
|         action.assign_view_users.add(self.user2) | ||||
|         action.assign_view_users.add(self.user3) | ||||
|         action.assign_view_groups.add(self.group1) | ||||
|         action.assign_view_groups.add(self.group2) | ||||
|         action.assign_change_users.add(self.user2) | ||||
|         action.assign_change_users.add(self.user3) | ||||
|         action.assign_change_groups.add(self.group1) | ||||
|         action.assign_change_groups.add(self.group2) | ||||
|         action.assign_custom_fields.add(self.cf1) | ||||
|         action.assign_custom_fields.add(self.cf2) | ||||
|         action.save() | ||||
|  | ||||
|         action2 = WorkflowAction.objects.create( | ||||
|             type=WorkflowAction.WorkflowActionType.REMOVAL, | ||||
|         ) | ||||
|         action2.remove_correspondents.add(self.c) | ||||
|         action2.remove_tags.add(self.t1) | ||||
|         action2.remove_document_types.add(self.dt) | ||||
|         action2.remove_storage_paths.add(self.sp) | ||||
|         action2.remove_owners.add(self.user2) | ||||
|         action2.remove_custom_fields.add(self.cf1) | ||||
|         action2.remove_view_users.add(self.user3) | ||||
|         action2.remove_change_users.add(self.user3) | ||||
|         action2.remove_view_groups.add(self.group1) | ||||
|         action2.remove_change_groups.add(self.group1) | ||||
|         action2.save() | ||||
|  | ||||
|         w = Workflow.objects.create( | ||||
|             name="Workflow 1", | ||||
|             order=0, | ||||
|         ) | ||||
|         w.triggers.add(trigger) | ||||
|         w.actions.add(action) | ||||
|         w.actions.add(action2) | ||||
|         w.save() | ||||
|  | ||||
|         test_file = self.SAMPLE_DIR / "simple.pdf" | ||||
|  | ||||
|         with mock.patch("documents.tasks.ProgressManager", DummyProgressManager): | ||||
|             with self.assertLogs("paperless.matching", level="INFO") as cm: | ||||
|                 tasks.consume_file( | ||||
|                     ConsumableDocument( | ||||
|                         source=DocumentSource.ConsumeFolder, | ||||
|                         original_file=test_file, | ||||
|                     ), | ||||
|                     None, | ||||
|                 ) | ||||
|                 m.assert_called_once() | ||||
|                 _, overrides = m.call_args | ||||
|                 self.assertIsNone(overrides["override_correspondent_id"]) | ||||
|                 self.assertIsNone(overrides["override_document_type_id"]) | ||||
|                 self.assertEqual( | ||||
|                     overrides["override_tag_ids"], | ||||
|                     [self.t2.pk, self.t3.pk], | ||||
|                 ) | ||||
|                 self.assertIsNone(overrides["override_storage_path_id"]) | ||||
|                 self.assertIsNone(overrides["override_owner_id"]) | ||||
|                 self.assertEqual(overrides["override_view_users"], [self.user2.pk]) | ||||
|                 self.assertEqual(overrides["override_view_groups"], [self.group2.pk]) | ||||
|                 self.assertEqual(overrides["override_change_users"], [self.user2.pk]) | ||||
|                 self.assertEqual(overrides["override_change_groups"], [self.group2.pk]) | ||||
|                 self.assertEqual( | ||||
|                     overrides["override_title"], | ||||
|                     "Doc from {correspondent}", | ||||
|                 ) | ||||
|                 self.assertEqual( | ||||
|                     overrides["override_custom_field_ids"], | ||||
|                     [self.cf2.pk], | ||||
|                 ) | ||||
|  | ||||
|         info = cm.output[0] | ||||
|         expected_str = f"Document matched {trigger} from {w}" | ||||
|         self.assertIn(expected_str, info) | ||||
|  | ||||
|     @mock.patch("documents.consumer.Consumer.try_consume_file") | ||||
|     def test_removal_action_document_consumed_removeall(self, m): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Workflow with assignment and removal actions with remove all fields set | ||||
|         WHEN: | ||||
|             - File that matches is consumed | ||||
|         THEN: | ||||
|             - Action removals are applied | ||||
|         """ | ||||
|         trigger = WorkflowTrigger.objects.create( | ||||
|             type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION, | ||||
|             filter_filename="*simple*", | ||||
|         ) | ||||
|         action = WorkflowAction.objects.create( | ||||
|             assign_title="Doc from {correspondent}", | ||||
|             assign_correspondent=self.c, | ||||
|             assign_document_type=self.dt, | ||||
|             assign_storage_path=self.sp, | ||||
|             assign_owner=self.user2, | ||||
|         ) | ||||
|         action.assign_tags.add(self.t1) | ||||
|         action.assign_tags.add(self.t2) | ||||
|         action.assign_tags.add(self.t3) | ||||
|         action.assign_view_users.add(self.user3.pk) | ||||
|         action.assign_view_groups.add(self.group1.pk) | ||||
|         action.assign_change_users.add(self.user3.pk) | ||||
|         action.assign_change_groups.add(self.group1.pk) | ||||
|         action.assign_custom_fields.add(self.cf1.pk) | ||||
|         action.assign_custom_fields.add(self.cf2.pk) | ||||
|         action.save() | ||||
|  | ||||
|         action2 = WorkflowAction.objects.create( | ||||
|             type=WorkflowAction.WorkflowActionType.REMOVAL, | ||||
|             remove_all_correspondents=True, | ||||
|             remove_all_tags=True, | ||||
|             remove_all_document_types=True, | ||||
|             remove_all_storage_paths=True, | ||||
|             remove_all_custom_fields=True, | ||||
|             remove_all_owners=True, | ||||
|             remove_all_permissions=True, | ||||
|         ) | ||||
|  | ||||
|         w = Workflow.objects.create( | ||||
|             name="Workflow 1", | ||||
|             order=0, | ||||
|         ) | ||||
|         w.triggers.add(trigger) | ||||
|         w.actions.add(action) | ||||
|         w.actions.add(action2) | ||||
|         w.save() | ||||
|  | ||||
|         test_file = self.SAMPLE_DIR / "simple.pdf" | ||||
|  | ||||
|         with mock.patch("documents.tasks.ProgressManager", DummyProgressManager): | ||||
|             with self.assertLogs("paperless.matching", level="INFO") as cm: | ||||
|                 tasks.consume_file( | ||||
|                     ConsumableDocument( | ||||
|                         source=DocumentSource.ConsumeFolder, | ||||
|                         original_file=test_file, | ||||
|                     ), | ||||
|                     None, | ||||
|                 ) | ||||
|                 m.assert_called_once() | ||||
|                 _, overrides = m.call_args | ||||
|                 self.assertIsNone(overrides["override_correspondent_id"]) | ||||
|                 self.assertIsNone(overrides["override_document_type_id"]) | ||||
|                 self.assertEqual( | ||||
|                     overrides["override_tag_ids"], | ||||
|                     [], | ||||
|                 ) | ||||
|                 self.assertIsNone(overrides["override_storage_path_id"]) | ||||
|                 self.assertIsNone(overrides["override_owner_id"]) | ||||
|                 self.assertEqual(overrides["override_view_users"], []) | ||||
|                 self.assertEqual(overrides["override_view_groups"], []) | ||||
|                 self.assertEqual(overrides["override_change_users"], []) | ||||
|                 self.assertEqual(overrides["override_change_groups"], []) | ||||
|                 self.assertEqual( | ||||
|                     overrides["override_custom_field_ids"], | ||||
|                     [], | ||||
|                 ) | ||||
|  | ||||
|         info = cm.output[0] | ||||
|         expected_str = f"Document matched {trigger} from {w}" | ||||
|         self.assertIn(expected_str, info) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: paperless-ngx\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2024-02-26 13:34-0800\n" | ||||
| "POT-Creation-Date: 2024-02-27 10:51-0800\n" | ||||
| "PO-Revision-Date: 2022-02-17 04:17\n" | ||||
| "Last-Translator: \n" | ||||
| "Language-Team: English\n" | ||||
| @@ -53,7 +53,7 @@ msgstr "" | ||||
| msgid "Automatic" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:62 documents/models.py:397 documents/models.py:1102 | ||||
| #: documents/models.py:62 documents/models.py:397 documents/models.py:1218 | ||||
| #: paperless_mail/models.py:18 paperless_mail/models.py:93 | ||||
| msgid "name" | ||||
| msgstr "" | ||||
| @@ -687,102 +687,174 @@ msgstr "" | ||||
| msgid "workflow triggers" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1000 | ||||
| #: documents/models.py:1002 | ||||
| msgid "Assignment" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1003 | ||||
| #: documents/models.py:1006 | ||||
| msgid "Removal" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1010 | ||||
| msgid "Workflow Action Type" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1009 | ||||
| #: documents/models.py:1016 | ||||
| msgid "assign title" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1014 | ||||
| #: documents/models.py:1021 | ||||
| msgid "" | ||||
| "Assign a document title, can include some placeholders, see documentation." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1022 paperless_mail/models.py:216 | ||||
| #: documents/models.py:1030 paperless_mail/models.py:216 | ||||
| msgid "assign this tag" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1030 paperless_mail/models.py:224 | ||||
| #: documents/models.py:1039 paperless_mail/models.py:224 | ||||
| msgid "assign this document type" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1038 paperless_mail/models.py:238 | ||||
| #: documents/models.py:1048 paperless_mail/models.py:238 | ||||
| msgid "assign this correspondent" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1046 | ||||
| #: documents/models.py:1057 | ||||
| msgid "assign this storage path" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1055 | ||||
| #: documents/models.py:1066 | ||||
| msgid "assign this owner" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1062 | ||||
| #: documents/models.py:1073 | ||||
| msgid "grant view permissions to these users" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1069 | ||||
| #: documents/models.py:1080 | ||||
| msgid "grant view permissions to these groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1076 | ||||
| #: documents/models.py:1087 | ||||
| msgid "grant change permissions to these users" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1083 | ||||
| #: documents/models.py:1094 | ||||
| msgid "grant change permissions to these groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1090 | ||||
| #: documents/models.py:1101 | ||||
| msgid "assign these custom fields" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1094 | ||||
| msgid "workflow action" | ||||
| #: documents/models.py:1108 | ||||
| msgid "remove these tag(s)" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1095 | ||||
| msgid "workflow actions" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1104 paperless_mail/models.py:95 | ||||
| msgid "order" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1110 | ||||
| msgid "triggers" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1117 | ||||
| msgid "actions" | ||||
| #: documents/models.py:1113 | ||||
| msgid "remove all tags" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1120 | ||||
| msgid "remove these document type(s)" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1125 | ||||
| msgid "remove all document types" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1132 | ||||
| msgid "remove these correspondent(s)" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1137 | ||||
| msgid "remove all correspondents" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1144 | ||||
| msgid "remove these storage path(s)" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1149 | ||||
| msgid "remove all storage paths" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1156 | ||||
| msgid "remove these owner(s)" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1161 | ||||
| msgid "remove all owners" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1168 | ||||
| msgid "remove view permissions for these users" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1175 | ||||
| msgid "remove view permissions for these groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1182 | ||||
| msgid "remove change permissions for these users" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1189 | ||||
| msgid "remove change permissions for these groups" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1194 | ||||
| msgid "remove all permissions" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1201 | ||||
| msgid "remove these custom fields" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1206 | ||||
| msgid "remove all custom fields" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1210 | ||||
| msgid "workflow action" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1211 | ||||
| msgid "workflow actions" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1220 paperless_mail/models.py:95 | ||||
| msgid "order" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1226 | ||||
| msgid "triggers" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1233 | ||||
| msgid "actions" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/models.py:1236 | ||||
| msgid "enabled" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:113 | ||||
| #: documents/serialisers.py:114 | ||||
| #, python-format | ||||
| msgid "Invalid regular expression: %(error)s" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:407 | ||||
| #: documents/serialisers.py:408 | ||||
| msgid "Invalid color." | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:1073 | ||||
| #: documents/serialisers.py:1070 | ||||
| #, python-format | ||||
| msgid "File type %(type)s not supported" | ||||
| msgstr "" | ||||
|  | ||||
| #: documents/serialisers.py:1176 | ||||
| #: documents/serialisers.py:1173 | ||||
| msgid "Invalid variable detected." | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon