Split actions into email + webhook

This commit is contained in:
shamoon 2024-11-02 08:46:41 -07:00
parent 93a58da426
commit 76672b0760
No known key found for this signature in database
9 changed files with 405 additions and 294 deletions

View File

@ -322,15 +322,23 @@
</div> </div>
</div> </div>
} }
@case (WorkflowActionType.Notification) { @case (WorkflowActionType.Email) {
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-text i18n-title title="Notification subject" formControlName="notification_subject" [error]="error?.actions?.[i]?.notification_subject"></pngx-input-text> <pngx-input-text i18n-title title="Email subject" formControlName="email_subject" [error]="error?.actions?.[i]?.email_subject"></pngx-input-text>
<pngx-input-textarea i18n-title title="Notification body" formControlName="notification_body" [error]="error?.actions?.[i]?.notification_body"></pngx-input-textarea> <pngx-input-textarea i18n-title title="Email body" formControlName="email_body" [error]="error?.actions?.[i]?.email_body"></pngx-input-textarea>
<pngx-input-text i18n-title title="Notification emails" formControlName="notification_destination_emails" [error]="error?.actions?.[i]?.notification_destination_emails"></pngx-input-text> <pngx-input-text i18n-title title="Email recipients" formControlName="email_to" [error]="error?.actions?.[i]?.email_to"></pngx-input-text>
<pngx-input-text i18n-title title="Notification url" formControlName="notification_destination_url" [error]="error?.actions?.[i]?.notification_destination_url"></pngx-input-text> <pngx-input-switch i18n-title title="Attach document" formControlName="email_include_document"></pngx-input-switch>
<pngx-input-text i18n-title title="Notification headers" formControlName="notification_destination_url_headers" [error]="error?.actions?.[i]?.notification_destination_url_headers"></pngx-input-text> </div>
<pngx-input-switch i18n-title title="Notification include document" formControlName="notification_include_document"></pngx-input-switch> </div>
}
@case (WorkflowActionType.Webhook) {
<div class="row">
<div class="col">
<pngx-input-text i18n-title title="Webhook url" formControlName="webhook_url" [error]="error?.actions?.[i]?.webhook_url"></pngx-input-text>
<pngx-input-text i18n-title title="Webhook params" formControlName="webhook_params" [error]="error?.actions?.[i]?.webhook_params"></pngx-input-text>
<pngx-input-text i18n-title title="Webhook headers" formControlName="webhook_headers" [error]="error?.actions?.[i]?.webhook_headers"></pngx-input-text>
<pngx-input-switch i18n-title title="Include document" formControlName="webhook_include_document"></pngx-input-switch>
</div> </div>
</div> </div>
} }

View File

@ -97,8 +97,12 @@ export const WORKFLOW_ACTION_OPTIONS = [
name: $localize`Removal`, name: $localize`Removal`,
}, },
{ {
id: WorkflowActionType.Notification, id: WorkflowActionType.Email,
name: $localize`Notification`, name: $localize`Email`,
},
{
id: WorkflowActionType.Webhook,
name: $localize`Webhook`,
}, },
] ]
@ -406,19 +410,15 @@ export class WorkflowEditDialogComponent
remove_all_custom_fields: new FormControl( remove_all_custom_fields: new FormControl(
action.remove_all_custom_fields action.remove_all_custom_fields
), ),
notification_subject: new FormControl(action.notification_subject), email_subject: new FormControl(action.email_subject),
notification_body: new FormControl(action.notification_body), email_body: new FormControl(action.email_body),
notification_destination_emails: new FormControl( email_to: new FormControl(action.email_to),
action.notification_destination_emails email_include_document: new FormControl(action.email_include_document),
), webhook_url: new FormControl(action.webhook_url),
notification_destination_url: new FormControl( webhook_params: new FormControl(action.webhook_params),
action.notification_destination_url webhook_headers: new FormControl(action.webhook_headers),
), webhook_include_document: new FormControl(
notification_destination_url_headers: new FormControl( action.webhook_include_document
action.notification_destination_url_headers
),
notification_include_document: new FormControl(
action.notification_include_document
), ),
}), }),
{ emitEvent } { emitEvent }
@ -521,12 +521,14 @@ export class WorkflowEditDialogComponent
remove_all_permissions: false, remove_all_permissions: false,
remove_custom_fields: [], remove_custom_fields: [],
remove_all_custom_fields: false, remove_all_custom_fields: false,
notification_subject: null, email_subject: null,
notification_body: null, email_body: null,
notification_destination_emails: null, email_to: null,
notification_destination_url: null, email_include_document: false,
notification_destination_url_headers: null, webhook_url: null,
notification_include_document: null, webhook_params: null,
webhook_headers: null,
webhook_include_document: false,
} }
this.object.actions.push(action) this.object.actions.push(action)
this.createActionField(action) this.createActionField(action)

