Migration from ConsumptionTemplate to Workflow
This commit is contained in:
parent
cf869b1356
commit
6984fcf821
@ -26,8 +26,7 @@ from documents.data_models import DocumentMetadataOverrides
|
|||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.file_handling import generate_unique_filename
|
from documents.file_handling import generate_unique_filename
|
||||||
from documents.loggers import LoggingMixin
|
from documents.loggers import LoggingMixin
|
||||||
from documents.matching import document_matches_template
|
from documents.matching import document_matches_workflow
|
||||||
from documents.models import ConsumptionTemplate
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
@ -36,6 +35,8 @@ from documents.models import DocumentType
|
|||||||
from documents.models import FileInfo
|
from documents.models import FileInfo
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
|
from documents.models import Workflow
|
||||||
|
from documents.models import WorkflowTrigger
|
||||||
from documents.parsers import DocumentParser
|
from documents.parsers import DocumentParser
|
||||||
from documents.parsers import ParseError
|
from documents.parsers import ParseError
|
||||||
from documents.parsers import get_parser_class_for_mime_type
|
from documents.parsers import get_parser_class_for_mime_type
|
||||||
@ -611,47 +612,54 @@ class Consumer(LoggingMixin):
|
|||||||
file name filters, path filters or mail rule filter if specified
|
file name filters, path filters or mail rule filter if specified
|
||||||
"""
|
"""
|
||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
for template in ConsumptionTemplate.objects.all().order_by("order"):
|
for workflow in Workflow.objects.all().order_by("order"):
|
||||||
template_overrides = DocumentMetadataOverrides()
|
template_overrides = DocumentMetadataOverrides()
|
||||||
|
|
||||||
if document_matches_template(input_doc, template):
|
if document_matches_workflow(
|
||||||
if template.assign_title is not None:
|
input_doc,
|
||||||
template_overrides.title = template.assign_title
|
workflow,
|
||||||
if template.assign_tags is not None:
|
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
|
):
|
||||||
|
for action in workflow.actions.all():
|
||||||
|
if action.assign_title is not None:
|
||||||
|
template_overrides.title = action.assign_title
|
||||||
|
if action.assign_tags is not None:
|
||||||
template_overrides.tag_ids = [
|
template_overrides.tag_ids = [
|
||||||
tag.pk for tag in template.assign_tags.all()
|
tag.pk for tag in action.assign_tags.all()
|
||||||
]
|
]
|
||||||
if template.assign_correspondent is not None:
|
if action.assign_correspondent is not None:
|
||||||
template_overrides.correspondent_id = (
|
template_overrides.correspondent_id = (
|
||||||
template.assign_correspondent.pk
|
action.assign_correspondent.pk
|
||||||
)
|
)
|
||||||
if template.assign_document_type is not None:
|
if action.assign_document_type is not None:
|
||||||
template_overrides.document_type_id = (
|
template_overrides.document_type_id = (
|
||||||
template.assign_document_type.pk
|
action.assign_document_type.pk
|
||||||
)
|
)
|
||||||
if template.assign_storage_path is not None:
|
if action.assign_storage_path is not None:
|
||||||
template_overrides.storage_path_id = template.assign_storage_path.pk
|
template_overrides.storage_path_id = (
|
||||||
if template.assign_owner is not None:
|
action.assign_storage_path.pk
|
||||||
template_overrides.owner_id = template.assign_owner.pk
|
)
|
||||||
if template.assign_view_users is not None:
|
if action.assign_owner is not None:
|
||||||
|
template_overrides.owner_id = action.assign_owner.pk
|
||||||
|
if action.assign_view_users is not None:
|
||||||
template_overrides.view_users = [
|
template_overrides.view_users = [
|
||||||
user.pk for user in template.assign_view_users.all()
|
user.pk for user in action.assign_view_users.all()
|
||||||
]
|
]
|
||||||
if template.assign_view_groups is not None:
|
if action.assign_view_groups is not None:
|
||||||
template_overrides.view_groups = [
|
template_overrides.view_groups = [
|
||||||
group.pk for group in template.assign_view_groups.all()
|
group.pk for group in action.assign_view_groups.all()
|
||||||
]
|
]
|
||||||
if template.assign_change_users is not None:
|
if action.assign_change_users is not None:
|
||||||
template_overrides.change_users = [
|
template_overrides.change_users = [
|
||||||
user.pk for user in template.assign_change_users.all()
|
user.pk for user in action.assign_change_users.all()
|
||||||
]
|
]
|
||||||
if template.assign_change_groups is not None:
|
if action.assign_change_groups is not None:
|
||||||
template_overrides.change_groups = [
|
template_overrides.change_groups = [
|
||||||
group.pk for group in template.assign_change_groups.all()
|
group.pk for group in action.assign_change_groups.all()
|
||||||
]
|
]
|
||||||
if template.assign_custom_fields is not None:
|
if action.assign_custom_fields is not None:
|
||||||
template_overrides.custom_field_ids = [
|
template_overrides.custom_field_ids = [
|
||||||
field.pk for field in template.assign_custom_fields.all()
|
field.pk for field in action.assign_custom_fields.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
overrides.update(template_overrides)
|
overrides.update(template_overrides)
|
||||||
|
@ -5,13 +5,14 @@ from fnmatch import fnmatch
|
|||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.models import ConsumptionTemplate
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
|
from documents.models import Workflow
|
||||||
|
from documents.models import WorkflowTrigger
|
||||||
from documents.permissions import get_objects_for_user_owner_aware
|
from documents.permissions import get_objects_for_user_owner_aware
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.matching")
|
logger = logging.getLogger("paperless.matching")
|
||||||
@ -237,65 +238,75 @@ def _split_match(matching_model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def document_matches_template(
|
def document_matches_workflow(
|
||||||
document: ConsumableDocument,
|
document: ConsumableDocument,
|
||||||
template: ConsumptionTemplate,
|
workflow: Workflow,
|
||||||
|
trigger_type: WorkflowTrigger.WorkflowTriggerType,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if the incoming document matches all filters and
|
Returns True if the incoming document matches all filters and
|
||||||
settings from the template, False otherwise
|
settings from the workflow trigger, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def log_match_failure(reason: str):
|
def log_match_failure(reason: str):
|
||||||
logger.info(f"Document did not match template {template.name}")
|
logger.info(f"Document did not match {workflow}")
|
||||||
logger.debug(reason)
|
logger.debug(reason)
|
||||||
|
|
||||||
|
trigger_matched = True
|
||||||
|
triggers = workflow.triggers.filter(type=trigger_type)
|
||||||
|
if len(triggers) == 0:
|
||||||
|
trigger_matched = False
|
||||||
|
else:
|
||||||
|
for trigger in triggers:
|
||||||
# Document source vs template source
|
# Document source vs template source
|
||||||
if document.source not in [int(x) for x in list(template.sources)]:
|
if document.source not in [int(x) for x in list(trigger.sources)]:
|
||||||
log_match_failure(
|
log_match_failure(
|
||||||
f"Document source {document.source.name} not in"
|
f"Document source {document.source.name} not in"
|
||||||
f" {[DocumentSource(int(x)).name for x in template.sources]}",
|
f" {[DocumentSource(int(x)).name for x in trigger.sources]}",
|
||||||
)
|
)
|
||||||
return False
|
trigger_matched = False
|
||||||
|
|
||||||
# Document mail rule vs template mail rule
|
# Document mail rule vs template mail rule
|
||||||
if (
|
if (
|
||||||
document.mailrule_id is not None
|
document.mailrule_id is not None
|
||||||
and template.filter_mailrule is not None
|
and trigger.filter_mailrule is not None
|
||||||
and document.mailrule_id != template.filter_mailrule.pk
|
and document.mailrule_id != trigger.filter_mailrule.pk
|
||||||
):
|
):
|
||||||
log_match_failure(
|
log_match_failure(
|
||||||
f"Document mail rule {document.mailrule_id}"
|
f"Document mail rule {document.mailrule_id}"
|
||||||
f" != {template.filter_mailrule.pk}",
|
f" != {trigger.filter_mailrule.pk}",
|
||||||
)
|
)
|
||||||
return False
|
trigger_matched = False
|
||||||
|
|
||||||
# Document filename vs template filename
|
# Document filename vs template filename
|
||||||
if (
|
if (
|
||||||
template.filter_filename is not None
|
trigger.filter_filename is not None
|
||||||
and len(template.filter_filename) > 0
|
and len(trigger.filter_filename) > 0
|
||||||
and not fnmatch(
|
and not fnmatch(
|
||||||
document.original_file.name.lower(),
|
document.original_file.name.lower(),
|
||||||
template.filter_filename.lower(),
|
trigger.filter_filename.lower(),
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
log_match_failure(
|
log_match_failure(
|
||||||
f"Document filename {document.original_file.name} does not match"
|
f"Document filename {document.original_file.name} does not match"
|
||||||
f" {template.filter_filename.lower()}",
|
f" {trigger.filter_filename.lower()}",
|
||||||
)
|
)
|
||||||
return False
|
trigger_matched = False
|
||||||
|
|
||||||
# Document path vs template path
|
# Document path vs template path
|
||||||
if (
|
if (
|
||||||
template.filter_path is not None
|
trigger.filter_path is not None
|
||||||
and len(template.filter_path) > 0
|
and len(trigger.filter_path) > 0
|
||||||
and not document.original_file.match(template.filter_path)
|
and not document.original_file.match(trigger.filter_path)
|
||||||
):
|
):
|
||||||
log_match_failure(
|
log_match_failure(
|
||||||
f"Document path {document.original_file}"
|
f"Document path {document.original_file}"
|
||||||
f" does not match {template.filter_path}",
|
f" does not match {trigger.filter_path}",
|
||||||
)
|
)
|
||||||
return False
|
trigger_matched = False
|
||||||
|
|
||||||
logger.info(f"Document matched template {template.name}")
|
if trigger_matched:
|
||||||
|
logger.info(f"Document matched {trigger} from {workflow}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
return trigger_matched
|
||||||
|
@ -0,0 +1,431 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2023-12-23 22:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import multiselectfield.db.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.management import create_permissions
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
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 paperless_mail.models import MailRule
|
||||||
|
|
||||||
|
|
||||||
|
def add_workflow_permissions(apps, schema_editor):
|
||||||
|
# create permissions without waiting for post_migrate signal
|
||||||
|
for app_config in apps.get_app_configs():
|
||||||
|
app_config.models_module = True
|
||||||
|
create_permissions(app_config, apps=apps, verbosity=0)
|
||||||
|
app_config.models_module = None
|
||||||
|
|
||||||
|
add_permission = Permission.objects.get(codename="add_document")
|
||||||
|
workflow_permissions = Permission.objects.filter(
|
||||||
|
codename__contains="workflow",
|
||||||
|
)
|
||||||
|
|
||||||
|
for user in User.objects.filter(Q(user_permissions=add_permission)).distinct():
|
||||||
|
user.user_permissions.add(*workflow_permissions)
|
||||||
|
|
||||||
|
for group in Group.objects.filter(Q(permissions=add_permission)).distinct():
|
||||||
|
group.permissions.add(*workflow_permissions)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_workflow_permissions(apps, schema_editor):
|
||||||
|
workflow_permissions = Permission.objects.filter(
|
||||||
|
codename__contains="workflow_permissions",
|
||||||
|
)
|
||||||
|
|
||||||
|
for user in User.objects.all():
|
||||||
|
user.user_permissions.remove(*workflow_permissions)
|
||||||
|
|
||||||
|
for group in Group.objects.all():
|
||||||
|
group.permissions.remove(*workflow_permissions)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_consumption_templates(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Migrate consumption templates to workflows. At this point ConsumptionTemplate still exists
|
||||||
|
but objects are not returned as their true model so we have to manually do that
|
||||||
|
"""
|
||||||
|
model_name = "ConsumptionTemplate"
|
||||||
|
app_name = "documents"
|
||||||
|
|
||||||
|
ConsumptionTemplate = apps.get_model(app_label=app_name, model_name=model_name)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for template in ConsumptionTemplate.objects.all():
|
||||||
|
trigger = WorkflowTrigger(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
|
sources=template.sources,
|
||||||
|
filter_path=template.filter_path,
|
||||||
|
filter_filename=template.filter_filename,
|
||||||
|
)
|
||||||
|
if template.filter_mailrule is not None:
|
||||||
|
trigger.filter_mailrule = MailRule.objects.get(
|
||||||
|
id=template.filter_mailrule.id,
|
||||||
|
)
|
||||||
|
trigger.save()
|
||||||
|
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
assign_title=template.assign_title,
|
||||||
|
)
|
||||||
|
if template.assign_document_type is not None:
|
||||||
|
action.assign_document_type = DocumentType.objects.get(
|
||||||
|
id=template.assign_document_type.id,
|
||||||
|
)
|
||||||
|
if template.assign_correspondent is not None:
|
||||||
|
action.assign_correspondent = Correspondent.objects.get(
|
||||||
|
id=template.assign_correspondent.id,
|
||||||
|
)
|
||||||
|
if template.assign_storage_path is not None:
|
||||||
|
action.assign_storage_path = StoragePath.objects.get(
|
||||||
|
id=template.assign_storage_path.id,
|
||||||
|
)
|
||||||
|
if template.assign_owner is not None:
|
||||||
|
action.assign_owner = User.objects.get(id=template.assign_owner.id)
|
||||||
|
if template.assign_tags is not None:
|
||||||
|
action.assign_tags.set(
|
||||||
|
Tag.objects.filter(
|
||||||
|
id__in=[t.id for t in template.assign_tags.all()],
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
if template.assign_view_users is not None:
|
||||||
|
action.assign_view_users.set(
|
||||||
|
User.objects.filter(
|
||||||
|
id__in=[u.id for u in template.assign_view_users.all()],
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
if template.assign_view_groups is not None:
|
||||||
|
action.assign_view_groups.set(
|
||||||
|
Group.objects.filter(
|
||||||
|
id__in=[g.id for g in template.assign_view_groups.all()],
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
if template.assign_change_users is not None:
|
||||||
|
action.assign_change_users.set(
|
||||||
|
User.objects.filter(
|
||||||
|
id__in=[u.id for u in template.assign_change_users.all()],
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
if template.assign_change_groups is not None:
|
||||||
|
action.assign_change_groups.set(
|
||||||
|
Group.objects.filter(
|
||||||
|
id__in=[g.id for g in template.assign_change_groups.all()],
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
if template.assign_custom_fields is not None:
|
||||||
|
action.assign_custom_fields.set(
|
||||||
|
CustomField.objects.filter(
|
||||||
|
id__in=[cf.id for cf in template.assign_custom_fields.all()],
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
workflow = Workflow.objects.create(
|
||||||
|
name=template.name,
|
||||||
|
order=template.order,
|
||||||
|
)
|
||||||
|
workflow.triggers.set([trigger])
|
||||||
|
workflow.actions.set([action])
|
||||||
|
workflow.save()
|
||||||
|
|
||||||
|
|
||||||
|
def unmigrate_consumption_templates(apps, schema_editor):
|
||||||
|
model_name = "ConsumptionTemplate"
|
||||||
|
app_name = "documents"
|
||||||
|
|
||||||
|
ConsumptionTemplate = apps.get_model(app_label=app_name, model_name=model_name)
|
||||||
|
|
||||||
|
for workflow in Workflow.objects.all():
|
||||||
|
template = ConsumptionTemplate.objects.create(
|
||||||
|
name=workflow.name,
|
||||||
|
order=workflow.order,
|
||||||
|
sources=workflow.sources,
|
||||||
|
filter_path=workflow.triggers.first().filter_path,
|
||||||
|
filter_filename=workflow.triggers.first().filter_filename,
|
||||||
|
filter_mailrule=workflow.triggers.first().filter_mailrule,
|
||||||
|
assign_title=workflow.actions.first().assign_title,
|
||||||
|
assign_document_type=workflow.actions.first().assign_document_type,
|
||||||
|
assign_correspondent=workflow.actions.first().assign_correspondent,
|
||||||
|
assign_storage_path=workflow.actions.first().assign_storage_path,
|
||||||
|
assign_owner=workflow.actions.first().assign_owner,
|
||||||
|
)
|
||||||
|
template.assign_tags.set(workflow.actions.first().assign_tags.all())
|
||||||
|
template.assign_view_users.set(workflow.actions.first().assign_view_users.all())
|
||||||
|
template.assign_view_groups.set(
|
||||||
|
workflow.actions.first().assign_view_groups.all(),
|
||||||
|
)
|
||||||
|
template.assign_change_users.set(
|
||||||
|
workflow.actions.first().assign_change_users.all(),
|
||||||
|
)
|
||||||
|
template.assign_change_groups.set(
|
||||||
|
workflow.actions.first().assign_change_groups.all(),
|
||||||
|
)
|
||||||
|
template.assign_custom_fields.set(
|
||||||
|
workflow.actions.first().assign_custom_fields.all(),
|
||||||
|
)
|
||||||
|
template.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("paperless_mail", "0023_remove_mailrule_filter_attachment_filename_and_more"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
("documents", "1043_alter_savedviewfilterrule_rule_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Workflow",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(max_length=256, unique=True, verbose_name="name"),
|
||||||
|
),
|
||||||
|
("order", models.IntegerField(default=0, verbose_name="order")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WorkflowAction",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_title",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Assign a document title, can include some placeholders, see documentation.",
|
||||||
|
max_length=256,
|
||||||
|
null=True,
|
||||||
|
verbose_name="assign title",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_change_groups",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="auth.group",
|
||||||
|
verbose_name="grant change permissions to these groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_change_users",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="grant change permissions to these users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_correspondent",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="documents.correspondent",
|
||||||
|
verbose_name="assign this correspondent",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_custom_fields",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.customfield",
|
||||||
|
verbose_name="assign these custom fields",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_document_type",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="documents.documenttype",
|
||||||
|
verbose_name="assign this document type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_owner",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="assign this owner",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_storage_path",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="documents.storagepath",
|
||||||
|
verbose_name="assign this storage path",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_tags",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
to="documents.tag",
|
||||||
|
verbose_name="assign this tag",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_view_groups",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="auth.group",
|
||||||
|
verbose_name="grant view permissions to these groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"assign_view_users",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="grant view permissions to these users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "workflow action",
|
||||||
|
"verbose_name_plural": "workflow actions",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WorkflowTrigger",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"type",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Consumption"),
|
||||||
|
(2, "Document Added"),
|
||||||
|
(3, "Document Updated"),
|
||||||
|
],
|
||||||
|
default=1,
|
||||||
|
verbose_name="Workflow Trigger Type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sources",
|
||||||
|
multiselectfield.db.fields.MultiSelectField(
|
||||||
|
choices=[
|
||||||
|
(1, "Consume Folder"),
|
||||||
|
(2, "Api Upload"),
|
||||||
|
(3, "Mail Fetch"),
|
||||||
|
],
|
||||||
|
default="1,2,3",
|
||||||
|
max_length=5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"filter_path",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Only consume documents with a path that matches this if specified. Wildcards specified as * are allowed. Case insensitive.",
|
||||||
|
max_length=256,
|
||||||
|
null=True,
|
||||||
|
verbose_name="filter path",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"filter_filename",
|
||||||
|
models.CharField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.",
|
||||||
|
max_length=256,
|
||||||
|
null=True,
|
||||||
|
verbose_name="filter filename",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"filter_mailrule",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="paperless_mail.mailrule",
|
||||||
|
verbose_name="filter documents from this mail rule",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "workflow trigger",
|
||||||
|
"verbose_name_plural": "workflow triggers",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
add_workflow_permissions,
|
||||||
|
remove_workflow_permissions,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflow",
|
||||||
|
name="actions",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="workflows",
|
||||||
|
to="documents.workflowaction",
|
||||||
|
verbose_name="actions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflow",
|
||||||
|
name="triggers",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="workflows",
|
||||||
|
to="documents.workflowtrigger",
|
||||||
|
verbose_name="triggers",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_consumption_templates,
|
||||||
|
unmigrate_consumption_templates,
|
||||||
|
),
|
||||||
|
migrations.DeleteModel("ConsumptionTemplate"),
|
||||||
|
]
|
@ -888,15 +888,22 @@ if settings.AUDIT_LOG_ENABLED:
|
|||||||
auditlog.register(CustomFieldInstance)
|
auditlog.register(CustomFieldInstance)
|
||||||
|
|
||||||
|
|
||||||
class ConsumptionTemplate(models.Model):
|
class WorkflowTrigger(models.Model):
|
||||||
|
class WorkflowTriggerType(models.IntegerChoices):
|
||||||
|
CONSUMPTION = 1, _("Consumption")
|
||||||
|
DOCUMENT_ADDED = 2, _("Document Added")
|
||||||
|
DOCUMENT_UPDATED = 3, _("Document Updated")
|
||||||
|
|
||||||
class DocumentSourceChoices(models.IntegerChoices):
|
class DocumentSourceChoices(models.IntegerChoices):
|
||||||
CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder")
|
CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder")
|
||||||
API_UPLOAD = DocumentSource.ApiUpload.value, _("Api Upload")
|
API_UPLOAD = DocumentSource.ApiUpload.value, _("Api Upload")
|
||||||
MAIL_FETCH = DocumentSource.MailFetch.value, _("Mail Fetch")
|
MAIL_FETCH = DocumentSource.MailFetch.value, _("Mail Fetch")
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=256, unique=True)
|
type = models.PositiveIntegerField(
|
||||||
|
_("Workflow Trigger Type"),
|
||||||
order = models.IntegerField(_("order"), default=0)
|
choices=WorkflowTriggerType.choices,
|
||||||
|
default=WorkflowTriggerType.CONSUMPTION,
|
||||||
|
)
|
||||||
|
|
||||||
sources = MultiSelectField(
|
sources = MultiSelectField(
|
||||||
max_length=5,
|
max_length=5,
|
||||||
@ -936,6 +943,15 @@ class ConsumptionTemplate(models.Model):
|
|||||||
verbose_name=_("filter documents from this mail rule"),
|
verbose_name=_("filter documents from this mail rule"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("workflow trigger")
|
||||||
|
verbose_name_plural = _("workflow triggers")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"WorfklowTrigger: {self.pk}"
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowAction(models.Model):
|
||||||
assign_title = models.CharField(
|
assign_title = models.CharField(
|
||||||
_("assign title"),
|
_("assign title"),
|
||||||
max_length=256,
|
max_length=256,
|
||||||
@ -1022,8 +1038,31 @@ class ConsumptionTemplate(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("consumption template")
|
verbose_name = _("workflow action")
|
||||||
verbose_name_plural = _("consumption templates")
|
verbose_name_plural = _("workflow actions")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"WorkflowAction {self.pk}"
|
||||||
|
|
||||||
|
|
||||||
|
class Workflow(models.Model):
|
||||||
|
name = models.CharField(_("name"), max_length=256, unique=True)
|
||||||
|
|
||||||
|
order = models.IntegerField(_("order"), default=0)
|
||||||
|
|
||||||
|
triggers = models.ManyToManyField(
|
||||||
|
WorkflowTrigger,
|
||||||
|
related_name="workflows",
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("triggers"),
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = models.ManyToManyField(
|
||||||
|
WorkflowAction,
|
||||||
|
related_name="workflows",
|
||||||
|
blank=False,
|
||||||
|
verbose_name=_("actions"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Workflow: {self.name}"
|
||||||
|
@ -2,6 +2,7 @@ import datetime
|
|||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
from celery import states
|
from celery import states
|
||||||
@ -24,7 +25,6 @@ from rest_framework.fields import SerializerMethodField
|
|||||||
|
|
||||||
from documents import bulk_edit
|
from documents import bulk_edit
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.models import ConsumptionTemplate
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
@ -38,6 +38,9 @@ from documents.models import ShareLink
|
|||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.models import UiSettings
|
from documents.models import UiSettings
|
||||||
|
from documents.models import Workflow
|
||||||
|
from documents.models import WorkflowAction
|
||||||
|
from documents.models import WorkflowTrigger
|
||||||
from documents.parsers import is_mime_type_supported
|
from documents.parsers import is_mime_type_supported
|
||||||
from documents.permissions import get_groups_with_only_permission
|
from documents.permissions import get_groups_with_only_permission
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
@ -1258,10 +1261,9 @@ class BulkEditObjectPermissionsSerializer(serializers.Serializer, SetPermissions
|
|||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class ConsumptionTemplateSerializer(serializers.ModelSerializer):
|
class WorkflowTriggerSerializer(serializers.ModelSerializer):
|
||||||
order = serializers.IntegerField(required=False)
|
|
||||||
sources = fields.MultipleChoiceField(
|
sources = fields.MultipleChoiceField(
|
||||||
choices=ConsumptionTemplate.DocumentSourceChoices.choices,
|
choices=WorkflowTrigger.DocumentSourceChoices.choices,
|
||||||
allow_empty=False,
|
allow_empty=False,
|
||||||
default={
|
default={
|
||||||
DocumentSource.ConsumeFolder,
|
DocumentSource.ConsumeFolder,
|
||||||
@ -1269,32 +1271,21 @@ class ConsumptionTemplateSerializer(serializers.ModelSerializer):
|
|||||||
DocumentSource.MailFetch,
|
DocumentSource.MailFetch,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assign_correspondent = CorrespondentField(allow_null=True, required=False)
|
|
||||||
assign_tags = TagsField(many=True, allow_null=True, required=False)
|
type = serializers.ChoiceField(
|
||||||
assign_document_type = DocumentTypeField(allow_null=True, required=False)
|
choices=WorkflowTrigger.WorkflowTriggerType.choices,
|
||||||
assign_storage_path = StoragePathField(allow_null=True, required=False)
|
label="Trigger Type",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsumptionTemplate
|
model = WorkflowTrigger
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
|
||||||
"order",
|
|
||||||
"sources",
|
"sources",
|
||||||
|
"type",
|
||||||
"filter_path",
|
"filter_path",
|
||||||
"filter_filename",
|
"filter_filename",
|
||||||
"filter_mailrule",
|
"filter_mailrule",
|
||||||
"assign_title",
|
|
||||||
"assign_tags",
|
|
||||||
"assign_correspondent",
|
|
||||||
"assign_document_type",
|
|
||||||
"assign_storage_path",
|
|
||||||
"assign_owner",
|
|
||||||
"assign_view_users",
|
|
||||||
"assign_view_groups",
|
|
||||||
"assign_change_users",
|
|
||||||
"assign_change_groups",
|
|
||||||
"assign_custom_fields",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
@ -1302,12 +1293,6 @@ class ConsumptionTemplateSerializer(serializers.ModelSerializer):
|
|||||||
attrs["sources"] = {DocumentSource.MailFetch.value}
|
attrs["sources"] = {DocumentSource.MailFetch.value}
|
||||||
|
|
||||||
# Empty strings treated as None to avoid unexpected behavior
|
# Empty strings treated as None to avoid unexpected behavior
|
||||||
if (
|
|
||||||
"assign_title" in attrs
|
|
||||||
and attrs["assign_title"] is not None
|
|
||||||
and len(attrs["assign_title"]) == 0
|
|
||||||
):
|
|
||||||
attrs["assign_title"] = None
|
|
||||||
if (
|
if (
|
||||||
"filter_filename" in attrs
|
"filter_filename" in attrs
|
||||||
and attrs["filter_filename"] is not None
|
and attrs["filter_filename"] is not None
|
||||||
@ -1322,7 +1307,8 @@ class ConsumptionTemplateSerializer(serializers.ModelSerializer):
|
|||||||
attrs["filter_path"] = None
|
attrs["filter_path"] = None
|
||||||
|
|
||||||
if (
|
if (
|
||||||
"filter_mailrule" not in attrs
|
attrs["type"] == WorkflowTrigger.WorkflowTriggerType.CONSUMPTION
|
||||||
|
and "filter_mailrule" not in attrs
|
||||||
and ("filter_filename" not in attrs or attrs["filter_filename"] is None)
|
and ("filter_filename" not in attrs or attrs["filter_filename"] is None)
|
||||||
and ("filter_path" not in attrs or attrs["filter_path"] is None)
|
and ("filter_path" not in attrs or attrs["filter_path"] is None)
|
||||||
):
|
):
|
||||||
@ -1331,3 +1317,89 @@ class ConsumptionTemplateSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||||
|
assign_correspondent = CorrespondentField(allow_null=True, required=False)
|
||||||
|
assign_tags = TagsField(many=True, allow_null=True, required=False)
|
||||||
|
assign_document_type = DocumentTypeField(allow_null=True, required=False)
|
||||||
|
assign_storage_path = StoragePathField(allow_null=True, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WorkflowAction
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"assign_title",
|
||||||
|
"assign_tags",
|
||||||
|
"assign_correspondent",
|
||||||
|
"assign_document_type",
|
||||||
|
"assign_storage_path",
|
||||||
|
"assign_owner",
|
||||||
|
"assign_view_users",
|
||||||
|
"assign_view_groups",
|
||||||
|
"assign_change_users",
|
||||||
|
"assign_change_groups",
|
||||||
|
"assign_custom_fields",
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
# Empty strings treated as None to avoid unexpected behavior
|
||||||
|
if (
|
||||||
|
"assign_title" in attrs
|
||||||
|
and attrs["assign_title"] is not None
|
||||||
|
and len(attrs["assign_title"]) == 0
|
||||||
|
):
|
||||||
|
attrs["assign_title"] = None
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowSerializer(serializers.ModelSerializer):
|
||||||
|
order = serializers.IntegerField(required=False)
|
||||||
|
|
||||||
|
triggers = WorkflowTriggerSerializer(many=True)
|
||||||
|
actions = WorkflowActionSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Workflow
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"order",
|
||||||
|
"triggers",
|
||||||
|
"actions",
|
||||||
|
]
|
||||||
|
|
||||||
|
def create(self, validated_data: Any) -> Any:
|
||||||
|
if "triggers" in validated_data:
|
||||||
|
# WorkflowTrigger.objects.update_or_create(triggers)
|
||||||
|
triggers = validated_data.pop("triggers")
|
||||||
|
|
||||||
|
if "actions" in validated_data:
|
||||||
|
# WorkflowAction.objects.update_or_create(actions)
|
||||||
|
actions = validated_data.pop("actions")
|
||||||
|
|
||||||
|
instance = super().create(validated_data)
|
||||||
|
|
||||||
|
set_triggers = []
|
||||||
|
set_actions = []
|
||||||
|
|
||||||
|
if triggers is not None:
|
||||||
|
for trigger in triggers:
|
||||||
|
print(trigger)
|
||||||
|
trigger_instance = WorkflowTrigger.objects.filter(**trigger).first()
|
||||||
|
if trigger_instance is not None:
|
||||||
|
set_triggers.append(trigger_instance)
|
||||||
|
|
||||||
|
if actions is not None:
|
||||||
|
for action in actions:
|
||||||
|
print(action)
|
||||||
|
action_instance = WorkflowAction.objects.filter(**action).first()
|
||||||
|
if action_instance is not None:
|
||||||
|
set_actions.append(action_instance)
|
||||||
|
|
||||||
|
instance.triggers.set(set_triggers)
|
||||||
|
instance.actions.set(set_actions)
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
return instance
|
||||||
|
@ -6,19 +6,23 @@ from rest_framework import status
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.models import ConsumptionTemplate
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
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 documents.tests.utils import DirectoriesMixin
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
|
|
||||||
|
|
||||||
class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
|
class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||||
ENDPOINT = "/api/consumption_templates/"
|
ENDPOINT = "/api/workflows/"
|
||||||
|
ENDPOINT_TRIGGERS = "/api/workflow_triggers/"
|
||||||
|
ENDPOINT_ACTIONS = "/api/workflow_actions/"
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@ -42,104 +46,158 @@ class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
|
|||||||
data_type="integer",
|
data_type="integer",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.ct = ConsumptionTemplate.objects.create(
|
self.trigger = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}",
|
sources=f"{int(DocumentSource.ApiUpload)},{int(DocumentSource.ConsumeFolder)},{int(DocumentSource.MailFetch)}",
|
||||||
filter_filename="*simple*",
|
filter_filename="*simple*",
|
||||||
filter_path="*/samples/*",
|
filter_path="*/samples/*",
|
||||||
|
)
|
||||||
|
self.action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
assign_owner=self.user2,
|
assign_owner=self.user2,
|
||||||
)
|
)
|
||||||
self.ct.assign_tags.add(self.t1)
|
self.action.assign_tags.add(self.t1)
|
||||||
self.ct.assign_tags.add(self.t2)
|
self.action.assign_tags.add(self.t2)
|
||||||
self.ct.assign_tags.add(self.t3)
|
self.action.assign_tags.add(self.t3)
|
||||||
self.ct.assign_view_users.add(self.user3.pk)
|
self.action.assign_view_users.add(self.user3.pk)
|
||||||
self.ct.assign_view_groups.add(self.group1.pk)
|
self.action.assign_view_groups.add(self.group1.pk)
|
||||||
self.ct.assign_change_users.add(self.user3.pk)
|
self.action.assign_change_users.add(self.user3.pk)
|
||||||
self.ct.assign_change_groups.add(self.group1.pk)
|
self.action.assign_change_groups.add(self.group1.pk)
|
||||||
self.ct.assign_custom_fields.add(self.cf1.pk)
|
self.action.assign_custom_fields.add(self.cf1.pk)
|
||||||
self.ct.assign_custom_fields.add(self.cf2.pk)
|
self.action.assign_custom_fields.add(self.cf2.pk)
|
||||||
self.ct.save()
|
self.action.save()
|
||||||
|
|
||||||
def test_api_get_consumption_template(self):
|
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:
|
GIVEN:
|
||||||
- API request to get all consumption template
|
- API request to get all workflows
|
||||||
WHEN:
|
WHEN:
|
||||||
- API is called
|
- API is called
|
||||||
THEN:
|
THEN:
|
||||||
- Existing consumption templates are returned
|
- Existing workflows are returned
|
||||||
"""
|
"""
|
||||||
response = self.client.get(self.ENDPOINT, format="json")
|
response = self.client.get(self.ENDPOINT, format="json")
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["count"], 1)
|
self.assertEqual(response.data["count"], 1)
|
||||||
|
|
||||||
resp_consumption_template = response.data["results"][0]
|
resp_workflow = response.data["results"][0]
|
||||||
self.assertEqual(resp_consumption_template["id"], self.ct.id)
|
self.assertEqual(resp_workflow["id"], self.workflow.id)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
resp_consumption_template["assign_correspondent"],
|
resp_workflow["actions"][0]["assign_correspondent"],
|
||||||
self.ct.assign_correspondent.pk,
|
self.action.assign_correspondent.pk,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_api_create_consumption_template(self):
|
def test_api_create_workflow(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- API request to create a consumption template
|
- API request to create a workflow, trigger and action
|
||||||
WHEN:
|
WHEN:
|
||||||
- API is called
|
- API is called
|
||||||
THEN:
|
THEN:
|
||||||
- Correct HTTP response
|
- Correct HTTP response
|
||||||
- New template is created
|
- 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(
|
response = self.client.post(
|
||||||
self.ENDPOINT,
|
self.ENDPOINT,
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"name": "Template 2",
|
"name": "Workflow 2",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
"sources": [DocumentSource.ApiUpload],
|
"sources": [DocumentSource.ApiUpload],
|
||||||
"filter_filename": "*test*",
|
"type": trigger_response.data["type"],
|
||||||
|
"filter_filename": trigger_response.data["filter_filename"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"assign_title": action_response.data["assign_title"],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(ConsumptionTemplate.objects.count(), 2)
|
self.assertEqual(Workflow.objects.count(), 2)
|
||||||
|
|
||||||
def test_api_create_invalid_consumption_template(self):
|
def test_api_create_invalid_workflow_trigger(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- API request to create a consumption template
|
- API request to create a workflow trigger
|
||||||
- Neither file name nor path filter are specified
|
- Neither type or file name nor path filter are specified
|
||||||
WHEN:
|
WHEN:
|
||||||
- API is called
|
- API is called
|
||||||
THEN:
|
THEN:
|
||||||
- Correct HTTP 400 response
|
- Correct HTTP 400 response
|
||||||
- No template is created
|
- No objects are created
|
||||||
"""
|
"""
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.ENDPOINT,
|
self.ENDPOINT_TRIGGERS,
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"name": "Template 2",
|
|
||||||
"order": 1,
|
|
||||||
"sources": [DocumentSource.ApiUpload],
|
"sources": [DocumentSource.ApiUpload],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
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):
|
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:
|
GIVEN:
|
||||||
- API request to create a consumption template
|
- API request to create a workflow trigger and action
|
||||||
- Path or filename filter or assign title are empty string
|
- Path or filename filter or assign title are empty string
|
||||||
WHEN:
|
WHEN:
|
||||||
- API is called
|
- API is called
|
||||||
@ -147,31 +205,40 @@ class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
|
|||||||
- Template is created but filter or title assignment is not set if ""
|
- Template is created but filter or title assignment is not set if ""
|
||||||
"""
|
"""
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.ENDPOINT,
|
self.ENDPOINT_TRIGGERS,
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"name": "Template 2",
|
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
"order": 1,
|
|
||||||
"sources": [DocumentSource.ApiUpload],
|
"sources": [DocumentSource.ApiUpload],
|
||||||
"filter_filename": "*test*",
|
"filter_filename": "*test*",
|
||||||
"filter_path": "",
|
"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": "",
|
"assign_title": "",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
ct = ConsumptionTemplate.objects.get(name="Template 2")
|
action = WorkflowAction.objects.get(id=response.data["id"])
|
||||||
self.assertEqual(ct.filter_filename, "*test*")
|
self.assertIsNone(action.assign_title)
|
||||||
self.assertIsNone(ct.filter_path)
|
|
||||||
self.assertIsNone(ct.assign_title)
|
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.ENDPOINT,
|
self.ENDPOINT_TRIGGERS,
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"name": "Template 3",
|
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
"order": 1,
|
|
||||||
"sources": [DocumentSource.ApiUpload],
|
"sources": [DocumentSource.ApiUpload],
|
||||||
"filter_filename": "",
|
"filter_filename": "",
|
||||||
"filter_path": "*/test/*",
|
"filter_path": "*/test/*",
|
||||||
@ -180,18 +247,18 @@ class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
ct2 = ConsumptionTemplate.objects.get(name="Template 3")
|
trigger2 = WorkflowTrigger.objects.get(id=response.data["id"])
|
||||||
self.assertEqual(ct2.filter_path, "*/test/*")
|
self.assertEqual(trigger2.filter_path, "*/test/*")
|
||||||
self.assertIsNone(ct2.filter_filename)
|
self.assertIsNone(trigger2.filter_filename)
|
||||||
|
|
||||||
def test_api_create_consumption_template_with_mailrule(self):
|
def test_api_create_workflow_trigger_with_mailrule(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- API request to create a consumption template with a mail rule but no MailFetch source
|
- API request to create a workflow trigger with a mail rule but no MailFetch source
|
||||||
WHEN:
|
WHEN:
|
||||||
- API is called
|
- API is called
|
||||||
THEN:
|
THEN:
|
||||||
- New template is created with MailFetch as source
|
- New trigger is created with MailFetch as source
|
||||||
"""
|
"""
|
||||||
account1 = MailAccount.objects.create(
|
account1 = MailAccount.objects.create(
|
||||||
name="Email1",
|
name="Email1",
|
||||||
@ -219,11 +286,10 @@ class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
|
|||||||
attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
|
attachment_type=MailRule.AttachmentProcessing.ATTACHMENTS_ONLY,
|
||||||
)
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.ENDPOINT,
|
self.ENDPOINT_TRIGGERS,
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"name": "Template 2",
|
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
"order": 1,
|
|
||||||
"sources": [DocumentSource.ApiUpload],
|
"sources": [DocumentSource.ApiUpload],
|
||||||
"filter_mailrule": rule1.pk,
|
"filter_mailrule": rule1.pk,
|
||||||
},
|
},
|
||||||
@ -231,6 +297,6 @@ class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(ConsumptionTemplate.objects.count(), 2)
|
self.assertEqual(WorkflowTrigger.objects.count(), 2)
|
||||||
ct = ConsumptionTemplate.objects.get(name="Template 2")
|
trigger = WorkflowTrigger.objects.get(id=response.data["id"])
|
||||||
self.assertEqual(ct.sources, [int(DocumentSource.MailFetch).__str__()])
|
self.assertEqual(trigger.sources, [int(DocumentSource.MailFetch).__str__()])
|
@ -9,12 +9,14 @@ from django.contrib.auth.models import User
|
|||||||
from documents import tasks
|
from documents import tasks
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.models import ConsumptionTemplate
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
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 documents.tests.utils import DirectoriesMixin
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
@ -22,7 +24,7 @@ from paperless_mail.models import MailRule
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||||
SAMPLE_DIR = Path(__file__).parent / "samples"
|
SAMPLE_DIR = Path(__file__).parent / "samples"
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@ -73,39 +75,47 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
return super().setUp()
|
return super().setUp()
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
def test_consumption_template_match(self, m):
|
def test_workflow_match(self, m):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing consumption template
|
- Existing workflow
|
||||||
WHEN:
|
WHEN:
|
||||||
- File that matches is consumed
|
- File that matches is consumed
|
||||||
THEN:
|
THEN:
|
||||||
- Template overrides are applied
|
- Template overrides are applied
|
||||||
"""
|
"""
|
||||||
ct = ConsumptionTemplate.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_filename="*simple*",
|
filter_filename="*simple*",
|
||||||
filter_path="*/samples/*",
|
filter_path="*/samples/*",
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
assign_owner=self.user2,
|
assign_owner=self.user2,
|
||||||
)
|
)
|
||||||
ct.assign_tags.add(self.t1)
|
action.assign_tags.add(self.t1)
|
||||||
ct.assign_tags.add(self.t2)
|
action.assign_tags.add(self.t2)
|
||||||
ct.assign_tags.add(self.t3)
|
action.assign_tags.add(self.t3)
|
||||||
ct.assign_view_users.add(self.user3.pk)
|
action.assign_view_users.add(self.user3.pk)
|
||||||
ct.assign_view_groups.add(self.group1.pk)
|
action.assign_view_groups.add(self.group1.pk)
|
||||||
ct.assign_change_users.add(self.user3.pk)
|
action.assign_change_users.add(self.user3.pk)
|
||||||
ct.assign_change_groups.add(self.group1.pk)
|
action.assign_change_groups.add(self.group1.pk)
|
||||||
ct.assign_custom_fields.add(self.cf1.pk)
|
action.assign_custom_fields.add(self.cf1.pk)
|
||||||
ct.assign_custom_fields.add(self.cf2.pk)
|
action.assign_custom_fields.add(self.cf2.pk)
|
||||||
ct.save()
|
action.save()
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
self.assertEqual(ct.__str__(), "Template 1")
|
self.assertEqual(w.__str__(), "Workflow: Workflow 1")
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
@ -142,40 +152,48 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
)
|
)
|
||||||
|
|
||||||
info = cm.output[0]
|
info = cm.output[0]
|
||||||
expected_str = f"Document matched template {ct}"
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
self.assertIn(expected_str, info)
|
self.assertIn(expected_str, info)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
def test_consumption_template_match_mailrule(self, m):
|
def test_workflow_match_mailrule(self, m):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing consumption template
|
- Existing workflow
|
||||||
WHEN:
|
WHEN:
|
||||||
- File that matches is consumed via mail rule
|
- File that matches is consumed via mail rule
|
||||||
THEN:
|
THEN:
|
||||||
- Template overrides are applied
|
- Template overrides are applied
|
||||||
"""
|
"""
|
||||||
ct = ConsumptionTemplate.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_mailrule=self.rule1,
|
filter_mailrule=self.rule1,
|
||||||
|
)
|
||||||
|
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
assign_owner=self.user2,
|
assign_owner=self.user2,
|
||||||
)
|
)
|
||||||
ct.assign_tags.add(self.t1)
|
action.assign_tags.add(self.t1)
|
||||||
ct.assign_tags.add(self.t2)
|
action.assign_tags.add(self.t2)
|
||||||
ct.assign_tags.add(self.t3)
|
action.assign_tags.add(self.t3)
|
||||||
ct.assign_view_users.add(self.user3.pk)
|
action.assign_view_users.add(self.user3.pk)
|
||||||
ct.assign_view_groups.add(self.group1.pk)
|
action.assign_view_groups.add(self.group1.pk)
|
||||||
ct.assign_change_users.add(self.user3.pk)
|
action.assign_change_users.add(self.user3.pk)
|
||||||
ct.assign_change_groups.add(self.group1.pk)
|
action.assign_change_groups.add(self.group1.pk)
|
||||||
ct.save()
|
action.save()
|
||||||
|
|
||||||
self.assertEqual(ct.__str__(), "Template 1")
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.async_to_sync"):
|
||||||
@ -208,45 +226,64 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
)
|
)
|
||||||
|
|
||||||
info = cm.output[0]
|
info = cm.output[0]
|
||||||
expected_str = f"Document matched template {ct}"
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
self.assertIn(expected_str, info)
|
self.assertIn(expected_str, info)
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
def test_consumption_template_match_multiple(self, m):
|
def test_workflow_match_multiple(self, m):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Multiple existing consumption template
|
- Multiple existing workflow
|
||||||
WHEN:
|
WHEN:
|
||||||
- File that matches is consumed
|
- File that matches is consumed
|
||||||
THEN:
|
THEN:
|
||||||
- Template overrides are applied with subsequent templates only overwriting empty values
|
- Template overrides are applied with subsequent templates only overwriting empty values
|
||||||
or merging if multiple
|
or merging if multiple
|
||||||
"""
|
"""
|
||||||
ct1 = ConsumptionTemplate.objects.create(
|
trigger1 = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_path="*/samples/*",
|
filter_path="*/samples/*",
|
||||||
|
)
|
||||||
|
action1 = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
)
|
)
|
||||||
ct1.assign_tags.add(self.t1)
|
action1.assign_tags.add(self.t1)
|
||||||
ct1.assign_tags.add(self.t2)
|
action1.assign_tags.add(self.t2)
|
||||||
ct1.assign_view_users.add(self.user2)
|
action1.assign_view_users.add(self.user2)
|
||||||
ct1.save()
|
action1.save()
|
||||||
ct2 = ConsumptionTemplate.objects.create(
|
|
||||||
name="Template 2",
|
w1 = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
order=0,
|
order=0,
|
||||||
|
)
|
||||||
|
w1.triggers.add(trigger1)
|
||||||
|
w1.actions.add(action1)
|
||||||
|
w1.save()
|
||||||
|
|
||||||
|
trigger2 = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_filename="*simple*",
|
filter_filename="*simple*",
|
||||||
|
)
|
||||||
|
action2 = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c2,
|
assign_correspondent=self.c2,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
)
|
)
|
||||||
ct2.assign_tags.add(self.t3)
|
action2.assign_tags.add(self.t3)
|
||||||
ct1.assign_view_users.add(self.user3)
|
action2.assign_view_users.add(self.user3)
|
||||||
ct2.save()
|
action2.save()
|
||||||
|
|
||||||
|
w2 = Workflow.objects.create(
|
||||||
|
name="Workflow 2",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w2.triggers.add(trigger2)
|
||||||
|
w2.actions.add(action2)
|
||||||
|
w2.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
@ -276,33 +313,43 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
[self.user2.pk, self.user3.pk],
|
[self.user2.pk, self.user3.pk],
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_str = f"Document matched template {ct1}"
|
expected_str = f"Document matched {trigger1} from {w1}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = f"Document matched template {ct2}"
|
expected_str = f"Document matched {trigger2} from {w2}"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
def test_consumption_template_no_match_filename(self, m):
|
def test_workflow_no_match_filename(self, m):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing consumption template
|
- Existing workflow
|
||||||
WHEN:
|
WHEN:
|
||||||
- File that does not match on filename is consumed
|
- File that does not match on filename is consumed
|
||||||
THEN:
|
THEN:
|
||||||
- Template overrides are not applied
|
- Template overrides are not applied
|
||||||
"""
|
"""
|
||||||
ct = ConsumptionTemplate.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_filename="*foobar*",
|
filter_filename="*foobar*",
|
||||||
filter_path=None,
|
filter_path=None,
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
assign_owner=self.user2,
|
assign_owner=self.user2,
|
||||||
)
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
@ -328,32 +375,42 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
self.assertIsNone(overrides["override_change_groups"])
|
self.assertIsNone(overrides["override_change_groups"])
|
||||||
self.assertIsNone(overrides["override_title"])
|
self.assertIsNone(overrides["override_title"])
|
||||||
|
|
||||||
expected_str = f"Document did not match template {ct}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = f"Document filename {test_file.name} does not match"
|
expected_str = f"Document filename {test_file.name} does not match"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
def test_consumption_template_no_match_path(self, m):
|
def test_workflow_no_match_path(self, m):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing consumption template
|
- Existing workflow
|
||||||
WHEN:
|
WHEN:
|
||||||
- File that does not match on path is consumed
|
- File that does not match on path is consumed
|
||||||
THEN:
|
THEN:
|
||||||
- Template overrides are not applied
|
- Template overrides are not applied
|
||||||
"""
|
"""
|
||||||
ct = ConsumptionTemplate.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_path="*foo/bar*",
|
filter_path="*foo/bar*",
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
assign_owner=self.user2,
|
assign_owner=self.user2,
|
||||||
)
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
@ -379,32 +436,42 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
self.assertIsNone(overrides["override_change_groups"])
|
self.assertIsNone(overrides["override_change_groups"])
|
||||||
self.assertIsNone(overrides["override_title"])
|
self.assertIsNone(overrides["override_title"])
|
||||||
|
|
||||||
expected_str = f"Document did not match template {ct}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = f"Document path {test_file} does not match"
|
expected_str = f"Document path {test_file} does not match"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
def test_consumption_template_no_match_mail_rule(self, m):
|
def test_workflow_no_match_mail_rule(self, m):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing consumption template
|
- Existing workflow
|
||||||
WHEN:
|
WHEN:
|
||||||
- File that does not match on source is consumed
|
- File that does not match on source is consumed
|
||||||
THEN:
|
THEN:
|
||||||
- Template overrides are not applied
|
- Template overrides are not applied
|
||||||
"""
|
"""
|
||||||
ct = ConsumptionTemplate.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ApiUpload},{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_mailrule=self.rule1,
|
filter_mailrule=self.rule1,
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
assign_owner=self.user2,
|
assign_owner=self.user2,
|
||||||
)
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
@ -431,32 +498,42 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
self.assertIsNone(overrides["override_change_groups"])
|
self.assertIsNone(overrides["override_change_groups"])
|
||||||
self.assertIsNone(overrides["override_title"])
|
self.assertIsNone(overrides["override_title"])
|
||||||
|
|
||||||
expected_str = f"Document did not match template {ct}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = "Document mail rule 99 !="
|
expected_str = "Document mail rule 99 !="
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
def test_consumption_template_no_match_source(self, m):
|
def test_workflow_no_match_source(self, m):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- Existing consumption template
|
- Existing workflow
|
||||||
WHEN:
|
WHEN:
|
||||||
- File that does not match on source is consumed
|
- File that does not match on source is consumed
|
||||||
THEN:
|
THEN:
|
||||||
- Template overrides are not applied
|
- Template overrides are not applied
|
||||||
"""
|
"""
|
||||||
ct = ConsumptionTemplate.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
name="Template 1",
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
order=0,
|
|
||||||
sources=f"{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
sources=f"{DocumentSource.ConsumeFolder},{DocumentSource.MailFetch}",
|
||||||
filter_path="*",
|
filter_path="*",
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
assign_title="Doc from {correspondent}",
|
assign_title="Doc from {correspondent}",
|
||||||
assign_correspondent=self.c,
|
assign_correspondent=self.c,
|
||||||
assign_document_type=self.dt,
|
assign_document_type=self.dt,
|
||||||
assign_storage_path=self.sp,
|
assign_storage_path=self.sp,
|
||||||
assign_owner=self.user2,
|
assign_owner=self.user2,
|
||||||
)
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
@ -482,7 +559,7 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas
|
|||||||
self.assertIsNone(overrides["override_change_groups"])
|
self.assertIsNone(overrides["override_change_groups"])
|
||||||
self.assertIsNone(overrides["override_title"])
|
self.assertIsNone(overrides["override_title"])
|
||||||
|
|
||||||
expected_str = f"Document did not match template {ct}"
|
expected_str = f"Document did not match {w}"
|
||||||
self.assertIn(expected_str, cm.output[0])
|
self.assertIn(expected_str, cm.output[0])
|
||||||
expected_str = f"Document source {DocumentSource.ApiUpload.name} not in ['{DocumentSource.ConsumeFolder.name}', '{DocumentSource.MailFetch.name}']"
|
expected_str = f"Document source {DocumentSource.ApiUpload.name} not in ['{DocumentSource.ConsumeFolder.name}', '{DocumentSource.MailFetch.name}']"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
@ -76,7 +76,6 @@ from documents.matching import match_correspondents
|
|||||||
from documents.matching import match_document_types
|
from documents.matching import match_document_types
|
||||||
from documents.matching import match_storage_paths
|
from documents.matching import match_storage_paths
|
||||||
from documents.matching import match_tags
|
from documents.matching import match_tags
|
||||||
from documents.models import ConsumptionTemplate
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
@ -87,6 +86,9 @@ from documents.models import SavedView
|
|||||||
from documents.models import ShareLink
|
from documents.models import ShareLink
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
|
from documents.models import Workflow
|
||||||
|
from documents.models import WorkflowAction
|
||||||
|
from documents.models import WorkflowTrigger
|
||||||
from documents.parsers import get_parser_class_for_mime_type
|
from documents.parsers import get_parser_class_for_mime_type
|
||||||
from documents.parsers import parse_date_generator
|
from documents.parsers import parse_date_generator
|
||||||
from documents.permissions import PaperlessAdminPermissions
|
from documents.permissions import PaperlessAdminPermissions
|
||||||
@ -98,7 +100,6 @@ from documents.serialisers import AcknowledgeTasksViewSerializer
|
|||||||
from documents.serialisers import BulkDownloadSerializer
|
from documents.serialisers import BulkDownloadSerializer
|
||||||
from documents.serialisers import BulkEditObjectPermissionsSerializer
|
from documents.serialisers import BulkEditObjectPermissionsSerializer
|
||||||
from documents.serialisers import BulkEditSerializer
|
from documents.serialisers import BulkEditSerializer
|
||||||
from documents.serialisers import ConsumptionTemplateSerializer
|
|
||||||
from documents.serialisers import CorrespondentSerializer
|
from documents.serialisers import CorrespondentSerializer
|
||||||
from documents.serialisers import CustomFieldSerializer
|
from documents.serialisers import CustomFieldSerializer
|
||||||
from documents.serialisers import DocumentListSerializer
|
from documents.serialisers import DocumentListSerializer
|
||||||
@ -112,6 +113,9 @@ from documents.serialisers import TagSerializer
|
|||||||
from documents.serialisers import TagSerializerVersion1
|
from documents.serialisers import TagSerializerVersion1
|
||||||
from documents.serialisers import TasksViewSerializer
|
from documents.serialisers import TasksViewSerializer
|
||||||
from documents.serialisers import UiSettingsViewSerializer
|
from documents.serialisers import UiSettingsViewSerializer
|
||||||
|
from documents.serialisers import WorkflowActionSerializer
|
||||||
|
from documents.serialisers import WorkflowSerializer
|
||||||
|
from documents.serialisers import WorkflowTriggerSerializer
|
||||||
from documents.tasks import consume_file
|
from documents.tasks import consume_file
|
||||||
from paperless import version
|
from paperless import version
|
||||||
from paperless.db import GnuPG
|
from paperless.db import GnuPG
|
||||||
@ -1373,16 +1377,26 @@ class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConsumptionTemplateViewSet(ModelViewSet):
|
class WorkflowTriggerViewSet(ModelViewSet):
|
||||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
|
|
||||||
serializer_class = ConsumptionTemplateSerializer
|
serializer_class = WorkflowTriggerSerializer
|
||||||
pagination_class = StandardPagination
|
pagination_class = StandardPagination
|
||||||
|
|
||||||
model = ConsumptionTemplate
|
model = WorkflowTrigger
|
||||||
|
|
||||||
queryset = (
|
queryset = WorkflowTrigger.objects.all()
|
||||||
ConsumptionTemplate.objects.prefetch_related(
|
|
||||||
|
|
||||||
|
class WorkflowActionViewSet(ModelViewSet):
|
||||||
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
|
|
||||||
|
serializer_class = WorkflowActionSerializer
|
||||||
|
pagination_class = StandardPagination
|
||||||
|
|
||||||
|
model = WorkflowAction
|
||||||
|
|
||||||
|
queryset = WorkflowAction.objects.all().prefetch_related(
|
||||||
"assign_tags",
|
"assign_tags",
|
||||||
"assign_view_users",
|
"assign_view_users",
|
||||||
"assign_view_groups",
|
"assign_view_groups",
|
||||||
@ -1390,8 +1404,23 @@ class ConsumptionTemplateViewSet(ModelViewSet):
|
|||||||
"assign_change_groups",
|
"assign_change_groups",
|
||||||
"assign_custom_fields",
|
"assign_custom_fields",
|
||||||
)
|
)
|
||||||
.all()
|
|
||||||
|
|
||||||
|
class WorkflowViewSet(ModelViewSet):
|
||||||
|
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||||
|
|
||||||
|
serializer_class = WorkflowSerializer
|
||||||
|
pagination_class = StandardPagination
|
||||||
|
|
||||||
|
model = Workflow
|
||||||
|
|
||||||
|
queryset = (
|
||||||
|
Workflow.objects.all()
|
||||||
.order_by("order")
|
.order_by("order")
|
||||||
|
.prefetch_related(
|
||||||
|
"triggers",
|
||||||
|
"actions",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ from documents.views import AcknowledgeTasksView
|
|||||||
from documents.views import BulkDownloadView
|
from documents.views import BulkDownloadView
|
||||||
from documents.views import BulkEditObjectPermissionsView
|
from documents.views import BulkEditObjectPermissionsView
|
||||||
from documents.views import BulkEditView
|
from documents.views import BulkEditView
|
||||||
from documents.views import ConsumptionTemplateViewSet
|
|
||||||
from documents.views import CorrespondentViewSet
|
from documents.views import CorrespondentViewSet
|
||||||
from documents.views import CustomFieldViewSet
|
from documents.views import CustomFieldViewSet
|
||||||
from documents.views import DocumentTypeViewSet
|
from documents.views import DocumentTypeViewSet
|
||||||
@ -34,6 +33,9 @@ from documents.views import TagViewSet
|
|||||||
from documents.views import TasksViewSet
|
from documents.views import TasksViewSet
|
||||||
from documents.views import UiSettingsView
|
from documents.views import UiSettingsView
|
||||||
from documents.views import UnifiedSearchViewSet
|
from documents.views import UnifiedSearchViewSet
|
||||||
|
from documents.views import WorkflowActionViewSet
|
||||||
|
from documents.views import WorkflowTriggerViewSet
|
||||||
|
from documents.views import WorkflowViewSet
|
||||||
from paperless.consumers import StatusConsumer
|
from paperless.consumers import StatusConsumer
|
||||||
from paperless.views import FaviconView
|
from paperless.views import FaviconView
|
||||||
from paperless.views import GenerateAuthTokenView
|
from paperless.views import GenerateAuthTokenView
|
||||||
@ -58,7 +60,9 @@ api_router.register(r"groups", GroupViewSet, basename="groups")
|
|||||||
api_router.register(r"mail_accounts", MailAccountViewSet)
|
api_router.register(r"mail_accounts", MailAccountViewSet)
|
||||||
api_router.register(r"mail_rules", MailRuleViewSet)
|
api_router.register(r"mail_rules", MailRuleViewSet)
|
||||||
api_router.register(r"share_links", ShareLinkViewSet)
|
api_router.register(r"share_links", ShareLinkViewSet)
|
||||||
api_router.register(r"consumption_templates", ConsumptionTemplateViewSet)
|
api_router.register(r"workflow_triggers", WorkflowTriggerViewSet)
|
||||||
|
api_router.register(r"workflow_actions", WorkflowActionViewSet)
|
||||||
|
api_router.register(r"workflows", WorkflowViewSet)
|
||||||
api_router.register(r"custom_fields", CustomFieldViewSet)
|
api_router.register(r"custom_fields", CustomFieldViewSet)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user