From c4d59e0e77340ac144680a9d9747ce9785250b0c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:17:19 -0700 Subject: [PATCH] Messing around with notification action to start --- .../workflow-edit-dialog.component.html | 11 +++ .../workflow-edit-dialog.component.ts | 20 ++++++ src-ui/src/app/data/workflow-action.ts | 11 +++ src/documents/context_processors.py | 3 +- ...rkflowaction_notification_body_and_more.py | 71 +++++++++++++++++++ src/documents/models.py | 46 ++++++++++++ src/documents/serialisers.py | 37 ++++++++++ src/documents/signals/handlers.py | 66 +++++++++++++++++ src/paperless/settings.py | 1 + 9 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 src/documents/migrations/1056_workflowaction_notification_body_and_more.py 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 907af6c9e..0f44e0ebd 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,6 +322,17 @@ } + @case (WorkflowActionType.Notification) { +
+
+ + + + + +
+
+ } } 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 646085105..9e1a751c3 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 @@ -96,6 +96,10 @@ export const WORKFLOW_ACTION_OPTIONS = [ id: WorkflowActionType.Removal, name: $localize`Removal`, }, + { + id: WorkflowActionType.Notification, + name: $localize`Notification`, + }, ] const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter( @@ -402,6 +406,17 @@ 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_include_document: new FormControl( + action.notification_include_document + ), }), { emitEvent } ) @@ -503,6 +518,11 @@ 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_include_document: null, } 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 ff64d19b3..c0bd55d46 100644 --- a/src-ui/src/app/data/workflow-action.ts +++ b/src-ui/src/app/data/workflow-action.ts @@ -3,6 +3,7 @@ import { ObjectWithId } from './object-with-id' export enum WorkflowActionType { Assignment = 1, Removal = 2, + Notification = 3, } export interface WorkflowAction extends ObjectWithId { type: WorkflowActionType @@ -62,4 +63,14 @@ export interface WorkflowAction extends ObjectWithId { remove_custom_fields?: number[] // [CustomField.id] remove_all_custom_fields?: boolean + + notification_subject?: string + + notification_body?: string + + notification_destination_emails?: string + + notification_destination_url?: string + + notification_include_document?: boolean } diff --git a/src/documents/context_processors.py b/src/documents/context_processors.py index a9200ac11..abae96a4a 100644 --- a/src/documents/context_processors.py +++ b/src/documents/context_processors.py @@ -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, diff --git a/src/documents/migrations/1056_workflowaction_notification_body_and_more.py b/src/documents/migrations/1056_workflowaction_notification_body_and_more.py new file mode 100644 index 000000000..e7d8bb908 --- /dev/null +++ b/src/documents/migrations/1056_workflowaction_notification_body_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 5.1.1 on 2024-10-23 17:15 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1055_alter_storagepath_path"), + ] + + operations = [ + migrations.AddField( + model_name="workflowaction", + name="notification_body", + field=models.TextField( + blank=True, + help_text="The body (message) of the notification, can include some placeholders, see documentation.", + null=True, + verbose_name="notification 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", + ), + ), + migrations.AddField( + model_name="workflowaction", + name="notification_destination_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_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( + model_name="workflowaction", + name="type", + field=models.PositiveIntegerField( + choices=[(1, "Assignment"), (2, "Removal"), (3, "Notification")], + default=1, + verbose_name="Workflow Action Type", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 6ba63a7e4..3225cae55 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1166,6 +1166,10 @@ class WorkflowAction(models.Model): 2, _("Removal"), ) + NOTIFICATION = ( + 3, + _("Notification"), + ) type = models.PositiveIntegerField( _("Workflow Action Type"), @@ -1367,6 +1371,48 @@ class WorkflowAction(models.Model): verbose_name=_("remove all custom fields"), ) + notification_subject = models.CharField( + _("notification subject"), + max_length=256, + null=True, + blank=True, + help_text=_( + "The subject of the notification, can include some placeholders, " + "see documentation.", + ), + ) + + notification_body = models.TextField( + _("notification body"), + null=True, + blank=True, + help_text=_( + "The body (message) of the notification, can include some placeholders, " + "see documentation.", + ), + ) + + notification_destination_emails = models.TextField( + _("notification destination emails"), + null=True, + blank=True, + help_text=_( + "The destination email addresses for the notification, comma separated.", + ), + ) + + notification_destination_url = models.URLField( + _("notification destination url"), + null=True, + blank=True, + help_text=_("The destination URL for the notification."), + ) + + notification_include_document = models.BooleanField( + default=False, + verbose_name=_("include document in notification"), + ) + class Meta: verbose_name = _("workflow action") verbose_name_plural = _("workflow actions") diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 8c7973f96..5d209205c 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1847,6 +1847,11 @@ class WorkflowActionSerializer(serializers.ModelSerializer): "remove_view_groups", "remove_change_users", "remove_change_groups", + "notification_subject", + "notification_body", + "notification_destination_emails", + "notification_destination_url", + "notification_include_document", ] def validate(self, attrs): @@ -1884,6 +1889,38 @@ class WorkflowActionSerializer(serializers.ModelSerializer): {"assign_title": f'Invalid f-string detected: "{e.args[0]}"'}, ) + if ( + "notification_subject" in attrs + and attrs["notification_subject"] is not None + and len(attrs["notification_subject"]) > 0 + and not ( + attrs["notification_destination_emails"] + or attrs["notification_destination_url"] + ) + ): + raise serializers.ValidationError( + "Notification subject requires destination emails or URL", + ) + + if ( + ( + ( + "notification_destination_emails" in attrs + and attrs["notification_destination_emails"] is not None + and len(attrs["notification_destination_emails"]) > 0 + ) + or ( + "notification_destination_url" in attrs + and attrs["notification_destination_url"] is not None + and len(attrs["notification_destination_url"]) > 0 + ) + ) + and not attrs["notification_subject"] + and not attrs["notification_body"] + ): + raise serializers.ValidationError( + "Notification subject and body required", + ) return attrs diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index c6d6c4090..10dd5ddcd 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -2,6 +2,7 @@ import logging import os import shutil +import httpx from celery import states from celery.signals import before_task_publish from celery.signals import task_failure @@ -12,6 +13,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 @@ -866,6 +868,68 @@ def run_workflows( ): overrides.custom_field_ids.remove(field.pk) + def notification_action(): + subject = parse_doc_title_w_placeholders( + action.notification_subject, + 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), + ) + body = action.notification_body.format( + title=subject, + document=document, + ) + # TODO: if doc exists, construct URL + 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}, + ) + else: + try: + email = EmailMessage( + subject=subject, + body=body, + to=action.notification_destination_emails.split(","), + ) + if action.notification_include_document: + email.attach_file(document.source_path) + email.send() + except Exception as e: + logger.exception( + f"Error occurred sending notification email: {e}", + extra={"group": logging_group}, + ) + if action.notification_destination_url: + try: + data = { + "title": subject, + "message": body, + } + files = None + if action.notification_include_document: + with open(document.source_path, "rb") as f: + files = {"document": f} + httpx.post( + action.notification_destination_url, + data=data, + files=files, + ) + else: + httpx.post( + action.notification_destination_url, + data=data, + ) + except Exception as e: + logger.exception( + f"Error occurred sending notification to destination URL: {e}", + extra={"group": logging_group}, + ) + use_overrides = overrides is not None messages = [] @@ -911,6 +975,8 @@ def run_workflows( assignment_action() elif action.type == WorkflowAction.WorkflowActionType.REMOVAL: removal_action() + elif action.type == WorkflowAction.WorkflowActionType.NOTIFICATION: + notification_action() if not use_overrides: # save first before setting tags diff --git a/src/paperless/settings.py b/src/paperless/settings.py index c9462966d..a32c78ef5 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -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"