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 ""
From aa0da2f516d7c928ccedc40516360cbac8759204 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 Mar 2024 13:15:03 -0800
Subject: [PATCH 03/24] Chore(deps): Bump the django group with 1 update
(#6000)
Bumps the django group with 1 update: [django](https://github.com/django/django).
Updates `django` from 4.2.10 to 4.2.11
- [Commits](https://github.com/django/django/compare/4.2.10...4.2.11)
---
updated-dependencies:
- dependency-name: django
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: django
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Pipfile | 2 +-
Pipfile.lock | 14 +++++++-------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/Pipfile b/Pipfile
index 281f44e8b..b5656439f 100644
--- a/Pipfile
+++ b/Pipfile
@@ -7,7 +7,7 @@ name = "pypi"
dateparser = "~=1.2"
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
-django = "~=4.2.10"
+django = "~=4.2.11"
django-allauth = "*"
django-auditlog = "*"
django-celery-results = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 8705b5d7f..18e0a7561 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "cdd620926150607295aa42447d26c89d2a372309e3229c4f8f711ebe0ed19670"
+ "sha256": "e88ab81dcd44e04defdc7efbbd6e2c5e1e522b3cdb580419569fc433c00139ef"
},
"pipfile-spec": 6,
"requires": {},
@@ -463,12 +463,12 @@
},
"django": {
"hashes": [
- "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1",
- "sha256:b1260ed381b10a11753c73444408e19869f3241fc45c985cd55a30177c789d13"
+ "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4",
+ "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==4.2.10"
+ "version": "==4.2.11"
},
"django-allauth": {
"hashes": [
@@ -1959,11 +1959,11 @@
},
"typing-extensions": {
"hashes": [
- "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783",
- "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"
+ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
+ "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
],
"markers": "python_version < '3.11'",
- "version": "==4.9.0"
+ "version": "==4.10.0"
},
"tzdata": {
"hashes": [
From 2fa742c94bbec57998ad1fb91c091fadc2fc8397 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 Mar 2024 22:08:13 +0000
Subject: [PATCH 04/24] Chore(deps-dev): Bump the development group with 2
updates (#5998)
* Chore(deps-dev): Bump the development group with 2 updates
Bumps the development group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [mkdocs-material](https://github.com/squidfunk/mkdocs-material).
Updates `ruff` from 0.2.2 to 0.3.0
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.2.2...v0.3.0)
Updates `mkdocs-material` from 9.5.11 to 9.5.12
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.11...9.5.12)
---
updated-dependencies:
- dependency-name: ruff
dependency-type: direct:development
update-type: version-update:semver-minor
dependency-group: development
- dependency-name: mkdocs-material
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: development
...
Signed-off-by: dependabot[bot]
* Updates ruff hook version as well
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
Pipfile.lock | 48 ++++++++++++++++++++---------------------
2 files changed, 25 insertions(+), 25 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d17836e81..4ee14f187 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -47,7 +47,7 @@ repos:
exclude: "(^Pipfile\\.lock$)"
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.2.2'
+ rev: 'v0.3.0'
hooks:
- id: ruff
- repo: https://github.com/psf/black-pre-commit-mirror
diff --git a/Pipfile.lock b/Pipfile.lock
index 18e0a7561..93d69e86e 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -3052,12 +3052,12 @@
},
"mkdocs-material": {
"hashes": [
- "sha256:788ee0f3e036dca2dc20298d65e480297d348a44c9d7b2ee05c5262983e66072",
- "sha256:7af7f8af0dea16175558f3fb9245d26c83a17199baa5f157755e63d7437bf971"
+ "sha256:5f69cef6a8aaa4050b812f72b1094fda3d079b9a51cf27a247244c03ec455e97",
+ "sha256:d6f0c269f015e48c76291cdc79efb70f7b33bbbf42d649cfe475522ebee61b1f"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==9.5.11"
+ "version": "==9.5.12"
},
"mkdocs-material-extensions": {
"hashes": [
@@ -3365,11 +3365,11 @@
},
"python-dateutil": {
"hashes": [
- "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
- "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
+ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.8.2"
+ "version": "==2.9.0.post0"
},
"pywavelets": {
"hashes": [
@@ -3576,27 +3576,27 @@
},
"ruff": {
"hashes": [
- "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6",
- "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e",
- "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c",
- "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9",
- "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e",
- "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3",
- "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba",
- "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001",
- "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726",
- "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e",
- "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd",
- "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d",
- "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39",
- "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325",
- "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d",
- "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73",
- "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"
+ "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a",
+ "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f",
+ "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b",
+ "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77",
+ "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb",
+ "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932",
+ "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933",
+ "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2",
+ "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944",
+ "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a",
+ "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49",
+ "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4",
+ "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e",
+ "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e",
+ "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f",
+ "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19",
+ "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==0.2.2"
+ "version": "==0.3.0"
},
"scipy": {
"hashes": [
From 6379e7b54fa54f2b869c7dc7645dde538feb2a68 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 Mar 2024 22:27:59 +0000
Subject: [PATCH 05/24] Chore(deps): Bump the small-changes group with 3
updates (#6001)
Bumps the small-changes group with 3 updates: [python-dateutil](https://github.com/dateutil/dateutil), [python-ipware](https://github.com/un33k/python-ipware) and [redis](https://github.com/redis/redis-py).
Updates `python-dateutil` from 2.8.2 to 2.9.0.post0
- [Release notes](https://github.com/dateutil/dateutil/releases)
- [Changelog](https://github.com/dateutil/dateutil/blob/master/NEWS)
- [Commits](https://github.com/dateutil/dateutil/compare/2.8.2...2.9.0.post0)
Updates `python-ipware` from 2.0.1 to 2.0.2
- [Changelog](https://github.com/un33k/python-ipware/blob/main/CHANGELOG.md)
- [Commits](https://github.com/un33k/python-ipware/compare/v2.0.1...v2.0.2)
Updates `redis` from 5.0.1 to 5.0.2
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v5.0.1...v5.0.2)
---
updated-dependencies:
- dependency-name: python-dateutil
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: small-changes
- dependency-name: python-ipware
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: small-changes
- dependency-name: redis
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: small-changes
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Pipfile.lock | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/Pipfile.lock b/Pipfile.lock
index 93d69e86e..e171f1748 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -43,7 +43,7 @@
"sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f",
"sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"
],
- "markers": "python_full_version <= '3.11.2'",
+ "markers": "python_version >= '3.7'",
"version": "==4.0.3"
},
"billiard": {
@@ -1376,12 +1376,12 @@
},
"python-dateutil": {
"hashes": [
- "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
- "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
+ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==2.8.2"
+ "version": "==2.9.0.post0"
},
"python-dotenv": {
"hashes": [
@@ -1402,12 +1402,12 @@
},
"python-ipware": {
"hashes": [
- "sha256:1992920ef553165dfa35e6ea5a90762f64f3b943cc22ee6b4ec02e2c86d31178",
- "sha256:9ba4805152ebb85ad5b53797185cd1ce6231e1db60155834f326c8cd61e8af34"
+ "sha256:3a94fd073b93e12b13617e291f13eda3495d3ba68b580e3e30174ea84ac63041",
+ "sha256:86e30cc3af62cec42284dedd49c8a14e436e73c96433c8645ec0b476ff4ad7ec"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==2.0.1"
+ "version": "==2.0.2"
},
"python-magic": {
"hashes": [
@@ -1599,11 +1599,12 @@
"hiredis"
],
"hashes": [
- "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f",
- "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"
+ "sha256:3f82cc80d350e93042c8e6e7a5d0596e4dd68715babffba79492733e1f367037",
+ "sha256:4caa8e1fcb6f3c0ef28dba99535101d80934b7d4cd541bbb47f4a3826ee472d1"
],
+ "index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==5.0.1"
+ "version": "==5.0.2"
},
"regex": {
"hashes": [
From 6779042242a76b579bd9dfa3924900c17c04a153 Mon Sep 17 00:00:00 2001
From: Trenton H <797416+stumpylog@users.noreply.github.com>
Date: Mon, 4 Mar 2024 14:37:36 -0800
Subject: [PATCH 06/24] Feature: Allow a user to disable the pixel limit for
OCR entirely (#5996)
---
docs/advanced_usage.md | 2 +-
docs/configuration.md | 4 +++-
...plicationconfiguration_max_image_pixels.py | 24 +++++++++++++++++++
src/paperless/models.py | 2 +-
src/paperless_tesseract/parsers.py | 23 +++++++++---------
5 files changed, 40 insertions(+), 15 deletions(-)
create mode 100644 src/paperless/migrations/0003_alter_applicationconfiguration_max_image_pixels.py
diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md
index d4ff80f87..863be639b 100644
--- a/docs/advanced_usage.md
+++ b/docs/advanced_usage.md
@@ -437,7 +437,7 @@ with Prometheus, as it exports metrics. For details on its capabilities,
refer to the [Flower](https://flower.readthedocs.io/en/latest/index.html)
documentation.
-Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration/#PAPERLESS_ENABLE_FLOWER).
+Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration.md#PAPERLESS_ENABLE_FLOWER).
To configure Flower further, create a `flowerconfig.py` and
place it into the `src/paperless` directory. For a Docker
installation, you can use volumes to accomplish this:
diff --git a/docs/configuration.md b/docs/configuration.md
index 5fd14caf1..c7b710c66 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -766,6 +766,8 @@ but could result in missing text content.
If unset, will default to the value determined by
[Pillow](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.MAX_IMAGE_PIXELS).
+ Setting this value to 0 will entirely disable the limit. See the below warning.
+
!!! note
Increasing this limit could cause Paperless to consume additional
@@ -775,7 +777,7 @@ but could result in missing text content.
!!! warning
The limit is intended to prevent malicious files from consuming
- system resources and causing crashes and other errors. Only increase
+ system resources and causing crashes and other errors. Only change
this value if you are certain your documents are not malicious and
you need the text which was not OCRed
diff --git a/src/paperless/migrations/0003_alter_applicationconfiguration_max_image_pixels.py b/src/paperless/migrations/0003_alter_applicationconfiguration_max_image_pixels.py
new file mode 100644
index 000000000..c27feefb3
--- /dev/null
+++ b/src/paperless/migrations/0003_alter_applicationconfiguration_max_image_pixels.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.10 on 2024-03-04 17:30
+
+import django.core.validators
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("paperless", "0002_applicationconfiguration_app_logo_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="applicationconfiguration",
+ name="max_image_pixels",
+ field=models.FloatField(
+ null=True,
+ validators=[django.core.validators.MinValueValidator(0.0)],
+ verbose_name="Sets the maximum image size for decompression",
+ ),
+ ),
+ ]
diff --git a/src/paperless/models.py b/src/paperless/models.py
index 72805dc56..1f6cfbced 100644
--- a/src/paperless/models.py
+++ b/src/paperless/models.py
@@ -151,7 +151,7 @@ class ApplicationConfiguration(AbstractSingletonModel):
max_image_pixels = models.FloatField(
verbose_name=_("Sets the maximum image size for decompression"),
null=True,
- validators=[MinValueValidator(1_000_000.0)],
+ validators=[MinValueValidator(0.0)],
)
color_conversion_strategy = models.CharField(
diff --git a/src/paperless_tesseract/parsers.py b/src/paperless_tesseract/parsers.py
index 09086585e..020922703 100644
--- a/src/paperless_tesseract/parsers.py
+++ b/src/paperless_tesseract/parsers.py
@@ -293,20 +293,19 @@ class RasterisedDocumentParser(DocumentParser):
f"they will not be used. Error: {e}",
)
- if self.settings.max_image_pixel is not None:
+ if (
+ self.settings.max_image_pixel is not None
+ and self.settings.max_image_pixel >= 0
+ ):
# Convert pixels to mega-pixels and provide to ocrmypdf
max_pixels_mpixels = self.settings.max_image_pixel / 1_000_000.0
- if max_pixels_mpixels > 0:
- self.log.debug(
- f"Calculated {max_pixels_mpixels} megapixels for OCR",
- )
-
- ocrmypdf_args["max_image_mpixels"] = max_pixels_mpixels
- else:
- self.log.warning(
- "There is an issue with PAPERLESS_OCR_MAX_IMAGE_PIXELS, "
- "this value must be at least 1 megapixel if set",
- )
+ msg = (
+ "OCR pixel limit is disabled!"
+ if max_pixels_mpixels == 0
+ else f"Calculated {max_pixels_mpixels} megapixels for OCR"
+ )
+ self.log.debug(msg)
+ ocrmypdf_args["max_image_mpixels"] = max_pixels_mpixels
return ocrmypdf_args
From 00a8f0cd6e648442eba50f14620ab833adce7858 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 4 Mar 2024 15:21:48 -0800
Subject: [PATCH 07/24] Enhancement: show ID when editing objects (#6003)
---
src-ui/messages.xlf | 448 +++++++++---------
.../correspondent-edit-dialog.component.html | 3 +
.../custom-field-edit-dialog.component.html | 3 +
.../document-type-edit-dialog.component.html | 3 +
.../group-edit-dialog.component.html | 3 +
.../mail-account-edit-dialog.component.html | 3 +
.../mail-rule-edit-dialog.component.html | 3 +
.../storage-path-edit-dialog.component.html | 3 +
.../tag-edit-dialog.component.html | 3 +
.../user-edit-dialog.component.html | 3 +
.../workflow-edit-dialog.component.html | 7 +-
11 files changed, 256 insertions(+), 226 deletions(-)
diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf
index 2c883b5ad..c12984c35 100644
--- a/src-ui/messages.xlf
+++ b/src-ui/messages.xlf
@@ -2,15 +2,15 @@
-
- Close
+
+ HH
node_modules/src/ngb-config.ts
13
-
- HH
+
+ Close
node_modules/src/ngb-config.ts
13
@@ -23,13 +23,6 @@
13
-
Select month
@@ -41,6 +34,13 @@
13
+
Previous month
@@ -145,15 +145,15 @@
13
-
- Increment hours
+
-