View File

@ -3,7 +3,8 @@ import { ObjectWithId } from './object-with-id'
export enum WorkflowActionType { export enum WorkflowActionType {
Assignment = 1, Assignment = 1,
Removal = 2, Removal = 2,
Notification = 3, Email = 3,
Webhook = 4,
} }
export interface WorkflowAction extends ObjectWithId { export interface WorkflowAction extends ObjectWithId {
type: WorkflowActionType type: WorkflowActionType
@ -64,15 +65,19 @@ export interface WorkflowAction extends ObjectWithId {
remove_all_custom_fields?: boolean remove_all_custom_fields?: boolean
notification_subject?: string email_subject?: string
notification_body?: string email_body?: string
notification_destination_emails?: string email_to?: string
notification_destination_url?: string email_include_document?: boolean
notification_destination_url_headers?: string webhook_url?: string
notification_include_document?: boolean webhook_params?: string
webhook_headers?: string
webhook_include_document?: boolean
} }

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.2 on 2024-10-26 19:07 # Generated by Django 5.1.1 on 2024-11-02 15:25
from django.db import migrations from django.db import migrations
from django.db import models from django.db import models
@ -12,68 +12,91 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name="workflowaction", model_name="workflowaction",
name="notification_body", name="email_body",
field=models.TextField( field=models.TextField(
blank=True, blank=True,
help_text="The body (message) of the notification, can include some placeholders, see documentation.", help_text="The body (message) of the email, can include some placeholders, see documentation.",
null=True, null=True,
verbose_name="notification body", verbose_name="email body",
), ),
), ),
migrations.AddField( migrations.AddField(
model_name="workflowaction", model_name="workflowaction",
name="notification_destination_emails", name="email_include_document",
field=models.TextField( field=models.BooleanField(
blank=True, default=False,
help_text="The destination email addresses for the notification, comma separated.", verbose_name="include document in email",
null=True,
verbose_name="notification destination emails",
), ),
), ),
migrations.AddField( migrations.AddField(
model_name="workflowaction", model_name="workflowaction",
name="notification_destination_url", name="email_subject",
field=models.CharField(
blank=True,
help_text="The subject of the email, can include some placeholders, see documentation.",
max_length=256,
null=True,
verbose_name="email subject",
),
),
migrations.AddField(
model_name="workflowaction",
name="email_to",
field=models.TextField(
blank=True,
help_text="The destination email addresses, comma separated.",
null=True,
verbose_name="emails to",
),
),
migrations.AddField(
model_name="workflowaction",
name="webhook_headers",
field=models.JSONField(
blank=True,
help_text="The headers to send with the webhook URL.",
null=True,
verbose_name="webhook headers",
),
),
migrations.AddField(
model_name="workflowaction",
name="webhook_include_document",
field=models.BooleanField(
default=False,
verbose_name="include document in webhook",
),
),
migrations.AddField(
model_name="workflowaction",
name="webhook_params",
field=models.JSONField(
blank=True,
help_text="The parameters to send with the webhook URL.",
null=True,
verbose_name="webhook parameters",
),
),
migrations.AddField(
model_name="workflowaction",
name="webhook_url",
field=models.URLField( field=models.URLField(
blank=True, blank=True,
help_text="The destination URL for the notification.", help_text="The destination URL for the notification.",
null=True, null=True,
verbose_name="notification destination url", verbose_name="webhook url",
),
),
migrations.AddField(
model_name="workflowaction",
name="notification_destination_url_headers",
field=models.JSONField(
blank=True,
help_text="The headers to send with the notification destination URL.",
null=True,
verbose_name="notification destination url headers",
),
),
migrations.AddField(
model_name="workflowaction",
name="notification_include_document",
field=models.BooleanField(
default=False,
verbose_name="include document in notification",
),
),
migrations.AddField(
model_name="workflowaction",
name="notification_subject",
field=models.CharField(
blank=True,
help_text="The subject of the notification, can include some placeholders, see documentation.",
max_length=256,
null=True,
verbose_name="notification subject",
), ),
), ),
migrations.AlterField( migrations.AlterField(
model_name="workflowaction", model_name="workflowaction",
name="type", name="type",
field=models.PositiveIntegerField( field=models.PositiveIntegerField(
choices=[(1, "Assignment"), (2, "Removal"), (3, "Notification")], choices=[
(1, "Assignment"),
(2, "Removal"),
(3, "Email"),
(4, "Webhook"),
],
default=1, default=1,
verbose_name="Workflow Action Type", verbose_name="Workflow Action Type",
), ),

View File

@ -1166,9 +1166,13 @@ class WorkflowAction(models.Model):
2, 2,
_("Removal"), _("Removal"),
) )
NOTIFICATION = ( EMAIL = (
3, 3,
_("Notification"), _("Email"),
)
WEBHOOK = (
4,
_("Webhook"),
) )
type = models.PositiveIntegerField( type = models.PositiveIntegerField(
@ -1371,53 +1375,65 @@ class WorkflowAction(models.Model):
verbose_name=_("remove all custom fields"), verbose_name=_("remove all custom fields"),
) )
notification_subject = models.CharField( email_subject = models.CharField(
_("notification subject"), _("email subject"),
max_length=256, max_length=256,
null=True, null=True,
blank=True, blank=True,
help_text=_( help_text=_(
"The subject of the notification, can include some placeholders, " "The subject of the email, can include some placeholders, "
"see documentation.", "see documentation.",
), ),
) )
notification_body = models.TextField( email_body = models.TextField(
_("notification body"), _("email body"),
null=True, null=True,
blank=True, blank=True,
help_text=_( help_text=_(
"The body (message) of the notification, can include some placeholders, " "The body (message) of the email, can include some placeholders, "
"see documentation.", "see documentation.",
), ),
) )
notification_destination_emails = models.TextField( email_to = models.TextField(
_("notification destination emails"), _("emails to"),
null=True, null=True,
blank=True, blank=True,
help_text=_( help_text=_(
"The destination email addresses for the notification, comma separated.", "The destination email addresses, comma separated.",
), ),
) )
notification_destination_url = models.URLField( email_include_document = models.BooleanField(
_("notification destination url"), default=False,
verbose_name=_("include document in email"),
)
webhook_url = models.URLField(
_("webhook url"),
null=True, null=True,
blank=True, blank=True,
help_text=_("The destination URL for the notification."), help_text=_("The destination URL for the notification."),
) )
notification_destination_url_headers = models.JSONField( webhook_params = models.JSONField(
_("notification destination url headers"), _("webhook parameters"),
null=True, null=True,
blank=True, blank=True,
help_text=_("The headers to send with the notification destination URL."), help_text=_("The parameters to send with the webhook URL."),
) )
notification_include_document = models.BooleanField( webhook_headers = models.JSONField(
_("webhook headers"),
null=True,
blank=True,
help_text=_("The headers to send with the webhook URL."),
)
webhook_include_document = models.BooleanField(
default=False, default=False,
verbose_name=_("include document in notification"), verbose_name=_("include document in webhook"),
) )
class Meta: class Meta:

