div {
cursor: not-allowed;
}
diff --git a/src-ui/src/app/data/workflow-action.ts b/src-ui/src/app/data/workflow-action.ts
index a0da5f03a..ff64d19b3 100644
--- a/src-ui/src/app/data/workflow-action.ts
+++ b/src-ui/src/app/data/workflow-action.ts
@@ -2,6 +2,7 @@ import { ObjectWithId } from './object-with-id'
export enum WorkflowActionType {
Assignment = 1,
+ Removal = 2,
}
export interface WorkflowAction extends ObjectWithId {
type: WorkflowActionType
@@ -27,4 +28,38 @@ export interface WorkflowAction extends ObjectWithId {
assign_change_groups?: number[] // [Group.id]
assign_custom_fields?: number[] // [CustomField.id]
+
+ remove_tags?: number[] // Tag.id
+
+ remove_all_tags?: boolean
+
+ remove_document_types?: number[] // [DocumentType.id]
+
+ remove_all_document_types?: boolean
+
+ remove_correspondents?: number[] // [Correspondent.id]
+
+ remove_all_correspondents?: boolean
+
+ remove_storage_paths?: number[] // [StoragePath.id]
+
+ remove_all_storage_paths?: boolean
+
+ remove_owners?: number[] // [User.id]
+
+ remove_all_owners?: boolean
+
+ remove_view_users?: number[] // [User.id]
+
+ remove_view_groups?: number[] // [Group.id]
+
+ remove_change_users?: number[] // [User.id]
+
+ remove_change_groups?: number[] // [Group.id]
+
+ remove_all_permissions?: boolean
+
+ remove_custom_fields?: number[] // [CustomField.id]
+
+ remove_all_custom_fields?: boolean
}
diff --git a/src/documents/consumer.py b/src/documents/consumer.py
index 93b41e60e..3b783cae9 100644
--- a/src/documents/consumer.py
+++ b/src/documents/consumer.py
@@ -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):
diff --git a/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py b/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py
new file mode 100644
index 000000000..6ce5da958
--- /dev/null
+++ b/src/documents/migrations/1046_workflowaction_remove_all_correspondents_and_more.py
@@ -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",
+ ),
+ ),
+ ]
diff --git a/src/documents/models.py b/src/documents/models.py
index 6dc24c801..8e7a16a60 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -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")
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index adcb0d251..5ea0e21c8 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -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)
diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py
index b6903d98c..85e8126c1 100644
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -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()
diff --git a/src/documents/tests/test_api_workflows.py b/src/documents/tests/test_api_workflows.py
index 0751d0df5..7f48347c0 100644
--- a/src/documents/tests/test_api_workflows.py
+++ b/src/documents/tests/test_api_workflows.py
@@ -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],
+ },
],
},
),
diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py
index 95f903239..509a8e54d 100644
--- a/src/documents/tests/test_workflows.py
+++ b/src/documents/tests/test_workflows.py
@@ -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)
diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po
index 0689b523c..4d56bdeb3 100644
--- a/src/locale/en_US/LC_MESSAGES/django.po
+++ b/src/locale/en_US/LC_MESSAGES/django.po
@@ -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 ""