mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-10-10 02:16:12 -05:00
Initial crack
This commit is contained in:
@@ -360,10 +360,9 @@ def existing_document_matches_workflow(
|
||||
)
|
||||
trigger_matched = False
|
||||
|
||||
# Document tags vs trigger has_tags
|
||||
if (
|
||||
trigger.filter_has_tags.all().count() > 0
|
||||
and document.tags.filter(
|
||||
# Document tags vs trigger has_tags (any of)
|
||||
if trigger.filter_has_tags.all().count() > 0 and (
|
||||
document.tags.filter(
|
||||
id__in=trigger.filter_has_tags.all().values_list("id"),
|
||||
).count()
|
||||
== 0
|
||||
@@ -374,6 +373,36 @@ def existing_document_matches_workflow(
|
||||
)
|
||||
trigger_matched = False
|
||||
|
||||
# Document tags vs trigger has_all_tags (all of)
|
||||
if trigger.filter_has_all_tags.all().count() > 0 and trigger_matched:
|
||||
required_tag_ids = set(
|
||||
trigger.filter_has_all_tags.all().values_list("id", flat=True),
|
||||
)
|
||||
document_tag_ids = set(
|
||||
document.tags.all().values_list("id", flat=True),
|
||||
)
|
||||
missing_tags = required_tag_ids - document_tag_ids
|
||||
if missing_tags:
|
||||
reason = (
|
||||
f"Document tags {document.tags.all()} do not contain all of"
|
||||
f" {trigger.filter_has_all_tags.all()}",
|
||||
)
|
||||
trigger_matched = False
|
||||
|
||||
# Document tags vs trigger has_not_tags (none of)
|
||||
if (
|
||||
trigger.filter_has_not_tags.all().count() > 0
|
||||
and trigger_matched
|
||||
and document.tags.filter(
|
||||
id__in=trigger.filter_has_not_tags.all().values_list("id"),
|
||||
).exists()
|
||||
):
|
||||
reason = (
|
||||
f"Document tags {document.tags.all()} include excluded tags"
|
||||
f" {trigger.filter_has_not_tags.all()}",
|
||||
)
|
||||
trigger_matched = False
|
||||
|
||||
# Document correspondent vs trigger has_correspondent
|
||||
if (
|
||||
trigger.filter_has_correspondent is not None
|
||||
@@ -438,6 +467,16 @@ def prefilter_documents_by_workflowtrigger(
|
||||
tags__in=trigger.filter_has_tags.all(),
|
||||
).distinct()
|
||||
|
||||
if trigger.filter_has_all_tags.all().count() > 0:
|
||||
for tag_id in trigger.filter_has_all_tags.all().values_list("id", flat=True):
|
||||
documents = documents.filter(tags__id=tag_id)
|
||||
documents = documents.distinct()
|
||||
|
||||
if trigger.filter_has_not_tags.all().count() > 0:
|
||||
documents = documents.exclude(
|
||||
tags__in=trigger.filter_has_not_tags.all(),
|
||||
).distinct()
|
||||
|
||||
if trigger.filter_has_correspondent is not None:
|
||||
documents = documents.filter(
|
||||
correspondent=trigger.filter_has_correspondent,
|
||||
|
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-07 16:09
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1071_tag_tn_ancestors_count_tag_tn_ancestors_pks_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="workflowtrigger",
|
||||
name="filter_has_all_tags",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_all",
|
||||
to="documents.tag",
|
||||
verbose_name="has all of these tag(s)",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workflowtrigger",
|
||||
name="filter_has_not_tags",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_not",
|
||||
to="documents.tag",
|
||||
verbose_name="does not have these tag(s)",
|
||||
),
|
||||
),
|
||||
]
|
@@ -1065,6 +1065,20 @@ class WorkflowTrigger(models.Model):
|
||||
verbose_name=_("has these tag(s)"),
|
||||
)
|
||||
|
||||
filter_has_all_tags = models.ManyToManyField(
|
||||
Tag,
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_all",
|
||||
verbose_name=_("has all of these tag(s)"),
|
||||
)
|
||||
|
||||
filter_has_not_tags = models.ManyToManyField(
|
||||
Tag,
|
||||
blank=True,
|
||||
related_name="workflowtriggers_has_not",
|
||||
verbose_name=_("does not have these tag(s)"),
|
||||
)
|
||||
|
||||
filter_has_document_type = models.ForeignKey(
|
||||
DocumentType,
|
||||
null=True,
|
||||
|
@@ -2194,6 +2194,8 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
|
||||
"match",
|
||||
"is_insensitive",
|
||||
"filter_has_tags",
|
||||
"filter_has_all_tags",
|
||||
"filter_has_not_tags",
|
||||
"filter_has_correspondent",
|
||||
"filter_has_document_type",
|
||||
"filter_has_storage_path",
|
||||
@@ -2414,6 +2416,8 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
if triggers is not None and triggers is not serializers.empty:
|
||||
for trigger in triggers:
|
||||
filter_has_tags = trigger.pop("filter_has_tags", None)
|
||||
filter_has_all_tags = trigger.pop("filter_has_all_tags", None)
|
||||
filter_has_not_tags = trigger.pop("filter_has_not_tags", None)
|
||||
# Convert sources to strings to handle django-multiselectfield v1.0 changes
|
||||
WorkflowTriggerSerializer.normalize_workflow_trigger_sources(trigger)
|
||||
trigger_instance, _ = WorkflowTrigger.objects.update_or_create(
|
||||
@@ -2422,6 +2426,10 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
if filter_has_tags is not None:
|
||||
trigger_instance.filter_has_tags.set(filter_has_tags)
|
||||
if filter_has_all_tags is not None:
|
||||
trigger_instance.filter_has_all_tags.set(filter_has_all_tags)
|
||||
if filter_has_not_tags is not None:
|
||||
trigger_instance.filter_has_not_tags.set(filter_has_not_tags)
|
||||
set_triggers.append(trigger_instance)
|
||||
|
||||
if actions is not None and actions is not serializers.empty:
|
||||
|
@@ -184,6 +184,8 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
"filter_filename": "*",
|
||||
"filter_path": "*/samples/*",
|
||||
"filter_has_tags": [self.t1.id],
|
||||
"filter_has_all_tags": [self.t2.id],
|
||||
"filter_has_not_tags": [self.t3.id],
|
||||
"filter_has_document_type": self.dt.id,
|
||||
"filter_has_correspondent": self.c.id,
|
||||
"filter_has_storage_path": self.sp.id,
|
||||
@@ -223,6 +225,20 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(Workflow.objects.count(), 2)
|
||||
workflow = Workflow.objects.get(name="Workflow 2")
|
||||
trigger = workflow.triggers.first()
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_tags.values_list("id", flat=True)),
|
||||
{self.t1.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_all_tags.values_list("id", flat=True)),
|
||||
{self.t2.id},
|
||||
)
|
||||
self.assertSetEqual(
|
||||
set(trigger.filter_has_not_tags.values_list("id", flat=True)),
|
||||
{self.t3.id},
|
||||
)
|
||||
|
||||
def test_api_create_invalid_workflow_trigger(self):
|
||||
"""
|
||||
@@ -376,6 +392,8 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
"filter_has_tags": [self.t1.id],
|
||||
"filter_has_all_tags": [self.t2.id],
|
||||
"filter_has_not_tags": [self.t3.id],
|
||||
"filter_has_correspondent": self.c.id,
|
||||
"filter_has_document_type": self.dt.id,
|
||||
},
|
||||
@@ -393,6 +411,14 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
workflow = Workflow.objects.get(id=response.data["id"])
|
||||
self.assertEqual(workflow.name, "Workflow Updated")
|
||||
self.assertEqual(workflow.triggers.first().filter_has_tags.first(), self.t1)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_all_tags.first(),
|
||||
self.t2,
|
||||
)
|
||||
self.assertEqual(
|
||||
workflow.triggers.first().filter_has_not_tags.first(),
|
||||
self.t3,
|
||||
)
|
||||
self.assertEqual(workflow.actions.first().assign_title, "Action New Title")
|
||||
|
||||
def test_api_update_workflow_no_trigger_actions(self):
|
||||
|
@@ -1083,6 +1083,82 @@ class TestWorkflows(
|
||||
expected_str = f"Document tags {doc.tags.all()} do not include {trigger.filter_has_tags.all()}"
|
||||
self.assertIn(expected_str, cm.output[1])
|
||||
|
||||
def test_document_added_no_match_all_tags(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
)
|
||||
trigger.filter_has_all_tags.set([self.t1, self.t2])
|
||||
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",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
doc.tags.set([self.t1])
|
||||
doc.save()
|
||||
|
||||
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 tags {doc.tags.all()} do not contain all of"
|
||||
f" {trigger.filter_has_all_tags.all()}"
|
||||
)
|
||||
self.assertIn(expected_str, cm.output[1])
|
||||
|
||||
def test_document_added_excluded_tags(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
)
|
||||
trigger.filter_has_not_tags.set([self.t3])
|
||||
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",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
doc.tags.set([self.t3])
|
||||
doc.save()
|
||||
|
||||
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 tags {doc.tags.all()} include excluded tags"
|
||||
f" {trigger.filter_has_not_tags.all()}"
|
||||
)
|
||||
self.assertIn(expected_str, cm.output[1])
|
||||
|
||||
def test_document_added_no_match_doctype(self):
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
|
Reference in New Issue
Block a user