View File

@ -1847,12 +1847,13 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
"remove_view_groups", "remove_view_groups",
"remove_change_users", "remove_change_users",
"remove_change_groups", "remove_change_groups",
"notification_subject", "email_to",
"notification_body", "email_subject",
"notification_destination_emails", "email_body",
"notification_destination_url", "email_include_document",
"notification_destination_url_headers", "webhook_url",
"notification_include_document", "webhook_params",
"webhook_headers",
] ]
def validate(self, attrs): def validate(self, attrs):
@ -1890,34 +1891,39 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
{"assign_title": f'Invalid f-string detected: "{e.args[0]}"'}, {"assign_title": f'Invalid f-string detected: "{e.args[0]}"'},
) )
if ( if "type" in attrs and attrs["type"] == WorkflowAction.WorkflowActionType.EMAIL:
"type" in attrs
and attrs["type"] == WorkflowAction.WorkflowActionType.NOTIFICATION
):
if ( if (
"notification_subject" not in attrs "email_subject" not in attrs
or attrs["notification_subject"] is None or attrs["email_subject"] is None
or len(attrs["notification_subject"]) == 0 or len(attrs["email_subject"]) == 0
or "notification_body" not in attrs or "email_body" not in attrs
or attrs["notification_body"] is None or attrs["email_body"] is None
or len(attrs["notification_body"]) == 0 or len(attrs["email_body"]) == 0
): ):
raise serializers.ValidationError( raise serializers.ValidationError(
"Notification subject and body required", "Email subject and body required",
) )
elif ( elif (
"notification_destination_emails" not in attrs "email_to" not in attrs
or attrs["notification_destination_emails"] is None or attrs["email_to"] is None
or len(attrs["notification_destination_emails"]) == 0 or len(attrs["email_to"]) == 0
) and (
"notification_destination_url" not in attrs
or attrs["notification_destination_url"] is None
or len(attrs["notification_destination_url"]) == 0
): ):
raise serializers.ValidationError( raise serializers.ValidationError(
"Notification destination emails or URL required", "Email recipient required",
) )
if (
"type" in attrs
and attrs["type"] == WorkflowAction.WorkflowActionType.WEBHOOK
) and (
"webhook_url" not in attrs
or attrs["webhook_url"] is None
or len(attrs["webhook_url"]) == 0
):
raise serializers.ValidationError(
"Webhook URL required",
)
return attrs return attrs

