Feature: Workflows (#5121)

This commit is contained in:
shamoon
2024-01-03 00:19:19 -08:00
committed by GitHub
parent ae7940e040
commit 1141b767a0
54 changed files with 4980 additions and 2011 deletions

View File

@@ -1,236 +0,0 @@
import json
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from documents.data_models import DocumentSource
from documents.models import ConsumptionTemplate
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.utils import DirectoriesMixin
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
ENDPOINT = "/api/consumption_templates/"
def setUp(self) -> None:
super().setUp()
user = User.objects.create_superuser(username="temp_admin")
self.client.force_authenticate(user=user)
self.user2 = User.objects.create(username="user2")
self.user3 = User.objects.create(username="user3")
self.group1 = Group.objects.create(name="group1")
self.c = Correspondent.objects.create(name="Correspondent Name")
self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
self.dt = DocumentType.objects.create(name="DocType Name")
self.t1 = Tag.objects.create(name="t1")
self.t2 = Tag.objects.create(name="t2")
self.t3 = Tag.objects.create(name="t3")
self.sp = StoragePath.objects.create(path="/test/")
self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string")
self.cf2 = CustomField.objects.create(
name="Custom Field 2",
data_type="integer",
)
self.ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}",
filter_filename="*simple*",
filter_path="*/samples/*",
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
self.ct.assign_tags.add(self.t1)
self.ct.assign_tags.add(self.t2)
self.ct.assign_tags.add(self.t3)
self.ct.assign_view_users.add(self.user3.pk)
self.ct.assign_view_groups.add(self.group1.pk)
self.ct.assign_change_users.add(self.user3.pk)
self.ct.assign_change_groups.add(self.group1.pk)
self.ct.assign_custom_fields.add(self.cf1.pk)
self.ct.assign_custom_fields.add(self.cf2.pk)
self.ct.save()
def test_api_get_consumption_template(self):
"""
GIVEN:
- API request to get all consumption template
WHEN:
- API is called
THEN:
- Existing consumption templates are returned
"""
response = self.client.get(self.ENDPOINT, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 1)
resp_consumption_template = response.data["results"][0]
self.assertEqual(resp_consumption_template["id"], self.ct.id)
self.assertEqual(
resp_consumption_template["assign_correspondent"],
self.ct.assign_correspondent.pk,
)
def test_api_create_consumption_template(self):
"""
GIVEN:
- API request to create a consumption template
WHEN:
- API is called
THEN:
- Correct HTTP response
- New template is created
"""
response = self.client.post(
self.ENDPOINT,
json.dumps(
{
"name": "Template 2",
"order": 1,
"sources": [DocumentSource.ApiUpload],
"filter_filename": "*test*",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(ConsumptionTemplate.objects.count(), 2)
def test_api_create_invalid_consumption_template(self):
"""
GIVEN:
- API request to create a consumption template
- Neither file name nor path filter are specified
WHEN:
- API is called
THEN:
- Correct HTTP 400 response
- No template is created
"""
response = self.client.post(
self.ENDPOINT,
json.dumps(
{
"name": "Template 2",
"order": 1,
"sources": [DocumentSource.ApiUpload],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(ConsumptionTemplate.objects.count(), 1)
def test_api_create_consumption_template_empty_fields(self):
"""
GIVEN:
- API request to create a consumption template
- Path or filename filter or assign title are empty string
WHEN:
- API is called
THEN:
- Template is created but filter or title assignment is not set if ""
"""
response = self.client.post(
self.ENDPOINT,
json.dumps(
{
"name": "Template 2",
"order": 1,
"sources": [DocumentSource.ApiUpload],
"filter_filename": "*test*",
"filter_path": "",
"assign_title": "",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
ct = ConsumptionTemplate.objects.get(name="Template 2")
self.assertEqual(ct.filter_filename, "*test*")
self.assertIsNone(ct.filter_path)
self.assertIsNone(ct.assign_title)
response = self.client.post(
self.ENDPOINT,
json.dumps(
{
"name": "Template 3",
"order": 1,
"sources": [DocumentSource.ApiUpload],
"filter_filename": "",
"filter_path": "*/test/*",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
ct2 = ConsumptionTemplate.objects.get(name="Template 3")
self.assertEqual(ct2.filter_path, "*/test/*")
self.assertIsNone(ct2.filter_filename)
def test_api_create_consumption_template_with_mailrule(self):
"""
GIVEN:
- API request to create a consumption template with a mail rule but no MailFetch source
WHEN:
- API is called
THEN:
- New template is created with MailFetch as source
"""
account1 = MailAccount.objects.create(
name="Email1",
username="username1",
password="password1",
imap_server="server.example.com",
imap_port=443,
imap_security=MailAccount.ImapSecurity.SSL,
character_set="UTF-8",
)
rule1 = MailRule.objects.create(
name="Rule1",
account=account1,
folder="INBOX",
filter_from="from@example.com",
filter_to="someone@somewhere.com",
filter_subject="subject",
filter_body="body",
filter_attachment_filename_include="file.pdf",
maximum_age=30,
action=MailRule.MailAction.MARK_READ,
assign_title_from=MailRule.TitleSource.FROM_SUBJECT,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
order=0,
attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
)
response = self.client.post(
self.ENDPOINT,
json.dumps(
{
"name": "Template 2",
"order": 1,
"sources": [DocumentSource.ApiUpload],
"filter_mailrule": rule1.pk,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(ConsumptionTemplate.objects.count(), 2)
ct = ConsumptionTemplate.objects.get(name="Template 2")
self.assertEqual(ct.sources, [int(DocumentSource.MailFetch).__str__()])

View File

@@ -0,0 +1,435 @@
import json
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
from documents.data_models import DocumentSource
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.tests.utils import DirectoriesMixin
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
class TestApiWorkflows(DirectoriesMixin, APITestCase):
ENDPOINT = "/api/workflows/"
ENDPOINT_TRIGGERS = "/api/workflow_triggers/"
ENDPOINT_ACTIONS = "/api/workflow_actions/"
def setUp(self) -> None:
super().setUp()
user = User.objects.create_superuser(username="temp_admin")
self.client.force_authenticate(user=user)
self.user2 = User.objects.create(username="user2")
self.user3 = User.objects.create(username="user3")
self.group1 = Group.objects.create(name="group1")
self.c = Correspondent.objects.create(name="Correspondent Name")
self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
self.dt = DocumentType.objects.create(name="DocType Name")
self.dt2 = DocumentType.objects.create(name="DocType Name 2")
self.t1 = Tag.objects.create(name="t1")
self.t2 = Tag.objects.create(name="t2")
self.t3 = Tag.objects.create(name="t3")
self.sp = StoragePath.objects.create(name="Storage Path 1", path="/test/")
self.sp2 = StoragePath.objects.create(name="Storage Path 2", path="/test2/")
self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string")
self.cf2 = CustomField.objects.create(
name="Custom Field 2",
data_type="integer",
)
self.trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}",
filter_filename="*simple*",
filter_path="*/samples/*",
)
self.action = WorkflowAction.objects.create(
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
self.action.assign_tags.add(self.t1)
self.action.assign_tags.add(self.t2)
self.action.assign_tags.add(self.t3)
self.action.assign_view_users.add(self.user3.pk)
self.action.assign_view_groups.add(self.group1.pk)
self.action.assign_change_users.add(self.user3.pk)
self.action.assign_change_groups.add(self.group1.pk)
self.action.assign_custom_fields.add(self.cf1.pk)
self.action.assign_custom_fields.add(self.cf2.pk)
self.action.save()
self.workflow = Workflow.objects.create(
name="Workflow 1",
order=0,
)
self.workflow.triggers.add(self.trigger)
self.workflow.actions.add(self.action)
self.workflow.save()
def test_api_get_workflow(self):
"""
GIVEN:
- API request to get all workflows
WHEN:
- API is called
THEN:
- Existing workflows are returned
"""
response = self.client.get(self.ENDPOINT, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 1)
resp_workflow = response.data["results"][0]
self.assertEqual(resp_workflow["id"], self.workflow.id)
self.assertEqual(
resp_workflow["actions"][0]["assign_correspondent"],
self.action.assign_correspondent.pk,
)
def test_api_create_workflow(self):
"""
GIVEN:
- API request to create a workflow, trigger and action separately
WHEN:
- API is called
THEN:
- Correct HTTP response
- New workflow, trigger and action are created
"""
trigger_response = self.client.post(
self.ENDPOINT_TRIGGERS,
json.dumps(
{
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
"sources": [DocumentSource.ApiUpload],
"filter_filename": "*",
},
),
content_type="application/json",
)
self.assertEqual(trigger_response.status_code, status.HTTP_201_CREATED)
action_response = self.client.post(
self.ENDPOINT_ACTIONS,
json.dumps(
{
"assign_title": "Action Title",
},
),
content_type="application/json",
)
self.assertEqual(action_response.status_code, status.HTTP_201_CREATED)
response = self.client.post(
self.ENDPOINT,
json.dumps(
{
"name": "Workflow 2",
"order": 1,
"triggers": [
{
"id": trigger_response.data["id"],
"sources": [DocumentSource.ApiUpload],
"type": trigger_response.data["type"],
"filter_filename": trigger_response.data["filter_filename"],
},
],
"actions": [
{
"id": action_response.data["id"],
"assign_title": action_response.data["assign_title"],
},
],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Workflow.objects.count(), 2)
def test_api_create_workflow_nested(self):
"""
GIVEN:
- API request to create a workflow with nested trigger and action
WHEN:
- API is called
THEN:
- Correct HTTP response
- New workflow, trigger and action are created
"""
response = self.client.post(
self.ENDPOINT,
json.dumps(
{
"name": "Workflow 2",
"order": 1,
"triggers": [
{
"sources": [DocumentSource.ApiUpload],
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
"filter_filename": "*",
"filter_path": "*/samples/*",
"filter_has_tags": [self.t1.id],
"filter_has_document_type": self.dt.id,
"filter_has_correspondent": self.c.id,
},
],
"actions": [
{
"assign_title": "Action Title",
"assign_tags": [self.t2.id],
"assign_document_type": self.dt2.id,
"assign_correspondent": self.c2.id,
"assign_storage_path": self.sp2.id,
"assign_owner": self.user2.id,
"assign_view_users": [self.user2.id],
"assign_view_groups": [self.group1.id],
"assign_change_users": [self.user2.id],
"assign_change_groups": [self.group1.id],
"assign_custom_fields": [self.cf2.id],
},
],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Workflow.objects.count(), 2)
def test_api_create_invalid_workflow_trigger(self):
"""
GIVEN:
- API request to create a workflow trigger
- Neither type or file name nor path filter are specified
WHEN:
- API is called
THEN:
- Correct HTTP 400 response
- No objects are created
"""
response = self.client.post(
self.ENDPOINT_TRIGGERS,
json.dumps(
{
"sources": [DocumentSource.ApiUpload],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response = self.client.post(
self.ENDPOINT_TRIGGERS,
json.dumps(
{
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
"sources": [DocumentSource.ApiUpload],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(WorkflowTrigger.objects.count(), 1)
def test_api_create_workflow_trigger_action_empty_fields(self):
"""
GIVEN:
- API request to create a workflow trigger and action
- Path or filename filter or assign title are empty string
WHEN:
- API is called
THEN:
- Template is created but filter or title assignment is not set if ""
"""
response = self.client.post(
self.ENDPOINT_TRIGGERS,
json.dumps(
{
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
"sources": [DocumentSource.ApiUpload],
"filter_filename": "*test*",
"filter_path": "",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
trigger = WorkflowTrigger.objects.get(id=response.data["id"])
self.assertEqual(trigger.filter_filename, "*test*")
self.assertIsNone(trigger.filter_path)
response = self.client.post(
self.ENDPOINT_ACTIONS,
json.dumps(
{
"assign_title": "",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
action = WorkflowAction.objects.get(id=response.data["id"])
self.assertIsNone(action.assign_title)
response = self.client.post(
self.ENDPOINT_TRIGGERS,
json.dumps(
{
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
"sources": [DocumentSource.ApiUpload],
"filter_filename": "",
"filter_path": "*/test/*",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
trigger2 = WorkflowTrigger.objects.get(id=response.data["id"])
self.assertEqual(trigger2.filter_path, "*/test/*")
self.assertIsNone(trigger2.filter_filename)
def test_api_create_workflow_trigger_with_mailrule(self):
"""
GIVEN:
- API request to create a workflow trigger with a mail rule but no MailFetch source
WHEN:
- API is called
THEN:
- New trigger is created with MailFetch as source
"""
account1 = MailAccount.objects.create(
name="Email1",
username="username1",
password="password1",
imap_server="server.example.com",
imap_port=443,
imap_security=MailAccount.ImapSecurity.SSL,
character_set="UTF-8",
)
rule1 = MailRule.objects.create(
name="Rule1",
account=account1,
folder="INBOX",
filter_from="from@example.com",
filter_to="someone@somewhere.com",
filter_subject="subject",
filter_body="body",
filter_attachment_filename_include="file.pdf",
maximum_age=30,
action=MailRule.MailAction.MARK_READ,
assign_title_from=MailRule.TitleSource.FROM_SUBJECT,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
order=0,
attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
)
response = self.client.post(
self.ENDPOINT_TRIGGERS,
json.dumps(
{
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
"sources": [DocumentSource.ApiUpload],
"filter_mailrule": rule1.pk,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(WorkflowTrigger.objects.count(), 2)
trigger = WorkflowTrigger.objects.get(id=response.data["id"])
self.assertEqual(trigger.sources, [int(DocumentSource.MailFetch).__str__()])
def test_api_update_workflow_nested_triggers_actions(self):
"""
GIVEN:
- Existing workflow with trigger and action
WHEN:
- API request to update an existing workflow with nested triggers actions
THEN:
- Triggers and actions are updated
"""
response = self.client.patch(
f"{self.ENDPOINT}{self.workflow.id}/",
json.dumps(
{
"name": "Workflow Updated",
"order": 1,
"triggers": [
{
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
"filter_has_tags": [self.t1.id],
"filter_has_correspondent": self.c.id,
"filter_has_document_type": self.dt.id,
},
],
"actions": [
{
"assign_title": "Action New Title",
},
],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
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.actions.first().assign_title, "Action New Title")
def test_api_auto_remove_orphaned_triggers_actions(self):
"""
GIVEN:
- Existing trigger and action
WHEN:
- API request is made which creates new trigger / actions
THEN:
- "Orphaned" triggers and actions are removed
"""
response = self.client.patch(
f"{self.ENDPOINT}{self.workflow.id}/",
json.dumps(
{
"name": "Workflow Updated",
"order": 1,
"triggers": [
{
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
"filter_has_tags": [self.t1.id],
"filter_has_correspondent": self.c.id,
"filter_has_document_type": self.dt.id,
},
],
"actions": [
{
"assign_title": "Action New Title",
},
],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
workflow = Workflow.objects.get(id=response.data["id"])
self.assertEqual(WorkflowTrigger.objects.all().count(), 1)
self.assertNotEqual(workflow.triggers.first().id, self.trigger.id)
self.assertEqual(WorkflowAction.objects.all().count(), 1)
self.assertNotEqual(workflow.actions.first().id, self.action.id)

View File

@@ -1,539 +0,0 @@
from pathlib import Path
from unittest import TestCase
from unittest import mock
import pytest
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from documents import tasks
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentSource
from documents.models import ConsumptionTemplate
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
@pytest.mark.django_db
class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
SAMPLE_DIR = Path(__file__).parent / "samples"
def setUp(self) -> None:
self.c = Correspondent.objects.create(name="Correspondent Name")
self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
self.dt = DocumentType.objects.create(name="DocType Name")
self.t1 = Tag.objects.create(name="t1")
self.t2 = Tag.objects.create(name="t2")
self.t3 = Tag.objects.create(name="t3")
self.sp = StoragePath.objects.create(path="/test/")
self.cf1 = CustomField.objects.create(name="Custom Field 1", data_type="string")
self.cf2 = CustomField.objects.create(
name="Custom Field 2",
data_type="integer",
)
self.user2 = User.objects.create(username="user2")
self.user3 = User.objects.create(username="user3")
self.group1 = Group.objects.create(name="group1")
account1 = MailAccount.objects.create(
name="Email1",
username="username1",
password="password1",
imap_server="server.example.com",
imap_port=443,
imap_security=MailAccount.ImapSecurity.SSL,
character_set="UTF-8",
)
self.rule1 = MailRule.objects.create(
name="Rule1",
account=account1,
folder="INBOX",
filter_from="from@example.com",
filter_to="someone@somewhere.com",
filter_subject="subject",
filter_body="body",
filter_attachment_filename_include="file.pdf",
maximum_age=30,
action=MailRule.MailAction.MARK_READ,
assign_title_from=MailRule.TitleSource.NONE,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
order=0,
attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
assign_owner_from_rule=False,
)
return super().setUp()
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_match(self, m):
"""
GIVEN:
- Existing consumption template
WHEN:
- File that matches is consumed
THEN:
- Template overrides are applied
"""
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_filename="*simple*",
filter_path="*/samples/*",
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
ct.assign_tags.add(self.t1)
ct.assign_tags.add(self.t2)
ct.assign_tags.add(self.t3)
ct.assign_view_users.add(self.user3.pk)
ct.assign_view_groups.add(self.group1.pk)
ct.assign_change_users.add(self.user3.pk)
ct.assign_change_groups.add(self.group1.pk)
ct.assign_custom_fields.add(self.cf1.pk)
ct.assign_custom_fields.add(self.cf2.pk)
ct.save()
self.assertEqual(ct.__str__(), "Template 1")
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="INFO") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=test_file,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
self.assertEqual(overrides["override_correspondent_id"], self.c.pk)
self.assertEqual(overrides["override_document_type_id"], self.dt.pk)
self.assertEqual(
overrides["override_tag_ids"],
[self.t1.pk, self.t2.pk, self.t3.pk],
)
self.assertEqual(overrides["override_storage_path_id"], self.sp.pk)
self.assertEqual(overrides["override_owner_id"], self.user2.pk)
self.assertEqual(overrides["override_view_users"], [self.user3.pk])
self.assertEqual(overrides["override_view_groups"], [self.group1.pk])
self.assertEqual(overrides["override_change_users"], [self.user3.pk])
self.assertEqual(overrides["override_change_groups"], [self.group1.pk])
self.assertEqual(
overrides["override_title"],
"Doc from {correspondent}",
)
self.assertEqual(
overrides["override_custom_field_ids"],
[self.cf1.pk, self.cf2.pk],
)
info = cm.output[0]
expected_str = f"Document matched template {ct}"
self.assertIn(expected_str, info)
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_match_mailrule(self, m):
"""
GIVEN:
- Existing consumption template
WHEN:
- File that matches is consumed via mail rule
THEN:
- Template overrides are applied
"""
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_mailrule=self.rule1,
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
ct.assign_tags.add(self.t1)
ct.assign_tags.add(self.t2)
ct.assign_tags.add(self.t3)
ct.assign_view_users.add(self.user3.pk)
ct.assign_view_groups.add(self.group1.pk)
ct.assign_change_users.add(self.user3.pk)
ct.assign_change_groups.add(self.group1.pk)
ct.save()
self.assertEqual(ct.__str__(), "Template 1")
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="INFO") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=test_file,
mailrule_id=self.rule1.pk,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
self.assertEqual(overrides["override_correspondent_id"], self.c.pk)
self.assertEqual(overrides["override_document_type_id"], self.dt.pk)
self.assertEqual(
overrides["override_tag_ids"],
[self.t1.pk, self.t2.pk, self.t3.pk],
)
self.assertEqual(overrides["override_storage_path_id"], self.sp.pk)
self.assertEqual(overrides["override_owner_id"], self.user2.pk)
self.assertEqual(overrides["override_view_users"], [self.user3.pk])
self.assertEqual(overrides["override_view_groups"], [self.group1.pk])
self.assertEqual(overrides["override_change_users"], [self.user3.pk])
self.assertEqual(overrides["override_change_groups"], [self.group1.pk])
self.assertEqual(
overrides["override_title"],
"Doc from {correspondent}",
)
info = cm.output[0]
expected_str = f"Document matched template {ct}"
self.assertIn(expected_str, info)
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_match_multiple(self, m):
"""
GIVEN:
- Multiple existing consumption template
WHEN:
- File that matches is consumed
THEN:
- Template overrides are applied with subsequent templates only overwriting empty values
or merging if multiple
"""
ct1 = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_path="*/samples/*",
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
)
ct1.assign_tags.add(self.t1)
ct1.assign_tags.add(self.t2)
ct1.assign_view_users.add(self.user2)
ct1.save()
ct2 = ConsumptionTemplate.objects.create(
name="Template 2",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_filename="*simple*",
assign_title="Doc from {correspondent}",
assign_correspondent=self.c2,
assign_storage_path=self.sp,
)
ct2.assign_tags.add(self.t3)
ct1.assign_view_users.add(self.user3)
ct2.save()
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="INFO") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=test_file,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
# template 1
self.assertEqual(overrides["override_correspondent_id"], self.c.pk)
self.assertEqual(overrides["override_document_type_id"], self.dt.pk)
# template 2
self.assertEqual(overrides["override_storage_path_id"], self.sp.pk)
# template 1 & 2
self.assertEqual(
overrides["override_tag_ids"],
[self.t1.pk, self.t2.pk, self.t3.pk],
)
self.assertEqual(
overrides["override_view_users"],
[self.user2.pk, self.user3.pk],
)
expected_str = f"Document matched template {ct1}"
self.assertIn(expected_str, cm.output[0])
expected_str = f"Document matched template {ct2}"
self.assertIn(expected_str, cm.output[1])
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_no_match_filename(self, m):
"""
GIVEN:
- Existing consumption template
WHEN:
- File that does not match on filename is consumed
THEN:
- Template overrides are not applied
"""
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_filename="*foobar*",
filter_path=None,
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=test_file,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
self.assertIsNone(overrides["override_correspondent_id"])
self.assertIsNone(overrides["override_document_type_id"])
self.assertIsNone(overrides["override_tag_ids"])
self.assertIsNone(overrides["override_storage_path_id"])
self.assertIsNone(overrides["override_owner_id"])
self.assertIsNone(overrides["override_view_users"])
self.assertIsNone(overrides["override_view_groups"])
self.assertIsNone(overrides["override_change_users"])
self.assertIsNone(overrides["override_change_groups"])
self.assertIsNone(overrides["override_title"])
expected_str = f"Document did not match template {ct}"
self.assertIn(expected_str, cm.output[0])
expected_str = f"Document filename {test_file.name} does not match"
self.assertIn(expected_str, cm.output[1])
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_no_match_path(self, m):
"""
GIVEN:
- Existing consumption template
WHEN:
- File that does not match on path is consumed
THEN:
- Template overrides are not applied
"""
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_path="*foo/bar*",
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=test_file,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
self.assertIsNone(overrides["override_correspondent_id"])
self.assertIsNone(overrides["override_document_type_id"])
self.assertIsNone(overrides["override_tag_ids"])
self.assertIsNone(overrides["override_storage_path_id"])
self.assertIsNone(overrides["override_owner_id"])
self.assertIsNone(overrides["override_view_users"])
self.assertIsNone(overrides["override_view_groups"])
self.assertIsNone(overrides["override_change_users"])
self.assertIsNone(overrides["override_change_groups"])
self.assertIsNone(overrides["override_title"])
expected_str = f"Document did not match template {ct}"
self.assertIn(expected_str, cm.output[0])
expected_str = f"Document path {test_file} does not match"
self.assertIn(expected_str, cm.output[1])
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_no_match_mail_rule(self, m):
"""
GIVEN:
- Existing consumption template
WHEN:
- File that does not match on source is consumed
THEN:
- Template overrides are not applied
"""
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_mailrule=self.rule1,
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=test_file,
mailrule_id=99,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
self.assertIsNone(overrides["override_correspondent_id"])
self.assertIsNone(overrides["override_document_type_id"])
self.assertIsNone(overrides["override_tag_ids"])
self.assertIsNone(overrides["override_storage_path_id"])
self.assertIsNone(overrides["override_owner_id"])
self.assertIsNone(overrides["override_view_users"])
self.assertIsNone(overrides["override_view_groups"])
self.assertIsNone(overrides["override_change_users"])
self.assertIsNone(overrides["override_change_groups"])
self.assertIsNone(overrides["override_title"])
expected_str = f"Document did not match template {ct}"
self.assertIn(expected_str, cm.output[0])
expected_str = "Document mail rule 99 !="
self.assertIn(expected_str, cm.output[1])
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_no_match_source(self, m):
"""
GIVEN:
- Existing consumption template
WHEN:
- File that does not match on source is consumed
THEN:
- Template overrides are not applied
"""
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_path="*",
assign_title="Doc from {correspondent}",
assign_correspondent=self.c,
assign_document_type=self.dt,
assign_storage_path=self.sp,
assign_owner=self.user2,
)
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ApiUpload,
original_file=test_file,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
self.assertIsNone(overrides["override_correspondent_id"])
self.assertIsNone(overrides["override_document_type_id"])
self.assertIsNone(overrides["override_tag_ids"])
self.assertIsNone(overrides["override_storage_path_id"])
self.assertIsNone(overrides["override_owner_id"])
self.assertIsNone(overrides["override_view_users"])
self.assertIsNone(overrides["override_view_groups"])
self.assertIsNone(overrides["override_change_users"])
self.assertIsNone(overrides["override_change_groups"])
self.assertIsNone(overrides["override_title"])
expected_str = f"Document did not match template {ct}"
self.assertIn(expected_str, cm.output[0])
expected_str = f"Document source {DocumentSource.ApiUpload.name} not in ['{DocumentSource.ConsumeFolder.name}', '{DocumentSource.MailFetch.name}']"
self.assertIn(expected_str, cm.output[1])
@mock.patch("documents.consumer.Consumer.try_consume_file")
def test_consumption_template_repeat_custom_fields(self, m):
"""
GIVEN:
- Existing consumption templates which assign the same custom field
WHEN:
- File that matches is consumed
THEN:
- Custom field is added the first time successfully
"""
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_filename="*simple*",
)
ct.assign_custom_fields.add(self.cf1.pk)
ct.save()
ct2 = ConsumptionTemplate.objects.create(
name="Template 2",
order=1,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_filename="*simple*",
)
ct2.assign_custom_fields.add(self.cf1.pk)
ct2.save()
test_file = self.SAMPLE_DIR / "simple.pdf"
with mock.patch("documents.tasks.async_to_sync"):
with self.assertLogs("paperless.matching", level="INFO") as cm:
tasks.consume_file(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
original_file=test_file,
),
None,
)
m.assert_called_once()
_, overrides = m.call_args
self.assertEqual(
overrides["override_custom_field_ids"],
[self.cf1.pk],
)
expected_str = f"Document matched template {ct}"
self.assertIn(expected_str, cm.output[0])
expected_str = f"Document matched template {ct2}"
self.assertIn(expected_str, cm.output[1])

View File

@@ -21,7 +21,6 @@ from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm
from documents.management.commands import document_exporter
from documents.models import ConsumptionTemplate
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import CustomFieldInstance
@@ -31,6 +30,9 @@ from documents.models import Note
from documents.models import StoragePath
from documents.models import Tag
from documents.models import User
from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.sanity_checker import check_sanity
from documents.settings import EXPORTER_FILE_NAME
from documents.tests.utils import DirectoriesMixin
@@ -109,7 +111,16 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.d4.storage_path = self.sp1
self.d4.save()
self.ct1 = ConsumptionTemplate.objects.create(name="CT 1", filter_path="*")
self.trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
sources=[1],
filter_filename="*",
)
self.action = WorkflowAction.objects.create(assign_title="new title")
self.workflow = Workflow.objects.create(name="Workflow 1", order="0")
self.workflow.triggers.add(self.trigger)
self.workflow.actions.add(self.action)
self.workflow.save()
super().setUp()
@@ -168,7 +179,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
manifest = self._do_export(use_filename_format=use_filename_format)
self.assertEqual(len(manifest), 178)
self.assertEqual(len(manifest), 190)
# dont include consumer or AnonymousUser users
self.assertEqual(
@@ -262,7 +273,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.assertEqual(Document.objects.get(id=self.d4.id).title, "wow_dec")
self.assertEqual(GroupObjectPermission.objects.count(), 1)
self.assertEqual(UserObjectPermission.objects.count(), 1)
self.assertEqual(Permission.objects.count(), 128)
self.assertEqual(Permission.objects.count(), 136)
messages = check_sanity()
# everything is alright after the test
self.assertEqual(len(messages), 0)
@@ -694,15 +705,15 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
os.path.join(self.dirs.media_dir, "documents"),
)
self.assertEqual(ContentType.objects.count(), 32)
self.assertEqual(Permission.objects.count(), 128)
self.assertEqual(ContentType.objects.count(), 34)
self.assertEqual(Permission.objects.count(), 136)
manifest = self._do_export()
with paperless_environment():
self.assertEqual(
len(list(filter(lambda e: e["model"] == "auth.permission", manifest))),
128,
136,
)
# add 1 more to db to show objects are not re-created by import
Permission.objects.create(
@@ -710,7 +721,7 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
codename="test_perm",
content_type_id=1,
)
self.assertEqual(Permission.objects.count(), 129)
self.assertEqual(Permission.objects.count(), 137)
# will cause an import error
self.user.delete()
@@ -719,5 +730,5 @@ class TestExportImport(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
with self.assertRaises(IntegrityError):
call_command("document_importer", "--no-progress-bar", self.target)
self.assertEqual(ContentType.objects.count(), 32)
self.assertEqual(Permission.objects.count(), 129)
self.assertEqual(ContentType.objects.count(), 34)
self.assertEqual(Permission.objects.count(), 137)

View File

@@ -33,11 +33,18 @@ class TestReverseMigrateConsumptionTemplate(TestMigrations):
self.Permission = apps.get_model("auth", "Permission")
self.user = User.objects.create(username="user1")
self.group = Group.objects.create(name="group1")
permission = self.Permission.objects.get(codename="add_consumptiontemplate")
self.user.user_permissions.add(permission.id)
self.group.permissions.add(permission.id)
permission = self.Permission.objects.filter(
codename="add_consumptiontemplate",
).first()
if permission is not None:
self.user.user_permissions.add(permission.id)
self.group.permissions.add(permission.id)
def test_remove_consumptiontemplate_permissions(self):
permission = self.Permission.objects.get(codename="add_consumptiontemplate")
self.assertFalse(self.user.has_perm(f"documents.{permission.codename}"))
self.assertFalse(permission in self.group.permissions.all())
permission = self.Permission.objects.filter(
codename="add_consumptiontemplate",
).first()
# can be None ? now that CTs removed
if permission is not None:
self.assertFalse(self.user.has_perm(f"documents.{permission.codename}"))
self.assertFalse(permission in self.group.permissions.all())

View File

@@ -0,0 +1,131 @@
from documents.data_models import DocumentSource
from documents.tests.utils import TestMigrations
class TestMigrateWorkflow(TestMigrations):
migrate_from = "1043_alter_savedviewfilterrule_rule_type"
migrate_to = "1044_workflow_workflowaction_workflowtrigger_and_more"
dependencies = (
("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"),
)
def setUpBeforeMigration(self, apps):
User = apps.get_model("auth", "User")
Group = apps.get_model("auth", "Group")
self.Permission = apps.get_model("auth", "Permission")
self.user = User.objects.create(username="user1")
self.group = Group.objects.create(name="group1")
permission = self.Permission.objects.get(codename="add_document")
self.user.user_permissions.add(permission.id)
self.group.permissions.add(permission.id)
# create a CT to migrate
c = apps.get_model("documents", "Correspondent").objects.create(
name="Correspondent Name",
)
dt = apps.get_model("documents", "DocumentType").objects.create(
name="DocType Name",
)
t1 = apps.get_model("documents", "Tag").objects.create(name="t1")
sp = apps.get_model("documents", "StoragePath").objects.create(path="/test/")
cf1 = apps.get_model("documents", "CustomField").objects.create(
name="Custom Field 1",
data_type="string",
)
ma = apps.get_model("paperless_mail", "MailAccount").objects.create(
name="MailAccount 1",
)
mr = apps.get_model("paperless_mail", "MailRule").objects.create(
name="MailRule 1",
order=0,
account=ma,
)
user2 = User.objects.create(username="user2")
user3 = User.objects.create(username="user3")
group2 = Group.objects.create(name="group2")
ConsumptionTemplate = apps.get_model("documents", "ConsumptionTemplate")
ct = ConsumptionTemplate.objects.create(
name="Template 1",
order=0,
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
filter_filename="*simple*",
filter_path="*/samples/*",
filter_mailrule=mr,
assign_title="Doc from {correspondent}",
assign_correspondent=c,
assign_document_type=dt,
assign_storage_path=sp,
assign_owner=user2,
)
ct.assign_tags.add(t1)
ct.assign_view_users.add(user3)
ct.assign_view_groups.add(group2)
ct.assign_change_users.add(user3)
ct.assign_change_groups.add(group2)
ct.assign_custom_fields.add(cf1)
ct.save()
def test_users_with_add_documents_get_add_and_workflow_templates_get_migrated(self):
permission = self.Permission.objects.get(codename="add_workflow")
self.assertTrue(permission in self.user.user_permissions.all())
self.assertTrue(permission in self.group.permissions.all())
Workflow = self.apps.get_model("documents", "Workflow")
self.assertEqual(Workflow.objects.all().count(), 1)
class TestReverseMigrateWorkflow(TestMigrations):
migrate_from = "1044_workflow_workflowaction_workflowtrigger_and_more"
migrate_to = "1043_alter_savedviewfilterrule_rule_type"
def setUpBeforeMigration(self, apps):
User = apps.get_model("auth", "User")
Group = apps.get_model("auth", "Group")
self.Permission = apps.get_model("auth", "Permission")
self.user = User.objects.create(username="user1")
self.group = Group.objects.create(name="group1")
permission = self.Permission.objects.filter(
codename="add_workflow",
).first()
if permission is not None:
self.user.user_permissions.add(permission.id)
self.group.permissions.add(permission.id)
Workflow = apps.get_model("documents", "Workflow")
WorkflowTrigger = apps.get_model("documents", "WorkflowTrigger")
WorkflowAction = apps.get_model("documents", "WorkflowAction")
trigger = WorkflowTrigger.objects.create(
type=0,
sources=[DocumentSource.ConsumeFolder],
filter_path="*/path/*",
filter_filename="*file*",
)
action = WorkflowAction.objects.create(
assign_title="assign title",
)
workflow = Workflow.objects.create(
name="workflow 1",
order=0,
)
workflow.triggers.set([trigger])
workflow.actions.set([action])
workflow.save()
def test_remove_workflow_permissions_and_migrate_workflows_to_consumption_templates(
self,
):
permission = self.Permission.objects.filter(
codename="add_workflow",
).first()
if permission is not None:
self.assertFalse(permission in self.user.user_permissions.all())
self.assertFalse(permission in self.group.permissions.all())
ConsumptionTemplate = self.apps.get_model("documents", "ConsumptionTemplate")
self.assertEqual(ConsumptionTemplate.objects.all().count(), 1)

File diff suppressed because it is too large Load Diff

View File

@@ -265,6 +265,7 @@ class TestMigrations(TransactionTestCase):
return apps.get_containing_app_config(type(self).__module__).name
migrate_from = None
dependencies = None
migrate_to = None
auto_migrate = True
@@ -277,6 +278,8 @@ class TestMigrations(TransactionTestCase):
type(self).__name__,
)
self.migrate_from = [(self.app, self.migrate_from)]
if self.dependencies is not None:
self.migrate_from.extend(self.dependencies)
self.migrate_to = [(self.app, self.migrate_to)]
executor = MigrationExecutor(connection)
old_apps = executor.loader.project_state(self.migrate_from).apps