From 75d7775f27c8759f1a03d48ad47f441e96e08c11 Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Thu, 19 Sep 2024 10:22:17 +0300 Subject: [PATCH] Migrate email templates to the new app --- email_notification/admin/__init__.py | 5 - email_notification/admin/email_template.py | 110 ----------- email_notification/apps.py | 6 - .../migrations/0010_delete_emailtemplate.py | 20 ++ email_notification/models.py | 85 --------- email_notification/tasks.py | 155 ---------------- email_notification/translation.py | 8 - locale/fi/LC_MESSAGES/django.po | 102 +++++------ locale/sv/LC_MESSAGES/django.po | 94 +++++----- reservations/admin/reservation/admin.py | 2 +- .../email}/email_tester.html | 0 tests/factories/email_template.py | 3 +- .../test_email_template_testing_forms.py | 6 +- ...est_email_template_testing_initial_data.py | 2 +- .../test_email_builder_render_application.py | 5 +- .../test_email_builder_render_reservation.py | 7 +- .../test_email_context_application.py | 2 +- .../test_email_context_reservation.py | 2 +- .../test_email_sender_application.py | 6 +- .../test_email_sender_reservation.py | 9 +- tests/test_email/test_email_validator.py | 4 +- .../test_verkkokauppa/test_pruning.py | 2 +- .../test_order_payment_webhooks.py | 2 +- .../test_application/test_send.py | 2 +- .../test_order/test_refresh.py | 2 +- .../test_reservation/test_adjust_time.py | 2 +- .../test_reservation/test_cancel.py | 3 +- .../test_reservation/test_confirm.py | 3 +- .../test_reservation/test_delete.py | 2 +- .../test_reservation/test_deny.py | 2 +- .../test_reservation/test_require_handling.py | 2 +- .../test_staff_adjust_time.py | 2 +- tests/test_utils/test_create_test_data.py | 3 +- .../test_notification_recipients.py | 2 +- tilavarauspalvelu/admin/__init__.py | 2 + .../admin/email_template/admin.py | 109 +++++++++++ .../admin/email_template/tester.py | 10 +- .../graphql/types/application/serializers.py | 2 +- .../serializers/adjust_time_serializers.py | 2 +- .../serializers/approve_serializers.py | 2 +- .../serializers/cancellation_serializers.py | 2 +- .../serializers/confirm_serializers.py | 2 +- .../serializers/deny_serializers.py | 2 +- .../handling_required_serializers.py | 2 +- .../staff_adjust_time_serializers.py | 2 +- tilavarauspalvelu/enums.py | 17 ++ .../exceptions.py | 0 .../migrations/0007_emailtemplate.py | 171 ++++++++++++++++++ tilavarauspalvelu/models/__init__.py | 2 + .../models/email_template/actions.py | 9 + .../models/email_template/model.py | 83 +++++++++ .../models/email_template/queryset.py | 10 + .../models/payment_order/model.py | 2 +- tilavarauspalvelu/tasks.py | 150 ++++++++++++++- .../templatetags.py | 4 +- tilavarauspalvelu/translation.py | 6 + .../utils/email}/__init__.py | 0 .../application_email_notification_sender.py | 4 +- .../utils/email}/email_builder_application.py | 5 +- .../utils/email}/email_builder_base.py | 7 +- .../utils/email}/email_builder_reservation.py | 8 +- .../utils/email}/email_sender.py | 13 +- .../utils/email}/email_validator.py | 6 +- .../reservation_email_notification_sender.py | 4 +- 64 files changed, 754 insertions(+), 546 deletions(-) delete mode 100644 email_notification/admin/__init__.py delete mode 100644 email_notification/admin/email_template.py delete mode 100644 email_notification/apps.py create mode 100644 email_notification/migrations/0010_delete_emailtemplate.py delete mode 100644 email_notification/models.py delete mode 100644 email_notification/tasks.py delete mode 100644 email_notification/translation.py rename {email_notification/templates => templates/email}/email_tester.html (100%) rename email_notification/admin/email_template_tester.py => tilavarauspalvelu/admin/email_template/tester.py (94%) rename {email_notification => tilavarauspalvelu}/exceptions.py (100%) create mode 100644 tilavarauspalvelu/migrations/0007_emailtemplate.py rename {email_notification => tilavarauspalvelu}/templatetags.py (91%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/__init__.py (100%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/application_email_notification_sender.py (77%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/email_builder_application.py (93%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/email_builder_base.py (94%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/email_builder_reservation.py (97%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/email_sender.py (92%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/email_validator.py (94%) rename {email_notification/helpers => tilavarauspalvelu/utils/email}/reservation_email_notification_sender.py (97%) diff --git a/email_notification/admin/__init__.py b/email_notification/admin/__init__.py deleted file mode 100644 index 6de3b958d..000000000 --- a/email_notification/admin/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .email_template import EmailTemplateAdmin - -__all__ = [ - "EmailTemplateAdmin", -] diff --git a/email_notification/admin/email_template.py b/email_notification/admin/email_template.py deleted file mode 100644 index 871897566..000000000 --- a/email_notification/admin/email_template.py +++ /dev/null @@ -1,110 +0,0 @@ -from admin_extra_buttons.api import ExtraButtonsMixin, button -from django.contrib import admin -from django.core.files.uploadedfile import InMemoryUploadedFile -from django.core.handlers.wsgi import WSGIRequest -from django.db.models.fields.files import FieldFile -from django.forms import ModelForm, ValidationError -from django.http import HttpResponseRedirect -from django.template.response import TemplateResponse -from modeltranslation.admin import TranslationAdmin - -from config.utils.commons import LanguageType -from email_notification.admin.email_template_tester import email_template_tester_admin_view -from email_notification.exceptions import EmailTemplateValidationError -from email_notification.helpers.email_builder_application import ApplicationEmailBuilder, ApplicationEmailContext -from email_notification.helpers.email_builder_reservation import ReservationEmailBuilder, ReservationEmailContext -from email_notification.helpers.email_validator import EmailTemplateValidator -from email_notification.models import EmailTemplate, EmailType - -__all__ = [ - "EmailTemplateAdmin", -] - - -class EmailTemplateAdminForm(ModelForm): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - # Set up the available 'type' choices - # Remove existing types from the choices, so that the same type cannot be added twice - existing_types = EmailTemplate.objects.values_list("type", flat=True) - if self.instance and self.instance.type: - existing_types = existing_types.exclude(type=self.instance.type) - available_types = [(value, label) for value, label in EmailType.choices if value not in existing_types] - self.fields["type"].choices = available_types - - def _get_validator(self) -> EmailTemplateValidator: - email_template_type = self.cleaned_data["type"] - - # Reservation - if email_template_type in ReservationEmailBuilder.email_template_types: - mock_context = ReservationEmailContext.from_mock_data() - # Application - elif email_template_type in ApplicationEmailBuilder.email_template_types: - mock_context = ApplicationEmailContext.from_mock_data() - else: - raise EmailTemplateValidationError(f"Email template type '{email_template_type}' is not supported.") - - return EmailTemplateValidator(context=mock_context) - - def _get_validated_field(self, field) -> str | None: - data: str | None = self.cleaned_data[field] - if not data: - return data - - try: - validator = self._get_validator() - validator.validate_string(data) - except EmailTemplateValidationError as e: - raise ValidationError(e.message) from e - return data - - def _get_validated_html_file(self, language: LanguageType) -> InMemoryUploadedFile | FieldFile | None: - file: InMemoryUploadedFile | FieldFile | None = self.cleaned_data[f"html_content_{language}"] - - if file and isinstance(file, InMemoryUploadedFile): - validator = self._get_validator() - validator.validate_html_file(file) - - return file - - def clean_subject_fi(self) -> str | None: - return self._get_validated_field("subject_fi") - - def clean_subject_en(self) -> str | None: - return self._get_validated_field("subject_en") - - def clean_subject_sv(self) -> str | None: - return self._get_validated_field("subject_sv") - - def clean_content_fi(self) -> str | None: - return self._get_validated_field("content_fi") - - def clean_content_en(self) -> str | None: - return self._get_validated_field("content_en") - - def clean_content_sv(self) -> str | None: - return self._get_validated_field("content_sv") - - def clean_html_content_fi(self) -> InMemoryUploadedFile | FieldFile | None: - return self._get_validated_html_file("fi") - - def clean_html_content_en(self) -> InMemoryUploadedFile | FieldFile | None: - return self._get_validated_html_file("en") - - def clean_html_content_sv(self) -> InMemoryUploadedFile | FieldFile | None: - return self._get_validated_html_file("sv") - - -@admin.register(EmailTemplate) -class EmailTemplateAdmin(ExtraButtonsMixin, TranslationAdmin): - form = EmailTemplateAdminForm - - list_display = [ - "type", - "name", - ] - - @button(label="Email Template Testing", change_form=True) - def template_tester(self, request: WSGIRequest, pk: int) -> TemplateResponse | HttpResponseRedirect: - return email_template_tester_admin_view(self, request, pk) diff --git a/email_notification/apps.py b/email_notification/apps.py deleted file mode 100644 index acc4abfaf..000000000 --- a/email_notification/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class EmailNotificationConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "email_notification" diff --git a/email_notification/migrations/0010_delete_emailtemplate.py b/email_notification/migrations/0010_delete_emailtemplate.py new file mode 100644 index 000000000..4f53351b3 --- /dev/null +++ b/email_notification/migrations/0010_delete_emailtemplate.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.1 on 2024-09-19 07:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("email_notification", "0009_rename_in_handling_to_in_allocation"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.DeleteModel( + name="EmailTemplate", + ), + ], + database_operations=[], + ), + ] diff --git a/email_notification/models.py b/email_notification/models.py deleted file mode 100644 index cafc7e383..000000000 --- a/email_notification/models.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.conf import settings -from django.db import models -from django.utils.translation import gettext_lazy as _ - -__all__ = [ - "EmailTemplate", - "EmailType", -] - - -class EmailType(models.TextChoices): - ACCESS_CODE_FOR_RESERVATION = "access_code_for_reservation" - APPLICATION_HANDLED = "application_handled" - APPLICATION_IN_ALLOCATION = "application_in_allocation" - APPLICATION_RECEIVED = "application_received" - HANDLING_REQUIRED_RESERVATION = "handling_required_reservation" - RESERVATION_CANCELLED = "reservation_cancelled" - RESERVATION_CONFIRMED = "reservation_confirmed" - RESERVATION_HANDLED_AND_CONFIRMED = "reservation_handled_and_confirmed" - RESERVATION_MODIFIED = "reservation_modified" - RESERVATION_NEEDS_TO_BE_PAID = "reservation_needs_to_be_paid" - RESERVATION_REJECTED = "reservation_rejected" - RESERVATION_WITH_PIN_CONFIRMED = "reservation_with_pin_confirmed" - STAFF_NOTIFICATION_RESERVATION_MADE = "staff_notification_reservation_made" - STAFF_NOTIFICATION_RESERVATION_REQUIRES_HANDLING = "staff_notification_reservation_requires_handling" - - -class EmailTemplate(models.Model): - type: EmailType = models.CharField( - max_length=254, - choices=EmailType.choices, - unique=True, - blank=False, - null=False, - verbose_name=_("Email type"), - help_text=_("Only one template per type can be created."), - ) - name: str = models.CharField( - max_length=255, - unique=True, - verbose_name=_("Unique name for this content"), - null=False, - blank=False, - ) - - subject: str = models.CharField(max_length=255, null=False, blank=False) - content: str = models.TextField( - verbose_name=_("Content"), - help_text=_("Email body content. Use curly brackets to indicate data specific fields e.g {{reservee_name}}."), - null=False, - blank=False, - ) - - html_content: str | None = models.FileField( - verbose_name=_("HTML content"), - help_text=_( - "Email body content as HTML. Use curly brackets to indicate data specific fields e.g {{reservee_name}}." - ), - null=True, - blank=True, - upload_to=settings.EMAIL_HTML_TEMPLATES_ROOT, - ) - - # Translated field hints - subject_fi: str | None - subject_en: str | None - subject_sv: str | None - content_fi: str | None - content_en: str | None - content_sv: str | None - html_content_fi: str | None - html_content_en: str | None - html_content_sv: str | None - - class Meta: - db_table = "email_template" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - choices = dict(EmailType.choices) - label = choices.get(self.type) or self.type - return f"{label}: {self.name}" diff --git a/email_notification/tasks.py b/email_notification/tasks.py deleted file mode 100644 index 934a1dd9e..000000000 --- a/email_notification/tasks.py +++ /dev/null @@ -1,155 +0,0 @@ -from django.conf import settings -from lookup_property import L - -from applications.enums import ApplicationRoundStatusChoice, ApplicationSectionStatusChoice -from applications.models import Application -from common.date_utils import local_datetime -from config.celery import app -from email_notification.exceptions import SendEmailNotificationError -from email_notification.helpers.email_sender import EmailNotificationSender -from email_notification.models import EmailType -from reservations.models import Reservation -from users.models import ReservationNotification, User -from utils.sentry import SentryLogger - -############### -# Reservation # -############### - - -@app.task(name="send_reservation_email") -def send_reservation_email_task(reservation_id: int, email_type: EmailType) -> None: - if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: - return - reservation = Reservation.objects.filter(id=reservation_id).first() - if not reservation: - return - - email_notification_sender = EmailNotificationSender(email_type=email_type, recipients=None) - email_notification_sender.send_reservation_email(reservation=reservation) - - -@app.task(name="send_staff_reservation_email") -def send_staff_reservation_email_task( - reservation_id: int, - email_type: EmailType, - notification_settings: list[ReservationNotification], -) -> None: - if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: - return - reservation = Reservation.objects.filter(id=reservation_id).first() - if not reservation: - return - - recipients = _get_reservation_staff_notification_recipients(reservation, notification_settings) - if not recipients: - return - - email_notification_sender = EmailNotificationSender(email_type=email_type, recipients=recipients) - email_notification_sender.send_reservation_email(reservation=reservation, forced_language=settings.LANGUAGE_CODE) - - -def _get_reservation_staff_notification_recipients( - reservation: Reservation, - notification_settings: list[ReservationNotification], -) -> list[str]: - """ - Get staff users who should receive reservation notifications based on their unit roles and notification settings. - - Get users with unit roles and notifications enabled, collect the ones that can manage relevant units, - have matching notification setting are not the reservation creator - """ - from tilavarauspalvelu.models import Unit - - notification_recipients: list[str] = [] - reservation_units = reservation.reservation_unit.all() - units = Unit.objects.filter(reservationunit__in=reservation_units).prefetch_related("unit_groups").distinct() - users = User.objects.filter(unit_roles__isnull=False).exclude(reservation_notification="NONE") - for user in users: - # Skip users who don't have the correct unit role - if not user.permissions.can_manage_reservations_for_units(units, any_unit=True): - continue - - # Skip users who don't have the correct notification setting - if not (any(user.reservation_notification.upper() == setting.upper() for setting in notification_settings)): - continue - - # Skip the reservation creator - if reservation.user and reservation.user.pk == user.pk: - continue - - notification_recipients.append(user.email) - - # Remove possible duplicates - return list(set(notification_recipients)) - - -############### -# Application # -############### - - -@app.task(name="send_application_email") -def send_application_email_task(application_id: int, email_type: EmailType) -> None: - if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: - return - application = Application.objects.filter(id=application_id).first() - if not application: - return - - email_notification_sender = EmailNotificationSender(email_type=email_type, recipients=None) - email_notification_sender.send_application_email(application=application) - - -@app.task(name="send_application_in_allocation_emails") -def send_application_in_allocation_email_task() -> None: - if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: - return - - # Don't try to send anything if the email template is not defined (EmailNotificationSender will raise an error) - try: - email_sender = EmailNotificationSender(email_type=EmailType.APPLICATION_IN_ALLOCATION, recipients=None) - except SendEmailNotificationError: - msg = "Tried to send an email, but Email Template for APPLICATION_IN_ALLOCATION was not found." - SentryLogger.log_message(msg, level="warning") - return - - # Get all applications that need a notification to be sent - applications = Application.objects.filter( - L(application_round__status=ApplicationRoundStatusChoice.IN_ALLOCATION.value), - L(status=ApplicationSectionStatusChoice.IN_ALLOCATION.value), - in_allocation_notification_sent_date__isnull=True, - application_sections__isnull=False, - ).order_by("created_date") - if not applications: - return - - email_sender.send_batch_application_emails(applications=applications) - applications.update(in_allocation_notification_sent_date=local_datetime()) - - -@app.task(name="send_application_handled_emails") -def send_application_handled_email_task() -> None: - if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: - return - - # Don't try to send anything if the email template is not defined (EmailNotificationSender will raise an error) - try: - email_sender = EmailNotificationSender(email_type=EmailType.APPLICATION_HANDLED, recipients=None) - except SendEmailNotificationError: - msg = "Tried to send an email, but Email Template for APPLICATION_HANDLED was not found." - SentryLogger.log_message(msg, level="warning") - return - - # Get all applications that need a notification to be sent - applications = Application.objects.filter( - L(application_round__status=ApplicationRoundStatusChoice.HANDLED.value), - L(status=ApplicationSectionStatusChoice.HANDLED.value), - results_ready_notification_sent_date__isnull=True, - application_sections__isnull=False, - ).order_by("created_date") - if not applications: - return - - email_sender.send_batch_application_emails(applications=applications) - applications.update(results_ready_notification_sent_date=local_datetime()) diff --git a/email_notification/translation.py b/email_notification/translation.py deleted file mode 100644 index a08ece217..000000000 --- a/email_notification/translation.py +++ /dev/null @@ -1,8 +0,0 @@ -from modeltranslation.translator import TranslationOptions, register - -from email_notification.models import EmailTemplate - - -@register(EmailTemplate) -class EmailTemplateTranslationOptions(TranslationOptions): - fields = ["subject", "content", "html_content"] diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index acbd11181..a72114f30 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -1346,57 +1346,6 @@ msgstr "Englanti" msgid "Swedish" msgstr "Ruotsi" -#: email_notification/admin/email_template_tester.py -#, python-format -msgid "Test Email '%s' successfully sent." -msgstr "Testisähköposti '%s' lähetetty onnistuneesti." - -#: email_notification/models.py -msgid "Email type" -msgstr "Sähköpostin tyyppi" - -#: email_notification/models.py -msgid "Only one template per type can be created." -msgstr "Vain yksi sähköpostipohja per tyyppi voidaan luoda." - -#: email_notification/models.py -msgid "Unique name for this content" -msgstr "Uniikki nimi tälle sisällölle" - -#: email_notification/models.py -msgid "Content" -msgstr "Sisältö" - -#: email_notification/models.py -msgid "" -"Email body content. Use curly brackets to indicate data specific fields e.g " -"{{reservee_name}}." -msgstr "" -"Sähköpostin sisältö. Käytä aaltosulkeita osoittamaan dataan liittyviä " -"kenttiä, esim {{reservee_name}}." - -#: email_notification/models.py -msgid "HTML content" -msgstr "HTML-sisältö" - -#: email_notification/models.py -msgid "" -"Email body content as HTML. Use curly brackets to indicate data specific " -"fields e.g {{reservee_name}}." -msgstr "" -"Sähköpostin sisältö HTML-muodossa. Käytä aaltosulkeita osoittamaan dataan " -"liittyviä kenttiä, esim {{reservee_name}}." - -#: email_notification/templates/email_tester.html -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -msgid "Home" -msgstr "Koti" - -#: email_notification/templates/email_tester.html -msgid "Email Template Testing" -msgstr "Email-pohjan testaus" - #: opening_hours/admin/origin_hauki_resource.py msgid "" "OriginHaukiResources with this specific hash never have any opening hours." @@ -2798,6 +2747,12 @@ msgstr "Vaihda salasana" msgid "Log out" msgstr "Kirjaudu ulos" +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +#: templates/email/email_tester.html +msgid "Home" +msgstr "Koti" + #: templates/admin/deny_reservation_confirmation.html msgid "Deny reservations" msgstr "Hylkää varauksia" @@ -2836,6 +2791,15 @@ msgstr "Ei, vie minut takaisin" msgid "Reset allocations" msgstr "Nollaa allokoinnit" +#: templates/email/email_tester.html +msgid "Email Template Testing" +msgstr "Email-pohjan testaus" + +#: tilavarauspalvelu/admin/email_template/tester.py +#, python-format +msgid "Test Email '%s' successfully sent." +msgstr "Testisähköposti '%s' lähetetty onnistuneesti." + #: tilavarauspalvelu/admin/general_role/admin.py msgid "Search by username, email, first name or last name" msgstr "Etsi käyttäjänimellä, sähköpostilla, etu- tai sukunimellä" @@ -3093,6 +3057,42 @@ msgstr "Varaaja" msgid "Notification manager" msgstr "Ilmoituksen hallitsija." +#: tilavarauspalvelu/models/email_template/model.py +msgid "Email type" +msgstr "Sähköpostin tyyppi" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "Only one template per type can be created." +msgstr "Vain yksi sähköpostipohja per tyyppi voidaan luoda." + +#: tilavarauspalvelu/models/email_template/model.py +msgid "Unique name for this content" +msgstr "Uniikki nimi tälle sisällölle" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "Content" +msgstr "Sisältö" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "" +"Email body content. Use curly brackets to indicate data specific fields e.g " +"{{reservee_name}}." +msgstr "" +"Sähköpostin sisältö. Käytä aaltosulkeita osoittamaan dataan liittyviä " +"kenttiä, esim {{reservee_name}}." + +#: tilavarauspalvelu/models/email_template/model.py +msgid "HTML content" +msgstr "HTML-sisältö" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "" +"Email body content as HTML. Use curly brackets to indicate data specific " +"fields e.g {{reservee_name}}." +msgstr "" +"Sähköpostin sisältö HTML-muodossa. Käytä aaltosulkeita osoittamaan dataan " +"liittyviä kenttiä, esim {{reservee_name}}." + #: tilavarauspalvelu/models/general_role/model.py msgid "General role" msgstr "Yleinen rooli" diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index b5009b7c5..1b16621ca 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/locale/sv/LC_MESSAGES/django.po @@ -1306,53 +1306,6 @@ msgstr "" msgid "Swedish" msgstr "" -#: email_notification/admin/email_template_tester.py -#, python-format -msgid "Test Email '%s' successfully sent." -msgstr "" - -#: email_notification/models.py -msgid "Email type" -msgstr "" - -#: email_notification/models.py -msgid "Only one template per type can be created." -msgstr "" - -#: email_notification/models.py -msgid "Unique name for this content" -msgstr "" - -#: email_notification/models.py -msgid "Content" -msgstr "" - -#: email_notification/models.py -msgid "" -"Email body content. Use curly brackets to indicate data specific fields e.g " -"{{reservee_name}}." -msgstr "" - -#: email_notification/models.py -msgid "HTML content" -msgstr "" - -#: email_notification/models.py -msgid "" -"Email body content as HTML. Use curly brackets to indicate data specific " -"fields e.g {{reservee_name}}." -msgstr "" - -#: email_notification/templates/email_tester.html -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -msgid "Home" -msgstr "" - -#: email_notification/templates/email_tester.html -msgid "Email Template Testing" -msgstr "" - #: opening_hours/admin/origin_hauki_resource.py msgid "" "OriginHaukiResources with this specific hash never have any opening hours." @@ -2762,6 +2715,12 @@ msgstr "" msgid "Log out" msgstr "" +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +#: templates/email/email_tester.html +msgid "Home" +msgstr "" + #: templates/admin/deny_reservation_confirmation.html msgid "Deny reservations" msgstr "" @@ -2800,6 +2759,15 @@ msgstr "" msgid "Reset allocations" msgstr "" +#: templates/email/email_tester.html +msgid "Email Template Testing" +msgstr "" + +#: tilavarauspalvelu/admin/email_template/tester.py +#, python-format +msgid "Test Email '%s' successfully sent." +msgstr "" + #: tilavarauspalvelu/admin/general_role/admin.py msgid "Search by username, email, first name or last name" msgstr "" @@ -3055,6 +3023,38 @@ msgstr "" msgid "Notification manager" msgstr "" +#: tilavarauspalvelu/models/email_template/model.py +msgid "Email type" +msgstr "" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "Only one template per type can be created." +msgstr "" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "Unique name for this content" +msgstr "" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "Content" +msgstr "" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "" +"Email body content. Use curly brackets to indicate data specific fields e.g " +"{{reservee_name}}." +msgstr "" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "HTML content" +msgstr "" + +#: tilavarauspalvelu/models/email_template/model.py +msgid "" +"Email body content as HTML. Use curly brackets to indicate data specific " +"fields e.g {{reservee_name}}." +msgstr "" + #: tilavarauspalvelu/models/general_role/model.py msgid "General role" msgstr "" diff --git a/reservations/admin/reservation/admin.py b/reservations/admin/reservation/admin.py index e0b67e42e..07d835f78 100644 --- a/reservations/admin/reservation/admin.py +++ b/reservations/admin/reservation/admin.py @@ -13,7 +13,6 @@ from rangefilter.filters import DateRangeFilterBuilder from common.date_utils import local_datetime -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.admin.reservation.filters import PaidReservationListFilter, RecurringReservationListFilter from reservations.admin.reservation.form import ReservationAdminForm from reservations.enums import ReservationStateChoice @@ -21,6 +20,7 @@ from reservations.tasks import refund_paid_reservation_task from tilavarauspalvelu.enums import OrderStatus from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender __all__ = [ "ReservationAdmin", diff --git a/email_notification/templates/email_tester.html b/templates/email/email_tester.html similarity index 100% rename from email_notification/templates/email_tester.html rename to templates/email/email_tester.html diff --git a/tests/factories/email_template.py b/tests/factories/email_template.py index 13b9eeaae..139900013 100644 --- a/tests/factories/email_template.py +++ b/tests/factories/email_template.py @@ -1,6 +1,7 @@ from factory import fuzzy -from email_notification.models import EmailTemplate, EmailType +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.models import EmailTemplate from ._base import GenericDjangoModelFactory diff --git a/tests/test_admin/test_email_template_testing_forms.py b/tests/test_admin/test_email_template_testing_forms.py index 6d266460e..19a74a208 100644 --- a/tests/test_admin/test_email_template_testing_forms.py +++ b/tests/test_admin/test_email_template_testing_forms.py @@ -1,11 +1,11 @@ import pytest -from email_notification.admin.email_template_tester import ( +from tests.factories import EmailTemplateFactory, ReservationUnitFactory +from tilavarauspalvelu.admin.email_template.tester import ( EmailTemplateTesterForm, EmailTemplateTesterReservationUnitSelectForm, ) -from email_notification.models import EmailType -from tests.factories import EmailTemplateFactory, ReservationUnitFactory +from tilavarauspalvelu.enums import EmailType pytestmark = [ pytest.mark.django_db, diff --git a/tests/test_admin/test_email_template_testing_initial_data.py b/tests/test_admin/test_email_template_testing_initial_data.py index 0a34f2504..01d8f46f1 100644 --- a/tests/test_admin/test_email_template_testing_initial_data.py +++ b/tests/test_admin/test_email_template_testing_initial_data.py @@ -1,8 +1,8 @@ import pytest from django.test import RequestFactory -from email_notification.admin.email_template_tester import _get_email_template_tester_form_initial_values from tests.factories import LocationFactory, ReservationUnitFactory, UnitFactory, UserFactory +from tilavarauspalvelu.admin.email_template.tester import _get_email_template_tester_form_initial_values pytestmark = [ pytest.mark.django_db, diff --git a/tests/test_email/test_email_builder_render_application.py b/tests/test_email/test_email_builder_render_application.py index 675fc862c..3e8866908 100644 --- a/tests/test_email/test_email_builder_render_application.py +++ b/tests/test_email/test_email_builder_render_application.py @@ -2,9 +2,10 @@ from django.core.files.uploadedfile import SimpleUploadedFile from applications.models import Application -from email_notification.helpers.email_builder_application import ApplicationEmailBuilder -from email_notification.models import EmailTemplate, EmailType from tests.factories import ApplicationFactory, EmailTemplateFactory +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.models import EmailTemplate +from tilavarauspalvelu.utils.email.email_builder_application import ApplicationEmailBuilder pytestmark = [ pytest.mark.django_db, diff --git a/tests/test_email/test_email_builder_render_reservation.py b/tests/test_email/test_email_builder_render_reservation.py index be8525b20..c25ecce0c 100644 --- a/tests/test_email/test_email_builder_render_reservation.py +++ b/tests/test_email/test_email_builder_render_reservation.py @@ -6,11 +6,12 @@ import pytest from django.core.files.uploadedfile import SimpleUploadedFile -from email_notification.exceptions import EmailTemplateValidationError -from email_notification.helpers.email_builder_reservation import ReservationEmailBuilder, ReservationEmailContext -from email_notification.models import EmailTemplate, EmailType from reservations.models import Reservation from tests.factories import EmailTemplateFactory, ReservationFactory, ReservationUnitFactory +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.exceptions import EmailTemplateValidationError +from tilavarauspalvelu.models import EmailTemplate +from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailBuilder, ReservationEmailContext pytestmark = [ pytest.mark.django_db, diff --git a/tests/test_email/test_email_context_application.py b/tests/test_email/test_email_context_application.py index 33c0f3999..f5f66dba8 100644 --- a/tests/test_email/test_email_context_application.py +++ b/tests/test_email/test_email_context_application.py @@ -3,7 +3,7 @@ import pytest from config.utils.commons import LanguageType -from email_notification.helpers.email_builder_application import ApplicationEmailContext +from tilavarauspalvelu.utils.email.email_builder_application import ApplicationEmailContext @pytest.mark.parametrize("language", ["fi", "en", "sv"]) diff --git a/tests/test_email/test_email_context_reservation.py b/tests/test_email/test_email_context_reservation.py index bb67009f2..bfc511e05 100644 --- a/tests/test_email/test_email_context_reservation.py +++ b/tests/test_email/test_email_context_reservation.py @@ -4,7 +4,6 @@ from django.utils.timezone import get_default_timezone from common.utils import get_attr_by_language -from email_notification.helpers.email_builder_reservation import ReservationEmailContext from reservations.enums import CustomerTypeChoice from reservations.models import Reservation from tests.factories import ( @@ -16,6 +15,7 @@ UserFactory, ) from tilavarauspalvelu.models import Location +from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailContext if TYPE_CHECKING: from reservation_units.models import ReservationUnit diff --git a/tests/test_email/test_email_sender_application.py b/tests/test_email/test_email_sender_application.py index a2ee1c172..a1550a815 100644 --- a/tests/test_email/test_email_sender_application.py +++ b/tests/test_email/test_email_sender_application.py @@ -3,11 +3,11 @@ from applications.models import Application from common.date_utils import local_datetime -from email_notification.helpers.email_sender import EmailNotificationSender -from email_notification.models import EmailType -from email_notification.tasks import send_application_handled_email_task, send_application_in_allocation_email_task from tests.factories import ApplicationFactory, EmailTemplateFactory from tests.helpers import patch_method +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.tasks import send_application_handled_email_task, send_application_in_allocation_email_task +from tilavarauspalvelu.utils.email.email_sender import EmailNotificationSender from utils.sentry import SentryLogger # Applied to all tests diff --git a/tests/test_email/test_email_sender_reservation.py b/tests/test_email/test_email_sender_reservation.py index 1a5ed19b5..5d452de6e 100644 --- a/tests/test_email/test_email_sender_reservation.py +++ b/tests/test_email/test_email_sender_reservation.py @@ -5,11 +5,12 @@ import pytest from django.test import override_settings -from email_notification.admin.email_template_tester import EmailTemplateTesterForm -from email_notification.helpers.email_sender import EmailNotificationSender -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender -from email_notification.models import EmailTemplate, EmailType from tests.factories import EmailTemplateFactory, ReservationFactory, UserFactory +from tilavarauspalvelu.admin.email_template.tester import EmailTemplateTesterForm +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.models import EmailTemplate +from tilavarauspalvelu.utils.email.email_sender import EmailNotificationSender +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from users.models import ReservationNotification if TYPE_CHECKING: diff --git a/tests/test_email/test_email_validator.py b/tests/test_email/test_email_validator.py index d060d4d99..e4dd040c2 100644 --- a/tests/test_email/test_email_validator.py +++ b/tests/test_email/test_email_validator.py @@ -5,8 +5,8 @@ from django.conf import settings from django.core.exceptions import ValidationError -from email_notification.helpers.email_builder_reservation import ReservationEmailContext -from email_notification.helpers.email_validator import EmailTemplateValidator +from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailContext +from tilavarauspalvelu.utils.email.email_validator import EmailTemplateValidator def test_email_validator__raises__validation_error_on_invalid_file_extension(): diff --git a/tests/test_external_services/test_verkkokauppa/test_pruning.py b/tests/test_external_services/test_verkkokauppa/test_pruning.py index 2768a5e49..f101658cf 100644 --- a/tests/test_external_services/test_verkkokauppa/test_pruning.py +++ b/tests/test_external_services/test_verkkokauppa/test_pruning.py @@ -4,12 +4,12 @@ from freezegun import freeze_time from common.date_utils import local_datetime -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from reservations.tasks import update_expired_orders_task from tests.factories import PaymentFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus diff --git a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py index 496c1e688..49f46eaac 100644 --- a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py +++ b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py @@ -3,12 +3,12 @@ import pytest from django.urls import reverse -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from tests.factories import PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method from tests.test_external_services.test_verkkokauppa.test_webhooks.helpers import get_mock_order_payment_api from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger diff --git a/tests/test_graphql_api/test_application/test_send.py b/tests/test_graphql_api/test_application/test_send.py index 7533c0223..b16d81953 100644 --- a/tests/test_graphql_api/test_application/test_send.py +++ b/tests/test_graphql_api/test_application/test_send.py @@ -1,9 +1,9 @@ import pytest from applications.enums import ApplicationStatusChoice -from email_notification.helpers.application_email_notification_sender import ApplicationEmailNotificationSender from tests.factories import ApplicationFactory from tests.helpers import patch_method +from tilavarauspalvelu.utils.email.application_email_notification_sender import ApplicationEmailNotificationSender from .helpers import SEND_MUTATION diff --git a/tests/test_graphql_api/test_order/test_refresh.py b/tests/test_graphql_api/test_order/test_refresh.py index 9658c78a9..972fe7cdb 100644 --- a/tests/test_graphql_api/test_order/test_refresh.py +++ b/tests/test_graphql_api/test_order/test_refresh.py @@ -2,11 +2,11 @@ import pytest -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from tests.factories import PaymentFactory from tests.helpers import patch_method from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient diff --git a/tests/test_graphql_api/test_reservation/test_adjust_time.py b/tests/test_graphql_api/test_reservation/test_adjust_time.py index d803a7f06..02b094d1d 100644 --- a/tests/test_graphql_api/test_reservation/test_adjust_time.py +++ b/tests/test_graphql_api/test_reservation/test_adjust_time.py @@ -7,7 +7,6 @@ from django.utils.timezone import get_default_timezone from common.date_utils import local_date, local_datetime -from email_notification.models import EmailType from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnitHierarchy from reservations.enums import ReservationStateChoice @@ -23,6 +22,7 @@ SpaceFactory, UserFactory, ) +from tilavarauspalvelu.enums import EmailType from .helpers import ADJUST_MUTATION, get_adjust_data diff --git a/tests/test_graphql_api/test_reservation/test_cancel.py b/tests/test_graphql_api/test_reservation/test_cancel.py index b20e93f95..e30e271ca 100644 --- a/tests/test_graphql_api/test_reservation/test_cancel.py +++ b/tests/test_graphql_api/test_reservation/test_cancel.py @@ -6,12 +6,11 @@ import pytest from common.date_utils import local_datetime -from email_notification.models import EmailType from reservations.enums import ReservationStateChoice from reservations.models import ReservationCancelReason from tests.factories import EmailTemplateFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method -from tilavarauspalvelu.enums import OrderStatus, PaymentType +from tilavarauspalvelu.enums import EmailType, OrderStatus, PaymentType from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from .helpers import CANCEL_MUTATION, get_cancel_data diff --git a/tests/test_graphql_api/test_reservation/test_confirm.py b/tests/test_graphql_api/test_reservation/test_confirm.py index 3b3ffb364..b8d94bcb6 100644 --- a/tests/test_graphql_api/test_reservation/test_confirm.py +++ b/tests/test_graphql_api/test_reservation/test_confirm.py @@ -3,7 +3,6 @@ import pytest from graphene_django_extensions.testing import build_mutation -from email_notification.models import EmailType from reservation_units.enums import PricingType from reservations.enums import ReservationStateChoice from tests.factories import ( @@ -15,7 +14,7 @@ UserFactory, ) from tests.helpers import patch_method -from tilavarauspalvelu.enums import OrderStatus, PaymentType +from tilavarauspalvelu.enums import EmailType, OrderStatus, PaymentType from tilavarauspalvelu.models import PaymentOrder from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CreateOrderError from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient diff --git a/tests/test_graphql_api/test_reservation/test_delete.py b/tests/test_graphql_api/test_reservation/test_delete.py index 94e413ad8..416f894f4 100644 --- a/tests/test_graphql_api/test_reservation/test_delete.py +++ b/tests/test_graphql_api/test_reservation/test_delete.py @@ -2,12 +2,12 @@ import pytest -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from reservations.models import Reservation from tests.factories import OrderFactory, PaymentFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient diff --git a/tests/test_graphql_api/test_reservation/test_deny.py b/tests/test_graphql_api/test_reservation/test_deny.py index 42b7d3852..a08652edb 100644 --- a/tests/test_graphql_api/test_reservation/test_deny.py +++ b/tests/test_graphql_api/test_reservation/test_deny.py @@ -3,9 +3,9 @@ import pytest from common.date_utils import local_datetime -from email_notification.models import EmailType from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import EmailTemplateFactory, ReservationFactory +from tilavarauspalvelu.enums import EmailType from .helpers import DENY_MUTATION, get_deny_data diff --git a/tests/test_graphql_api/test_reservation/test_require_handling.py b/tests/test_graphql_api/test_reservation/test_require_handling.py index 0c6ac3dfb..f053a4d1b 100644 --- a/tests/test_graphql_api/test_reservation/test_require_handling.py +++ b/tests/test_graphql_api/test_reservation/test_require_handling.py @@ -1,8 +1,8 @@ import pytest -from email_notification.models import EmailType from reservations.enums import ReservationStateChoice from tests.factories import EmailTemplateFactory, ReservationFactory, UserFactory +from tilavarauspalvelu.enums import EmailType from users.models import ReservationNotification from .helpers import REQUIRE_HANDLING_MUTATION, get_require_handling_data diff --git a/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py b/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py index 905cb231f..30136f445 100644 --- a/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py +++ b/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py @@ -4,11 +4,11 @@ import pytest from common.date_utils import DEFAULT_TIMEZONE, local_datetime, next_hour -from email_notification.models import EmailType from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnitHierarchy from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import EmailTemplateFactory, ReservationFactory +from tilavarauspalvelu.enums import EmailType from .helpers import ADJUST_STAFF_MUTATION, get_staff_adjust_data diff --git a/tests/test_utils/test_create_test_data.py b/tests/test_utils/test_create_test_data.py index e3aeaf3f9..82b32c07e 100644 --- a/tests/test_utils/test_create_test_data.py +++ b/tests/test_utils/test_create_test_data.py @@ -4,7 +4,6 @@ from common.management.commands.create_test_data import create_test_data from common.models import RequestLog, SQLLog -from email_notification.models import EmailTemplate from reservation_units.models import ( Introduction, Keyword, @@ -24,7 +23,7 @@ ReservationStatistic, ReservationStatisticsReservationUnit, ) -from tilavarauspalvelu.models import Building, PaymentOrder, RealEstate +from tilavarauspalvelu.models import Building, EmailTemplate, PaymentOrder, RealEstate from users.models import PersonalInfoViewLog apps_to_check: list[str] = [ diff --git a/tests/test_utils/test_notification_recipients.py b/tests/test_utils/test_notification_recipients.py index 44c11ba84..adc875ffb 100644 --- a/tests/test_utils/test_notification_recipients.py +++ b/tests/test_utils/test_notification_recipients.py @@ -1,7 +1,7 @@ import pytest -from email_notification.tasks import _get_reservation_staff_notification_recipients from tests.factories import ReservationFactory, ReservationUnitFactory, UserFactory +from tilavarauspalvelu.tasks import _get_reservation_staff_notification_recipients from users.models import ReservationNotification pytestmark = [ diff --git a/tilavarauspalvelu/admin/__init__.py b/tilavarauspalvelu/admin/__init__.py index bddd40c68..85fd94a36 100644 --- a/tilavarauspalvelu/admin/__init__.py +++ b/tilavarauspalvelu/admin/__init__.py @@ -1,3 +1,4 @@ +from .email_template.admin import EmailTemplateAdmin from .general_role.admin import GeneralRoleAdmin from .payment_accounting.admin import PaymentAccountingAdmin from .payment_merchant.admin import PaymentMerchantAdmin @@ -12,6 +13,7 @@ from .unit_role.admin import UnitRoleAdmin __all__ = [ + "EmailTemplateAdmin", "GeneralRoleAdmin", "PaymentAccountingAdmin", "PaymentMerchantAdmin", diff --git a/tilavarauspalvelu/admin/email_template/admin.py b/tilavarauspalvelu/admin/email_template/admin.py index e69de29bb..b1de62003 100644 --- a/tilavarauspalvelu/admin/email_template/admin.py +++ b/tilavarauspalvelu/admin/email_template/admin.py @@ -0,0 +1,109 @@ +from admin_extra_buttons.decorators import button +from admin_extra_buttons.mixins import ExtraButtonsMixin +from django.contrib import admin +from django.core.exceptions import ValidationError +from django.core.files.uploadedfile import InMemoryUploadedFile +from django.core.handlers.wsgi import WSGIRequest +from django.db.models.fields.files import FieldFile +from django.forms import ModelForm +from django.http import HttpResponseRedirect +from django.template.response import TemplateResponse +from modeltranslation.admin import TranslationAdmin + +from config.utils.commons import LanguageType +from tilavarauspalvelu.admin.email_template.tester import email_template_tester_admin_view +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.exceptions import EmailTemplateValidationError +from tilavarauspalvelu.models import EmailTemplate +from tilavarauspalvelu.utils.email.email_builder_application import ApplicationEmailBuilder, ApplicationEmailContext +from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailBuilder, ReservationEmailContext +from tilavarauspalvelu.utils.email.email_validator import EmailTemplateValidator + + +class EmailTemplateAdminForm(ModelForm): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + # Set up the available 'type' choices + # Remove existing types from the choices, so that the same type cannot be added twice + existing_types = EmailTemplate.objects.values_list("type", flat=True) + if self.instance and self.instance.type: + existing_types = existing_types.exclude(type=self.instance.type) + available_types = [(value, label) for value, label in EmailType.choices if value not in existing_types] + self.fields["type"].choices = available_types + + def _get_validator(self) -> EmailTemplateValidator: + email_template_type = self.cleaned_data["type"] + + # Reservation + if email_template_type in ReservationEmailBuilder.email_template_types: + mock_context = ReservationEmailContext.from_mock_data() + # Application + elif email_template_type in ApplicationEmailBuilder.email_template_types: + mock_context = ApplicationEmailContext.from_mock_data() + else: + raise EmailTemplateValidationError(f"Email template type '{email_template_type}' is not supported.") + + return EmailTemplateValidator(context=mock_context) + + def _get_validated_field(self, field) -> str | None: + data: str | None = self.cleaned_data[field] + if not data: + return data + + try: + validator = self._get_validator() + validator.validate_string(data) + except EmailTemplateValidationError as e: + raise ValidationError(e.message) from e + return data + + def _get_validated_html_file(self, language: LanguageType) -> InMemoryUploadedFile | FieldFile | None: + file: InMemoryUploadedFile | FieldFile | None = self.cleaned_data[f"html_content_{language}"] + + if file and isinstance(file, InMemoryUploadedFile): + validator = self._get_validator() + validator.validate_html_file(file) + + return file + + def clean_subject_fi(self) -> str | None: + return self._get_validated_field("subject_fi") + + def clean_subject_en(self) -> str | None: + return self._get_validated_field("subject_en") + + def clean_subject_sv(self) -> str | None: + return self._get_validated_field("subject_sv") + + def clean_content_fi(self) -> str | None: + return self._get_validated_field("content_fi") + + def clean_content_en(self) -> str | None: + return self._get_validated_field("content_en") + + def clean_content_sv(self) -> str | None: + return self._get_validated_field("content_sv") + + def clean_html_content_fi(self) -> InMemoryUploadedFile | FieldFile | None: + return self._get_validated_html_file("fi") + + def clean_html_content_en(self) -> InMemoryUploadedFile | FieldFile | None: + return self._get_validated_html_file("en") + + def clean_html_content_sv(self) -> InMemoryUploadedFile | FieldFile | None: + return self._get_validated_html_file("sv") + + +@admin.register(EmailTemplate) +class EmailTemplateAdmin(ExtraButtonsMixin, TranslationAdmin): + form = EmailTemplateAdminForm + + list_display = [ + "type", + "name", + ] + + @button(label="Email Template Testing", change_form=True) + def template_tester(self, request: WSGIRequest, pk: int) -> TemplateResponse | HttpResponseRedirect: + return email_template_tester_admin_view(self, request, pk) diff --git a/email_notification/admin/email_template_tester.py b/tilavarauspalvelu/admin/email_template/tester.py similarity index 94% rename from email_notification/admin/email_template_tester.py rename to tilavarauspalvelu/admin/email_template/tester.py index 1c9e9ed60..0030be3fa 100644 --- a/email_notification/admin/email_template_tester.py +++ b/tilavarauspalvelu/admin/email_template/tester.py @@ -8,15 +8,15 @@ from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ -from email_notification.helpers.email_builder_reservation import ReservationEmailBuilder -from email_notification.helpers.email_sender import EmailNotificationSender -from email_notification.models import EmailTemplate from reservation_units.models import ReservationUnit +from tilavarauspalvelu.models import EmailTemplate +from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailBuilder +from tilavarauspalvelu.utils.email.email_sender import EmailNotificationSender if TYPE_CHECKING: from django.http import HttpResponseRedirect - from email_notification.admin import EmailTemplateAdmin + from tilavarauspalvelu.admin.email_template.admin import EmailTemplateAdmin from tilavarauspalvelu.models import Location @@ -136,4 +136,4 @@ def email_template_tester_admin_view( context["form"] = form context["reservation_unit_form"] = EmailTemplateTesterReservationUnitSelectForm() - return TemplateResponse(request, "email_tester.html", context=context) + return TemplateResponse(request, "email/email_tester.html", context=context) diff --git a/tilavarauspalvelu/api/graphql/types/application/serializers.py b/tilavarauspalvelu/api/graphql/types/application/serializers.py index 76ddef24b..45e13c070 100644 --- a/tilavarauspalvelu/api/graphql/types/application/serializers.py +++ b/tilavarauspalvelu/api/graphql/types/application/serializers.py @@ -12,7 +12,6 @@ from applications.enums import ApplicationStatusChoice from applications.models import AllocatedTimeSlot, Application, ReservationUnitOption from common.fields.serializer import CurrentUserDefaultNullable -from email_notification.helpers.application_email_notification_sender import ApplicationEmailNotificationSender from tilavarauspalvelu.api.graphql.extensions import error_codes from tilavarauspalvelu.api.graphql.types.address.serializers import AddressSerializer from tilavarauspalvelu.api.graphql.types.application_section.serializers import ( @@ -20,6 +19,7 @@ ) from tilavarauspalvelu.api.graphql.types.organisation.serializers import OrganisationSerializer from tilavarauspalvelu.api.graphql.types.person.serializers import PersonSerializer +from tilavarauspalvelu.utils.email.application_email_notification_sender import ApplicationEmailNotificationSender if TYPE_CHECKING: from common.typing import AnyUser diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py index daa9d1c02..1b255b120 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py @@ -4,7 +4,6 @@ from django.utils.timezone import get_default_timezone from graphene_django_extensions.fields import EnumFriendlyChoiceField -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservation_units.models import ReservationUnit from reservations.enums import ReservationStateChoice, ReservationTypeChoice from reservations.models import Reservation @@ -14,6 +13,7 @@ ReservationPriceMixin, ReservationSchedulingMixin, ) +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py index f2d2e1305..24e26a27d 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py @@ -5,10 +5,10 @@ from rest_framework.exceptions import ValidationError from common.date_utils import local_datetime -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender __all__ = [ "ReservationApproveSerializer", diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py index 51c99fef5..ef8a582d7 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py @@ -5,12 +5,12 @@ from rest_framework.exceptions import ValidationError from common.date_utils import local_datetime -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from reservations.models import Reservation from reservations.tasks import refund_paid_reservation_task from tilavarauspalvelu.api.graphql.extensions import error_codes from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender if TYPE_CHECKING: from tilavarauspalvelu.models import PaymentOrder diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py index 973e341db..ae5263953 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py @@ -3,7 +3,6 @@ from django.conf import settings from graphene_django_extensions.fields import EnumFriendlyChoiceField -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservation_units.enums import PaymentType, PricingType from reservation_units.utils.reservation_unit_pricing_helper import ReservationUnitPricingHelper from reservations.enums import ReservationStateChoice @@ -12,6 +11,7 @@ from tilavarauspalvelu.api.graphql.types.reservation.serializers.update_serializers import ReservationUpdateSerializer from tilavarauspalvelu.enums import Language, OrderStatus from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.helpers import create_mock_verkkokauppa_order, get_verkkokauppa_order_params from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CreateOrderError from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py index 7d109fecb..d40700cc0 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py @@ -6,10 +6,10 @@ from common.date_utils import local_datetime from common.utils import comma_sep_str -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender __all__ = [ "ReservationDenySerializer", diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py index 2c00f47e1..12d70a48b 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py @@ -5,10 +5,10 @@ from rest_framework.exceptions import ValidationError from common.utils import comma_sep_str -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender __all__ = [ "ReservationRequiresHandlingSerializer", diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py index 0a6a8c3b4..3f4dd9ff3 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py @@ -5,12 +5,12 @@ from graphene_django_extensions.fields import EnumFriendlyChoiceField from common.date_utils import local_datetime -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice, ReservationTypeChoice from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeyUpdateSerializer from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode from tilavarauspalvelu.api.graphql.types.reservation.serializers.mixins import ReservationSchedulingMixin +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender if TYPE_CHECKING: from reservation_units.models import ReservationUnit diff --git a/tilavarauspalvelu/enums.py b/tilavarauspalvelu/enums.py index c62d283a5..5d5cbb5c9 100644 --- a/tilavarauspalvelu/enums.py +++ b/tilavarauspalvelu/enums.py @@ -203,3 +203,20 @@ def permission_choices(cls) -> list[tuple[str, tuple[str, str]]]: # but we also don't duplicate permissions from the roles above. This should be fine, # as the enum is not really used in our code but meant for the frontend. UserPermissionChoice = models.TextChoices("UserPermissionChoice", UserRoleChoice.permission_choices()) + + +class EmailType(models.TextChoices): + ACCESS_CODE_FOR_RESERVATION = "access_code_for_reservation" + APPLICATION_HANDLED = "application_handled" + APPLICATION_IN_ALLOCATION = "application_in_allocation" + APPLICATION_RECEIVED = "application_received" + HANDLING_REQUIRED_RESERVATION = "handling_required_reservation" + RESERVATION_CANCELLED = "reservation_cancelled" + RESERVATION_CONFIRMED = "reservation_confirmed" + RESERVATION_HANDLED_AND_CONFIRMED = "reservation_handled_and_confirmed" + RESERVATION_MODIFIED = "reservation_modified" + RESERVATION_NEEDS_TO_BE_PAID = "reservation_needs_to_be_paid" + RESERVATION_REJECTED = "reservation_rejected" + RESERVATION_WITH_PIN_CONFIRMED = "reservation_with_pin_confirmed" + STAFF_NOTIFICATION_RESERVATION_MADE = "staff_notification_reservation_made" + STAFF_NOTIFICATION_RESERVATION_REQUIRES_HANDLING = "staff_notification_reservation_requires_handling" diff --git a/email_notification/exceptions.py b/tilavarauspalvelu/exceptions.py similarity index 100% rename from email_notification/exceptions.py rename to tilavarauspalvelu/exceptions.py diff --git a/tilavarauspalvelu/migrations/0007_emailtemplate.py b/tilavarauspalvelu/migrations/0007_emailtemplate.py new file mode 100644 index 000000000..f19316a6a --- /dev/null +++ b/tilavarauspalvelu/migrations/0007_emailtemplate.py @@ -0,0 +1,171 @@ +# Generated by Django 5.1.1 on 2024-09-19 07:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tilavarauspalvelu", "0006_generalrole_unitrole"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name="EmailTemplate", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.CharField( + choices=[ + ("access_code_for_reservation", "Access Code For Reservation"), + ("application_handled", "Application Handled"), + ("application_in_allocation", "Application In Allocation"), + ("application_received", "Application Received"), + ("handling_required_reservation", "Handling Required Reservation"), + ("reservation_cancelled", "Reservation Cancelled"), + ("reservation_confirmed", "Reservation Confirmed"), + ("reservation_handled_and_confirmed", "Reservation Handled And Confirmed"), + ("reservation_modified", "Reservation Modified"), + ("reservation_needs_to_be_paid", "Reservation Needs To Be Paid"), + ("reservation_rejected", "Reservation Rejected"), + ("reservation_with_pin_confirmed", "Reservation With Pin Confirmed"), + ("staff_notification_reservation_made", "Staff Notification Reservation Made"), + ( + "staff_notification_reservation_requires_handling", + "Staff Notification Reservation Requires Handling", + ), + ], + help_text="Only one template per type can be created.", + max_length=254, + unique=True, + verbose_name="Email type", + ), + ), + ( + "name", + models.CharField( + max_length=255, + unique=True, + verbose_name="Unique name for this content", + ), + ), + ("subject", models.CharField(max_length=255)), + ("subject_fi", models.CharField(max_length=255, null=True)), + ("subject_en", models.CharField(max_length=255, null=True)), + ("subject_sv", models.CharField(max_length=255, null=True)), + ( + "content", + models.TextField( + help_text=( + "Email body content. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + verbose_name="Content", + ), + ), + ( + "content_fi", + models.TextField( + help_text=( + "Email body content. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + null=True, + verbose_name="Content", + ), + ), + ( + "content_en", + models.TextField( + help_text=( + "Email body content. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + null=True, + verbose_name="Content", + ), + ), + ( + "content_sv", + models.TextField( + help_text=( + "Email body content. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + null=True, + verbose_name="Content", + ), + ), + ( + "html_content", + models.FileField( + blank=True, + help_text=( + "Email body content as HTML. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + null=True, + upload_to="email_html_templates", + verbose_name="HTML content", + ), + ), + ( + "html_content_fi", + models.FileField( + blank=True, + help_text=( + "Email body content as HTML. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + null=True, + upload_to="email_html_templates", + verbose_name="HTML content", + ), + ), + ( + "html_content_en", + models.FileField( + blank=True, + help_text=( + "Email body content as HTML. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + null=True, + upload_to="email_html_templates", + verbose_name="HTML content", + ), + ), + ( + "html_content_sv", + models.FileField( + blank=True, + help_text=( + "Email body content as HTML. Use curly brackets to indicate data " + "specific fields e.g {{reservee_name}}." + ), + null=True, + upload_to="email_html_templates", + verbose_name="HTML content", + ), + ), + ], + options={ + "db_table": "email_template", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + ], + database_operations=[], + ), + ] diff --git a/tilavarauspalvelu/models/__init__.py b/tilavarauspalvelu/models/__init__.py index 04508f1ed..13278b6da 100644 --- a/tilavarauspalvelu/models/__init__.py +++ b/tilavarauspalvelu/models/__init__.py @@ -1,4 +1,5 @@ from .building.model import Building +from .email_template.model import EmailTemplate from .general_role.model import GeneralRole from .location.model import Location from .payment_accounting.model import PaymentAccounting @@ -17,6 +18,7 @@ __all__ = [ "Building", + "EmailTemplate", "GeneralRole", "Location", "PaymentAccounting", diff --git a/tilavarauspalvelu/models/email_template/actions.py b/tilavarauspalvelu/models/email_template/actions.py index e69de29bb..58c5972fa 100644 --- a/tilavarauspalvelu/models/email_template/actions.py +++ b/tilavarauspalvelu/models/email_template/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import EmailTemplate + + +class EmailTemplateActions: + def __init__(self, email_template: "EmailTemplate") -> None: + self.email_template = email_template diff --git a/tilavarauspalvelu/models/email_template/model.py b/tilavarauspalvelu/models/email_template/model.py index e69de29bb..a75329334 100644 --- a/tilavarauspalvelu/models/email_template/model.py +++ b/tilavarauspalvelu/models/email_template/model.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from tilavarauspalvelu.enums import EmailType + +from .queryset import EmailTemplateQuerySet + +if TYPE_CHECKING: + from .actions import EmailTemplateActions + + +class EmailTemplate(models.Model): + type: EmailType = models.CharField( + max_length=254, + choices=EmailType.choices, + unique=True, + blank=False, + null=False, + verbose_name=_("Email type"), + help_text=_("Only one template per type can be created."), + ) + name: str = models.CharField( + max_length=255, + unique=True, + verbose_name=_("Unique name for this content"), + null=False, + blank=False, + ) + + subject: str = models.CharField(max_length=255, null=False, blank=False) + content: str = models.TextField( + verbose_name=_("Content"), + help_text=_("Email body content. Use curly brackets to indicate data specific fields e.g {{reservee_name}}."), + null=False, + blank=False, + ) + + html_content: str | None = models.FileField( + verbose_name=_("HTML content"), + help_text=_( + "Email body content as HTML. Use curly brackets to indicate data specific fields e.g {{reservee_name}}." + ), + null=True, + blank=True, + upload_to=settings.EMAIL_HTML_TEMPLATES_ROOT, + ) + + objects = EmailTemplateQuerySet.as_manager() + + # Translated field hints + subject_fi: str | None + subject_en: str | None + subject_sv: str | None + content_fi: str | None + content_en: str | None + content_sv: str | None + html_content_fi: str | None + html_content_en: str | None + html_content_sv: str | None + + class Meta: + db_table = "email_template" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + choices = dict(EmailType.choices) + label = choices.get(self.type) or self.type + return f"{label}: {self.name}" + + @cached_property + def actions(self) -> EmailTemplateActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import EmailTemplateActions + + return EmailTemplateActions(self) diff --git a/tilavarauspalvelu/models/email_template/queryset.py b/tilavarauspalvelu/models/email_template/queryset.py index e69de29bb..62770b3d0 100644 --- a/tilavarauspalvelu/models/email_template/queryset.py +++ b/tilavarauspalvelu/models/email_template/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "EmailTemplateQuerySet", +] + + +class EmailTemplateQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/payment_order/model.py b/tilavarauspalvelu/models/payment_order/model.py index 73d3d9905..2fbcc2540 100644 --- a/tilavarauspalvelu/models/payment_order/model.py +++ b/tilavarauspalvelu/models/payment_order/model.py @@ -11,9 +11,9 @@ from django.utils.translation import gettext_lazy as _ from common.date_utils import local_datetime -from email_notification.helpers.reservation_email_notification_sender import ReservationEmailNotificationSender from reservations.enums import ReservationStateChoice from tilavarauspalvelu.enums import Language, OrderStatus, PaymentType +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment diff --git a/tilavarauspalvelu/tasks.py b/tilavarauspalvelu/tasks.py index 64ef195e6..339f77991 100644 --- a/tilavarauspalvelu/tasks.py +++ b/tilavarauspalvelu/tasks.py @@ -1,13 +1,23 @@ +from django.conf import settings from django.db.transaction import atomic +from lookup_property import L +from applications.enums import ApplicationRoundStatusChoice, ApplicationSectionStatusChoice +from applications.models import Application +from common.date_utils import local_datetime from config.celery import app -from tilavarauspalvelu.models import Space, Unit -from tilavarauspalvelu.utils.importers.tprek_unit_importer import TprekUnitImporter +from reservations.models import Reservation +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.exceptions import SendEmailNotificationError +from tilavarauspalvelu.utils.email.email_sender import EmailNotificationSender +from users.models import ReservationNotification, User +from utils.sentry import SentryLogger @app.task(name="rebuild_space_tree_hierarchy") def rebuild_space_tree_hierarchy() -> None: from reservation_units.models import ReservationUnitHierarchy + from tilavarauspalvelu.models import Space with atomic(): Space.objects.rebuild() @@ -16,6 +26,142 @@ def rebuild_space_tree_hierarchy() -> None: @app.task(name="update_units_from_tprek") def update_units_from_tprek() -> None: + from tilavarauspalvelu.models import Unit + from tilavarauspalvelu.utils.importers.tprek_unit_importer import TprekUnitImporter + units_to_update = Unit.objects.exclude(tprek_id__isnull=True) tprek_unit_importer = TprekUnitImporter() tprek_unit_importer.update_unit_from_tprek(units_to_update) + + +@app.task(name="send_reservation_email") +def send_reservation_email_task(reservation_id: int, email_type: EmailType) -> None: + if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: + return + reservation = Reservation.objects.filter(id=reservation_id).first() + if not reservation: + return + + email_notification_sender = EmailNotificationSender(email_type=email_type, recipients=None) + email_notification_sender.send_reservation_email(reservation=reservation) + + +@app.task(name="send_staff_reservation_email") +def send_staff_reservation_email_task( + reservation_id: int, + email_type: EmailType, + notification_settings: list[ReservationNotification], +) -> None: + if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: + return + reservation = Reservation.objects.filter(id=reservation_id).first() + if not reservation: + return + + recipients = _get_reservation_staff_notification_recipients(reservation, notification_settings) + if not recipients: + return + + email_notification_sender = EmailNotificationSender(email_type=email_type, recipients=recipients) + email_notification_sender.send_reservation_email(reservation=reservation, forced_language=settings.LANGUAGE_CODE) + + +def _get_reservation_staff_notification_recipients( + reservation: Reservation, + notification_settings: list[ReservationNotification], +) -> list[str]: + """ + Get staff users who should receive reservation notifications based on their unit roles and notification settings. + + Get users with unit roles and notifications enabled, collect the ones that can manage relevant units, + have matching notification setting are not the reservation creator + """ + from tilavarauspalvelu.models import Unit + + notification_recipients: list[str] = [] + reservation_units = reservation.reservation_unit.all() + units = Unit.objects.filter(reservationunit__in=reservation_units).prefetch_related("unit_groups").distinct() + users = User.objects.filter(unit_roles__isnull=False).exclude(reservation_notification="NONE") + for user in users: + # Skip users who don't have the correct unit role + if not user.permissions.can_manage_reservations_for_units(units, any_unit=True): + continue + + # Skip users who don't have the correct notification setting + if not (any(user.reservation_notification.upper() == setting.upper() for setting in notification_settings)): + continue + + # Skip the reservation creator + if reservation.user and reservation.user.pk == user.pk: + continue + + notification_recipients.append(user.email) + + # Remove possible duplicates + return list(set(notification_recipients)) + + +@app.task(name="send_application_email") +def send_application_email_task(application_id: int, email_type: EmailType) -> None: + if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: + return + application = Application.objects.filter(id=application_id).first() + if not application: + return + + email_notification_sender = EmailNotificationSender(email_type=email_type, recipients=None) + email_notification_sender.send_application_email(application=application) + + +@app.task(name="send_application_in_allocation_emails") +def send_application_in_allocation_email_task() -> None: + if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: + return + + # Don't try to send anything if the email template is not defined (EmailNotificationSender will raise an error) + try: + email_sender = EmailNotificationSender(email_type=EmailType.APPLICATION_IN_ALLOCATION, recipients=None) + except SendEmailNotificationError: + msg = "Tried to send an email, but Email Template for APPLICATION_IN_ALLOCATION was not found." + SentryLogger.log_message(msg, level="warning") + return + + # Get all applications that need a notification to be sent + applications = Application.objects.filter( + L(application_round__status=ApplicationRoundStatusChoice.IN_ALLOCATION.value), + L(status=ApplicationSectionStatusChoice.IN_ALLOCATION.value), + in_allocation_notification_sent_date__isnull=True, + application_sections__isnull=False, + ).order_by("created_date") + if not applications: + return + + email_sender.send_batch_application_emails(applications=applications) + applications.update(in_allocation_notification_sent_date=local_datetime()) + + +@app.task(name="send_application_handled_emails") +def send_application_handled_email_task() -> None: + if not settings.SEND_RESERVATION_NOTIFICATION_EMAILS: + return + + # Don't try to send anything if the email template is not defined (EmailNotificationSender will raise an error) + try: + email_sender = EmailNotificationSender(email_type=EmailType.APPLICATION_HANDLED, recipients=None) + except SendEmailNotificationError: + msg = "Tried to send an email, but Email Template for APPLICATION_HANDLED was not found." + SentryLogger.log_message(msg, level="warning") + return + + # Get all applications that need a notification to be sent + applications = Application.objects.filter( + L(application_round__status=ApplicationRoundStatusChoice.HANDLED.value), + L(status=ApplicationSectionStatusChoice.HANDLED.value), + results_ready_notification_sent_date__isnull=True, + application_sections__isnull=False, + ).order_by("created_date") + if not applications: + return + + email_sender.send_batch_application_emails(applications=applications) + applications.update(results_ready_notification_sent_date=local_datetime()) diff --git a/email_notification/templatetags.py b/tilavarauspalvelu/templatetags.py similarity index 91% rename from email_notification/templatetags.py rename to tilavarauspalvelu/templatetags.py index 9c3b15182..2d9f41e6c 100644 --- a/email_notification/templatetags.py +++ b/tilavarauspalvelu/templatetags.py @@ -8,12 +8,10 @@ register = template.Library() +@register.filter(name="currency") def format_currency(price: Decimal | float) -> str: if not isinstance(price, Decimal) and not isinstance(price, int) and not isinstance(price, float): raise jinja2.TemplateError(f"Error trying to format value as currency. '{price}' is not a number.") price = round_decimal(Decimal(price), 2) return f"{price:,.2f}".replace(",", " ").replace(".", ",") - - -register.filter("currency", format_currency) diff --git a/tilavarauspalvelu/translation.py b/tilavarauspalvelu/translation.py index f4b1c96bf..7aafaf3c5 100644 --- a/tilavarauspalvelu/translation.py +++ b/tilavarauspalvelu/translation.py @@ -3,6 +3,7 @@ from .models import Service, TermsOfUse from .models.building.model import Building +from .models.email_template.model import EmailTemplate from .models.location.model import Location from .models.real_estate.model import RealEstate from .models.resource.model import Resource @@ -60,3 +61,8 @@ class ServiceSectorTranslationOptions(TranslationOptions): @register(Location) class LocationTranslationOptions(TranslationOptions): fields = ["address_street", "address_city"] + + +@register(EmailTemplate) +class EmailTemplateTranslationOptions(TranslationOptions): + fields = ["subject", "content", "html_content"] diff --git a/email_notification/helpers/__init__.py b/tilavarauspalvelu/utils/email/__init__.py similarity index 100% rename from email_notification/helpers/__init__.py rename to tilavarauspalvelu/utils/email/__init__.py diff --git a/email_notification/helpers/application_email_notification_sender.py b/tilavarauspalvelu/utils/email/application_email_notification_sender.py similarity index 77% rename from email_notification/helpers/application_email_notification_sender.py rename to tilavarauspalvelu/utils/email/application_email_notification_sender.py index f4d60f481..d84ca1361 100644 --- a/email_notification/helpers/application_email_notification_sender.py +++ b/tilavarauspalvelu/utils/email/application_email_notification_sender.py @@ -1,6 +1,6 @@ from applications.models import Application -from email_notification.models import EmailType -from email_notification.tasks import send_application_email_task +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.tasks import send_application_email_task class ApplicationEmailNotificationSender: diff --git a/email_notification/helpers/email_builder_application.py b/tilavarauspalvelu/utils/email/email_builder_application.py similarity index 93% rename from email_notification/helpers/email_builder_application.py rename to tilavarauspalvelu/utils/email/email_builder_application.py index 83e042035..ee7a174a7 100644 --- a/email_notification/helpers/email_builder_application.py +++ b/tilavarauspalvelu/utils/email/email_builder_application.py @@ -7,12 +7,13 @@ from django.conf import settings from common.utils import safe_getattr -from email_notification.helpers.email_builder_base import BaseEmailBuilder, BaseEmailContext -from email_notification.models import EmailTemplate, EmailType +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.utils.email.email_builder_base import BaseEmailBuilder, BaseEmailContext if TYPE_CHECKING: from applications.models import Application from config.utils.commons import LanguageType + from tilavarauspalvelu.models import EmailTemplate @dataclass diff --git a/email_notification/helpers/email_builder_base.py b/tilavarauspalvelu/utils/email/email_builder_base.py similarity index 94% rename from email_notification/helpers/email_builder_base.py rename to tilavarauspalvelu/utils/email/email_builder_base.py index 09d96af0c..c23eb9566 100644 --- a/email_notification/helpers/email_builder_base.py +++ b/tilavarauspalvelu/utils/email/email_builder_base.py @@ -8,14 +8,15 @@ from django.conf import settings from django.utils.timezone import get_default_timezone -from email_notification.exceptions import EmailBuilderConfigurationError -from email_notification.helpers.email_validator import EmailTemplateValidator +from tilavarauspalvelu.exceptions import EmailBuilderConfigurationError +from tilavarauspalvelu.utils.email.email_validator import EmailTemplateValidator if TYPE_CHECKING: from django.db.models.fields.files import FieldFile from config.utils.commons import LanguageType - from email_notification.models import EmailTemplate, EmailType + from tilavarauspalvelu.enums import EmailType + from tilavarauspalvelu.models import EmailTemplate @dataclass diff --git a/email_notification/helpers/email_builder_reservation.py b/tilavarauspalvelu/utils/email/email_builder_reservation.py similarity index 97% rename from email_notification/helpers/email_builder_reservation.py rename to tilavarauspalvelu/utils/email/email_builder_reservation.py index d63b8a8da..706bc6227 100644 --- a/email_notification/helpers/email_builder_reservation.py +++ b/tilavarauspalvelu/utils/email/email_builder_reservation.py @@ -10,15 +10,15 @@ from django.utils.timezone import get_default_timezone from common.utils import get_attr_by_language -from email_notification.helpers.email_builder_base import BaseEmailBuilder, BaseEmailContext -from email_notification.models import EmailTemplate, EmailType from reservations.enums import CustomerTypeChoice +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.utils.email.email_builder_base import BaseEmailBuilder, BaseEmailContext if TYPE_CHECKING: from config.utils.commons import LanguageType - from email_notification.admin.email_template_tester import EmailTemplateTesterForm from reservations.models import Reservation - from tilavarauspalvelu.models import Location + from tilavarauspalvelu.admin.email_template.tester import EmailTemplateTesterForm + from tilavarauspalvelu.models import EmailTemplate, Location type InstructionNameType = Literal["confirmed", "pending", "cancelled"] diff --git a/email_notification/helpers/email_sender.py b/tilavarauspalvelu/utils/email/email_sender.py similarity index 92% rename from email_notification/helpers/email_sender.py rename to tilavarauspalvelu/utils/email/email_sender.py index 3901c5953..e3e90ae3f 100644 --- a/email_notification/helpers/email_sender.py +++ b/tilavarauspalvelu/utils/email/email_sender.py @@ -8,19 +8,20 @@ from django.core.mail import EmailMultiAlternatives from common.utils import safe_getattr -from email_notification.exceptions import SendEmailNotificationError -from email_notification.helpers.email_builder_application import ApplicationEmailBuilder -from email_notification.helpers.email_builder_reservation import ReservationEmailBuilder -from email_notification.models import EmailTemplate, EmailType +from tilavarauspalvelu.exceptions import SendEmailNotificationError +from tilavarauspalvelu.models import EmailTemplate +from tilavarauspalvelu.utils.email.email_builder_application import ApplicationEmailBuilder +from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailBuilder if TYPE_CHECKING: from collections.abc import Iterable from applications.models import Application from config.utils.commons import LanguageType - from email_notification.admin.email_template_tester import EmailTemplateTesterForm - from email_notification.helpers.email_builder_base import BaseEmailBuilder from reservations.models import Reservation + from tilavarauspalvelu.admin.email_template.tester import EmailTemplateTesterForm + from tilavarauspalvelu.enums import EmailType + from tilavarauspalvelu.utils.email.email_builder_base import BaseEmailBuilder class EmailNotificationSender: diff --git a/email_notification/helpers/email_validator.py b/tilavarauspalvelu/utils/email/email_validator.py similarity index 94% rename from email_notification/helpers/email_validator.py rename to tilavarauspalvelu/utils/email/email_validator.py index 01271274b..d114ccf6d 100644 --- a/email_notification/helpers/email_validator.py +++ b/tilavarauspalvelu/utils/email/email_validator.py @@ -10,13 +10,13 @@ from jinja2.exceptions import TemplateError from jinja2.sandbox import SandboxedEnvironment -from email_notification.exceptions import EmailTemplateValidationError -from email_notification.templatetags import format_currency +from tilavarauspalvelu.exceptions import EmailTemplateValidationError +from tilavarauspalvelu.templatetags import format_currency if TYPE_CHECKING: from django.core.files.uploadedfile import InMemoryUploadedFile - from email_notification.helpers.email_builder_base import BaseEmailContext + from tilavarauspalvelu.utils.email.email_builder_base import BaseEmailContext EMAIL_TEMPLATE_SUPPORTED_EXPRESSIONS = ["if", "elif", "else", "endif"] FILTERS_MAP = {"currency": format_currency} diff --git a/email_notification/helpers/reservation_email_notification_sender.py b/tilavarauspalvelu/utils/email/reservation_email_notification_sender.py similarity index 97% rename from email_notification/helpers/reservation_email_notification_sender.py rename to tilavarauspalvelu/utils/email/reservation_email_notification_sender.py index 4926856f4..3947fe0a8 100644 --- a/email_notification/helpers/reservation_email_notification_sender.py +++ b/tilavarauspalvelu/utils/email/reservation_email_notification_sender.py @@ -1,8 +1,8 @@ from common.date_utils import local_datetime -from email_notification.models import EmailType -from email_notification.tasks import send_reservation_email_task, send_staff_reservation_email_task from reservations.enums import ReservationStateChoice, ReservationTypeChoice from reservations.models import Reservation +from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.tasks import send_reservation_email_task, send_staff_reservation_email_task from users.models import ReservationNotification