View File

@ -869,7 +869,14 @@ def run_workflows(
): ):
overrides.custom_field_ids.remove(field.pk) overrides.custom_field_ids.remove(field.pk)
def notification_action(): 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 = ( title = (
document.title document.title
if isinstance(document, Document) if isinstance(document, Document)
@ -878,9 +885,8 @@ def run_workflows(
doc_url = None doc_url = None
if isinstance(document, Document): if isinstance(document, Document):
doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/" doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/"
subject = parse_w_workflow_placeholders( subject = parse_w_workflow_placeholders(
action.notification_subject, action.email_subject,
document.correspondent.name if document.correspondent else "", document.correspondent.name if document.correspondent else "",
document.document_type.name if document.document_type else "", document.document_type.name if document.document_type else "",
document.owner.username if document.owner else "", document.owner.username if document.owner else "",
@ -891,7 +897,7 @@ def run_workflows(
doc_url, doc_url,
) )
body = parse_w_workflow_placeholders( body = parse_w_workflow_placeholders(
action.notification_body, action.email_body,
document.correspondent.name if document.correspondent else "", document.correspondent.name if document.correspondent else "",
document.document_type.name if document.document_type else "", document.document_type.name if document.document_type else "",
document.owner.username if document.owner else "", document.owner.username if document.owner else "",
@ -901,76 +907,83 @@ def run_workflows(
title, title,
doc_url, 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},
)
if action.notification_destination_emails: def webhook_action():
if not settings.EMAIL_ENABLED: title = (
logger.error( document.title
"Email backend has not been configured, cannot send email notifications", if isinstance(document, Document)
extra={"group": logging_group}, else str(document.original_file)
)
doc_url = None
if isinstance(document, Document):
doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/"
try:
params = {}
params_json = json.loads(action.webhook_params)
for key, value in params_json.items():
params[key] = parse_w_workflow_placeholders(
value,
document.correspondent.name if document.correspondent else "",
document.document_type.name if document.document_type else "",
document.owner.username if document.owner else "",
timezone.localtime(document.added),
document.original_filename or "",
timezone.localtime(document.created),
title,
doc_url,
) )
else: headers = None
if action.webhook_headers:
try: try:
email = EmailMessage( # headers are a JSON object with key-value pairs, needs to be converted to a Mapping[str, str]
subject=subject, header_mapping = json.loads(
body=body, action.webhook_headers,
to=action.notification_destination_emails.split(","),
)
if action.notification_include_document:
email.attach_file(document.source_path)
n_messages = email.send()
logger.debug(
f"Sent {n_messages} notification email(s) to {action.notification_destination_emails}",
extra={"group": logging_group},
) )
headers = {str(k): str(v) for k, v in header_mapping.items()}
except Exception as e: except Exception as e:
logger.exception( logger.error(
f"Error occurred sending notification email: {e}", f"Error occurred parsing webhook headers: {e}",
extra={"group": logging_group}, extra={"group": logging_group},
) )
if action.notification_destination_url: if action.webhook_include_document:
try: with open(document.source_path, "rb") as f:
data = { files = {"file": (document.original_filename, f)}
"title": subject,
"message": body,
}
files = None
headers = None
if action.notification_destination_url_headers:
try:
# headers are a JSON object with key-value pairs, needs to be converted to a Mapping[str, str]
header_mapping = json.loads(
action.notification_destination_url_headers,
)
headers = {str(k): str(v) for k, v in header_mapping.items()}
except Exception as e:
logger.error(
f"Error occurred parsing notification destination URL headers: {e}",
extra={"group": logging_group},
)
if action.notification_include_document:
with document.source_file as f:
files = {"document": f}
response = httpx.post(
action.notification_destination_url,
data=data,
headers=headers,
files=files,
)
logger.debug(
f"Response from notification destination URL: {response}",
extra={"group": logging_group},
)
else:
httpx.post( httpx.post(
action.notification_destination_url, action.webhook_url,
data=data, data=params,
files=files,
headers=headers, headers=headers,
) )
except Exception as e: else:
logger.exception( httpx.post(
f"Error occurred sending notification to destination URL: {e}", action.webhook_url,
extra={"group": logging_group}, data=params,
headers=headers,
) )
except Exception as e:
logger.exception(
f"Error occurred sending webhook: {e}",
extra={"group": logging_group},
)
use_overrides = overrides is not None use_overrides = overrides is not None
messages = [] messages = []
@ -1017,8 +1030,10 @@ def run_workflows(
assignment_action() assignment_action()
elif action.type == WorkflowAction.WorkflowActionType.REMOVAL: elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
removal_action() removal_action()
elif action.type == WorkflowAction.WorkflowActionType.NOTIFICATION: elif action.type == WorkflowAction.WorkflowActionType.EMAIL:
notification_action() email_action()
elif action.type == WorkflowAction.WorkflowActionType.WEBHOOK:
webhook_action()
if not use_overrides: if not use_overrides:
# save first before setting tags # save first before setting tags

View File

@ -434,10 +434,10 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
self.assertEqual(WorkflowAction.objects.all().count(), 1) self.assertEqual(WorkflowAction.objects.all().count(), 1)
self.assertNotEqual(workflow.actions.first().id, self.action.id) self.assertNotEqual(workflow.actions.first().id, self.action.id)
def test_notification_action_validation(self): def test_email_action_validation(self):
""" """
GIVEN: GIVEN:
- API request to create a workflow with a notification action - API request to create a workflow with an email action
WHEN: WHEN:
- API is called - API is called
THEN: THEN:
@ -458,14 +458,14 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
], ],
"actions": [ "actions": [
{ {
"type": WorkflowAction.WorkflowActionType.NOTIFICATION, "type": WorkflowAction.WorkflowActionType.EMAIL,
}, },
], ],
}, },
), ),
content_type="application/json", content_type="application/json",
) )
# Notification action requires subject and body # Notification action requires to, subject and body
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response = self.client.post( response = self.client.post(
@ -483,9 +483,9 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
], ],
"actions": [ "actions": [
{ {
"type": WorkflowAction.WorkflowActionType.NOTIFICATION, "type": WorkflowAction.WorkflowActionType.EMAIL,
"notification_subject": "Subject", "email_subject": "Subject",
"notification_body": "Body", "email_body": "Body",
}, },
], ],
}, },
@ -510,10 +510,69 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
], ],
"actions": [ "actions": [
{ {
"type": WorkflowAction.WorkflowActionType.NOTIFICATION, "type": WorkflowAction.WorkflowActionType.EMAIL,
"notification_subject": "Subject", "email_subject": "Subject",
"notification_body": "Body", "email_body": "Body",
"notification_destination_emails": "me@example.com", "email_to": "me@example.com",
},
],
},
),
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",
}, },
], ],
}, },

