mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-24 00:59:35 -06:00
Compare commits
1 Commits
dev
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9ee564673 |
@@ -700,11 +700,15 @@ src/documents/signals/handlers.py:0: error: Function is missing a type annotatio
|
||||
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
||||
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
||||
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
||||
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
|
||||
src/documents/signals/handlers.py:0: error: Incompatible return value type (got "tuple[DocumentMetadataOverrides | None, str]", expected "tuple[DocumentMetadataOverrides, str] | None") [return-value]
|
||||
src/documents/signals/handlers.py:0: error: Incompatible types in assignment (expression has type "list[Tag]", variable has type "set[Tag]") [assignment]
|
||||
src/documents/signals/handlers.py:0: error: Incompatible types in assignment (expression has type "tuple[Any, Any, Any]", variable has type "tuple[Any, Any]") [assignment]
|
||||
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "refresh_from_db" [union-attr]
|
||||
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "save" [union-attr]
|
||||
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "source_path" [union-attr]
|
||||
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "tags" [union-attr]
|
||||
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "tags" [union-attr]
|
||||
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "title" [union-attr]
|
||||
src/documents/signals/handlers.py:0: error: Item "None" of "Any | None" has no attribute "get" [union-attr]
|
||||
src/documents/signals/handlers.py:0: error: Item "None" of "Any | None" has no attribute "get" [union-attr]
|
||||
|
||||
@@ -30,7 +30,7 @@ RUN set -eux \
|
||||
# Purpose: Installs s6-overlay and rootfs
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here either
|
||||
FROM ghcr.io/astral-sh/uv:0.10.0-python3.12-trixie-slim AS s6-overlay-base
|
||||
FROM ghcr.io/astral-sh/uv:0.10.5-python3.12-trixie-slim AS s6-overlay-base
|
||||
|
||||
WORKDIR /usr/src/s6
|
||||
|
||||
|
||||
@@ -564,18 +564,6 @@ For security reasons, webhooks can be limited to specific ports and disallowed f
|
||||
[configuration settings](configuration.md#workflow-webhooks) to change this behavior. If you are allowing non-admins to create workflows,
|
||||
you may want to adjust these settings to prevent abuse.
|
||||
|
||||
##### Move to Trash {#workflow-action-move-to-trash}
|
||||
|
||||
"Move to Trash" actions move the document to the trash. The document can be restored
|
||||
from the trash until the trash is emptied (after the configured delay or manually).
|
||||
|
||||
The "Move to Trash" action will always be executed at the end of the workflow run,
|
||||
regardless of its position in the action list. After a "Move to Trash" action is executed
|
||||
no other workflow will be executed on the document.
|
||||
|
||||
If a "Move to Trash" action is executed in a consume pipeline, the consumption
|
||||
will be aborted and the file will be deleted.
|
||||
|
||||
#### Workflow placeholders
|
||||
|
||||
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
||||
|
||||
@@ -5355,13 +5355,6 @@
|
||||
<context context-type="linenumber">445</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7902569198692046993" datatype="html">
|
||||
<source>The document will be moved to the trash at the end of the workflow run.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">454</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4626030417479279989" datatype="html">
|
||||
<source>Consume Folder</source>
|
||||
<context-group purpose="location">
|
||||
@@ -5464,124 +5457,109 @@
|
||||
<context context-type="linenumber">144</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2048798344356757326" datatype="html">
|
||||
<source>Move to trash</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">148</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">1087</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||
<context context-type="linenumber">760</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4522609911791833187" datatype="html">
|
||||
<source>Has any of these tags</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">217</context>
|
||||
<context context-type="linenumber">213</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4166903555074156852" datatype="html">
|
||||
<source>Has all of these tags</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">224</context>
|
||||
<context context-type="linenumber">220</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6624363795312783141" datatype="html">
|
||||
<source>Does not have these tags</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">231</context>
|
||||
<context context-type="linenumber">227</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7168528512669831184" datatype="html">
|
||||
<source>Has any of these correspondents</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">238</context>
|
||||
<context context-type="linenumber">234</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5281365940563983618" datatype="html">
|
||||
<source>Has correspondent</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">246</context>
|
||||
<context context-type="linenumber">242</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6884498632428600393" datatype="html">
|
||||
<source>Does not have correspondents</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">254</context>
|
||||
<context context-type="linenumber">250</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4806713133917046341" datatype="html">
|
||||
<source>Has document type</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">262</context>
|
||||
<context context-type="linenumber">258</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8801397520369995032" datatype="html">
|
||||
<source>Has any of these document types</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">270</context>
|
||||
<context context-type="linenumber">266</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1507843981661822403" datatype="html">
|
||||
<source>Does not have document types</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">278</context>
|
||||
<context context-type="linenumber">274</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4277260190522078330" datatype="html">
|
||||
<source>Has storage path</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">286</context>
|
||||
<context context-type="linenumber">282</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8858580062214623097" datatype="html">
|
||||
<source>Has any of these storage paths</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">294</context>
|
||||
<context context-type="linenumber">290</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6070943364927280151" datatype="html">
|
||||
<source>Does not have storage paths</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">302</context>
|
||||
<context context-type="linenumber">298</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6250799006816371860" datatype="html">
|
||||
<source>Matches custom field query</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">310</context>
|
||||
<context context-type="linenumber">306</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3138206142174978019" datatype="html">
|
||||
<source>Create new workflow</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">539</context>
|
||||
<context context-type="linenumber">535</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5996779210524133604" datatype="html">
|
||||
<source>Edit workflow</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||
<context context-type="linenumber">543</context>
|
||||
<context context-type="linenumber">539</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5457837313196342910" datatype="html">
|
||||
@@ -7795,6 +7773,17 @@
|
||||
<context context-type="linenumber">758</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2048798344356757326" datatype="html">
|
||||
<source>Move to trash</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||
<context context-type="linenumber">1087</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||
<context context-type="linenumber">760</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7295637485862454066" datatype="html">
|
||||
<source>Error deleting document</source>
|
||||
<context-group purpose="location">
|
||||
|
||||
@@ -448,13 +448,6 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case (WorkflowActionType.MoveToTrash) {
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p class="text-muted small" i18n>The document will be moved to the trash at the end of the workflow run.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -143,10 +143,6 @@ export const WORKFLOW_ACTION_OPTIONS = [
|
||||
id: WorkflowActionType.PasswordRemoval,
|
||||
name: $localize`Password removal`,
|
||||
},
|
||||
{
|
||||
id: WorkflowActionType.MoveToTrash,
|
||||
name: $localize`Move to trash`,
|
||||
},
|
||||
]
|
||||
|
||||
export enum TriggerFilterType {
|
||||
|
||||
@@ -6,7 +6,6 @@ export enum WorkflowActionType {
|
||||
Email = 3,
|
||||
Webhook = 4,
|
||||
PasswordRemoval = 5,
|
||||
MoveToTrash = 6,
|
||||
}
|
||||
|
||||
export interface WorkflowActionEmail extends ObjectWithId {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-14 19:19
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "0011_optimize_integer_field_sizes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="workflowaction",
|
||||
name="type",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, "Assignment"),
|
||||
(2, "Removal"),
|
||||
(3, "Email"),
|
||||
(4, "Webhook"),
|
||||
(5, "Password removal"),
|
||||
(6, "Move to trash"),
|
||||
],
|
||||
default=1,
|
||||
verbose_name="Workflow Action Type",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1409,10 +1409,6 @@ class WorkflowAction(models.Model):
|
||||
5,
|
||||
_("Password removal"),
|
||||
)
|
||||
MOVE_TO_TRASH = (
|
||||
6,
|
||||
_("Move to trash"),
|
||||
)
|
||||
|
||||
type = models.PositiveSmallIntegerField(
|
||||
_("Workflow Action Type"),
|
||||
|
||||
@@ -48,7 +48,6 @@ from documents.permissions import get_objects_for_user_owner_aware
|
||||
from documents.templating.utils import convert_format_str_to_template_format
|
||||
from documents.workflows.actions import build_workflow_action_context
|
||||
from documents.workflows.actions import execute_email_action
|
||||
from documents.workflows.actions import execute_move_to_trash_action
|
||||
from documents.workflows.actions import execute_password_removal_action
|
||||
from documents.workflows.actions import execute_webhook_action
|
||||
from documents.workflows.mutations import apply_assignment_to_document
|
||||
@@ -59,8 +58,6 @@ from documents.workflows.utils import get_workflows_for_trigger
|
||||
from paperless.config import AIConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import uuid
|
||||
|
||||
from documents.classifier import DocumentClassifier
|
||||
from documents.data_models import ConsumableDocument
|
||||
from documents.data_models import DocumentMetadataOverrides
|
||||
@@ -730,7 +727,7 @@ def add_to_index(sender, document, **kwargs) -> None:
|
||||
def run_workflows_added(
|
||||
sender,
|
||||
document: Document,
|
||||
logging_group: uuid.UUID | None = None,
|
||||
logging_group=None,
|
||||
original_file=None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
@@ -746,7 +743,7 @@ def run_workflows_added(
|
||||
def run_workflows_updated(
|
||||
sender,
|
||||
document: Document,
|
||||
logging_group: uuid.UUID | None = None,
|
||||
logging_group=None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
run_workflows(
|
||||
@@ -760,7 +757,7 @@ def run_workflows(
|
||||
trigger_type: WorkflowTrigger.WorkflowTriggerType,
|
||||
document: Document | ConsumableDocument,
|
||||
workflow_to_run: Workflow | None = None,
|
||||
logging_group: uuid.UUID | None = None,
|
||||
logging_group=None,
|
||||
overrides: DocumentMetadataOverrides | None = None,
|
||||
original_file: Path | None = None,
|
||||
) -> tuple[DocumentMetadataOverrides, str] | None:
|
||||
@@ -786,33 +783,14 @@ def run_workflows(
|
||||
|
||||
for workflow in workflows:
|
||||
if not use_overrides:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(document, Document)
|
||||
try:
|
||||
# This can be called from bulk_update_documents, which may be running multiple times
|
||||
# Refresh this so the matching data is fresh and instance fields are re-freshed
|
||||
# Otherwise, this instance might be behind and overwrite the work another process did
|
||||
document.refresh_from_db()
|
||||
doc_tag_ids = list(document.tags.values_list("pk", flat=True))
|
||||
except Document.DoesNotExist:
|
||||
# Document was hard deleted by a previous workflow or another process
|
||||
logger.info(
|
||||
"Document no longer exists, skipping remaining workflows",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
break
|
||||
|
||||
# Check if document was soft deleted (moved to trash)
|
||||
if document.is_deleted:
|
||||
logger.info(
|
||||
"Document was moved to trash, skipping remaining workflows",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
break
|
||||
|
||||
if matching.document_matches_workflow(document, workflow, trigger_type):
|
||||
action: WorkflowAction
|
||||
has_move_to_trash_action = False
|
||||
for action in workflow.actions.order_by("order", "pk"):
|
||||
message = f"Applying {action} from {workflow}"
|
||||
if not use_overrides:
|
||||
@@ -856,8 +834,6 @@ def run_workflows(
|
||||
)
|
||||
elif action.type == WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL:
|
||||
execute_password_removal_action(action, document, logging_group)
|
||||
elif action.type == WorkflowAction.WorkflowActionType.MOVE_TO_TRASH:
|
||||
has_move_to_trash_action = True
|
||||
|
||||
if not use_overrides:
|
||||
# limit title to 128 characters
|
||||
@@ -872,12 +848,7 @@ def run_workflows(
|
||||
document=document if not use_overrides else None,
|
||||
)
|
||||
|
||||
if has_move_to_trash_action:
|
||||
execute_move_to_trash_action(action, document, logging_group)
|
||||
|
||||
if use_overrides:
|
||||
if TYPE_CHECKING:
|
||||
assert overrides is not None
|
||||
return overrides, "\n".join(messages)
|
||||
|
||||
|
||||
|
||||
@@ -896,210 +896,3 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
"Passwords are required",
|
||||
str(response.data["non_field_errors"][0]),
|
||||
)
|
||||
|
||||
def test_trash_action_validation(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a workflow with a trash action
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP response
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 2",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 3",
|
||||
"order": 2,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_trash_action_as_last_action_valid(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a workflow with multiple actions
|
||||
- Move to trash action is the last action
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Workflow is created successfully
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow with Move to Trash Last",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||
"assign_title": "Assigned Title",
|
||||
},
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.REMOVAL,
|
||||
"remove_all_tags": True,
|
||||
},
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_update_workflow_add_trash_at_end_valid(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing workflow without trash action
|
||||
WHEN:
|
||||
- PATCH to add trash action at end
|
||||
THEN:
|
||||
- HTTP 200 success
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow to Add Move to Trash",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||
"assign_title": "First Action",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
workflow_id = response.data["id"]
|
||||
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}{workflow_id}/",
|
||||
json.dumps(
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||
"assign_title": "First Action",
|
||||
},
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_update_workflow_remove_trash_action_valid(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing workflow with trash action
|
||||
WHEN:
|
||||
- PATCH to remove trash action
|
||||
THEN:
|
||||
- HTTP 200 success
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow to Remove move to trash",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||
"assign_title": "First Action",
|
||||
},
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
workflow_id = response.data["id"]
|
||||
|
||||
response = self.client.patch(
|
||||
f"{self.ENDPOINT}{workflow_id}/",
|
||||
json.dumps(
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||
"assign_title": "Only Action",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@@ -3,11 +3,9 @@ import json
|
||||
import shutil
|
||||
import socket
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@@ -57,7 +55,6 @@ from documents.models import WorkflowActionEmail
|
||||
from documents.models import WorkflowActionWebhook
|
||||
from documents.models import WorkflowRun
|
||||
from documents.models import WorkflowTrigger
|
||||
from documents.plugins.base import StopConsumeTaskError
|
||||
from documents.serialisers import WorkflowTriggerSerializer
|
||||
from documents.signals import document_consumption_finished
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
@@ -3917,427 +3914,6 @@ class TestWorkflows(
|
||||
)
|
||||
assert mock_remove_password.call_count == 2
|
||||
|
||||
def test_workflow_trash_action_soft_delete(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with delete action
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
THEN:
|
||||
- Document is moved to trash (soft deleted)
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
)
|
||||
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,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
self.assertEqual(Document.deleted_objects.count(), 0)
|
||||
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
self.assertEqual(Document.deleted_objects.count(), 1)
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("django.core.mail.message.EmailMessage.send")
|
||||
def test_workflow_trash_with_email_action(self, mock_email_send):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow with email action, then move to trash action
|
||||
WHEN:
|
||||
- Document matches and workflow runs
|
||||
THEN:
|
||||
- Email is sent first
|
||||
- Document is moved to trash (soft deleted)
|
||||
"""
|
||||
mock_email_send.return_value = 1
|
||||
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
email_action = WorkflowActionEmail.objects.create(
|
||||
subject="Document deleted: {doc_title}",
|
||||
body="Document {doc_title} will be deleted",
|
||||
to="user@example.com",
|
||||
include_document=False,
|
||||
)
|
||||
email_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.EMAIL,
|
||||
email=email_action,
|
||||
)
|
||||
trash_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow with email then move to trash",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(email_workflow_action, trash_workflow_action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
self.assertEqual(Document.deleted_objects.count(), 0)
|
||||
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
mock_email_send.assert_called_once()
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
self.assertEqual(Document.deleted_objects.count(), 1)
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("documents.workflows.webhooks.send_webhook.delay")
|
||||
def test_workflow_trash_with_webhook_action(self, mock_webhook_delay):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow with webhook action (include_document=True), then move to trash action
|
||||
WHEN:
|
||||
- Document matches and workflow runs
|
||||
THEN:
|
||||
- Webhook .delay() is called with complete data including file bytes
|
||||
- Document is moved to trash (soft deleted)
|
||||
- Webhook task has all necessary data and doesn't rely on document existence
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
webhook_action = WorkflowActionWebhook.objects.create(
|
||||
use_params=True,
|
||||
params={
|
||||
"title": "{{doc_title}}",
|
||||
"message": "Document being deleted",
|
||||
},
|
||||
url="https://paperless-ngx.com/webhook",
|
||||
include_document=True,
|
||||
)
|
||||
webhook_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
webhook=webhook_action,
|
||||
)
|
||||
trash_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow with webhook then move to trash",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(webhook_workflow_action, trash_workflow_action)
|
||||
w.save()
|
||||
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
self.dirs.scratch_dir / "simple.pdf",
|
||||
)
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="simple.pdf",
|
||||
filename=test_file,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
self.assertEqual(Document.deleted_objects.count(), 0)
|
||||
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
mock_webhook_delay.assert_called_once()
|
||||
call_kwargs = mock_webhook_delay.call_args[1]
|
||||
self.assertEqual(call_kwargs["url"], "https://paperless-ngx.com/webhook")
|
||||
self.assertEqual(
|
||||
call_kwargs["data"],
|
||||
{"title": "sample test", "message": "Document being deleted"},
|
||||
)
|
||||
self.assertIsNotNone(call_kwargs["files"])
|
||||
self.assertIn("file", call_kwargs["files"])
|
||||
self.assertEqual(call_kwargs["files"]["file"][0], "simple.pdf")
|
||||
self.assertEqual(call_kwargs["files"]["file"][2], "application/pdf")
|
||||
self.assertIsInstance(call_kwargs["files"]["file"][1], bytes)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
self.assertEqual(Document.deleted_objects.count(), 1)
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("django.core.mail.message.EmailMessage.send")
|
||||
def test_workflow_trash_after_email_failure(self, mock_email_send) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow with email action (that fails), then move to trash action
|
||||
WHEN:
|
||||
- Document matches and workflow runs
|
||||
- Email action raises exception
|
||||
THEN:
|
||||
- Email failure is logged
|
||||
- Move to Trash still executes successfully (soft delete)
|
||||
"""
|
||||
mock_email_send.side_effect = Exception("Email server error")
|
||||
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
email_action = WorkflowActionEmail.objects.create(
|
||||
subject="Document deleted: {doc_title}",
|
||||
body="Document {doc_title} will be deleted",
|
||||
to="user@example.com",
|
||||
include_document=False,
|
||||
)
|
||||
email_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.EMAIL,
|
||||
email=email_action,
|
||||
)
|
||||
trash_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow with failing email then move to trash",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(email_workflow_action, trash_workflow_action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
self.assertEqual(Document.deleted_objects.count(), 0)
|
||||
|
||||
with self.assertLogs("paperless.workflows.actions", level="ERROR") as cm:
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
expected_str = "Error occurred sending notification email"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
self.assertEqual(Document.deleted_objects.count(), 1)
|
||||
|
||||
def test_multiple_workflows_trash_then_assignment(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow 1 (order=0) with move to trash action
|
||||
- Workflow 2 (order=1) with assignment action
|
||||
- Both workflows match the same document
|
||||
WHEN:
|
||||
- Workflows run sequentially
|
||||
THEN:
|
||||
- First workflow runs and deletes document (soft delete)
|
||||
- Second workflow does not trigger (document no longer exists)
|
||||
- Logs confirm move to trash and skipping of remaining workflows
|
||||
"""
|
||||
trigger1 = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
trash_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
)
|
||||
w1 = Workflow.objects.create(
|
||||
name="Workflow 1 - Move to Trash",
|
||||
order=0,
|
||||
)
|
||||
w1.triggers.add(trigger1)
|
||||
w1.actions.add(trash_workflow_action)
|
||||
w1.save()
|
||||
|
||||
trigger2 = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
assignment_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||
assign_correspondent=self.c2,
|
||||
)
|
||||
w2 = Workflow.objects.create(
|
||||
name="Workflow 2 - Assignment",
|
||||
order=1,
|
||||
)
|
||||
w2.triggers.add(trigger2)
|
||||
w2.actions.add(assignment_action)
|
||||
w2.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 1)
|
||||
self.assertEqual(Document.deleted_objects.count(), 0)
|
||||
|
||||
with self.assertLogs("paperless", level="DEBUG") as cm:
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
self.assertEqual(Document.deleted_objects.count(), 1)
|
||||
|
||||
# We check logs instead of WorkflowRun.objects.count() because when the document
|
||||
# is soft-deleted, the WorkflowRun is cascade-deleted (hard delete) since it does
|
||||
# not inherit from the SoftDeleteModel. The logs confirm that the first workflow
|
||||
# executed the move to trash and remaining workflows were skipped.
|
||||
log_output = "\n".join(cm.output)
|
||||
self.assertIn("Moved document", log_output)
|
||||
self.assertIn("to trash", log_output)
|
||||
self.assertIn(
|
||||
"Document was moved to trash, skipping remaining workflows",
|
||||
log_output,
|
||||
)
|
||||
|
||||
def test_workflow_delete_action_during_consumption(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow with consumption trigger and delete action
|
||||
WHEN:
|
||||
- Document is being consumed and workflow runs
|
||||
THEN:
|
||||
- StopConsumeTaskError is raised to halt consumption
|
||||
- Original file is deleted
|
||||
- No document is created
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
sources=f"{DocumentSource.ConsumeFolder}",
|
||||
filter_filename="*",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow Delete During Consumption",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
# Create a test file to be consumed
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
self.dirs.scratch_dir / "simple.pdf",
|
||||
)
|
||||
test_file_path = Path(test_file)
|
||||
self.assertTrue(test_file_path.exists())
|
||||
|
||||
# Create a ConsumableDocument
|
||||
consumable_doc = ConsumableDocument(
|
||||
source=DocumentSource.ConsumeFolder,
|
||||
original_file=test_file_path,
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
|
||||
# Run workflows with overrides (consumption flow)
|
||||
with self.assertRaises(StopConsumeTaskError) as context:
|
||||
run_workflows(
|
||||
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
consumable_doc,
|
||||
overrides=DocumentMetadataOverrides(),
|
||||
)
|
||||
|
||||
self.assertIn("deleted by workflow action", str(context.exception))
|
||||
|
||||
# File should be deleted
|
||||
self.assertFalse(test_file_path.exists())
|
||||
|
||||
# No document should be created
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
|
||||
def test_workflow_delete_action_during_consumption_with_assignment(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Workflow with consumption trigger, assignment action, then delete action
|
||||
WHEN:
|
||||
- Document is being consumed and workflow runs
|
||||
THEN:
|
||||
- StopConsumeTaskError is raised to halt consumption
|
||||
- Original file is deleted
|
||||
- No document is created (even though assignment would have worked)
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
sources=f"{DocumentSource.ConsumeFolder}",
|
||||
filter_filename="*",
|
||||
)
|
||||
assignment_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.ASSIGNMENT,
|
||||
assign_title="This should not be applied",
|
||||
assign_correspondent=self.c,
|
||||
)
|
||||
trash_workflow_action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow Assignment then Delete During Consumption",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(assignment_action, trash_workflow_action)
|
||||
w.save()
|
||||
|
||||
# Create a test file to be consumed
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
self.dirs.scratch_dir / "simple2.pdf",
|
||||
)
|
||||
test_file_path = Path(test_file)
|
||||
self.assertTrue(test_file_path.exists())
|
||||
|
||||
# Create a ConsumableDocument
|
||||
consumable_doc = ConsumableDocument(
|
||||
source=DocumentSource.ConsumeFolder,
|
||||
original_file=test_file_path,
|
||||
)
|
||||
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
|
||||
# Run workflows with overrides (consumption flow)
|
||||
with self.assertRaises(StopConsumeTaskError):
|
||||
run_workflows(
|
||||
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
consumable_doc,
|
||||
overrides=DocumentMetadataOverrides(),
|
||||
)
|
||||
|
||||
# File should be deleted
|
||||
self.assertFalse(test_file_path.exists())
|
||||
|
||||
# No document should be created
|
||||
self.assertEqual(Document.objects.count(), 0)
|
||||
|
||||
|
||||
class TestWebhookSend:
|
||||
def test_send_webhook_data_or_json(
|
||||
@@ -4380,17 +3956,13 @@ class TestWebhookSend:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resolve_to(monkeypatch: pytest.MonkeyPatch) -> Callable[[str], None]:
|
||||
def resolve_to(monkeypatch):
|
||||
"""
|
||||
Force DNS resolution to a specific IP for any hostname.
|
||||
"""
|
||||
|
||||
def _set(ip: str) -> None:
|
||||
def fake_getaddrinfo(
|
||||
host: str,
|
||||
*_args: object,
|
||||
**_kwargs: object,
|
||||
) -> list[tuple[Any, ...]]:
|
||||
def _set(ip: str):
|
||||
def fake_getaddrinfo(host, *_args, **_kwargs):
|
||||
return [(socket.AF_INET, None, None, "", (ip, 0))]
|
||||
|
||||
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
|
||||
@@ -4531,7 +4103,7 @@ class TestWebhookSecurity:
|
||||
def test_strips_user_supplied_host_header(
|
||||
self,
|
||||
httpx_mock: HTTPXMock,
|
||||
resolve_to: Callable[[str], None],
|
||||
resolve_to,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
@@ -4597,7 +4169,7 @@ class TestDateWorkflowLocalization(
|
||||
self,
|
||||
title_template: str,
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document added workflow with title template using localize_date filter
|
||||
@@ -4662,7 +4234,7 @@ class TestDateWorkflowLocalization(
|
||||
self,
|
||||
title_template: str,
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with title template using localize_date filter
|
||||
@@ -4738,7 +4310,7 @@ class TestDateWorkflowLocalization(
|
||||
settings: SettingsWrapper,
|
||||
title_template: str,
|
||||
expected_title: str,
|
||||
) -> None:
|
||||
):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
sources=f"{DocumentSource.ApiUpload}",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
@@ -16,7 +15,6 @@ from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import WorkflowAction
|
||||
from documents.models import WorkflowTrigger
|
||||
from documents.plugins.base import StopConsumeTaskError
|
||||
from documents.signals import document_consumption_finished
|
||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||
from documents.workflows.webhooks import send_webhook
|
||||
@@ -340,33 +338,3 @@ def execute_password_removal_action(
|
||||
document.pk,
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
|
||||
def execute_move_to_trash_action(
|
||||
action: WorkflowAction,
|
||||
document: Document | ConsumableDocument,
|
||||
logging_group: uuid.UUID | None,
|
||||
) -> None:
|
||||
"""
|
||||
Execute a move to trash action for a workflow on an existing document or a
|
||||
document in consumption. In case of an existing document it soft-deletes
|
||||
the document. In case of consumption it aborts consumption and deletes the
|
||||
file.
|
||||
"""
|
||||
if isinstance(document, Document):
|
||||
document.delete()
|
||||
logger.debug(
|
||||
f"Moved document {document} to trash",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
else:
|
||||
if document.original_file.exists():
|
||||
document.original_file.unlink()
|
||||
logger.info(
|
||||
f"Workflow move to trash action triggered during consumption, "
|
||||
f"deleting file {document.original_file}",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
raise StopConsumeTaskError(
|
||||
"Document deleted by workflow action during consumption",
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-24 00:43+0000\n"
|
||||
"POT-Creation-Date: 2026-02-16 17:32+0000\n"
|
||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
@@ -89,7 +89,7 @@ msgstr ""
|
||||
msgid "Automatic"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:66 documents/models.py:444 documents/models.py:1663
|
||||
#: documents/models.py:66 documents/models.py:444 documents/models.py:1659
|
||||
#: paperless_mail/models.py:23 paperless_mail/models.py:143
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
@@ -252,7 +252,7 @@ msgid "The position of this document in your physical document archive."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:313 documents/models.py:688 documents/models.py:742
|
||||
#: documents/models.py:1706
|
||||
#: documents/models.py:1702
|
||||
msgid "document"
|
||||
msgstr ""
|
||||
|
||||
@@ -1093,197 +1093,193 @@ msgid "Password removal"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1414
|
||||
msgid "Move to trash"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1418
|
||||
msgid "Workflow Action Type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1423 documents/models.py:1665
|
||||
#: documents/models.py:1419 documents/models.py:1661
|
||||
#: paperless_mail/models.py:145
|
||||
msgid "order"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1426
|
||||
#: documents/models.py:1422
|
||||
msgid "assign title"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1430
|
||||
#: documents/models.py:1426
|
||||
msgid "Assign a document title, must be a Jinja2 template, see documentation."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1438 paperless_mail/models.py:274
|
||||
#: documents/models.py:1434 paperless_mail/models.py:274
|
||||
msgid "assign this tag"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1447 paperless_mail/models.py:282
|
||||
#: documents/models.py:1443 paperless_mail/models.py:282
|
||||
msgid "assign this document type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1456 paperless_mail/models.py:296
|
||||
#: documents/models.py:1452 paperless_mail/models.py:296
|
||||
msgid "assign this correspondent"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1465
|
||||
#: documents/models.py:1461
|
||||
msgid "assign this storage path"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1474
|
||||
#: documents/models.py:1470
|
||||
msgid "assign this owner"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1481
|
||||
#: documents/models.py:1477
|
||||
msgid "grant view permissions to these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1488
|
||||
#: documents/models.py:1484
|
||||
msgid "grant view permissions to these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1495
|
||||
#: documents/models.py:1491
|
||||
msgid "grant change permissions to these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1502
|
||||
#: documents/models.py:1498
|
||||
msgid "grant change permissions to these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1509
|
||||
#: documents/models.py:1505
|
||||
msgid "assign these custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1513
|
||||
#: documents/models.py:1509
|
||||
msgid "custom field values"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1517
|
||||
#: documents/models.py:1513
|
||||
msgid "Optional values to assign to the custom fields."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1526
|
||||
#: documents/models.py:1522
|
||||
msgid "remove these tag(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1531
|
||||
#: documents/models.py:1527
|
||||
msgid "remove all tags"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1538
|
||||
#: documents/models.py:1534
|
||||
msgid "remove these document type(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1543
|
||||
#: documents/models.py:1539
|
||||
msgid "remove all document types"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1550
|
||||
#: documents/models.py:1546
|
||||
msgid "remove these correspondent(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1555
|
||||
#: documents/models.py:1551
|
||||
msgid "remove all correspondents"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1562
|
||||
#: documents/models.py:1558
|
||||
msgid "remove these storage path(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1567
|
||||
#: documents/models.py:1563
|
||||
msgid "remove all storage paths"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1574
|
||||
#: documents/models.py:1570
|
||||
msgid "remove these owner(s)"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1579
|
||||
#: documents/models.py:1575
|
||||
msgid "remove all owners"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1586
|
||||
#: documents/models.py:1582
|
||||
msgid "remove view permissions for these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1593
|
||||
#: documents/models.py:1589
|
||||
msgid "remove view permissions for these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1600
|
||||
#: documents/models.py:1596
|
||||
msgid "remove change permissions for these users"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1607
|
||||
#: documents/models.py:1603
|
||||
msgid "remove change permissions for these groups"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1612
|
||||
#: documents/models.py:1608
|
||||
msgid "remove all permissions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1619
|
||||
#: documents/models.py:1615
|
||||
msgid "remove these custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1624
|
||||
#: documents/models.py:1620
|
||||
msgid "remove all custom fields"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1633
|
||||
#: documents/models.py:1629
|
||||
msgid "email"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1642
|
||||
#: documents/models.py:1638
|
||||
msgid "webhook"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1646
|
||||
#: documents/models.py:1642
|
||||
msgid "passwords"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1650
|
||||
#: documents/models.py:1646
|
||||
msgid ""
|
||||
"Passwords to try when removing PDF protection. Separate with commas or new "
|
||||
"lines."
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1655
|
||||
#: documents/models.py:1651
|
||||
msgid "workflow action"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1656
|
||||
#: documents/models.py:1652
|
||||
msgid "workflow actions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1671
|
||||
#: documents/models.py:1667
|
||||
msgid "triggers"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1678
|
||||
#: documents/models.py:1674
|
||||
msgid "actions"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1681 paperless_mail/models.py:154
|
||||
#: documents/models.py:1677 paperless_mail/models.py:154
|
||||
msgid "enabled"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1692
|
||||
#: documents/models.py:1688
|
||||
msgid "workflow"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1696
|
||||
#: documents/models.py:1692
|
||||
msgid "workflow trigger type"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1710
|
||||
#: documents/models.py:1706
|
||||
msgid "date run"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1716
|
||||
#: documents/models.py:1712
|
||||
msgid "workflow run"
|
||||
msgstr ""
|
||||
|
||||
#: documents/models.py:1717
|
||||
#: documents/models.py:1713
|
||||
msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user