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"