mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: email, webhook workflow actions (#8108)
This commit is contained in:
@@ -43,7 +43,7 @@ from documents.plugins.helpers import ProgressStatusOptions
|
||||
from documents.signals import document_consumption_finished
|
||||
from documents.signals import document_consumption_started
|
||||
from documents.signals.handlers import run_workflows
|
||||
from documents.templating.title import parse_doc_title_w_placeholders
|
||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||
from documents.utils import copy_basic_file_stats
|
||||
from documents.utils import copy_file_with_basic_stats
|
||||
from documents.utils import run_subprocess
|
||||
@@ -666,7 +666,7 @@ class ConsumerPlugin(
|
||||
else None
|
||||
)
|
||||
|
||||
return parse_doc_title_w_placeholders(
|
||||
return parse_w_workflow_placeholders(
|
||||
title,
|
||||
correspondent_name,
|
||||
doc_type_name,
|
||||
|
@@ -18,8 +18,7 @@ def settings(request):
|
||||
)
|
||||
|
||||
return {
|
||||
"EMAIL_ENABLED": django_settings.EMAIL_HOST != "localhost"
|
||||
or django_settings.EMAIL_HOST_USER != "",
|
||||
"EMAIL_ENABLED": django_settings.EMAIL_ENABLED,
|
||||
"DISABLE_REGULAR_LOGIN": django_settings.DISABLE_REGULAR_LOGIN,
|
||||
"REDIRECT_LOGIN_TO_SSO": django_settings.REDIRECT_LOGIN_TO_SSO,
|
||||
"ACCOUNT_ALLOW_SIGNUPS": django_settings.ACCOUNT_ALLOW_SIGNUPS,
|
||||
|
@@ -0,0 +1,154 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-26 04:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="WorkflowActionEmail",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"subject",
|
||||
models.CharField(
|
||||
help_text="The subject of the email, can include some placeholders, see documentation.",
|
||||
max_length=256,
|
||||
verbose_name="email subject",
|
||||
),
|
||||
),
|
||||
(
|
||||
"body",
|
||||
models.TextField(
|
||||
help_text="The body (message) of the email, can include some placeholders, see documentation.",
|
||||
verbose_name="email body",
|
||||
),
|
||||
),
|
||||
(
|
||||
"to",
|
||||
models.TextField(
|
||||
help_text="The destination email addresses, comma separated.",
|
||||
verbose_name="emails to",
|
||||
),
|
||||
),
|
||||
(
|
||||
"include_document",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="include document in email",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="WorkflowActionWebhook",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"url",
|
||||
models.URLField(
|
||||
help_text="The destination URL for the notification.",
|
||||
verbose_name="webhook url",
|
||||
),
|
||||
),
|
||||
(
|
||||
"use_params",
|
||||
models.BooleanField(default=True, verbose_name="use parameters"),
|
||||
),
|
||||
(
|
||||
"params",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
help_text="The parameters to send with the webhook URL if body not used.",
|
||||
null=True,
|
||||
verbose_name="webhook parameters",
|
||||
),
|
||||
),
|
||||
(
|
||||
"body",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="The body to send with the webhook URL if parameters not used.",
|
||||
null=True,
|
||||
verbose_name="webhook body",
|
||||
),
|
||||
),
|
||||
(
|
||||
"headers",
|
||||
models.JSONField(
|
||||
blank=True,
|
||||
help_text="The headers to send with the webhook URL.",
|
||||
null=True,
|
||||
verbose_name="webhook headers",
|
||||
),
|
||||
),
|
||||
(
|
||||
"include_document",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="include document in webhook",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="workflowaction",
|
||||
name="type",
|
||||
field=models.PositiveIntegerField(
|
||||
choices=[
|
||||
(1, "Assignment"),
|
||||
(2, "Removal"),
|
||||
(3, "Email"),
|
||||
(4, "Webhook"),
|
||||
],
|
||||
default=1,
|
||||
verbose_name="Workflow Action Type",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workflowaction",
|
||||
name="email",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="action",
|
||||
to="documents.workflowactionemail",
|
||||
verbose_name="email",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workflowaction",
|
||||
name="webhook",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="action",
|
||||
to="documents.workflowactionwebhook",
|
||||
verbose_name="webhook",
|
||||
),
|
||||
),
|
||||
]
|
@@ -63,7 +63,7 @@ def reverse_migrate_customfield_selects(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"),
|
||||
("documents", "1059_workflowactionemail_workflowactionwebhook_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
@@ -1160,6 +1160,85 @@ class WorkflowTrigger(models.Model):
|
||||
return f"WorkflowTrigger {self.pk}"
|
||||
|
||||
|
||||
class WorkflowActionEmail(models.Model):
|
||||
subject = models.CharField(
|
||||
_("email subject"),
|
||||
max_length=256,
|
||||
null=False,
|
||||
help_text=_(
|
||||
"The subject of the email, can include some placeholders, "
|
||||
"see documentation.",
|
||||
),
|
||||
)
|
||||
|
||||
body = models.TextField(
|
||||
_("email body"),
|
||||
null=False,
|
||||
help_text=_(
|
||||
"The body (message) of the email, can include some placeholders, "
|
||||
"see documentation.",
|
||||
),
|
||||
)
|
||||
|
||||
to = models.TextField(
|
||||
_("emails to"),
|
||||
null=False,
|
||||
help_text=_(
|
||||
"The destination email addresses, comma separated.",
|
||||
),
|
||||
)
|
||||
|
||||
include_document = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("include document in email"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Workflow Email Action {self.pk}"
|
||||
|
||||
|
||||
class WorkflowActionWebhook(models.Model):
|
||||
url = models.URLField(
|
||||
_("webhook url"),
|
||||
null=False,
|
||||
help_text=_("The destination URL for the notification."),
|
||||
)
|
||||
|
||||
use_params = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("use parameters"),
|
||||
)
|
||||
|
||||
params = models.JSONField(
|
||||
_("webhook parameters"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("The parameters to send with the webhook URL if body not used."),
|
||||
)
|
||||
|
||||
body = models.TextField(
|
||||
_("webhook body"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("The body to send with the webhook URL if parameters not used."),
|
||||
)
|
||||
|
||||
headers = models.JSONField(
|
||||
_("webhook headers"),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("The headers to send with the webhook URL."),
|
||||
)
|
||||
|
||||
include_document = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("include document in webhook"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Workflow Webhook Action {self.pk}"
|
||||
|
||||
|
||||
class WorkflowAction(models.Model):
|
||||
class WorkflowActionType(models.IntegerChoices):
|
||||
ASSIGNMENT = (
|
||||
@@ -1170,6 +1249,14 @@ class WorkflowAction(models.Model):
|
||||
2,
|
||||
_("Removal"),
|
||||
)
|
||||
EMAIL = (
|
||||
3,
|
||||
_("Email"),
|
||||
)
|
||||
WEBHOOK = (
|
||||
4,
|
||||
_("Webhook"),
|
||||
)
|
||||
|
||||
type = models.PositiveIntegerField(
|
||||
_("Workflow Action Type"),
|
||||
@@ -1371,6 +1458,24 @@ class WorkflowAction(models.Model):
|
||||
verbose_name=_("remove all custom fields"),
|
||||
)
|
||||
|
||||
email = models.ForeignKey(
|
||||
WorkflowActionEmail,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="action",
|
||||
verbose_name=_("email"),
|
||||
)
|
||||
|
||||
webhook = models.ForeignKey(
|
||||
WorkflowActionWebhook,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="action",
|
||||
verbose_name=_("webhook"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("workflow action")
|
||||
verbose_name_plural = _("workflow actions")
|
||||
|
@@ -49,6 +49,8 @@ from documents.models import Tag
|
||||
from documents.models import UiSettings
|
||||
from documents.models import Workflow
|
||||
from documents.models import WorkflowAction
|
||||
from documents.models import WorkflowActionEmail
|
||||
from documents.models import WorkflowActionWebhook
|
||||
from documents.models import WorkflowTrigger
|
||||
from documents.parsers import is_mime_type_supported
|
||||
from documents.permissions import get_groups_with_only_permission
|
||||
@@ -1818,12 +1820,44 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
|
||||
return attrs
|
||||
|
||||
|
||||
class WorkflowActionEmailSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = WorkflowActionEmail
|
||||
fields = [
|
||||
"id",
|
||||
"subject",
|
||||
"body",
|
||||
"to",
|
||||
"include_document",
|
||||
]
|
||||
|
||||
|
||||
class WorkflowActionWebhookSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = WorkflowActionWebhook
|
||||
fields = [
|
||||
"id",
|
||||
"url",
|
||||
"use_params",
|
||||
"params",
|
||||
"body",
|
||||
"headers",
|
||||
"include_document",
|
||||
]
|
||||
|
||||
|
||||
class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False, allow_null=True)
|
||||
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)
|
||||
email = WorkflowActionEmailSerializer(allow_null=True, required=False)
|
||||
webhook = WorkflowActionWebhookSerializer(allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = WorkflowAction
|
||||
@@ -1858,6 +1892,8 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||
"remove_view_groups",
|
||||
"remove_change_users",
|
||||
"remove_change_groups",
|
||||
"email",
|
||||
"webhook",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
@@ -1895,6 +1931,24 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||
{"assign_title": f'Invalid f-string detected: "{e.args[0]}"'},
|
||||
)
|
||||
|
||||
if (
|
||||
"type" in attrs
|
||||
and attrs["type"] == WorkflowAction.WorkflowActionType.EMAIL
|
||||
and "email" not in attrs
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Email data is required for email actions",
|
||||
)
|
||||
|
||||
if (
|
||||
"type" in attrs
|
||||
and attrs["type"] == WorkflowAction.WorkflowActionType.WEBHOOK
|
||||
and "webhook" not in attrs
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Webhook data is required for webhook actions",
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -1949,11 +2003,34 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
remove_change_users = action.pop("remove_change_users", None)
|
||||
remove_change_groups = action.pop("remove_change_groups", None)
|
||||
|
||||
email_data = action.pop("email", None)
|
||||
webhook_data = action.pop("webhook", None)
|
||||
|
||||
action_instance, _ = WorkflowAction.objects.update_or_create(
|
||||
id=action.get("id"),
|
||||
defaults=action,
|
||||
)
|
||||
|
||||
if email_data is not None:
|
||||
serializer = WorkflowActionEmailSerializer(data=email_data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
email, _ = WorkflowActionEmail.objects.update_or_create(
|
||||
id=email_data.get("id"),
|
||||
defaults=serializer.validated_data,
|
||||
)
|
||||
action_instance.email = email
|
||||
action_instance.save()
|
||||
|
||||
if webhook_data is not None:
|
||||
serializer = WorkflowActionWebhookSerializer(data=webhook_data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
webhook, _ = WorkflowActionWebhook.objects.update_or_create(
|
||||
id=webhook_data.get("id"),
|
||||
defaults=serializer.validated_data,
|
||||
)
|
||||
action_instance.webhook = webhook
|
||||
action_instance.save()
|
||||
|
||||
if assign_tags is not None:
|
||||
action_instance.assign_tags.set(assign_tags)
|
||||
if assign_view_users is not None:
|
||||
@@ -2006,6 +2083,9 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
if action.workflows.all().count() == 0:
|
||||
action.delete()
|
||||
|
||||
WorkflowActionEmail.objects.filter(action=None).delete()
|
||||
WorkflowActionWebhook.objects.filter(action=None).delete()
|
||||
|
||||
def create(self, validated_data) -> Workflow:
|
||||
if "triggers" in validated_data:
|
||||
triggers = validated_data.pop("triggers")
|
||||
|
@@ -2,6 +2,8 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import httpx
|
||||
from celery import shared_task
|
||||
from celery import states
|
||||
from celery.signals import before_task_publish
|
||||
from celery.signals import task_failure
|
||||
@@ -12,6 +14,7 @@ from django.contrib.admin.models import ADDITION
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.mail import EmailMessage
|
||||
from django.db import DatabaseError
|
||||
from django.db import close_old_connections
|
||||
from django.db import models
|
||||
@@ -41,7 +44,7 @@ from documents.models import WorkflowRun
|
||||
from documents.models import WorkflowTrigger
|
||||
from documents.permissions import get_objects_for_user_owner_aware
|
||||
from documents.permissions import set_permissions_for_object
|
||||
from documents.templating.title import parse_doc_title_w_placeholders
|
||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||
|
||||
logger = logging.getLogger("paperless.handlers")
|
||||
|
||||
@@ -570,6 +573,30 @@ def run_workflows_updated(sender, document: Document, logging_group=None, **kwar
|
||||
)
|
||||
|
||||
|
||||
@shared_task(
|
||||
retry_backoff=True,
|
||||
autoretry_for=(httpx.HTTPStatusError,),
|
||||
max_retries=3,
|
||||
throws=(httpx.HTTPError,),
|
||||
)
|
||||
def send_webhook(url, data, headers, files):
|
||||
try:
|
||||
httpx.post(
|
||||
url,
|
||||
data=data,
|
||||
files=files,
|
||||
headers=headers,
|
||||
).raise_for_status()
|
||||
logger.info(
|
||||
f"Webhook sent to {url}",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed attempt sending webhook to {url}: {e}",
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
def run_workflows(
|
||||
trigger_type: WorkflowTrigger.WorkflowTriggerType,
|
||||
document: Document | ConsumableDocument,
|
||||
@@ -622,7 +649,7 @@ def run_workflows(
|
||||
if action.assign_title:
|
||||
if not use_overrides:
|
||||
try:
|
||||
document.title = parse_doc_title_w_placeholders(
|
||||
document.title = parse_w_workflow_placeholders(
|
||||
action.assign_title,
|
||||
document.correspondent.name if document.correspondent else "",
|
||||
document.document_type.name if document.document_type else "",
|
||||
@@ -879,6 +906,151 @@ def run_workflows(
|
||||
):
|
||||
overrides.custom_field_ids.remove(field.pk)
|
||||
|
||||
def email_action():
|
||||
if not settings.EMAIL_ENABLED:
|
||||
logger.error(
|
||||
"Email backend has not been configured, cannot send email notifications",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
return
|
||||
|
||||
title = (
|
||||
document.title
|
||||
if isinstance(document, Document)
|
||||
else str(document.original_file)
|
||||
)
|
||||
doc_url = None
|
||||
if isinstance(document, Document):
|
||||
doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/"
|
||||
correspondent = document.correspondent.name if document.correspondent else ""
|
||||
document_type = document.document_type.name if document.document_type else ""
|
||||
owner_username = document.owner.username if document.owner else ""
|
||||
filename = document.original_filename or ""
|
||||
added = timezone.localtime(document.added)
|
||||
created = timezone.localtime(document.created)
|
||||
subject = parse_w_workflow_placeholders(
|
||||
action.email.subject,
|
||||
correspondent,
|
||||
document_type,
|
||||
owner_username,
|
||||
added,
|
||||
filename,
|
||||
created,
|
||||
title,
|
||||
doc_url,
|
||||
)
|
||||
body = parse_w_workflow_placeholders(
|
||||
action.email.body,
|
||||
correspondent,
|
||||
document_type,
|
||||
owner_username,
|
||||
added,
|
||||
filename,
|
||||
created,
|
||||
title,
|
||||
doc_url,
|
||||
)
|
||||
try:
|
||||
email = EmailMessage(
|
||||
subject=subject,
|
||||
body=body,
|
||||
to=action.email.to.split(","),
|
||||
)
|
||||
if action.email.include_document:
|
||||
email.attach_file(document.source_path)
|
||||
n_messages = email.send()
|
||||
logger.debug(
|
||||
f"Sent {n_messages} notification email(s) to {action.email.to}",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error occurred sending notification email: {e}",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
def webhook_action():
|
||||
title = (
|
||||
document.title
|
||||
if isinstance(document, Document)
|
||||
else str(document.original_file)
|
||||
)
|
||||
doc_url = None
|
||||
if isinstance(document, Document):
|
||||
doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/"
|
||||
correspondent = document.correspondent.name if document.correspondent else ""
|
||||
document_type = document.document_type.name if document.document_type else ""
|
||||
owner_username = document.owner.username if document.owner else ""
|
||||
filename = document.original_filename or ""
|
||||
added = timezone.localtime(document.added)
|
||||
created = timezone.localtime(document.created)
|
||||
|
||||
try:
|
||||
data = {}
|
||||
if action.webhook.use_params:
|
||||
try:
|
||||
for key, value in action.webhook.params.items():
|
||||
data[key] = parse_w_workflow_placeholders(
|
||||
value,
|
||||
correspondent,
|
||||
document_type,
|
||||
owner_username,
|
||||
added,
|
||||
filename,
|
||||
created,
|
||||
title,
|
||||
doc_url,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error occurred parsing webhook params: {e}",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
else:
|
||||
data = parse_w_workflow_placeholders(
|
||||
action.webhook.body,
|
||||
correspondent,
|
||||
document_type,
|
||||
owner_username,
|
||||
added,
|
||||
filename,
|
||||
created,
|
||||
title,
|
||||
doc_url,
|
||||
)
|
||||
headers = {}
|
||||
if action.webhook.headers:
|
||||
try:
|
||||
headers = {
|
||||
str(k): str(v) for k, v in action.webhook.headers.items()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error occurred parsing webhook headers: {e}",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
files = None
|
||||
if action.webhook.include_document:
|
||||
with open(document.source_path, "rb") as f:
|
||||
files = {
|
||||
"file": (document.original_filename, f, document.mime_type),
|
||||
}
|
||||
send_webhook.delay(
|
||||
url=action.webhook.url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
files=files,
|
||||
)
|
||||
logger.debug(
|
||||
f"Webhook to {action.webhook.url} queued",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error occurred sending webhook: {e}",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
use_overrides = overrides is not None
|
||||
messages = []
|
||||
|
||||
@@ -924,6 +1096,10 @@ def run_workflows(
|
||||
assignment_action()
|
||||
elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
|
||||
removal_action()
|
||||
elif action.type == WorkflowAction.WorkflowActionType.EMAIL:
|
||||
email_action()
|
||||
elif action.type == WorkflowAction.WorkflowActionType.WEBHOOK:
|
||||
webhook_action()
|
||||
|
||||
if not use_overrides:
|
||||
# save first before setting tags
|
||||
|
@@ -2,14 +2,16 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_doc_title_w_placeholders(
|
||||
title: str,
|
||||
def parse_w_workflow_placeholders(
|
||||
text: str,
|
||||
correspondent_name: str,
|
||||
doc_type_name: str,
|
||||
owner_username: str,
|
||||
local_added: datetime,
|
||||
original_filename: str,
|
||||
created: datetime | None = None,
|
||||
doc_title: str | None = None,
|
||||
doc_url: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Available title placeholders for Workflows depend on what has already been assigned,
|
||||
@@ -43,4 +45,8 @@ def parse_doc_title_w_placeholders(
|
||||
"created_time": created.strftime("%H:%M"),
|
||||
},
|
||||
)
|
||||
return title.format(**formatting).strip()
|
||||
if doc_title is not None:
|
||||
formatting.update({"doc_title": doc_title})
|
||||
if doc_url is not None:
|
||||
formatting.update({"doc_url": doc_url})
|
||||
return text.format(**formatting).strip()
|
@@ -433,3 +433,158 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
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)
|
||||
|
||||
def test_email_action_validation(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a workflow with an email action
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP response
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 2",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.EMAIL,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
# Notification action requires to, subject and body
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 2",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.EMAIL,
|
||||
"email": {
|
||||
"subject": "Subject",
|
||||
"body": "Body",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
# Notification action requires destination emails or url
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 2",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.EMAIL,
|
||||
"email": {
|
||||
"subject": "Subject",
|
||||
"body": "Body",
|
||||
"to": "me@example.com",
|
||||
"include_document": False,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_webhook_action_validation(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a workflow with a notification action
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP response
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 2",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
# Notification action requires url
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 2",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
"webhook": {
|
||||
"url": "https://example.com",
|
||||
"include_document": False,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
@@ -4,8 +4,8 @@ from documents.tests.utils import TestMigrations
|
||||
|
||||
|
||||
class TestMigrateCustomFieldSelects(TestMigrations):
|
||||
migrate_from = "1058_workflowtrigger_schedule_date_custom_field_and_more"
|
||||
migrate_to = "1059_alter_customfieldinstance_value_select"
|
||||
migrate_from = "1059_workflowactionemail_workflowactionwebhook_and_more"
|
||||
migrate_to = "1060_alter_customfieldinstance_value_select"
|
||||
|
||||
def setUpBeforeMigration(self, apps):
|
||||
CustomField = apps.get_model("documents.CustomField")
|
||||
@@ -43,8 +43,8 @@ class TestMigrateCustomFieldSelects(TestMigrations):
|
||||
|
||||
|
||||
class TestMigrationCustomFieldSelectsReverse(TestMigrations):
|
||||
migrate_from = "1059_alter_customfieldinstance_value_select"
|
||||
migrate_to = "1058_workflowtrigger_schedule_date_custom_field_and_more"
|
||||
migrate_from = "1060_alter_customfieldinstance_value_select"
|
||||
migrate_to = "1059_workflowactionemail_workflowactionwebhook_and_more"
|
||||
|
||||
def setUpBeforeMigration(self, apps):
|
||||
CustomField = apps.get_model("documents.CustomField")
|
||||
|
@@ -1,20 +1,25 @@
|
||||
import shutil
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from django.utils import timezone
|
||||
from guardian.shortcuts import assign_perm
|
||||
from guardian.shortcuts import get_groups_with_perms
|
||||
from guardian.shortcuts import get_users_with_perms
|
||||
from httpx import HTTPStatusError
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.signals.handlers import run_workflows
|
||||
from documents.signals.handlers import send_webhook
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
||||
from documents import tasks
|
||||
from documents.data_models import ConsumableDocument
|
||||
from documents.data_models import DocumentSource
|
||||
@@ -29,19 +34,25 @@ from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import Workflow
|
||||
from documents.models import WorkflowAction
|
||||
from documents.models import WorkflowActionEmail
|
||||
from documents.models import WorkflowActionWebhook
|
||||
from documents.models import WorkflowRun
|
||||
from documents.models import WorkflowTrigger
|
||||
from documents.signals import document_consumption_finished
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from documents.tests.utils import DummyProgressManager
|
||||
from documents.tests.utils import FileSystemAssertsMixin
|
||||
from documents.tests.utils import SampleDirMixin
|
||||
from paperless_mail.models import MailAccount
|
||||
from paperless_mail.models import MailRule
|
||||
|
||||
|
||||
class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
||||
SAMPLE_DIR = Path(__file__).parent / "samples"
|
||||
|
||||
class TestWorkflows(
|
||||
DirectoriesMixin,
|
||||
FileSystemAssertsMixin,
|
||||
SampleDirMixin,
|
||||
APITestCase,
|
||||
):
|
||||
def setUp(self) -> None:
|
||||
self.c = Correspondent.objects.create(name="Correspondent Name")
|
||||
self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
|
||||
@@ -2077,3 +2088,477 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
||||
self.assertEqual(doc.owner, self.user2)
|
||||
self.assertEqual(doc.tags.all().count(), 1)
|
||||
self.assertIn(self.t2, doc.tags.all())
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("httpx.post")
|
||||
@mock.patch("django.core.mail.message.EmailMessage.send")
|
||||
def test_workflow_email_action(self, mock_email_send, mock_post):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with email action
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
THEN:
|
||||
- email is sent
|
||||
"""
|
||||
mock_post.return_value = mock.Mock(
|
||||
status_code=200,
|
||||
json=mock.Mock(return_value={"status": "ok"}),
|
||||
)
|
||||
mock_email_send.return_value = 1
|
||||
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
email_action = WorkflowActionEmail.objects.create(
|
||||
subject="Test Notification: {doc_title}",
|
||||
body="Test message: {doc_url}",
|
||||
to="user@example.com",
|
||||
include_document=False,
|
||||
)
|
||||
self.assertEqual(str(email_action), f"Workflow Email Action {email_action.id}")
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.EMAIL,
|
||||
email=email_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
mock_email_send.assert_called_once()
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("httpx.post")
|
||||
@mock.patch("django.core.mail.message.EmailMessage.send")
|
||||
def test_workflow_email_include_file(self, mock_email_send, mock_post):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with email action
|
||||
- Include document is set to True
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
THEN:
|
||||
- Notification includes document file
|
||||
"""
|
||||
|
||||
# move the file
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
self.dirs.scratch_dir / "simple.pdf",
|
||||
)
|
||||
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
email_action = WorkflowActionEmail.objects.create(
|
||||
subject="Test Notification: {doc_title}",
|
||||
body="Test message: {doc_url}",
|
||||
to="me@example.com",
|
||||
include_document=True,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.EMAIL,
|
||||
email=email_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
filename=test_file,
|
||||
)
|
||||
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
mock_email_send.assert_called_once()
|
||||
|
||||
@override_settings(
|
||||
EMAIL_ENABLED=False,
|
||||
)
|
||||
def test_workflow_email_action_no_email_setup(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with email action
|
||||
- Email is not enabled
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
THEN:
|
||||
- Error is logged
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
email_action = WorkflowActionEmail.objects.create(
|
||||
subject="Test Notification: {doc_title}",
|
||||
body="Test message: {doc_url}",
|
||||
to="me@example.com",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.EMAIL,
|
||||
email=email_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.handlers", level="ERROR") as cm:
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
expected_str = "Email backend has not been configured"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
||||
@override_settings(
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("django.core.mail.message.EmailMessage.send")
|
||||
def test_workflow_email_action_fail(self, mock_email_send):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with email action
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
- An error occurs during email send
|
||||
THEN:
|
||||
- Error is logged
|
||||
"""
|
||||
mock_email_send.side_effect = Exception("Error occurred sending email")
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
email_action = WorkflowActionEmail.objects.create(
|
||||
subject="Test Notification: {doc_title}",
|
||||
body="Test message: {doc_url}",
|
||||
to="me@example.com",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.EMAIL,
|
||||
email=email_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.handlers", level="ERROR") as cm:
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
expected_str = "Error occurred sending email"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("documents.signals.handlers.send_webhook.delay")
|
||||
def test_workflow_webhook_action_body(self, mock_post):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with webhook action which uses body
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
THEN:
|
||||
- Webhook is sent with body
|
||||
"""
|
||||
mock_post.return_value = mock.Mock(
|
||||
status_code=200,
|
||||
json=mock.Mock(return_value={"status": "ok"}),
|
||||
)
|
||||
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
webhook_action = WorkflowActionWebhook.objects.create(
|
||||
use_params=False,
|
||||
body="Test message: {doc_url}",
|
||||
url="http://paperless-ngx.com",
|
||||
include_document=False,
|
||||
)
|
||||
self.assertEqual(
|
||||
str(webhook_action),
|
||||
f"Workflow Webhook Action {webhook_action.id}",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
webhook=webhook_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
url="http://paperless-ngx.com",
|
||||
data=f"Test message: http://localhost:8000/documents/{doc.id}/",
|
||||
headers={},
|
||||
files=None,
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
@mock.patch("documents.signals.handlers.send_webhook.delay")
|
||||
def test_workflow_webhook_action_w_files(self, mock_post):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with webhook action which includes document
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
THEN:
|
||||
- Webhook is sent with file
|
||||
"""
|
||||
mock_post.return_value = mock.Mock(
|
||||
status_code=200,
|
||||
json=mock.Mock(return_value={"status": "ok"}),
|
||||
)
|
||||
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
webhook_action = WorkflowActionWebhook.objects.create(
|
||||
use_params=False,
|
||||
body="Test message: {doc_url}",
|
||||
url="http://paperless-ngx.com",
|
||||
include_document=True,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
webhook=webhook_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
self.dirs.scratch_dir / "simple.pdf",
|
||||
)
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="simple.pdf",
|
||||
filename=test_file,
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
url="http://paperless-ngx.com",
|
||||
data=f"Test message: http://localhost:8000/documents/{doc.id}/",
|
||||
headers={},
|
||||
files={"file": ("simple.pdf", mock.ANY, "application/pdf")},
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
PAPERLESS_EMAIL_HOST="localhost",
|
||||
EMAIL_ENABLED=True,
|
||||
PAPERLESS_URL="http://localhost:8000",
|
||||
)
|
||||
def test_workflow_webhook_action_fail(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with webhook action
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
- An error occurs during webhook
|
||||
THEN:
|
||||
- Error is logged
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
webhook_action = WorkflowActionWebhook.objects.create(
|
||||
use_params=True,
|
||||
params={
|
||||
"title": "Test webhook: {doc_title}",
|
||||
"body": "Test message: {doc_url}",
|
||||
},
|
||||
url="http://paperless-ngx.com",
|
||||
include_document=True,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
webhook=webhook_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
# fails because no file
|
||||
with self.assertLogs("paperless.handlers", level="ERROR") as cm:
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
expected_str = "Error occurred sending webhook"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
||||
def test_workflow_webhook_action_url_invalid_params_headers(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- Document updated workflow with webhook action
|
||||
- Invalid params and headers JSON
|
||||
WHEN:
|
||||
- Document that matches is updated
|
||||
THEN:
|
||||
- Error is logged
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
webhook_action = WorkflowActionWebhook.objects.create(
|
||||
url="http://paperless-ngx.com",
|
||||
use_params=True,
|
||||
params="invalid",
|
||||
headers="invalid",
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
webhook=webhook_action,
|
||||
)
|
||||
w = Workflow.objects.create(
|
||||
name="Workflow 1",
|
||||
order=0,
|
||||
)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
correspondent=self.c,
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.handlers", level="ERROR") as cm:
|
||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||
|
||||
expected_str = "Error occurred parsing webhook params"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
expected_str = "Error occurred parsing webhook headers"
|
||||
self.assertIn(expected_str, cm.output[1])
|
||||
|
||||
@mock.patch("httpx.post")
|
||||
def test_workflow_webhook_send_webhook_task(self, mock_post):
|
||||
mock_post.return_value = mock.Mock(
|
||||
status_code=200,
|
||||
json=mock.Mock(return_value={"status": "ok"}),
|
||||
raise_for_status=mock.Mock(),
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.handlers") as cm:
|
||||
send_webhook(
|
||||
url="http://paperless-ngx.com",
|
||||
data="Test message",
|
||||
headers={},
|
||||
files=None,
|
||||
)
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
"http://paperless-ngx.com",
|
||||
data="Test message",
|
||||
headers={},
|
||||
files=None,
|
||||
)
|
||||
|
||||
expected_str = "Webhook sent to http://paperless-ngx.com"
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
||||
@mock.patch("httpx.post")
|
||||
def test_workflow_webhook_send_webhook_retry(self, mock_http):
|
||||
mock_http.return_value.raise_for_status = mock.Mock(
|
||||
side_effect=HTTPStatusError(
|
||||
"Error",
|
||||
request=mock.Mock(),
|
||||
response=mock.Mock(),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.handlers") as cm:
|
||||
with self.assertRaises(HTTPStatusError):
|
||||
send_webhook(
|
||||
url="http://paperless-ngx.com",
|
||||
data="Test message",
|
||||
headers={},
|
||||
files=None,
|
||||
)
|
||||
|
||||
self.assertEqual(mock_http.call_count, 1)
|
||||
|
||||
expected_str = (
|
||||
"Failed attempt sending webhook to http://paperless-ngx.com"
|
||||
)
|
||||
self.assertIn(expected_str, cm.output[0])
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1195,6 +1195,7 @@ DEFAULT_FROM_EMAIL: Final[str] = os.getenv("PAPERLESS_EMAIL_FROM", EMAIL_HOST_US
|
||||
EMAIL_USE_TLS: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_TLS")
|
||||
EMAIL_USE_SSL: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_SSL")
|
||||
EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] "
|
||||
EMAIL_ENABLED = EMAIL_HOST != "localhost" or EMAIL_HOST_USER != ""
|
||||
if DEBUG: # pragma: no cover
|
||||
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
|
||||
EMAIL_FILE_PATH = BASE_DIR / "sent_emails"
|
||||
|
Reference in New Issue
Block a user