mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-18 00:46:25 +00:00
Feature: Workflows (#5121)
This commit is contained in:
@@ -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__()])
|
435
src/documents/tests/test_api_workflows.py
Normal file
435
src/documents/tests/test_api_workflows.py
Normal 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)
|
@@ -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])
|
@@ -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)
|
||||
|
@@ -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())
|
||||
|
131
src/documents/tests/test_migration_workflows.py
Normal file
131
src/documents/tests/test_migration_workflows.py
Normal 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)
|
1017
src/documents/tests/test_workflows.py
Normal file
1017
src/documents/tests/test_workflows.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
Reference in New Issue
Block a user