Messing around with notification action to start

This commit is contained in:
shamoon 2024-10-23 10:17:19 -07:00
parent cc93bc41df
commit c4d59e0e77
No known key found for this signature in database
9 changed files with 264 additions and 2 deletions

View File

@ -322,6 +322,17 @@
</div> </div>
</div> </div>
} }
@case (WorkflowActionType.Notification) {
<div class="row">
<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="Notification body" formControlName="notification_body" [error]="error?.actions?.[i]?.notification_body"></pngx-input-text>
<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="Notification url" formControlName="notification_destination_url" [error]="error?.actions?.[i]?.notification_destination_url"></pngx-input-text>
<pngx-input-switch i18n-title title="Notification include document" formControlName="notification_include_document"></pngx-input-switch>
</div>
</div>
}
} }
</div> </div>
</ng-template> </ng-template>

View File

@ -96,6 +96,10 @@ export const WORKFLOW_ACTION_OPTIONS = [
id: WorkflowActionType.Removal, id: WorkflowActionType.Removal,
name: $localize`Removal`, name: $localize`Removal`,
}, },
{
id: WorkflowActionType.Notification,
name: $localize`Notification`,
},
] ]
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter( const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
@ -402,6 +406,17 @@ 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),
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 } { emitEvent }
) )
@ -503,6 +518,11 @@ 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,
notification_body: null,
notification_destination_emails: null,
notification_destination_url: null,
notification_include_document: null,
} }
this.object.actions.push(action) this.object.actions.push(action)
this.createActionField(action) this.createActionField(action)

View File

@ -3,6 +3,7 @@ import { ObjectWithId } from './object-with-id'
export enum WorkflowActionType { export enum WorkflowActionType {
Assignment = 1, Assignment = 1,
Removal = 2, Removal = 2,
Notification = 3,
} }
export interface WorkflowAction extends ObjectWithId { export interface WorkflowAction extends ObjectWithId {
type: WorkflowActionType type: WorkflowActionType
@ -62,4 +63,14 @@ export interface WorkflowAction extends ObjectWithId {
remove_custom_fields?: number[] // [CustomField.id] remove_custom_fields?: number[] // [CustomField.id]
remove_all_custom_fields?: boolean remove_all_custom_fields?: boolean
notification_subject?: string
notification_body?: string
notification_destination_emails?: string
notification_destination_url?: string
notification_include_document?: boolean
} }

View File

@ -18,8 +18,7 @@ def settings(request):
) )
return { return {
"EMAIL_ENABLED": django_settings.EMAIL_HOST != "localhost" "EMAIL_ENABLED": django_settings.EMAIL_ENABLED,
or django_settings.EMAIL_HOST_USER != "",
"DISABLE_REGULAR_LOGIN": django_settings.DISABLE_REGULAR_LOGIN, "DISABLE_REGULAR_LOGIN": django_settings.DISABLE_REGULAR_LOGIN,
"REDIRECT_LOGIN_TO_SSO": django_settings.REDIRECT_LOGIN_TO_SSO, "REDIRECT_LOGIN_TO_SSO": django_settings.REDIRECT_LOGIN_TO_SSO,
"ACCOUNT_ALLOW_SIGNUPS": django_settings.ACCOUNT_ALLOW_SIGNUPS, "ACCOUNT_ALLOW_SIGNUPS": django_settings.ACCOUNT_ALLOW_SIGNUPS,

View File

@ -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",
),
),
]

View File

@ -1166,6 +1166,10 @@ class WorkflowAction(models.Model):
2, 2,
_("Removal"), _("Removal"),
) )
NOTIFICATION = (
3,
_("Notification"),
)
type = models.PositiveIntegerField( type = models.PositiveIntegerField(
_("Workflow Action Type"), _("Workflow Action Type"),
@ -1367,6 +1371,48 @@ class WorkflowAction(models.Model):
verbose_name=_("remove all custom fields"), 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: class Meta:
verbose_name = _("workflow action") verbose_name = _("workflow action")
verbose_name_plural = _("workflow actions") verbose_name_plural = _("workflow actions")

View File

@ -1847,6 +1847,11 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
"remove_view_groups", "remove_view_groups",
"remove_change_users", "remove_change_users",
"remove_change_groups", "remove_change_groups",
"notification_subject",
"notification_body",
"notification_destination_emails",
"notification_destination_url",
"notification_include_document",
] ]
def validate(self, attrs): def validate(self, attrs):
@ -1884,6 +1889,38 @@ 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 (
"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 return attrs

View File

@ -2,6 +2,7 @@ import logging
import os import os
import shutil import shutil
import httpx
from celery import states from celery import states
from celery.signals import before_task_publish from celery.signals import before_task_publish
from celery.signals import task_failure 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.admin.models import LogEntry
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.mail import EmailMessage
from django.db import DatabaseError from django.db import DatabaseError
from django.db import close_old_connections from django.db import close_old_connections
from django.db import models from django.db import models
@ -866,6 +868,68 @@ def run_workflows(
): ):
overrides.custom_field_ids.remove(field.pk) 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 use_overrides = overrides is not None
messages = [] messages = []
@ -911,6 +975,8 @@ 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:
notification_action()
if not use_overrides: if not use_overrides:
# save first before setting tags # save first before setting tags

View File

@ -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_TLS: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_TLS")
EMAIL_USE_SSL: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_SSL") EMAIL_USE_SSL: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_SSL")
EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] " EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] "
EMAIL_ENABLED = EMAIL_HOST != "localhost" or EMAIL_HOST_USER != ""
if DEBUG: # pragma: no cover if DEBUG: # pragma: no cover
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = BASE_DIR / "sent_emails" EMAIL_FILE_PATH = BASE_DIR / "sent_emails"