From 0a92e0fee8bd7c0f2d4e374ffdcfc9d45cb6d82d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:25:58 -0800 Subject: [PATCH] Enhancement: Add 'any of' workflow trigger filters --- .../workflow-edit-dialog.component.spec.ts | 49 ++++++++- .../workflow-edit-dialog.component.ts | 72 ++++++++++++ src-ui/src/app/data/workflow-trigger.ts | 6 + src/documents/matching.py | 49 +++++++++ ..._filter_has_any_correspondents_and_more.py | 43 ++++++++ src/documents/models.py | 21 ++++ src/documents/serialisers.py | 27 +++++ src/documents/tests/test_api_workflows.py | 30 +++++ src/documents/tests/test_workflows.py | 103 ++++++++++++++++++ 9 files changed, 398 insertions(+), 2 deletions(-) create mode 100644 src/documents/migrations/1075_workflowtrigger_filter_has_any_correspondents_and_more.py diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts index 0736e2215..8ec7bcfb4 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.spec.ts @@ -416,6 +416,9 @@ describe('WorkflowEditDialogComponent', () => { return newFilter } + const correspondentAny = addFilterOfType(TriggerFilterType.CorrespondentAny) + correspondentAny.get('values').setValue([11]) + const correspondentIs = addFilterOfType(TriggerFilterType.CorrespondentIs) correspondentIs.get('values').setValue(1) @@ -425,12 +428,18 @@ describe('WorkflowEditDialogComponent', () => { const documentTypeIs = addFilterOfType(TriggerFilterType.DocumentTypeIs) documentTypeIs.get('values').setValue(1) + const documentTypeAny = addFilterOfType(TriggerFilterType.DocumentTypeAny) + documentTypeAny.get('values').setValue([12]) + const documentTypeNot = addFilterOfType(TriggerFilterType.DocumentTypeNot) documentTypeNot.get('values').setValue([1]) const storagePathIs = addFilterOfType(TriggerFilterType.StoragePathIs) storagePathIs.get('values').setValue(1) + const storagePathAny = addFilterOfType(TriggerFilterType.StoragePathAny) + storagePathAny.get('values').setValue([13]) + const storagePathNot = addFilterOfType(TriggerFilterType.StoragePathNot) storagePathNot.get('values').setValue([1]) @@ -445,10 +454,13 @@ describe('WorkflowEditDialogComponent', () => { expect(formValues.triggers[0].filter_has_tags).toEqual([1]) expect(formValues.triggers[0].filter_has_all_tags).toEqual([2, 3]) expect(formValues.triggers[0].filter_has_not_tags).toEqual([4]) + expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([11]) expect(formValues.triggers[0].filter_has_correspondent).toEqual(1) expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([1]) + expect(formValues.triggers[0].filter_has_any_document_types).toEqual([12]) expect(formValues.triggers[0].filter_has_document_type).toEqual(1) expect(formValues.triggers[0].filter_has_not_document_types).toEqual([1]) + expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([13]) expect(formValues.triggers[0].filter_has_storage_path).toEqual(1) expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([1]) expect(formValues.triggers[0].filter_custom_field_query).toEqual( @@ -511,16 +523,22 @@ describe('WorkflowEditDialogComponent', () => { setFilter(TriggerFilterType.TagsAll, 11) setFilter(TriggerFilterType.TagsNone, 12) + setFilter(TriggerFilterType.CorrespondentAny, 16) setFilter(TriggerFilterType.CorrespondentNot, 13) + setFilter(TriggerFilterType.DocumentTypeAny, 17) setFilter(TriggerFilterType.DocumentTypeNot, 14) + setFilter(TriggerFilterType.StoragePathAny, 18) setFilter(TriggerFilterType.StoragePathNot, 15) const formValues = component['getFormValues']() expect(formValues.triggers[0].filter_has_all_tags).toEqual([11]) expect(formValues.triggers[0].filter_has_not_tags).toEqual([12]) + expect(formValues.triggers[0].filter_has_any_correspondents).toEqual([16]) expect(formValues.triggers[0].filter_has_not_correspondents).toEqual([13]) + expect(formValues.triggers[0].filter_has_any_document_types).toEqual([17]) expect(formValues.triggers[0].filter_has_not_document_types).toEqual([14]) + expect(formValues.triggers[0].filter_has_any_storage_paths).toEqual([18]) expect(formValues.triggers[0].filter_has_not_storage_paths).toEqual([15]) }) @@ -644,8 +662,11 @@ describe('WorkflowEditDialogComponent', () => { filter_has_tags: [], filter_has_all_tags: [], filter_has_not_tags: [], + filter_has_any_correspondents: [], filter_has_not_correspondents: [], + filter_has_any_document_types: [], filter_has_not_document_types: [], + filter_has_any_storage_paths: [], filter_has_not_storage_paths: [], filter_has_correspondent: null, filter_has_document_type: null, @@ -703,11 +724,14 @@ describe('WorkflowEditDialogComponent', () => { trigger.filter_has_tags = [1] trigger.filter_has_all_tags = [2, 3] trigger.filter_has_not_tags = [4] + trigger.filter_has_any_correspondents = [10] as any trigger.filter_has_correspondent = 5 as any trigger.filter_has_not_correspondents = [6] as any trigger.filter_has_document_type = 7 as any + trigger.filter_has_any_document_types = [11] as any trigger.filter_has_not_document_types = [8] as any trigger.filter_has_storage_path = 9 as any + trigger.filter_has_any_storage_paths = [12] as any trigger.filter_has_not_storage_paths = [10] as any trigger.filter_custom_field_query = JSON.stringify([ 'AND', @@ -718,8 +742,8 @@ describe('WorkflowEditDialogComponent', () => { component.ngOnInit() const triggerGroup = component.triggerFields.at(0) as FormGroup const filters = component.getFiltersFormArray(triggerGroup) - expect(filters.length).toBe(10) - const customFieldFilter = filters.at(9) as FormGroup + expect(filters.length).toBe(13) + const customFieldFilter = filters.at(12) as FormGroup expect(customFieldFilter.get('type').value).toBe( TriggerFilterType.CustomFieldQuery ) @@ -728,12 +752,27 @@ describe('WorkflowEditDialogComponent', () => { }) it('should expose select metadata helpers', () => { + expect(component.isSelectMultiple(TriggerFilterType.CorrespondentAny)).toBe( + true + ) expect(component.isSelectMultiple(TriggerFilterType.CorrespondentNot)).toBe( true ) expect(component.isSelectMultiple(TriggerFilterType.CorrespondentIs)).toBe( false ) + expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeAny)).toBe( + true + ) + expect(component.isSelectMultiple(TriggerFilterType.DocumentTypeIs)).toBe( + false + ) + expect(component.isSelectMultiple(TriggerFilterType.StoragePathAny)).toBe( + true + ) + expect(component.isSelectMultiple(TriggerFilterType.StoragePathIs)).toBe( + false + ) component.correspondents = [{ id: 1, name: 'C1' } as any] component.documentTypes = [{ id: 2, name: 'DT' } as any] @@ -745,9 +784,15 @@ describe('WorkflowEditDialogComponent', () => { expect( component.getFilterSelectItems(TriggerFilterType.DocumentTypeIs) ).toEqual(component.documentTypes) + expect( + component.getFilterSelectItems(TriggerFilterType.DocumentTypeAny) + ).toEqual(component.documentTypes) expect( component.getFilterSelectItems(TriggerFilterType.StoragePathIs) ).toEqual(component.storagePaths) + expect( + component.getFilterSelectItems(TriggerFilterType.StoragePathAny) + ).toEqual(component.storagePaths) expect(component.getFilterSelectItems(TriggerFilterType.TagsAll)).toEqual( [] ) diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts index f6d9e60f5..c637e95ca 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts @@ -145,10 +145,13 @@ export enum TriggerFilterType { TagsAny = 'tags_any', TagsAll = 'tags_all', TagsNone = 'tags_none', + CorrespondentAny = 'correspondent_any', CorrespondentIs = 'correspondent_is', CorrespondentNot = 'correspondent_not', + DocumentTypeAny = 'document_type_any', DocumentTypeIs = 'document_type_is', DocumentTypeNot = 'document_type_not', + StoragePathAny = 'storage_path_any', StoragePathIs = 'storage_path_is', StoragePathNot = 'storage_path_not', CustomFieldQuery = 'custom_field_query', @@ -172,8 +175,11 @@ type TriggerFilterAggregate = { filter_has_tags: number[] filter_has_all_tags: number[] filter_has_not_tags: number[] + filter_has_any_correspondents: number[] filter_has_not_correspondents: number[] + filter_has_any_document_types: number[] filter_has_not_document_types: number[] + filter_has_any_storage_paths: number[] filter_has_not_storage_paths: number[] filter_has_correspondent: number | null filter_has_document_type: number | null @@ -219,6 +225,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [ allowMultipleEntries: false, allowMultipleValues: true, }, + { + id: TriggerFilterType.CorrespondentAny, + name: $localize`Has any of these correspondents`, + inputType: 'select', + allowMultipleEntries: false, + allowMultipleValues: true, + selectItems: 'correspondents', + }, { id: TriggerFilterType.CorrespondentIs, name: $localize`Has correspondent`, @@ -243,6 +257,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [ allowMultipleValues: false, selectItems: 'documentTypes', }, + { + id: TriggerFilterType.DocumentTypeAny, + name: $localize`Has any of these document types`, + inputType: 'select', + allowMultipleEntries: false, + allowMultipleValues: true, + selectItems: 'documentTypes', + }, { id: TriggerFilterType.DocumentTypeNot, name: $localize`Does not have document types`, @@ -259,6 +281,14 @@ const TRIGGER_FILTER_DEFINITIONS: TriggerFilterDefinition[] = [ allowMultipleValues: false, selectItems: 'storagePaths', }, + { + id: TriggerFilterType.StoragePathAny, + name: $localize`Has any of these storage paths`, + inputType: 'select', + allowMultipleEntries: false, + allowMultipleValues: true, + selectItems: 'storagePaths', + }, { id: TriggerFilterType.StoragePathNot, name: $localize`Does not have storage paths`, @@ -306,6 +336,15 @@ const FILTER_HANDLERS: Record = { extract: (trigger) => trigger.filter_has_not_tags, hasValue: (value) => Array.isArray(value) && value.length > 0, }, + [TriggerFilterType.CorrespondentAny]: { + apply: (aggregate, values) => { + aggregate.filter_has_any_correspondents = Array.isArray(values) + ? [...values] + : [values] + }, + extract: (trigger) => trigger.filter_has_any_correspondents, + hasValue: (value) => Array.isArray(value) && value.length > 0, + }, [TriggerFilterType.CorrespondentIs]: { apply: (aggregate, values) => { aggregate.filter_has_correspondent = Array.isArray(values) @@ -333,6 +372,15 @@ const FILTER_HANDLERS: Record = { extract: (trigger) => trigger.filter_has_document_type, hasValue: (value) => value !== null && value !== undefined, }, + [TriggerFilterType.DocumentTypeAny]: { + apply: (aggregate, values) => { + aggregate.filter_has_any_document_types = Array.isArray(values) + ? [...values] + : [values] + }, + extract: (trigger) => trigger.filter_has_any_document_types, + hasValue: (value) => Array.isArray(value) && value.length > 0, + }, [TriggerFilterType.DocumentTypeNot]: { apply: (aggregate, values) => { aggregate.filter_has_not_document_types = Array.isArray(values) @@ -351,6 +399,15 @@ const FILTER_HANDLERS: Record = { extract: (trigger) => trigger.filter_has_storage_path, hasValue: (value) => value !== null && value !== undefined, }, + [TriggerFilterType.StoragePathAny]: { + apply: (aggregate, values) => { + aggregate.filter_has_any_storage_paths = Array.isArray(values) + ? [...values] + : [values] + }, + extract: (trigger) => trigger.filter_has_any_storage_paths, + hasValue: (value) => Array.isArray(value) && value.length > 0, + }, [TriggerFilterType.StoragePathNot]: { apply: (aggregate, values) => { aggregate.filter_has_not_storage_paths = Array.isArray(values) @@ -642,8 +699,11 @@ export class WorkflowEditDialogComponent filter_has_tags: [], filter_has_all_tags: [], filter_has_not_tags: [], + filter_has_any_correspondents: [], filter_has_not_correspondents: [], + filter_has_any_document_types: [], filter_has_not_document_types: [], + filter_has_any_storage_paths: [], filter_has_not_storage_paths: [], filter_has_correspondent: null, filter_has_document_type: null, @@ -670,10 +730,16 @@ export class WorkflowEditDialogComponent trigger.filter_has_tags = aggregate.filter_has_tags trigger.filter_has_all_tags = aggregate.filter_has_all_tags trigger.filter_has_not_tags = aggregate.filter_has_not_tags + trigger.filter_has_any_correspondents = + aggregate.filter_has_any_correspondents trigger.filter_has_not_correspondents = aggregate.filter_has_not_correspondents + trigger.filter_has_any_document_types = + aggregate.filter_has_any_document_types trigger.filter_has_not_document_types = aggregate.filter_has_not_document_types + trigger.filter_has_any_storage_paths = + aggregate.filter_has_any_storage_paths trigger.filter_has_not_storage_paths = aggregate.filter_has_not_storage_paths trigger.filter_has_correspondent = @@ -856,8 +922,11 @@ export class WorkflowEditDialogComponent case TriggerFilterType.TagsAny: case TriggerFilterType.TagsAll: case TriggerFilterType.TagsNone: + case TriggerFilterType.CorrespondentAny: case TriggerFilterType.CorrespondentNot: + case TriggerFilterType.DocumentTypeAny: case TriggerFilterType.DocumentTypeNot: + case TriggerFilterType.StoragePathAny: case TriggerFilterType.StoragePathNot: return true default: @@ -1179,8 +1248,11 @@ export class WorkflowEditDialogComponent filter_has_tags: [], filter_has_all_tags: [], filter_has_not_tags: [], + filter_has_any_correspondents: [], filter_has_not_correspondents: [], + filter_has_any_document_types: [], filter_has_not_document_types: [], + filter_has_any_storage_paths: [], filter_has_not_storage_paths: [], filter_custom_field_query: null, filter_has_correspondent: null, diff --git a/src-ui/src/app/data/workflow-trigger.ts b/src-ui/src/app/data/workflow-trigger.ts index 888b18cc3..2bc89f188 100644 --- a/src-ui/src/app/data/workflow-trigger.ts +++ b/src-ui/src/app/data/workflow-trigger.ts @@ -44,10 +44,16 @@ export interface WorkflowTrigger extends ObjectWithId { filter_has_not_tags?: number[] // Tag.id[] + filter_has_any_correspondents?: number[] // Correspondent.id[] + filter_has_not_correspondents?: number[] // Correspondent.id[] + filter_has_any_document_types?: number[] // DocumentType.id[] + filter_has_not_document_types?: number[] // DocumentType.id[] + filter_has_any_storage_paths?: number[] // StoragePath.id[] + filter_has_not_storage_paths?: number[] // StoragePath.id[] filter_custom_field_query?: string diff --git a/src/documents/matching.py b/src/documents/matching.py index 198ead64c..9276ad583 100644 --- a/src/documents/matching.py +++ b/src/documents/matching.py @@ -403,6 +403,18 @@ def existing_document_matches_workflow( f"Document tags {list(document.tags.all())} include excluded tags {list(trigger_has_not_tags_qs)}", ) + allowed_correspondent_ids = set( + trigger.filter_has_any_correspondents.values_list("id", flat=True), + ) + if ( + allowed_correspondent_ids + and document.correspondent_id not in allowed_correspondent_ids + ): + return ( + False, + f"Document correspondent {document.correspondent} is not one of {list(trigger.filter_has_any_correspondents.all())}", + ) + # Document correspondent vs trigger has_correspondent if ( trigger.filter_has_correspondent_id is not None @@ -424,6 +436,17 @@ def existing_document_matches_workflow( f"Document correspondent {document.correspondent} is excluded by {list(trigger.filter_has_not_correspondents.all())}", ) + allowed_document_type_ids = set( + trigger.filter_has_any_document_types.values_list("id", flat=True), + ) + if allowed_document_type_ids and ( + document.document_type_id not in allowed_document_type_ids + ): + return ( + False, + f"Document doc type {document.document_type} is not one of {list(trigger.filter_has_any_document_types.all())}", + ) + # Document document_type vs trigger has_document_type if ( trigger.filter_has_document_type_id is not None @@ -445,6 +468,17 @@ def existing_document_matches_workflow( f"Document doc type {document.document_type} is excluded by {list(trigger.filter_has_not_document_types.all())}", ) + allowed_storage_path_ids = set( + trigger.filter_has_any_storage_paths.values_list("id", flat=True), + ) + if allowed_storage_path_ids and ( + document.storage_path_id not in allowed_storage_path_ids + ): + return ( + False, + f"Document storage path {document.storage_path} is not one of {list(trigger.filter_has_any_storage_paths.all())}", + ) + # Document storage_path vs trigger has_storage_path if ( trigger.filter_has_storage_path_id is not None @@ -532,6 +566,10 @@ def prefilter_documents_by_workflowtrigger( # Correspondent, DocumentType, etc. filtering + if trigger.filter_has_any_correspondents.exists(): + documents = documents.filter( + correspondent__in=trigger.filter_has_any_correspondents.all(), + ) if trigger.filter_has_correspondent is not None: documents = documents.filter( correspondent=trigger.filter_has_correspondent, @@ -541,6 +579,10 @@ def prefilter_documents_by_workflowtrigger( correspondent__in=trigger.filter_has_not_correspondents.all(), ) + if trigger.filter_has_any_document_types.exists(): + documents = documents.filter( + document_type__in=trigger.filter_has_any_document_types.all(), + ) if trigger.filter_has_document_type is not None: documents = documents.filter( document_type=trigger.filter_has_document_type, @@ -550,6 +592,10 @@ def prefilter_documents_by_workflowtrigger( document_type__in=trigger.filter_has_not_document_types.all(), ) + if trigger.filter_has_any_storage_paths.exists(): + documents = documents.filter( + storage_path__in=trigger.filter_has_any_storage_paths.all(), + ) if trigger.filter_has_storage_path is not None: documents = documents.filter( storage_path=trigger.filter_has_storage_path, @@ -604,8 +650,11 @@ def document_matches_workflow( "filter_has_tags", "filter_has_all_tags", "filter_has_not_tags", + "filter_has_any_document_types", "filter_has_not_document_types", + "filter_has_any_correspondents", "filter_has_not_correspondents", + "filter_has_any_storage_paths", "filter_has_not_storage_paths", ) ) diff --git a/src/documents/migrations/1075_workflowtrigger_filter_has_any_correspondents_and_more.py b/src/documents/migrations/1075_workflowtrigger_filter_has_any_correspondents_and_more.py new file mode 100644 index 000000000..bc5cd30d1 --- /dev/null +++ b/src/documents/migrations/1075_workflowtrigger_filter_has_any_correspondents_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.7 on 2025-12-17 22:25 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="workflowtrigger", + name="filter_has_any_correspondents", + field=models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_any_correspondent", + to="documents.correspondent", + verbose_name="has one of these correspondents", + ), + ), + migrations.AddField( + model_name="workflowtrigger", + name="filter_has_any_document_types", + field=models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_any_document_type", + to="documents.documenttype", + verbose_name="has one of these document types", + ), + ), + migrations.AddField( + model_name="workflowtrigger", + name="filter_has_any_storage_paths", + field=models.ManyToManyField( + blank=True, + related_name="workflowtriggers_has_any_storage_path", + to="documents.storagepath", + verbose_name="has one of these storage paths", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 12dab2b6d..caeef6947 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1087,6 +1087,13 @@ class WorkflowTrigger(models.Model): verbose_name=_("has this document type"), ) + filter_has_any_document_types = models.ManyToManyField( + DocumentType, + blank=True, + related_name="workflowtriggers_has_any_document_type", + verbose_name=_("has one of these document types"), + ) + filter_has_not_document_types = models.ManyToManyField( DocumentType, blank=True, @@ -1109,6 +1116,13 @@ class WorkflowTrigger(models.Model): verbose_name=_("does not have these correspondent(s)"), ) + filter_has_any_correspondents = models.ManyToManyField( + Correspondent, + blank=True, + related_name="workflowtriggers_has_any_correspondent", + verbose_name=_("has one of these correspondents"), + ) + filter_has_storage_path = models.ForeignKey( StoragePath, null=True, @@ -1117,6 +1131,13 @@ class WorkflowTrigger(models.Model): verbose_name=_("has this storage path"), ) + filter_has_any_storage_paths = models.ManyToManyField( + StoragePath, + blank=True, + related_name="workflowtriggers_has_any_storage_path", + verbose_name=_("has one of these storage paths"), + ) + filter_has_not_storage_paths = models.ManyToManyField( StoragePath, blank=True, diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 5c90c6f1c..844994fcc 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -2275,8 +2275,11 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer): "filter_has_all_tags", "filter_has_not_tags", "filter_custom_field_query", + "filter_has_any_correspondents", "filter_has_not_correspondents", + "filter_has_any_document_types", "filter_has_not_document_types", + "filter_has_any_storage_paths", "filter_has_not_storage_paths", "filter_has_correspondent", "filter_has_document_type", @@ -2514,14 +2517,26 @@ class WorkflowSerializer(serializers.ModelSerializer): 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) + filter_has_any_correspondents = trigger.pop( + "filter_has_any_correspondents", + None, + ) filter_has_not_correspondents = trigger.pop( "filter_has_not_correspondents", None, ) + filter_has_any_document_types = trigger.pop( + "filter_has_any_document_types", + None, + ) filter_has_not_document_types = trigger.pop( "filter_has_not_document_types", None, ) + filter_has_any_storage_paths = trigger.pop( + "filter_has_any_storage_paths", + None, + ) filter_has_not_storage_paths = trigger.pop( "filter_has_not_storage_paths", None, @@ -2538,14 +2553,26 @@ class WorkflowSerializer(serializers.ModelSerializer): 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) + if filter_has_any_correspondents is not None: + trigger_instance.filter_has_any_correspondents.set( + filter_has_any_correspondents, + ) if filter_has_not_correspondents is not None: trigger_instance.filter_has_not_correspondents.set( filter_has_not_correspondents, ) + if filter_has_any_document_types is not None: + trigger_instance.filter_has_any_document_types.set( + filter_has_any_document_types, + ) if filter_has_not_document_types is not None: trigger_instance.filter_has_not_document_types.set( filter_has_not_document_types, ) + if filter_has_any_storage_paths is not None: + trigger_instance.filter_has_any_storage_paths.set( + filter_has_any_storage_paths, + ) if filter_has_not_storage_paths is not None: trigger_instance.filter_has_not_storage_paths.set( filter_has_not_storage_paths, diff --git a/src/documents/tests/test_api_workflows.py b/src/documents/tests/test_api_workflows.py index 9efdb8451..1d3efd457 100644 --- a/src/documents/tests/test_api_workflows.py +++ b/src/documents/tests/test_api_workflows.py @@ -186,8 +186,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): "filter_has_tags": [self.t1.id], "filter_has_all_tags": [self.t2.id], "filter_has_not_tags": [self.t3.id], + "filter_has_any_correspondents": [self.c.id], "filter_has_not_correspondents": [self.c2.id], + "filter_has_any_document_types": [self.dt.id], "filter_has_not_document_types": [self.dt2.id], + "filter_has_any_storage_paths": [self.sp.id], "filter_has_not_storage_paths": [self.sp2.id], "filter_custom_field_query": json.dumps( [ @@ -248,14 +251,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): set(trigger.filter_has_not_tags.values_list("id", flat=True)), {self.t3.id}, ) + self.assertSetEqual( + set(trigger.filter_has_any_correspondents.values_list("id", flat=True)), + {self.c.id}, + ) self.assertSetEqual( set(trigger.filter_has_not_correspondents.values_list("id", flat=True)), {self.c2.id}, ) + self.assertSetEqual( + set(trigger.filter_has_any_document_types.values_list("id", flat=True)), + {self.dt.id}, + ) self.assertSetEqual( set(trigger.filter_has_not_document_types.values_list("id", flat=True)), {self.dt2.id}, ) + self.assertSetEqual( + set(trigger.filter_has_any_storage_paths.values_list("id", flat=True)), + {self.sp.id}, + ) self.assertSetEqual( set(trigger.filter_has_not_storage_paths.values_list("id", flat=True)), {self.sp2.id}, @@ -419,8 +434,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): "filter_has_tags": [self.t1.id], "filter_has_all_tags": [self.t2.id], "filter_has_not_tags": [self.t3.id], + "filter_has_any_correspondents": [self.c.id], "filter_has_not_correspondents": [self.c2.id], + "filter_has_any_document_types": [self.dt.id], "filter_has_not_document_types": [self.dt2.id], + "filter_has_any_storage_paths": [self.sp.id], "filter_has_not_storage_paths": [self.sp2.id], "filter_custom_field_query": json.dumps( ["AND", [[self.cf1.id, "exact", "value"]]], @@ -450,14 +468,26 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): workflow.triggers.first().filter_has_not_tags.first(), self.t3, ) + self.assertEqual( + workflow.triggers.first().filter_has_any_correspondents.first(), + self.c, + ) self.assertEqual( workflow.triggers.first().filter_has_not_correspondents.first(), self.c2, ) + self.assertEqual( + workflow.triggers.first().filter_has_any_document_types.first(), + self.dt, + ) self.assertEqual( workflow.triggers.first().filter_has_not_document_types.first(), self.dt2, ) + self.assertEqual( + workflow.triggers.first().filter_has_any_storage_paths.first(), + self.sp, + ) self.assertEqual( workflow.triggers.first().filter_has_not_storage_paths.first(), self.sp2, diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 249183b6e..a9f96390c 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -1276,6 +1276,76 @@ class TestWorkflows( ) self.assertIn(expected_str, cm.output[1]) + def test_document_added_any_filters(self): + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, + ) + trigger.filter_has_any_correspondents.set([self.c]) + trigger.filter_has_any_document_types.set([self.dt]) + trigger.filter_has_any_storage_paths.set([self.sp]) + + matching_doc = Document.objects.create( + title="sample test", + correspondent=self.c, + document_type=self.dt, + storage_path=self.sp, + original_filename="sample.pdf", + checksum="checksum-any-match", + ) + + matched, reason = existing_document_matches_workflow(matching_doc, trigger) + self.assertTrue(matched) + self.assertIsNone(reason) + + wrong_correspondent = Document.objects.create( + title="wrong correspondent", + correspondent=self.c2, + document_type=self.dt, + storage_path=self.sp, + original_filename="sample2.pdf", + ) + matched, reason = existing_document_matches_workflow( + wrong_correspondent, + trigger, + ) + self.assertFalse(matched) + self.assertIn("correspondent", reason) + + other_document_type = DocumentType.objects.create(name="Other") + wrong_document_type = Document.objects.create( + title="wrong doc type", + correspondent=self.c, + document_type=other_document_type, + storage_path=self.sp, + original_filename="sample3.pdf", + checksum="checksum-wrong-doc-type", + ) + matched, reason = existing_document_matches_workflow( + wrong_document_type, + trigger, + ) + self.assertFalse(matched) + self.assertIn("doc type", reason) + + other_storage_path = StoragePath.objects.create( + name="Other path", + path="/other/", + ) + wrong_storage_path = Document.objects.create( + title="wrong storage", + correspondent=self.c, + document_type=self.dt, + storage_path=other_storage_path, + original_filename="sample4.pdf", + checksum="checksum-wrong-storage-path", + ) + matched, reason = existing_document_matches_workflow( + wrong_storage_path, + trigger, + ) + self.assertFalse(matched) + self.assertIn("storage path", reason) + def test_document_added_custom_field_query_no_match(self): trigger = WorkflowTrigger.objects.create( type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, @@ -1384,6 +1454,39 @@ class TestWorkflows( self.assertIn(doc1, filtered) self.assertNotIn(doc2, filtered) + def test_prefilter_documents_any_filters(self): + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, + ) + trigger.filter_has_any_correspondents.set([self.c]) + trigger.filter_has_any_document_types.set([self.dt]) + trigger.filter_has_any_storage_paths.set([self.sp]) + + allowed_document = Document.objects.create( + title="allowed", + correspondent=self.c, + document_type=self.dt, + storage_path=self.sp, + original_filename="doc-allowed.pdf", + checksum="checksum-any-allowed", + ) + blocked_document = Document.objects.create( + title="blocked", + correspondent=self.c2, + document_type=self.dt, + storage_path=self.sp, + original_filename="doc-blocked.pdf", + checksum="checksum-any-blocked", + ) + + filtered = prefilter_documents_by_workflowtrigger( + Document.objects.all(), + trigger, + ) + + self.assertIn(allowed_document, filtered) + self.assertNotIn(blocked_document, filtered) + def test_consumption_trigger_requires_filter_configuration(self): serializer = WorkflowTriggerSerializer( data={