Skip to content

Commit

Permalink
Merge pull request #14 from maykinmedia/feature/8-configure-email-not…
Browse files Browse the repository at this point in the history
…ifications

Configure email notifications
  • Loading branch information
SilviaAmAm authored May 14, 2024
2 parents 900c291 + 1c934d3 commit 213beae
Show file tree
Hide file tree
Showing 15 changed files with 292 additions and 13 deletions.
1 change: 1 addition & 0 deletions backend/requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ django-redis
django-rosetta
maykin-2fa
django-timeline-logger
django-solo

# API libraries
djangorestframework
Expand Down
4 changes: 3 additions & 1 deletion backend/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ django-sendfile2==0.7.1
django-simple-certmanager==2.0.0
# via zgw-consumers
django-solo==2.2.0
# via zgw-consumers
# via
# -r requirements/base.in
# zgw-consumers
django-timeline-logger==4.0.0
# via -r requirements/base.in
django-two-factor-auth[phonenumberslite,webauthn]==1.16.0
Expand Down
2 changes: 2 additions & 0 deletions backend/src/openarchiefbeheer/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,14 @@
"simple_certmanager",
"timeline_logger",
"django_filters",
"solo",
# Project applications.
"openarchiefbeheer.accounts",
"openarchiefbeheer.destruction",
"openarchiefbeheer.utils",
"openarchiefbeheer.logging",
"openarchiefbeheer.zaken",
"openarchiefbeheer.emails",
]

MIDDLEWARE = [
Expand Down
12 changes: 2 additions & 10 deletions backend/src/openarchiefbeheer/destruction/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from django.conf import settings
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from ordered_model.models import OrderedModel

from openarchiefbeheer.destruction.constants import ListItemStatus, ListStatus
from openarchiefbeheer.emails.utils import send_review_request_email


class DestructionList(models.Model):
Expand Down Expand Up @@ -140,17 +139,10 @@ def assign(self) -> None:

self.notify()

# TODO refine what we want to do with notifications
def notify(self) -> None:
if not self.user.email:
return

is_reviewer = self.user != self.destruction_list.author
if is_reviewer:
send_mail(
_("Destruction list review request"),
_("There is a destruction list review request for you."),
settings.DEFAULT_FROM_EMAIL,
[self.user.email],
fail_silently=False,
)
send_review_request_email(self.user, self.destruction_list)
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from unittest.mock import patch

from django.core import mail
from django.test import TestCase
Expand All @@ -13,6 +14,7 @@
from openarchiefbeheer.destruction.api.serializers import DestructionListSerializer
from openarchiefbeheer.destruction.constants import ListItemStatus
from openarchiefbeheer.destruction.tests.factories import DestructionListItemFactory
from openarchiefbeheer.emails.models import EmailConfig

factory = APIRequestFactory()

Expand Down Expand Up @@ -57,7 +59,15 @@ def test_create_destruction_list(self):

self.assertTrue(serializer.is_valid())

with freeze_time("2024-05-02T16:00:00+02:00"):
with (
patch(
"openarchiefbeheer.emails.utils.EmailConfig.get_solo",
return_value=EmailConfig(
subject_review_required="Destruction list review request"
),
),
freeze_time("2024-05-02T16:00:00+02:00"),
):
destruction_list = serializer.save()

assignees = destruction_list.assignees.order_by("order")
Expand Down Expand Up @@ -86,7 +96,7 @@ def test_create_destruction_list(self):
sent_mail = mail.outbox

self.assertEqual(len(sent_mail), 1)
self.assertEqual(sent_mail[0].subject, _("Destruction list review request"))
self.assertEqual(sent_mail[0].subject, "Destruction list review request")
self.assertEqual(sent_mail[0].recipients(), ["[email protected]"])

logs = TimelineLog.objects.filter(user=record_manager)
Expand Down
Empty file.
30 changes: 30 additions & 0 deletions backend/src/openarchiefbeheer/emails/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from solo.admin import SingletonModelAdmin

from .models import EmailConfig


@admin.register(EmailConfig)
class EmailConfigAdmin(SingletonModelAdmin):
fieldsets = [
(
_("Template review request"),
{
"fields": ["subject_review_required", "body_review_required"],
},
),
(
_("Template review reminder"),
{
"fields": ["subject_review_reminder", "body_review_reminder"],
},
),
(
_("Template changes requested"),
{
"fields": ["subject_changes_requested", "body_changes_requested"],
},
),
]
76 changes: 76 additions & 0 deletions backend/src/openarchiefbeheer/emails/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Generated by Django 4.2.11 on 2024-05-13 08:17

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="EmailConfig",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"subject_review_required",
models.CharField(
help_text="Subject of the email that will be sent to a reviewer when there is a destruction list ready to be reviewed.",
max_length=250,
verbose_name="subject review required",
),
),
(
"body_review_required",
models.TextField(
help_text="Body of the email that will be sent to a reviewer when there is a destruction list ready to be reviewed.",
verbose_name="body review required",
),
),
(
"subject_review_reminder",
models.CharField(
help_text="Subject of the email that will be sent to a reviewer after a configured period of time if they still haven't reviewed a destruction list.",
max_length=250,
verbose_name="subject review reminder",
),
),
(
"body_review_reminder",
models.TextField(
help_text="Body of the email that will be sent to a reviewer after a configured period of time if they still haven't reviewed a destruction list.",
verbose_name="body review reminder",
),
),
(
"subject_changes_requested",
models.CharField(
help_text="Subject of the email that will be sent to the record manager when a reviewer has requested changes to a destruction list.",
max_length=250,
verbose_name="subject changes requested",
),
),
(
"body_changes_requested",
models.TextField(
help_text="Body of the email that will be sent to the record manager when a reviewer has requested changes to a destruction list.",
verbose_name="body changes requested",
),
),
],
options={
"verbose_name": "email configuration",
"verbose_name_plural": "email configurations",
},
),
]
Empty file.
59 changes: 59 additions & 0 deletions backend/src/openarchiefbeheer/emails/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from django.db import models
from django.utils.translation import gettext_lazy as _

