mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-09-14 21:45:37 -05:00
Enhancement: add storage path as workflow trigger filter (#10771)
--------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -408,7 +408,7 @@ Currently, there are three events that correspond to workflow trigger 'types':
|
||||
but the document content has been extracted and metadata such as document type, tags, etc. have been set, so these can now
|
||||
be used for filtering.
|
||||
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
|
||||
tags, doc type, or correspondent.
|
||||
tags, doc type, correspondent or storage path.
|
||||
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
|
||||
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive
|
||||
offsets will trigger after the date, negative offsets will trigger before).
|
||||
@@ -452,10 +452,11 @@ Workflows allow you to filter by:
|
||||
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
|
||||
example, automatically assigning documents to different owners based on the upload directory.
|
||||
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
|
||||
- Content matching (`Added` and `Updated` triggers only). Filter document content using the matching settings.
|
||||
- Tags (`Added` and `Updated` triggers only). Filter for documents with any of the specified tags
|
||||
- Document type (`Added` and `Updated` triggers only). Filter documents with this doc type
|
||||
- Correspondent (`Added` and `Updated` triggers only). Filter documents with this correspondent
|
||||
- Content matching (`Added`, `Updated` and `Scheduled` triggers only). Filter document content using the matching settings.
|
||||
- Tags (`Added`, `Updated` and `Scheduled` triggers only). Filter for documents with any of the specified tags
|
||||
- Document type (`Added`, `Updated` and `Scheduled` triggers only). Filter documents with this doc type
|
||||
- Correspondent (`Added`, `Updated` and `Scheduled` triggers only). Filter documents with this correspondent
|
||||
- Storage path (`Added`, `Updated` and `Scheduled` triggers only). Filter documents with this storage path
|
||||
|
||||
### Workflow Actions
|
||||
|
||||
|
@@ -177,6 +177,7 @@
|
||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Has any of tags" formControlName="filter_has_tags"></pngx-input-tags>
|
||||
<pngx-input-select i18n-title title="Has correspondent" [items]="correspondents" [allowNull]="true" formControlName="filter_has_correspondent"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Has document type" [items]="documentTypes" [allowNull]="true" formControlName="filter_has_document_type"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Has storage path" [items]="storagePaths" [allowNull]="true" formControlName="filter_has_storage_path"></pngx-input-select>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@@ -412,6 +412,9 @@ export class WorkflowEditDialogComponent
|
||||
filter_has_document_type: new FormControl(
|
||||
trigger.filter_has_document_type
|
||||
),
|
||||
filter_has_storage_path: new FormControl(
|
||||
trigger.filter_has_storage_path
|
||||
),
|
||||
schedule_offset_days: new FormControl(trigger.schedule_offset_days),
|
||||
schedule_is_recurring: new FormControl(trigger.schedule_is_recurring),
|
||||
schedule_recurring_interval_days: new FormControl(
|
||||
@@ -536,6 +539,7 @@ export class WorkflowEditDialogComponent
|
||||
filter_has_tags: [],
|
||||
filter_has_correspondent: null,
|
||||
filter_has_document_type: null,
|
||||
filter_has_storage_path: null,
|
||||
matching_algorithm: MATCH_NONE,
|
||||
match: '',
|
||||
is_insensitive: true,
|
||||
|
@@ -44,6 +44,8 @@ export interface WorkflowTrigger extends ObjectWithId {
|
||||
|
||||
filter_has_document_type?: number // DocumentType.id
|
||||
|
||||
filter_has_storage_path?: number // StoragePath.id
|
||||
|
||||
schedule_offset_days?: number
|
||||
|
||||
schedule_is_recurring?: boolean
|
||||
|
@@ -386,6 +386,16 @@ def existing_document_matches_workflow(
|
||||
)
|
||||
trigger_matched = False
|
||||
|
||||
# Document storage_path vs trigger has_storage_path
|
||||
if (
|
||||
trigger.filter_has_storage_path is not None
|
||||
and document.storage_path != trigger.filter_has_storage_path
|
||||
):
|
||||
reason = (
|
||||
f"Document storage path {document.storage_path} does not match {trigger.filter_has_storage_path}",
|
||||
)
|
||||
trigger_matched = False
|
||||
|
||||
# Document original_filename vs trigger filename
|
||||
if (
|
||||
trigger.filter_filename is not None
|
||||
@@ -430,6 +440,11 @@ def prefilter_documents_by_workflowtrigger(
|
||||
document_type=trigger.filter_has_document_type,
|
||||
)
|
||||
|
||||
if trigger.filter_has_storage_path is not None:
|
||||
documents = documents.filter(
|
||||
storage_path=trigger.filter_has_storage_path,
|
||||
)
|
||||
|
||||
if trigger.filter_filename is not None and len(trigger.filter_filename) > 0:
|
||||
# the true fnmatch will actually run later so we just want a loose filter here
|
||||
regex = fnmatch_translate(trigger.filter_filename).lstrip("^").rstrip("$")
|
||||
|
@@ -1,51 +0,0 @@
|
||||
# Generated by Django 5.2.5 on 2025-08-27 22:02
|
||||
import logging
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
from django.db import transaction
|
||||
|
||||
from documents.templating.utils import convert_format_str_to_template_format
|
||||
|
||||
logger = logging.getLogger("paperless.migrations")
|
||||
|
||||
|
||||
def convert_from_format_to_template(apps, schema_editor):
|
||||
WorkflowActions = apps.get_model("documents", "WorkflowAction")
|
||||
|
||||
with transaction.atomic():
|
||||
for WorkflowAction in WorkflowActions.objects.all():
|
||||
WorkflowAction.assign_title = convert_format_str_to_template_format(
|
||||
WorkflowAction.assign_title,
|
||||
)
|
||||
logger.debug(
|
||||
"Converted WorkflowAction id %d title to template format: %s",
|
||||
WorkflowAction.id,
|
||||
WorkflowAction.assign_title,
|
||||
)
|
||||
WorkflowAction.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1068_alter_document_created"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="WorkflowAction",
|
||||
name="assign_title",
|
||||
field=models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=(
|
||||
"Assign a document title, can be a JINJA2 template, "
|
||||
"see documentation.",
|
||||
),
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
convert_from_format_to_template,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
]
|
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-11 17:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1068_alter_document_created"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="workflowtrigger",
|
||||
name="filter_has_storage_path",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="documents.storagepath",
|
||||
verbose_name="has this storage path",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="workflowaction",
|
||||
name="assign_title",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="Assign a document title, must be a Jinja2 template, see documentation.",
|
||||
null=True,
|
||||
verbose_name="assign title",
|
||||
),
|
||||
),
|
||||
]
|
@@ -1044,6 +1044,14 @@ class WorkflowTrigger(models.Model):
|
||||
verbose_name=_("has this correspondent"),
|
||||
)
|
||||
|
||||
filter_has_storage_path = models.ForeignKey(
|
||||
StoragePath,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("has this storage path"),
|
||||
)
|
||||
|
||||
schedule_offset_days = models.IntegerField(
|
||||
_("schedule offset days"),
|
||||
default=0,
|
||||
|
@@ -2054,6 +2054,7 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
|
||||
"filter_has_tags",
|
||||
"filter_has_correspondent",
|
||||
"filter_has_document_type",
|
||||
"filter_has_storage_path",
|
||||
"schedule_offset_days",
|
||||
"schedule_is_recurring",
|
||||
"schedule_recurring_interval_days",
|
||||
|
@@ -186,6 +186,7 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
"filter_has_tags": [self.t1.id],
|
||||
"filter_has_document_type": self.dt.id,
|
||||
"filter_has_correspondent": self.c.id,
|
||||
"filter_has_storage_path": self.sp.id,
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
|
@@ -1150,6 +1150,38 @@ class TestWorkflows(
|
||||
expected_str = f"Document correspondent {doc.correspondent} does not match {trigger.filter_has_correspondent}"
|
||||
self.assertIn(expected_str, cm.output[1])
|
||||
|
||||
def test_document_added_no_match_storage_path(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
filter_has_storage_path=self.sp,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
assign_title="Doc assign owner",
|
||||
assign_owner=self.user2,
|
||||
)
|
||||
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",
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||
document_consumption_finished.send(
|
||||
sender=self.__class__,
|
||||
document=doc,
|
||||
)
|
||||
expected_str = f"Document did not match {w}"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
expected_str = f"Document storage path {doc.storage_path} does not match {trigger.filter_has_storage_path}"
|
||||
self.assertIn(expected_str, cm.output[1])
|
||||
|
||||
def test_document_added_invalid_title_placeholders(self):
|
||||
"""
|
||||
GIVEN:
|
||||
@@ -1816,6 +1848,7 @@ class TestWorkflows(
|
||||
filter_filename="*sample*",
|
||||
filter_has_document_type=self.dt,
|
||||
filter_has_correspondent=self.c,
|
||||
filter_has_storage_path=self.sp,
|
||||
)
|
||||
trigger.filter_has_tags.set([self.t1])
|
||||
trigger.save()
|
||||
@@ -1836,6 +1869,7 @@ class TestWorkflows(
|
||||
title=f"sample test {i}",
|
||||
checksum=f"checksum{i}",
|
||||
correspondent=self.c,
|
||||
storage_path=self.sp,
|
||||
original_filename=f"sample_{i}.pdf",
|
||||
document_type=self.dt if i % 2 == 0 else None,
|
||||
)
|
||||
|
Reference in New Issue
Block a user