From 1ec6b70a7b65051e77047e562b32759f2e6520b8 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 13 May 2024 10:36:39 +0200 Subject: [PATCH 1/5] :heavy_plus_sign: [#8] Django solo --- backend/requirements/base.in | 1 + backend/requirements/base.txt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/requirements/base.in b/backend/requirements/base.in index d3d9d3f69..fed1aa74c 100644 --- a/backend/requirements/base.in +++ b/backend/requirements/base.in @@ -13,6 +13,7 @@ django-redis django-rosetta maykin-2fa django-timeline-logger +django-solo # API libraries djangorestframework diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt index 8c807b7ad..4e491cf82 100644 --- a/backend/requirements/base.txt +++ b/backend/requirements/base.txt @@ -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 From 03dc793da6712b990cacb3dde03b4d8b91a93e54 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 13 May 2024 10:37:34 +0200 Subject: [PATCH 2/5] :sparkles: [#8] Add model to configure emails --- backend/src/openarchiefbeheer/conf/base.py | 2 + .../src/openarchiefbeheer/emails/__init__.py | 0 backend/src/openarchiefbeheer/emails/admin.py | 30 ++++++++ .../emails/migrations/0001_initial.py | 76 +++++++++++++++++++ .../emails/migrations/__init__.py | 0 .../src/openarchiefbeheer/emails/models.py | 59 ++++++++++++++ 6 files changed, 167 insertions(+) create mode 100644 backend/src/openarchiefbeheer/emails/__init__.py create mode 100644 backend/src/openarchiefbeheer/emails/admin.py create mode 100644 backend/src/openarchiefbeheer/emails/migrations/0001_initial.py create mode 100644 backend/src/openarchiefbeheer/emails/migrations/__init__.py create mode 100644 backend/src/openarchiefbeheer/emails/models.py diff --git a/backend/src/openarchiefbeheer/conf/base.py b/backend/src/openarchiefbeheer/conf/base.py index 1f02408d1..da887699b 100644 --- a/backend/src/openarchiefbeheer/conf/base.py +++ b/backend/src/openarchiefbeheer/conf/base.py @@ -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 = [ diff --git a/backend/src/openarchiefbeheer/emails/__init__.py b/backend/src/openarchiefbeheer/emails/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/openarchiefbeheer/emails/admin.py b/backend/src/openarchiefbeheer/emails/admin.py new file mode 100644 index 000000000..0a01e7fe0 --- /dev/null +++ b/backend/src/openarchiefbeheer/emails/admin.py @@ -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"], + }, + ), + ] diff --git a/backend/src/openarchiefbeheer/emails/migrations/0001_initial.py b/backend/src/openarchiefbeheer/emails/migrations/0001_initial.py new file mode 100644 index 000000000..7418eaa73 --- /dev/null +++ b/backend/src/openarchiefbeheer/emails/migrations/0001_initial.py @@ -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", + }, + ), + ] diff --git a/backend/src/openarchiefbeheer/emails/migrations/__init__.py b/backend/src/openarchiefbeheer/emails/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/openarchiefbeheer/emails/models.py b/backend/src/openarchiefbeheer/emails/models.py new file mode 100644 index 000000000..326b56c2b --- /dev/null +++ b/backend/src/openarchiefbeheer/emails/models.py @@ -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" From b824e0195219341b5f074d696e117005977197a2 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 13 May 2024 10:39:12 +0200 Subject: [PATCH 3/5] :sparkles: [#8] Fixtures with default email templates --- .../openarchiefbeheer/fixtures/default_emails.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 backend/src/openarchiefbeheer/fixtures/default_emails.json diff --git a/backend/src/openarchiefbeheer/fixtures/default_emails.json b/backend/src/openarchiefbeheer/fixtures/default_emails.json new file mode 100644 index 000000000..71dab19eb --- /dev/null +++ b/backend/src/openarchiefbeheer/fixtures/default_emails.json @@ -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." + } + } +] \ No newline at end of file From ab47a3fcc2959d9475821d8fad8ba6ac5551504d Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 13 May 2024 12:37:05 +0200 Subject: [PATCH 4/5] :sparkles: [#8] Render templates with sandboxed backend --- .../openarchiefbeheer/destruction/models.py | 12 ++----- .../emails/render_backend.py | 31 +++++++++++++++++++ backend/src/openarchiefbeheer/emails/utils.py | 30 ++++++++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 backend/src/openarchiefbeheer/emails/render_backend.py create mode 100644 backend/src/openarchiefbeheer/emails/utils.py diff --git a/backend/src/openarchiefbeheer/destruction/models.py b/backend/src/openarchiefbeheer/destruction/models.py index 89468bf98..c1a7b33f9 100644 --- a/backend/src/openarchiefbeheer/destruction/models.py +++ b/backend/src/openarchiefbeheer/destruction/models.py @@ -1,5 +1,3 @@ -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 _ @@ -7,6 +5,7 @@ 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): @@ -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) diff --git a/backend/src/openarchiefbeheer/emails/render_backend.py b/backend/src/openarchiefbeheer/emails/render_backend.py new file mode 100644 index 000000000..6498dc64d --- /dev/null +++ b/backend/src/openarchiefbeheer/emails/render_backend.py @@ -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({}) diff --git a/backend/src/openarchiefbeheer/emails/utils.py b/backend/src/openarchiefbeheer/emails/utils.py new file mode 100644 index 000000000..cb6700e07 --- /dev/null +++ b/backend/src/openarchiefbeheer/emails/utils.py @@ -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, + ) From 1c934d32008c99b8cecf0d6104bb25d4052d9132 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Mon, 13 May 2024 12:37:33 +0200 Subject: [PATCH 5/5] :white_check_mark: [#8] Test notifying reviewer --- .../destruction/tests/test_serializers.py | 14 ++++++-- .../emails/tests/__init__.py | 0 .../emails/tests/test_rendering_emails.py | 32 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 backend/src/openarchiefbeheer/emails/tests/__init__.py create mode 100644 backend/src/openarchiefbeheer/emails/tests/test_rendering_emails.py diff --git a/backend/src/openarchiefbeheer/destruction/tests/test_serializers.py b/backend/src/openarchiefbeheer/destruction/tests/test_serializers.py index 9d1f732b7..36bfa9043 100644 --- a/backend/src/openarchiefbeheer/destruction/tests/test_serializers.py +++ b/backend/src/openarchiefbeheer/destruction/tests/test_serializers.py @@ -1,4 +1,5 @@ from datetime import datetime +from unittest.mock import patch from django.core import mail from django.test import TestCase @@ -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() @@ -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") @@ -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(), ["reviewer1@oab.nl"]) logs = TimelineLog.objects.filter(user=record_manager) diff --git a/backend/src/openarchiefbeheer/emails/tests/__init__.py b/backend/src/openarchiefbeheer/emails/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/openarchiefbeheer/emails/tests/test_rendering_emails.py b/backend/src/openarchiefbeheer/emails/tests/test_rendering_emails.py new file mode 100644 index 000000000..a08a63404 --- /dev/null +++ b/backend/src/openarchiefbeheer/emails/tests/test_rendering_emails.py @@ -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="reviewer1@test.nl") + 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." + )