from solo.models import SingletonModel


class EmailConfig(SingletonModel):
subject_review_required = models.CharField(
max_length=250,
verbose_name=_("subject review required"),
help_text=_(
"Subject of the email that will be sent to a reviewer "
"when there is a destruction list ready to be reviewed."
),
)
body_review_required = models.TextField(
verbose_name=_("body review required"),
help_text=_(
"Body of the email that will be sent to a reviewer "
"when there is a destruction list ready to be reviewed."
),
)
subject_review_reminder = models.CharField(
max_length=250,
verbose_name=_("subject review reminder"),
help_text=_(
"Subject of the email that will be sent to a reviewer "
"after a configured period of time if they still haven't reviewed a destruction list."
),
)
body_review_reminder = models.TextField(
verbose_name=_("body review reminder"),
help_text=_(
"Body of the email that will be sent to a reviewer "
"after a configured period of time if they still haven't reviewed a destruction list."
),
)
subject_changes_requested = models.CharField(
max_length=250,
verbose_name=_("subject changes requested"),
help_text=_(
"Subject of the email that will be sent to the record manager "
"when a reviewer has requested changes to a destruction list."
),
)
body_changes_requested = models.TextField(
verbose_name=_("body changes requested"),
help_text=_(
"Body of the email that will be sent to the record manager "
"when a reviewer has requested changes to a destruction list."
),
)

class Meta:
verbose_name = _("email configuration")
verbose_name_plural = _("email configurations")

def __str__(self):
return "Email configuration"
31 changes: 31 additions & 0 deletions backend/src/openarchiefbeheer/emails/render_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.template.backends.django import DjangoTemplates