View File

@ -1,4 +1,3 @@
import json
import shutil import shutil
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -2092,14 +2091,14 @@ class TestWorkflows(
) )
@mock.patch("httpx.post") @mock.patch("httpx.post")
@mock.patch("django.core.mail.message.EmailMessage.send") @mock.patch("django.core.mail.message.EmailMessage.send")
def test_workflow_notifcation_action(self, mock_email_send, mock_post): def test_workflow_email_action(self, mock_email_send, mock_post):
""" """
GIVEN: GIVEN:
- Document updated workflow with notification action - Document updated workflow with email action
WHEN: WHEN:
- Document that matches is updated - Document that matches is updated
THEN: THEN:
- Notification is sent - email is sent
""" """
mock_post.return_value = mock.Mock( mock_post.return_value = mock.Mock(
status_code=200, status_code=200,
@ -2111,13 +2110,11 @@ class TestWorkflows(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
) )
action = WorkflowAction.objects.create( action = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.NOTIFICATION, type=WorkflowAction.WorkflowActionType.EMAIL,
notification_subject="Test Notification: {doc_title}", email_subject="Test Notification: {doc_title}",
notification_body="Test message: {doc_url}", email_body="Test message: {doc_url}",
notification_destination_emails="user@example.com", email_to="user@example.com",
notification_destination_url="http://paperless-ngx.com", email_include_document=False,
notification_destination_url_headers=json.dumps({"x-api-key": "test"}),
notification_include_document=False,
) )
w = Workflow.objects.create( w = Workflow.objects.create(
name="Workflow 1", name="Workflow 1",
@ -2136,14 +2133,6 @@ class TestWorkflows(
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc) run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
mock_email_send.assert_called_once() mock_email_send.assert_called_once()
mock_post.assert_called_once_with(
"http://paperless-ngx.com",
data={
"title": "Test Notification: sample test",
"message": "Test message: http://localhost:8000/documents/1/",
},
headers={"x-api-key": "test"},
)
@override_settings( @override_settings(
PAPERLESS_EMAIL_HOST="localhost", PAPERLESS_EMAIL_HOST="localhost",
@ -2152,10 +2141,10 @@ class TestWorkflows(
) )
@mock.patch("httpx.post") @mock.patch("httpx.post")
@mock.patch("django.core.mail.message.EmailMessage.send") @mock.patch("django.core.mail.message.EmailMessage.send")
def test_workflow_notification_include_file(self, mock_email_send, mock_post): def test_workflow_email_include_file(self, mock_email_send, mock_post):
""" """
GIVEN: GIVEN:
- Document updated workflow with notification action - Document updated workflow with email action
- Include document is set to True - Include document is set to True
WHEN: WHEN:
- Document that matches is updated - Document that matches is updated
@ -2173,12 +2162,11 @@ class TestWorkflows(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
) )
action = WorkflowAction.objects.create( action = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.NOTIFICATION, type=WorkflowAction.WorkflowActionType.EMAIL,
notification_subject="Test Notification: {doc_title}", email_subject="Test Notification: {doc_title}",
notification_body="Test message: {doc_url}", email_body="Test message: {doc_url}",
notification_destination_emails="me@example.com", email_to="me@example.com",
notification_destination_url="http://paperless-ngx.com", email_include_document=True,
notification_include_document=True,
) )
w = Workflow.objects.create( w = Workflow.objects.create(
name="Workflow 1", name="Workflow 1",
@ -2198,69 +2186,13 @@ class TestWorkflows(
mock_email_send.assert_called_once() mock_email_send.assert_called_once()
mock_post.assert_called_once_with(
"http://paperless-ngx.com",
data={
"title": "Test Notification: sample test",
"message": "Test message: http://localhost:8000/documents/1/",
},
headers=None,
files={"document": mock.ANY},
)
@override_settings( @override_settings(
PAPERLESS_EMAIL_HOST="localhost", EMAIL_ENABLED=False,
EMAIL_ENABLED=True,
PAPERLESS_URL="http://localhost:8000",
) )
def test_workflow_notification_action_fail(self): def test_workflow_email_action_no_email_setup(self):
""" """
GIVEN: GIVEN:
- Document updated workflow with notification action - Document updated workflow with email action
WHEN:
- Document that matches is updated
- An error occurs during notification
THEN:
- Error is logged
"""
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
)
action = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.NOTIFICATION,
notification_subject="Test Notification: {doc_title}",
notification_body="Test message: {doc_url}",
notification_destination_emails="me@example.com",
notification_destination_url="http://paperless-ngx.com",
notification_include_document=True,
)
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 notification email"
self.assertIn(expected_str, cm.output[0])
expected_str = "Error occurred sending notification to destination URL"
self.assertIn(expected_str, cm.output[1])
def test_workflow_notification_action_no_email_setup(self):
"""
GIVEN:
- Document updated workflow with notification action
- Email is not enabled - Email is not enabled
WHEN: WHEN:
- Document that matches is updated - Document that matches is updated
@ -2271,10 +2203,10 @@ class TestWorkflows(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
) )
action = WorkflowAction.objects.create( action = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.NOTIFICATION, type=WorkflowAction.WorkflowActionType.EMAIL,
notification_subject="Test Notification: {doc_title}", email_subject="Test Notification: {doc_title}",
notification_body="Test message: {doc_url}", email_body="Test message: {doc_url}",
notification_destination_emails="me@example.com", email_to="me@example.com",
) )
w = Workflow.objects.create( w = Workflow.objects.create(
name="Workflow 1", name="Workflow 1",
@ -2296,10 +2228,56 @@ class TestWorkflows(
expected_str = "Email backend has not been configured" expected_str = "Email backend has not been configured"
self.assertIn(expected_str, cm.output[0]) self.assertIn(expected_str, cm.output[0])
def test_workflow_notification_action_url_invalid_headers(self): @override_settings(
PAPERLESS_EMAIL_HOST="localhost",
EMAIL_ENABLED=True,
PAPERLESS_URL="http://localhost:8000",
)
def test_workflow_webhook_action_fail(self):
""" """
GIVEN: GIVEN:
- Document updated workflow with notification action - 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,
)
action = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.WEBHOOK,
webhook_params='{"title": "Test webhook: {doc_title}", "body": "Test message: {doc_url}"}',
webhook_url="http://paperless-ngx.com",
webhook_include_document=True,
)
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])
@mock.patch("httpx.post")
def test_workflow_notification_action_url_invalid_headers(self, mock_post):
"""
GIVEN:
- Document updated workflow with webhook action
- Invalid headers JSON - Invalid headers JSON
WHEN: WHEN:
- Document that matches is updated - Document that matches is updated
@ -2310,11 +2288,10 @@ class TestWorkflows(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
) )
action = WorkflowAction.objects.create( action = WorkflowAction.objects.create(
type=WorkflowAction.WorkflowActionType.NOTIFICATION, type=WorkflowAction.WorkflowActionType.WEBHOOK,
notification_subject="Test Notification: {doc_title}", webhook_url="http://paperless-ngx.com",
notification_body="Test message: {doc_url}", webhook_params='{"title": "Test webhook: {doc_title}", "body": "Test message: {doc_url}"}',
notification_destination_url="http://paperless-ngx.com", webhook_headers="invalid",
notification_destination_url_headers="invalid",
) )
w = Workflow.objects.create( w = Workflow.objects.create(
name="Workflow 1", name="Workflow 1",
@ -2333,5 +2310,5 @@ class TestWorkflows(
with self.assertLogs("paperless.handlers", level="ERROR") as cm: with self.assertLogs("paperless.handlers", level="ERROR") as cm:
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc) run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
expected_str = "Error occurred parsing notification destination URL headers" expected_str = "Error occurred parsing webhook headers"
self.assertIn(expected_str, cm.output[0]) self.assertIn(expected_str, cm.output[0])