From 76672b0760e50a979cf0c8c2d714b9aa28adc7e3 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 2 Nov 2024 08:46:41 -0700 Subject: [PATCH] Split actions into email + webhook --- .../workflow-edit-dialog.component.html | 22 ++- .../workflow-edit-dialog.component.ts | 44 ++--- src-ui/src/app/data/workflow-action.ts | 19 +- ...057_workflowaction_email_body_and_more.py} | 107 ++++++----- src/documents/models.py | 52 ++++-- src/documents/serialisers.py | 56 +++--- src/documents/signals/handlers.py | 145 ++++++++------- src/documents/tests/test_api_workflows.py | 81 ++++++-- src/documents/tests/test_workflows.py | 173 ++++++++---------- 9 files changed, 405 insertions(+), 294 deletions(-) rename src/documents/migrations/{1057_workflowaction_notification_body_and_more.py => 1057_workflowaction_email_body_and_more.py} (51%) diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html index f6087627d..5bbc2776c 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -322,15 +322,23 @@ } - @case (WorkflowActionType.Notification) { + @case (WorkflowActionType.Email) {
- - - - - - + + + + +
+
+ } + @case (WorkflowActionType.Webhook) { +
+
+ + + +
} diff --git a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts index 56520f9a0..f36e73433 100644 --- a/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts @@ -97,8 +97,12 @@ export const WORKFLOW_ACTION_OPTIONS = [ name: $localize`Removal`, }, { - id: WorkflowActionType.Notification, - name: $localize`Notification`, + id: WorkflowActionType.Email, + name: $localize`Email`, + }, + { + id: WorkflowActionType.Webhook, + name: $localize`Webhook`, }, ] @@ -406,19 +410,15 @@ export class WorkflowEditDialogComponent remove_all_custom_fields: new FormControl( action.remove_all_custom_fields ), - notification_subject: new FormControl(action.notification_subject), - notification_body: new FormControl(action.notification_body), - notification_destination_emails: new FormControl( - action.notification_destination_emails - ), - notification_destination_url: new FormControl( - action.notification_destination_url - ), - notification_destination_url_headers: new FormControl( - action.notification_destination_url_headers - ), - notification_include_document: new FormControl( - action.notification_include_document + email_subject: new FormControl(action.email_subject), + email_body: new FormControl(action.email_body), + email_to: new FormControl(action.email_to), + email_include_document: new FormControl(action.email_include_document), + webhook_url: new FormControl(action.webhook_url), + webhook_params: new FormControl(action.webhook_params), + webhook_headers: new FormControl(action.webhook_headers), + webhook_include_document: new FormControl( + action.webhook_include_document ), }), { emitEvent } @@ -521,12 +521,14 @@ export class WorkflowEditDialogComponent remove_all_permissions: false, remove_custom_fields: [], remove_all_custom_fields: false, - notification_subject: null, - notification_body: null, - notification_destination_emails: null, - notification_destination_url: null, - notification_destination_url_headers: null, - notification_include_document: null, + email_subject: null, + email_body: null, + email_to: null, + email_include_document: false, + webhook_url: null, + webhook_params: null, + webhook_headers: null, + webhook_include_document: false, } this.object.actions.push(action) this.createActionField(action) diff --git a/src-ui/src/app/data/workflow-action.ts b/src-ui/src/app/data/workflow-action.ts index 096061cd0..4ddaaea8d 100644 --- a/src-ui/src/app/data/workflow-action.ts +++ b/src-ui/src/app/data/workflow-action.ts @@ -3,7 +3,8 @@ import { ObjectWithId } from './object-with-id' export enum WorkflowActionType { Assignment = 1, Removal = 2, - Notification = 3, + Email = 3, + Webhook = 4, } export interface WorkflowAction extends ObjectWithId { type: WorkflowActionType @@ -64,15 +65,19 @@ export interface WorkflowAction extends ObjectWithId { 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 } diff --git a/src/documents/migrations/1057_workflowaction_notification_body_and_more.py b/src/documents/migrations/1057_workflowaction_email_body_and_more.py similarity index 51% rename from src/documents/migrations/1057_workflowaction_notification_body_and_more.py rename to src/documents/migrations/1057_workflowaction_email_body_and_more.py index 6dba8b88a..404c5e7e4 100644 --- a/src/documents/migrations/1057_workflowaction_notification_body_and_more.py +++ b/src/documents/migrations/1057_workflowaction_email_body_and_more.py @@ -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 models @@ -12,68 +12,91 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name="workflowaction", - name="notification_body", + name="email_body", field=models.TextField( 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, - verbose_name="notification body", + verbose_name="email body", ), ), migrations.AddField( model_name="workflowaction", - name="notification_destination_emails", - field=models.TextField( - blank=True, - help_text="The destination email addresses for the notification, comma separated.", - null=True, - verbose_name="notification destination emails", + name="email_include_document", + field=models.BooleanField( + default=False, + verbose_name="include document in email", ), ), migrations.AddField( 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( blank=True, help_text="The destination URL for the notification.", null=True, - verbose_name="notification destination 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", + verbose_name="webhook url", ), ), migrations.AlterField( model_name="workflowaction", name="type", field=models.PositiveIntegerField( - choices=[(1, "Assignment"), (2, "Removal"), (3, "Notification")], + choices=[ + (1, "Assignment"), + (2, "Removal"), + (3, "Email"), + (4, "Webhook"), + ], default=1, verbose_name="Workflow Action Type", ), diff --git a/src/documents/models.py b/src/documents/models.py index 335fa168d..353b722b3 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1166,9 +1166,13 @@ class WorkflowAction(models.Model): 2, _("Removal"), ) - NOTIFICATION = ( + EMAIL = ( 3, - _("Notification"), + _("Email"), + ) + WEBHOOK = ( + 4, + _("Webhook"), ) type = models.PositiveIntegerField( @@ -1371,53 +1375,65 @@ class WorkflowAction(models.Model): verbose_name=_("remove all custom fields"), ) - notification_subject = models.CharField( - _("notification subject"), + email_subject = models.CharField( + _("email subject"), max_length=256, null=True, blank=True, help_text=_( - "The subject of the notification, can include some placeholders, " + "The subject of the email, can include some placeholders, " "see documentation.", ), ) - notification_body = models.TextField( - _("notification body"), + email_body = models.TextField( + _("email body"), null=True, blank=True, help_text=_( - "The body (message) of the notification, can include some placeholders, " + "The body (message) of the email, can include some placeholders, " "see documentation.", ), ) - notification_destination_emails = models.TextField( - _("notification destination emails"), + email_to = models.TextField( + _("emails to"), null=True, blank=True, help_text=_( - "The destination email addresses for the notification, comma separated.", + "The destination email addresses, comma separated.", ), ) - notification_destination_url = models.URLField( - _("notification destination url"), + email_include_document = models.BooleanField( + default=False, + verbose_name=_("include document in email"), + ) + + webhook_url = models.URLField( + _("webhook url"), null=True, blank=True, help_text=_("The destination URL for the notification."), ) - notification_destination_url_headers = models.JSONField( - _("notification destination url headers"), + webhook_params = models.JSONField( + _("webhook parameters"), null=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, - verbose_name=_("include document in notification"), + verbose_name=_("include document in webhook"), ) class Meta: diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index f9335e611..b1559a510 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1847,12 +1847,13 @@ class WorkflowActionSerializer(serializers.ModelSerializer): "remove_view_groups", "remove_change_users", "remove_change_groups", - "notification_subject", - "notification_body", - "notification_destination_emails", - "notification_destination_url", - "notification_destination_url_headers", - "notification_include_document", + "email_to", + "email_subject", + "email_body", + "email_include_document", + "webhook_url", + "webhook_params", + "webhook_headers", ] def validate(self, attrs): @@ -1890,34 +1891,39 @@ class WorkflowActionSerializer(serializers.ModelSerializer): {"assign_title": f'Invalid f-string detected: "{e.args[0]}"'}, ) - if ( - "type" in attrs - and attrs["type"] == WorkflowAction.WorkflowActionType.NOTIFICATION - ): + if "type" in attrs and attrs["type"] == WorkflowAction.WorkflowActionType.EMAIL: if ( - "notification_subject" not in attrs - or attrs["notification_subject"] is None - or len(attrs["notification_subject"]) == 0 - or "notification_body" not in attrs - or attrs["notification_body"] is None - or len(attrs["notification_body"]) == 0 + "email_subject" not in attrs + or attrs["email_subject"] is None + or len(attrs["email_subject"]) == 0 + or "email_body" not in attrs + or attrs["email_body"] is None + or len(attrs["email_body"]) == 0 ): raise serializers.ValidationError( - "Notification subject and body required", + "Email subject and body required", ) elif ( - "notification_destination_emails" not in attrs - or attrs["notification_destination_emails"] is None - or len(attrs["notification_destination_emails"]) == 0 - ) and ( - "notification_destination_url" not in attrs - or attrs["notification_destination_url"] is None - or len(attrs["notification_destination_url"]) == 0 + "email_to" not in attrs + or attrs["email_to"] is None + or len(attrs["email_to"]) == 0 ): 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 diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 8b4373282..5f206c7c3 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -869,7 +869,14 @@ def run_workflows( ): 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 = ( document.title if isinstance(document, Document) @@ -878,9 +885,8 @@ def run_workflows( doc_url = None if isinstance(document, Document): doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/" - subject = parse_w_workflow_placeholders( - action.notification_subject, + action.email_subject, document.correspondent.name if document.correspondent else "", document.document_type.name if document.document_type else "", document.owner.username if document.owner else "", @@ -891,7 +897,7 @@ def run_workflows( doc_url, ) body = parse_w_workflow_placeholders( - action.notification_body, + action.email_body, document.correspondent.name if document.correspondent else "", document.document_type.name if document.document_type else "", document.owner.username if document.owner else "", @@ -901,76 +907,83 @@ def run_workflows( 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}, + ) - if action.notification_destination_emails: - if not settings.EMAIL_ENABLED: - logger.error( - "Email backend has not been configured, cannot send email notifications", - 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}/" + + 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: - email = EmailMessage( - subject=subject, - body=body, - 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 are a JSON object with key-value pairs, needs to be converted to a Mapping[str, str] + header_mapping = json.loads( + action.webhook_headers, ) + headers = {str(k): str(v) for k, v in header_mapping.items()} except Exception as e: - logger.exception( - f"Error occurred sending notification email: {e}", + logger.error( + f"Error occurred parsing webhook headers: {e}", extra={"group": logging_group}, ) - if action.notification_destination_url: - try: - data = { - "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: + if action.webhook_include_document: + with open(document.source_path, "rb") as f: + files = {"file": (document.original_filename, f)} httpx.post( - action.notification_destination_url, - data=data, + action.webhook_url, + data=params, + files=files, headers=headers, ) - except Exception as e: - logger.exception( - f"Error occurred sending notification to destination URL: {e}", - extra={"group": logging_group}, + else: + httpx.post( + action.webhook_url, + 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 messages = [] @@ -1017,8 +1030,10 @@ def run_workflows( assignment_action() elif action.type == WorkflowAction.WorkflowActionType.REMOVAL: removal_action() - elif action.type == WorkflowAction.WorkflowActionType.NOTIFICATION: - notification_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 diff --git a/src/documents/tests/test_api_workflows.py b/src/documents/tests/test_api_workflows.py index eae36d766..711f304f6 100644 --- a/src/documents/tests/test_api_workflows.py +++ b/src/documents/tests/test_api_workflows.py @@ -434,10 +434,10 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): self.assertEqual(WorkflowAction.objects.all().count(), 1) self.assertNotEqual(workflow.actions.first().id, self.action.id) - def test_notification_action_validation(self): + def test_email_action_validation(self): """ GIVEN: - - API request to create a workflow with a notification action + - API request to create a workflow with an email action WHEN: - API is called THEN: @@ -458,14 +458,14 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): ], "actions": [ { - "type": WorkflowAction.WorkflowActionType.NOTIFICATION, + "type": WorkflowAction.WorkflowActionType.EMAIL, }, ], }, ), 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) response = self.client.post( @@ -483,9 +483,9 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): ], "actions": [ { - "type": WorkflowAction.WorkflowActionType.NOTIFICATION, - "notification_subject": "Subject", - "notification_body": "Body", + "type": WorkflowAction.WorkflowActionType.EMAIL, + "email_subject": "Subject", + "email_body": "Body", }, ], }, @@ -510,10 +510,69 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase): ], "actions": [ { - "type": WorkflowAction.WorkflowActionType.NOTIFICATION, - "notification_subject": "Subject", - "notification_body": "Body", - "notification_destination_emails": "me@example.com", + "type": WorkflowAction.WorkflowActionType.EMAIL, + "email_subject": "Subject", + "email_body": "Body", + "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", }, ], }, diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index dfc742f41..429cf4224 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -1,4 +1,3 @@ -import json import shutil from datetime import timedelta from typing import TYPE_CHECKING @@ -2092,14 +2091,14 @@ class TestWorkflows( ) @mock.patch("httpx.post") @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: - - Document updated workflow with notification action + - Document updated workflow with email action WHEN: - Document that matches is updated THEN: - - Notification is sent + - email is sent """ mock_post.return_value = mock.Mock( status_code=200, @@ -2111,13 +2110,11 @@ class TestWorkflows( 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="user@example.com", - notification_destination_url="http://paperless-ngx.com", - notification_destination_url_headers=json.dumps({"x-api-key": "test"}), - notification_include_document=False, + type=WorkflowAction.WorkflowActionType.EMAIL, + email_subject="Test Notification: {doc_title}", + email_body="Test message: {doc_url}", + email_to="user@example.com", + email_include_document=False, ) w = Workflow.objects.create( name="Workflow 1", @@ -2136,14 +2133,6 @@ class TestWorkflows( run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc) 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( PAPERLESS_EMAIL_HOST="localhost", @@ -2152,10 +2141,10 @@ class TestWorkflows( ) @mock.patch("httpx.post") @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: - - Document updated workflow with notification action + - Document updated workflow with email action - Include document is set to True WHEN: - Document that matches is updated @@ -2173,12 +2162,11 @@ class TestWorkflows( 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, + type=WorkflowAction.WorkflowActionType.EMAIL, + email_subject="Test Notification: {doc_title}", + email_body="Test message: {doc_url}", + email_to="me@example.com", + email_include_document=True, ) w = Workflow.objects.create( name="Workflow 1", @@ -2198,69 +2186,13 @@ class TestWorkflows( 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( - PAPERLESS_EMAIL_HOST="localhost", - EMAIL_ENABLED=True, - PAPERLESS_URL="http://localhost:8000", + EMAIL_ENABLED=False, ) - def test_workflow_notification_action_fail(self): + def test_workflow_email_action_no_email_setup(self): """ GIVEN: - - Document updated workflow with notification 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 + - Document updated workflow with email action - Email is not enabled WHEN: - Document that matches is updated @@ -2271,10 +2203,10 @@ class TestWorkflows( 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", + type=WorkflowAction.WorkflowActionType.EMAIL, + email_subject="Test Notification: {doc_title}", + email_body="Test message: {doc_url}", + email_to="me@example.com", ) w = Workflow.objects.create( name="Workflow 1", @@ -2296,10 +2228,56 @@ class TestWorkflows( expected_str = "Email backend has not been configured" 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: - - 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 WHEN: - Document that matches is updated @@ -2310,11 +2288,10 @@ class TestWorkflows( 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_url="http://paperless-ngx.com", - notification_destination_url_headers="invalid", + type=WorkflowAction.WorkflowActionType.WEBHOOK, + webhook_url="http://paperless-ngx.com", + webhook_params='{"title": "Test webhook: {doc_title}", "body": "Test message: {doc_url}"}', + webhook_headers="invalid", ) w = Workflow.objects.create( name="Workflow 1", @@ -2333,5 +2310,5 @@ class TestWorkflows( with self.assertLogs("paperless.handlers", level="ERROR") as cm: 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])