class SandboxedTemplates(DjangoTemplates):
def __init__(self, params: dict) -> None:
params = params.copy()
params.setdefault("NAME", "django_sandboxed")
# no file system paths to look up files (also blocks {% include %} etc)
params.setdefault("DIRS", [])
params.setdefault("APP_DIRS", False)
params.setdefault("OPTIONS", {})

super().__init__(params)

def get_templatetag_libraries(self, custom_libraries: dict) -> dict:
"""
The parent returns template tag libraries from installed
applications and the supplied custom_libraries argument.
"""
return {}

def template_dirs(self) -> list:
"""
The parent returns a list of directories to search for templates.
We only need to render from string.
"""
return []


def get_sandboxed_backend() -> SandboxedTemplates:
return SandboxedTemplates({})
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from unittest.mock import patch

from django.core import mail
from django.test import TestCase

from openarchiefbeheer.accounts.tests.factories import UserFactory
from openarchiefbeheer.destruction.tests.factories import DestructionListFactory

from ..models import EmailConfig
from ..utils import send_review_request_email


class RenderingEmailTemplatesTestCase(TestCase):

def test_render_email_templates(self):
user = UserFactory.create(username="reviewer1", email="[email protected]")
destruction_list = DestructionListFactory.create(name="List 1")

with patch(
"openarchiefbeheer.emails.utils.EmailConfig.get_solo",
return_value=EmailConfig(
body_review_required="This is a test user: {{ user }} and a test list: {{ list }}."
),
):
send_review_request_email(user, destruction_list)

messages = mail.outbox

self.assertEqual(len(messages), 1)
self.assertEqual(
messages[0].body, "This is a test user: reviewer1 and a test list: List 1."
)
30 changes: 30 additions & 0 deletions backend/src/openarchiefbeheer/emails/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import TYPE_CHECKING

from django.conf import settings
from django.core.mail import send_mail

if TYPE_CHECKING:
from openarchiefbeheer.accounts.models import User
from openarchiefbeheer.destruction.models import DestructionList

from .models import EmailConfig
from .render_backend import get_sandboxed_backend


def send_review_request_email(
user: "User", destruction_list: "DestructionList"
) -> None:
config = EmailConfig.get_solo()

backend = get_sandboxed_backend()
template = backend.from_string(config.body_review_required)

formatted_body = template.render(context={"user": user, "list": destruction_list})

send_mail(
subject=config.subject_review_required,
message=formatted_body,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
fail_silently=False,
)
14 changes: 14 additions & 0 deletions backend/src/openarchiefbeheer/fixtures/default_emails.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"model": "emails.emailconfig",
"pk": 1,
"fields": {
"subject_review_required": "Uw accordering van een vernietigingslijst wordt gevraagd",
"body_review_required": "Beste {{ user }},\r\n\r\nUw accordering van een vernietigingslijst wordt gevraagd. U kunt in de Open-Archiefbeheer web app de lijst bekijken om te controleren of de zaken op de lijst daadwerkelijk vernietigd kunnen worden.",
"subject_review_reminder": "Uw accordering van een vernietigingslijst wordt gevraagd (herinnering)",
"body_review_reminder": "Beste {{ user }}, \r\n\r\nU heeft kortgeleden een notificatie ontvangen over de Vernietigingslijst die wacht op uw goedkeuring. \r\n\r\nWij zien dat u nog niet geregeerd heeft, wilt u zo spoedig mogelijk op de Vernietigingslijst reageren.",
"subject_changes_requested": "Voorstel voor wijziging van uw vernietigingslijst",
"body_changes_requested": "Beste {{ user }},\r\n\r\nEr is een voorstel tot aanpassing van uw vernietigingslijst {{ list }}. U kunt de lijst en de voorgestelde wijziging in de Open-Archiefbeheer web app bekijken en af te handelen."
}
}
]

0 comments on commit 213beae

Please sign in to comment.