diff --git a/client/src/components/notifications/NotificationList.vue b/client/src/components/notifications/NotificationList.vue index 8d5bd404..e13d5104 100644 --- a/client/src/components/notifications/NotificationList.vue +++ b/client/src/components/notifications/NotificationList.vue @@ -56,7 +56,7 @@ function onNotificationClick(notification: Notification) { >
Notification icon int: verb="Alexandra hat einen neuen Beitrag erfasst", actor_avatar_url=avatar_urls[0], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[0], ), NotificationFactory( @@ -38,8 +37,7 @@ def create_default_notifications() -> int: verb="Alexandra hat einen neuen Beitrag erfasst", actor_avatar_url=avatar_urls[0], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[1], ), NotificationFactory( @@ -48,8 +46,7 @@ def create_default_notifications() -> int: verb="Alexandra hat einen neuen Beitrag erfasst", actor_avatar_url=avatar_urls[0], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[2], ), NotificationFactory( @@ -58,8 +55,7 @@ def create_default_notifications() -> int: verb="Alexandra hat einen neuen Beitrag erfasst", actor_avatar_url=avatar_urls[0], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[3], ), NotificationFactory( @@ -68,8 +64,7 @@ def create_default_notifications() -> int: verb="Bianca hat für den Auftrag Autoversicherung 3 eine Lösung abgegeben", actor_avatar_url=avatar_urls[1], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[4], ), NotificationFactory( @@ -78,8 +73,7 @@ def create_default_notifications() -> int: verb="Bianca hat für den Auftrag Autoversicherung 1 eine Lösung abgegeben", actor_avatar_url=avatar_urls[1], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[5], ), NotificationFactory( @@ -88,8 +82,7 @@ def create_default_notifications() -> int: verb="Bianca hat für den Auftrag Autoversicherung 2 eine Lösung abgegeben", actor_avatar_url=avatar_urls[1], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[6], ), NotificationFactory( @@ -98,8 +91,7 @@ def create_default_notifications() -> int: verb="Bianca hat für den Auftrag Autoversicherung 4 eine Lösung abgegeben", actor_avatar_url=avatar_urls[1], target_url="/", - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[7], ), NotificationFactory( @@ -108,8 +100,7 @@ def create_default_notifications() -> int: verb="Chantal hat eine Bewertung für den Transferauftrag 3 eingegeben", target_url="/", actor_avatar_url=avatar_urls[2], - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[8], ), NotificationFactory( @@ -118,8 +109,7 @@ def create_default_notifications() -> int: verb="Chantal hat eine Bewertung für den Transferauftrag 4 eingegeben", target_url="/", actor_avatar_url=avatar_urls[2], - notification_type=NotificationType.USER_INTERACTION, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.USER_INTERACTION, timestamp=timestamps[9], ), NotificationFactory( @@ -127,22 +117,21 @@ def create_default_notifications() -> int: actor=user, verb="Super, du kommst in deinem Lernpfad gut voran. Schaue dir jetzt die verfügbaren Prüfungstermine an.", target_url="/", - notification_type=NotificationType.PROGRESS, - course="Versicherungsvermittler/-in", + notification_category=NotificationCategory.PROGRESS, timestamp=timestamps[10], ), NotificationFactory( recipient=user, actor=user, verb="Wartungsarbeiten: 20.12.2022 08:00 - 12:00", - notification_type=NotificationType.INFORMATION, + notification_category=NotificationCategory.INFORMATION, timestamp=timestamps[11], ), NotificationFactory( recipient=user, actor=user, verb="Wartungsarbeiten: 31.01.2023 08:00 - 12:00", - notification_type=NotificationType.INFORMATION, + notification_category=NotificationCategory.INFORMATION, timestamp=timestamps[12], ), ) diff --git a/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py b/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py index 7e6dd80c..81e89677 100644 --- a/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py +++ b/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py @@ -3,16 +3,10 @@ from datetime import timedelta import structlog from django.utils import timezone -from django.utils.translation import gettext_lazy as _ from vbv_lernwelt.core.base import LoggedCommand -from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse -from vbv_lernwelt.notify.email.email_services import ( - create_template_data_from_course_session_attendance_course, - EmailTemplate, -) from vbv_lernwelt.notify.services import NotificationService logger = structlog.get_logger(__name__) @@ -20,20 +14,6 @@ logger = structlog.get_logger(__name__) PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2) -def send_attendance_course_reminder_notification( - recipient: User, attendance_course: CourseSessionAttendanceCourse -) -> str: - return NotificationService.send_information_notification( - recipient=recipient, - verb=_("Erinnerung: Bald findet ein Präsenzkurs statt"), - target_url=attendance_course.learning_content.get_frontend_url(), - email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, - template_data=create_template_data_from_course_session_attendance_course( - attendance_course=attendance_course - ), - ) - - def attendance_course_reminder_notification_job(): """Checks if an attendance course is coming up and sends a reminder to the participants""" start = timezone.now() @@ -63,7 +43,7 @@ def attendance_course_reminder_notification_job(): course_session_id=cs_id, ) for user in csu: - result = send_attendance_course_reminder_notification( + result = NotificationService.send_attendance_course_reminder_notification( user.user, attendance_course ) results_counter[result] += 1 diff --git a/server/vbv_lernwelt/notify/migrations/0002_auto_20230830_1606.py b/server/vbv_lernwelt/notify/migrations/0002_auto_20230830_1606.py new file mode 100644 index 00000000..d4292aa4 --- /dev/null +++ b/server/vbv_lernwelt/notify/migrations/0002_auto_20230830_1606.py @@ -0,0 +1,69 @@ +# Generated by Django 3.2.20 on 2023-08-30 14:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("course", "0004_auto_20230823_1744"), + ("notify", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="notification", + name="course", + ), + migrations.RemoveField( + model_name="notification", + name="notification_type", + ), + migrations.AddField( + model_name="notification", + name="course_session", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="course.coursesession", + ), + ), + migrations.AddField( + model_name="notification", + name="notification_category", + field=models.CharField( + choices=[ + ("USER_INTERACTION", "User Interaction"), + ("PROGRESS", "Progress"), + ("INFORMATION", "Information"), + ], + default="INFORMATION", + max_length=255, + ), + ), + migrations.AddField( + model_name="notification", + name="notification_trigger", + field=models.CharField( + choices=[ + ("ATTENDANCE_COURSE_REMINDER", "Attendance Course Reminder"), + ("CASEWORK_SUBMITTED", "Casework Submitted"), + ("CASEWORK_EVALUATED", "Casework Evaluated"), + ("NEW_FEEDBACK", "New Feedback"), + ], + default="ATTENDANCE_COURSE_REMINDER", + max_length=255, + ), + ), + migrations.AlterField( + model_name="notification", + name="actor_avatar_url", + field=models.CharField(blank=True, default="", max_length=2048), + ), + migrations.AlterField( + model_name="notification", + name="target_url", + field=models.CharField(blank=True, default="", max_length=2048), + ), + ] diff --git a/server/vbv_lernwelt/notify/migrations/0003_truncate_notifications.py b/server/vbv_lernwelt/notify/migrations/0003_truncate_notifications.py new file mode 100644 index 00000000..8751eb88 --- /dev/null +++ b/server/vbv_lernwelt/notify/migrations/0003_truncate_notifications.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.20 on 2023-08-30 14:10 + +from django.db import migrations + + +def init_user_notification_emails(apps=None, schema_editor=None): + User = apps.get_model("core", "User") + for u in User.objects.all(): + u.additional_json_data["email_notification_categories"] = ["NOTIFICATION"] + u.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("notify", "0002_auto_20230830_1606"), + ] + + operations = [ + migrations.RunSQL("truncate table notify_notification cascade;"), + migrations.RunPython(init_user_notification_emails), + ] diff --git a/server/vbv_lernwelt/notify/models.py b/server/vbv_lernwelt/notify/models.py index dd00ed99..dfe6df74 100644 --- a/server/vbv_lernwelt/notify/models.py +++ b/server/vbv_lernwelt/notify/models.py @@ -2,25 +2,43 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from notifications.base.models import AbstractNotification +from vbv_lernwelt.course.models import CourseSession -class NotificationType(models.TextChoices): + +class NotificationCategory(models.TextChoices): USER_INTERACTION = "USER_INTERACTION", _("User Interaction") PROGRESS = "PROGRESS", _("Progress") INFORMATION = "INFORMATION", _("Information") +class NotificationTrigger(models.TextChoices): + ATTENDANCE_COURSE_REMINDER = "ATTENDANCE_COURSE_REMINDER", _( + "Attendance Course Reminder" + ) + CASEWORK_SUBMITTED = "CASEWORK_SUBMITTED", _("Casework Submitted") + CASEWORK_EVALUATED = "CASEWORK_EVALUATED", _("Casework Evaluated") + NEW_FEEDBACK = "NEW_FEEDBACK", _("New Feedback") + + class Notification(AbstractNotification): # UUIDs are not supported by the notifications app... # id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - notification_type = models.CharField( - max_length=32, - choices=NotificationType.choices, - default=NotificationType.INFORMATION, + notification_category = models.CharField( + max_length=255, + choices=NotificationCategory.choices, + default=NotificationCategory.INFORMATION, + ) + notification_trigger = models.CharField( + max_length=255, + choices=NotificationTrigger.choices, + default="", + ) + target_url = models.CharField(max_length=2048, default="", blank=True) + actor_avatar_url = models.CharField(max_length=2048, default="", blank=True) + course_session = models.ForeignKey( + CourseSession, blank=True, null=True, on_delete=models.SET_NULL ) - target_url = models.URLField(blank=True, null=True) - actor_avatar_url = models.URLField(blank=True, null=True) - course = models.CharField(max_length=32, blank=True, null=True) class Meta(AbstractNotification.Meta): abstract = False diff --git a/server/vbv_lernwelt/notify/services.py b/server/vbv_lernwelt/notify/services.py index 34143871..6cc2848e 100644 --- a/server/vbv_lernwelt/notify/services.py +++ b/server/vbv_lernwelt/notify/services.py @@ -1,74 +1,121 @@ -from typing import Optional +from __future__ import annotations + +from gettext import gettext +from typing import TYPE_CHECKING import structlog +from django.db.models import Model from notifications.signals import notify from vbv_lernwelt.core.models import User -from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email -from vbv_lernwelt.notify.models import Notification, NotificationType +from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.notify.email.email_services import ( + create_template_data_from_course_session_attendance_course, + EmailTemplate, + send_email, +) +from vbv_lernwelt.notify.models import ( + Notification, + NotificationCategory, + NotificationTrigger, +) + +if TYPE_CHECKING: + from vbv_lernwelt.assignment.models import AssignmentCompletion + from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse + from vbv_lernwelt.feedback.models import FeedbackResponse logger = structlog.get_logger(__name__) class NotificationService: @classmethod - def send_user_interaction_notification( - cls, - recipient: User, - verb: str, - sender: User, - course: str, - target_url: str, - email_template: EmailTemplate, - template_data: dict = None, - ) -> str: + def send_assignment_submitted_notification( + cls, recipient: User, sender: User, assignment_completion: AssignmentCompletion + ): + verb = gettext( + "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben." + ) % { + "sender": sender.get_full_name(), + "assignment_title": assignment_completion.assignment.title, + } + return cls._send_notification( recipient=recipient, verb=verb, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, sender=sender, - course=course, - target_url=target_url, - notification_type=NotificationType.USER_INTERACTION, - email_template=email_template, - template_data=template_data, + target_url=assignment_completion.get_assignment_evaluation_frontend_url(), + course_session=assignment_completion.course_session, + action_object=assignment_completion, + email_template=EmailTemplate.CASEWORK_SUBMITTED, ) @classmethod - def send_progress_notification( + def send_assignment_evaluated_notification( cls, recipient: User, - verb: str, - course: str, + sender: User, + assignment_completion: AssignmentCompletion, target_url: str, - email_template: EmailTemplate, - template_data: dict = None, - ) -> str: + ): + verb = gettext( + "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet." + ) % { + "sender": sender.get_full_name(), + "assignment_title": assignment_completion.assignment.title, + } + return cls._send_notification( recipient=recipient, verb=verb, - course=course, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_EVALUATED, + sender=sender, target_url=target_url, - notification_type=NotificationType.PROGRESS, - email_template=email_template, - template_data=template_data, + course_session=assignment_completion.course_session, + action_object=assignment_completion, + email_template=EmailTemplate.CASEWORK_EVALUATED, ) @classmethod - def send_information_notification( + def send_new_feedback_notification( cls, recipient: User, - verb: str, - target_url: str, - email_template: EmailTemplate, - template_data: dict = None, - ) -> str: + feedback_response: FeedbackResponse, + ): + verb = f"New feedback for circle {feedback_response.circle.title}" + return cls._send_notification( recipient=recipient, verb=verb, - target_url=target_url, - notification_type=NotificationType.INFORMATION, - email_template=email_template, - template_data=template_data, + notification_category=NotificationCategory.INFORMATION, + notification_trigger=NotificationTrigger.NEW_FEEDBACK, + target_url=f"/course/{feedback_response.course_session.course.slug}/cockpit/feedback/{feedback_response.circle_id}/", + course_session=feedback_response.course_session, + action_object=feedback_response, + email_template=EmailTemplate.NEW_FEEDBACK, + ) + + @classmethod + def send_attendance_course_reminder_notification( + cls, + recipient: User, + attendance_course: CourseSessionAttendanceCourse, + ): + return cls._send_notification( + recipient=recipient, + verb="Erinnerung: Bald findet ein Präsenzkurs statt", + notification_category=NotificationCategory.INFORMATION, + notification_trigger=NotificationTrigger.ATTENDANCE_COURSE_REMINDER, + target_url=attendance_course.learning_content.get_frontend_url(), + action_object=attendance_course, + course_session=attendance_course.course_session, + email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, + template_data=create_template_data_from_course_session_attendance_course( + attendance_course=attendance_course + ), ) @classmethod @@ -76,20 +123,24 @@ class NotificationService: cls, recipient: User, verb: str, - notification_type: NotificationType, - email_template: EmailTemplate, - template_data: dict | None = None, + notification_category: NotificationCategory, + notification_trigger: NotificationTrigger, sender: User | None = None, - course: str | None = None, + action_object: Model | None = None, target_url: str | None = None, + course_session: CourseSession | None = None, + email_template: EmailTemplate | None = None, + template_data: dict | None = None, fail_silently: bool = True, ) -> str: if template_data is None: template_data = {} - notification_identifier = f"{notification_type.name}_{email_template.name}" + notification_identifier = ( + f"{notification_category.name}_{notification_trigger.name}" + ) - actor_avatar_url: Optional[str] = None + actor_avatar_url = "" if not sender: sender = User.objects.get(email="admin") else: @@ -98,8 +149,9 @@ class NotificationService: recipient=recipient.email, sender=sender.email, verb=verb, - notification_type=notification_type, - course=course, + notification_category=notification_category, + notification_trigger=notification_trigger, + course_session=course_session.title if course_session else "", target_url=target_url, template_data=template_data, ) @@ -108,16 +160,20 @@ class NotificationService: notification = NotificationService._find_duplicate_notification( recipient=recipient, verb=verb, - notification_type=notification_type, + notification_category=notification_category, + notification_trigger=notification_trigger, target_url=target_url, - template_name=email_template.name, - template_data=template_data, + course_session=course_session, ) emailed = False if notification and notification.emailed: emailed = True - if cls._should_send_email(notification_type, recipient) and not emailed: + if ( + email_template + and cls._should_send_email(notification_category, recipient) + and not emailed + ): log.debug("Try to send email") try: emailed = cls._send_email( @@ -153,16 +209,18 @@ class NotificationService: sender=sender, recipient=recipient, verb=verb, + action_object=action_object, emailed=emailed, - # The metadata is saved in the 'data' member of the AbstractNotification model - email_template=email_template.name, + # The extra arguments are saved in the 'data' member + email_template=email_template.name if email_template else "", template_data=template_data, ) sent_notification: Notification = response[0][1][0] # 🫨 sent_notification.target_url = target_url - sent_notification.notification_type = notification_type - sent_notification.course = course + sent_notification.notification_category = notification_category + sent_notification.notification_trigger = notification_trigger + sent_notification.course_session = course_session sent_notification.actor_avatar_url = actor_avatar_url sent_notification.save() log.info("Notification sent successfully", emailed=emailed) @@ -181,10 +239,10 @@ class NotificationService: @staticmethod def _should_send_email( - notification_type: NotificationType, recipient: User + notification_category: NotificationCategory, recipient: User ) -> bool: - return str(notification_type) in recipient.additional_json_data.get( - "email_notification_types", [] + return str(notification_category) in recipient.additional_json_data.get( + "email_notification_categories", [] ) @staticmethod @@ -206,10 +264,10 @@ class NotificationService: def _find_duplicate_notification( recipient: User, verb: str, - notification_type: NotificationType, - template_name: str, - template_data: dict, + notification_category: NotificationCategory, + notification_trigger: NotificationTrigger, target_url: str | None, + course_session: CourseSession | None, ) -> Notification | None: """Check if a notification with the same parameters has already been sent to the recipient. This is to prevent duplicate notifications from being sent and to protect against potential programming errors. @@ -217,10 +275,8 @@ class NotificationService: return Notification.objects.filter( recipient=recipient, verb=verb, - notification_type=notification_type, + notification_category=notification_category, + notification_trigger=notification_trigger, target_url=target_url, - data={ - "email_template": template_name, - "template_data": template_data, - }, + course_session=course_session, ).first() diff --git a/server/vbv_lernwelt/notify/tests/test_notify_api.py b/server/vbv_lernwelt/notify/tests/test_notify_api.py index fa9a2863..727841ab 100644 --- a/server/vbv_lernwelt/notify/tests/test_notify_api.py +++ b/server/vbv_lernwelt/notify/tests/test_notify_api.py @@ -4,7 +4,7 @@ from rest_framework.test import APITestCase from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.tests.factories import UserFactory -from vbv_lernwelt.notify.models import Notification, NotificationType +from vbv_lernwelt.notify.models import Notification, NotificationCategory from vbv_lernwelt.notify.tests.factories import NotificationFactory @@ -115,7 +115,7 @@ class TestNotificationSettingsApi(APITestCase): def test_store_retrieve_settings(self): notification_settings = json.dumps( - [NotificationType.INFORMATION, NotificationType.PROGRESS] + [NotificationCategory.INFORMATION, NotificationCategory.PROGRESS] ) api_path = "/api/notify/email_notification_settings/" @@ -128,7 +128,7 @@ class TestNotificationSettingsApi(APITestCase): self.assertEqual(response.json(), notification_settings) self.user.refresh_from_db() self.assertEqual( - self.user.additional_json_data["email_notification_types"], + self.user.additional_json_data["email_notification_categories"], notification_settings, ) diff --git a/server/vbv_lernwelt/notify/tests/test_send_attendance_course_reminders.py b/server/vbv_lernwelt/notify/tests/test_send_attendance_course_reminders.py index 04bc4cc2..de03f261 100644 --- a/server/vbv_lernwelt/notify/tests/test_send_attendance_course_reminders.py +++ b/server/vbv_lernwelt/notify/tests/test_send_attendance_course_reminders.py @@ -1,45 +1,21 @@ -from dataclasses import dataclass from datetime import datetime, timedelta -from unittest.mock import patch -from django.contrib.auth.models import User from django.test import TestCase from django.utils import timezone -from django.utils.translation import gettext_lazy as _ from freezegun import freeze_time from vbv_lernwelt.core.constants import TEST_COURSE_SESSION_BERN_ID from vbv_lernwelt.core.create_default_users import create_default_users from vbv_lernwelt.course.creators.test_course import create_test_course -from vbv_lernwelt.course.models import CourseSession, CourseSessionUser +from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse -from vbv_lernwelt.notify.email.email_services import EmailTemplate from vbv_lernwelt.notify.management.commands.send_attendance_course_reminders import ( attendance_course_reminder_notification_job, ) +from vbv_lernwelt.notify.models import Notification -@dataclass -class SentNotification: - recipient: User - verb: str - target_url: str - email_template: EmailTemplate - template_data: dict - - -sent_notifications: list[SentNotification] = [] - - -def on_send_notification(**kwargs) -> None: - sent_notifications.append(SentNotification(**kwargs)) - - -@patch( - "vbv_lernwelt.notify.services.NotificationService.send_information_notification", - on_send_notification, -) class TestAttendanceCourseReminders(TestCase): def setUp(self): create_default_users() @@ -57,13 +33,12 @@ class TestAttendanceCourseReminders(TestCase): location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", trainer="Roland Grossenbacher", ) - in_one_week = datetime.now() + timedelta(weeks=1) - self.csac.due_date.start = timezone.make_aware( - in_one_week.replace(hour=7, minute=30, second=0, microsecond=0) + ref_date_time = timezone.make_aware(datetime(2023, 8, 29)) + self.csac.due_date.start = ref_date_time.replace( + hour=7, minute=30, second=0, microsecond=0 ) - - self.csac.due_date.end = timezone.make_aware( - in_one_week.replace(hour=16, minute=15, second=0, microsecond=0) + self.csac.due_date.end = ref_date_time.replace( + hour=16, minute=15, second=0, microsecond=0 ) self.csac.due_date.save() @@ -89,41 +64,53 @@ class TestAttendanceCourseReminders(TestCase): self.csac_future.due_date.save() attendance_course_reminder_notification_job() - self.assertEquals(3, len(sent_notifications)) - recipients = CourseSessionUser.objects.filter( - course_session_id=self.csac.course_session.id - ) - self.assertEquals( - set(map(lambda n: n.recipient, sent_notifications)), - set(map(lambda csu: csu.user, recipients)), + + self.assertEquals(3, len(Notification.objects.all())) + notification = Notification.objects.get( + recipient__username="test-student1@example.com" ) - for notification in sent_notifications: - self.assertEquals( - _("Erinnerung: Bald findet ein Präsenzkurs statt"), - notification.verb, - ) - self.assertEquals( - "/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug", - notification.target_url, - ) - self.assertEquals( - self.csac.learning_content.title, - notification.template_data["attendance_course"], - ) - self.assertEquals( - self.csac.location, - notification.template_data["location"], - ) - self.assertEquals( - self.csac.trainer, - notification.template_data["trainer"], - ) - self.assertEquals( - self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"), - notification.template_data["start"], - ) - self.assertEquals( - self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"), - notification.template_data["end"], - ) + self.assertEquals( + "Erinnerung: Bald findet ein Präsenzkurs statt", + notification.verb, + ) + self.assertEquals( + "INFORMATION", + notification.notification_category, + ) + self.assertEquals( + "ATTENDANCE_COURSE_REMINDER", + notification.notification_trigger, + ) + self.assertEquals( + self.csac, + notification.action_object, + ) + self.assertEquals( + self.csac.course_session, + notification.course_session, + ) + self.assertEquals( + "/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug", + notification.target_url, + ) + self.assertEquals( + self.csac.learning_content.title, + notification.data["template_data"]["attendance_course"], + ) + self.assertEquals( + self.csac.location, + notification.data["template_data"]["location"], + ) + self.assertEquals( + self.csac.trainer, + notification.data["template_data"]["trainer"], + ) + self.assertEquals( + self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"), + notification.data["template_data"]["start"], + ) + self.assertEquals( + self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"), + notification.data["template_data"]["end"], + ) diff --git a/server/vbv_lernwelt/notify/tests/test_service.py b/server/vbv_lernwelt/notify/tests/test_service.py index 2ec5500c..6d8af677 100644 --- a/server/vbv_lernwelt/notify/tests/test_service.py +++ b/server/vbv_lernwelt/notify/tests/test_service.py @@ -5,7 +5,11 @@ from django.test import TestCase from vbv_lernwelt.core.models import User from vbv_lernwelt.core.tests.factories import UserFactory from vbv_lernwelt.notify.email.email_services import EmailTemplate -from vbv_lernwelt.notify.models import Notification, NotificationType +from vbv_lernwelt.notify.models import ( + Notification, + NotificationCategory, + NotificationTrigger, +) from vbv_lernwelt.notify.services import NotificationService @@ -24,142 +28,178 @@ class TestNotificationService(TestCase): self.admin = UserFactory(username="admin", email="admin") self.sender_username = "Bob" - UserFactory(username=self.sender_username, email="bob@gmail.com") + UserFactory(username=self.sender_username, email="bob@example.com") self.sender = User.objects.get(username=self.sender_username) self.recipient_username = "Alice" - UserFactory(username=self.recipient_username, email="alice@gmail.com") + UserFactory(username=self.recipient_username, email="alice@example.com") self.recipient = User.objects.get(username=self.recipient_username) - self.recipient.additional_json_data["email_notification_types"] = json.dumps( - ["USER_INTERACTION", "INFORMATION"] - ) self.recipient.save() self.client.login(username=self.recipient, password="pw") - def test_send_information_notification(self): - verb = "Wartungsarbeiten: 13.12 10:00 - 13:00 Uhr" - target_url = "https://www.vbv.ch" - self.notification_service.send_information_notification( - recipient=self.recipient, - verb=verb, - target_url=target_url, - email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, - ) - self.assertEqual(1, Notification.objects.count()) - notification: Notification = Notification.objects.first() - self.assertEqual(self.admin, notification.actor) - self.assertEqual(verb, notification.verb) - self.assertEqual(target_url, notification.target_url) - self.assertEqual( - str(NotificationType.INFORMATION), notification.notification_type - ) - self.assertTrue(notification.emailed) - - def test_send_progress_notification(self): - verb = "Super Fortschritt! Melde dich jetzt an." - target_url = "https://www.vbv.ch" - course = "Versicherungsvermittler/in" - self.notification_service.send_progress_notification( - recipient=self.recipient, - verb=verb, - target_url=target_url, - course=course, - email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, - ) - self.assertEqual(1, Notification.objects.count()) - notification: Notification = Notification.objects.first() - self.assertEqual(self.admin, notification.actor) - self.assertEqual(verb, notification.verb) - self.assertEqual(target_url, notification.target_url) - self.assertEqual(course, notification.course) - self.assertEqual(str(NotificationType.PROGRESS), notification.notification_type) - self.assertFalse(notification.emailed) - - def test_send_user_interaction_notification(self): + def test_send_notification_without_email(self): verb = "Anne hat deinen Auftrag bewertet" target_url = "https://www.vbv.ch" - course = "Versicherungsvermittler/in" - self.notification_service.send_user_interaction_notification( + result = self.notification_service._send_notification( sender=self.sender, recipient=self.recipient, verb=verb, target_url=target_url, - course=course, - email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, + fail_silently=False, ) + + self.assertEqual(result, "USER_INTERACTION_CASEWORK_SUBMITTED_success") + self.assertEqual(1, Notification.objects.count()) notification: Notification = Notification.objects.first() self.assertEqual(self.sender, notification.actor) self.assertEqual(verb, notification.verb) self.assertEqual(target_url, notification.target_url) - self.assertEqual(course, notification.course) self.assertEqual( - str(NotificationType.USER_INTERACTION), notification.notification_type + str(NotificationCategory.USER_INTERACTION), + notification.notification_category, + ) + self.assertFalse(notification.emailed) + + def test_send_notification_with_email(self): + self.recipient.additional_json_data[ + "email_notification_categories" + ] = json.dumps(["USER_INTERACTION"]) + self.recipient.save() + + verb = "Anne hat deinen Auftrag bewertet" + target_url = "https://www.vbv.ch" + result = self.notification_service._send_notification( + sender=self.sender, + recipient=self.recipient, + verb=verb, + target_url=target_url, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, + email_template=EmailTemplate.CASEWORK_SUBMITTED, + fail_silently=False, + ) + + self.assertEqual(result, "USER_INTERACTION_CASEWORK_SUBMITTED_emailed_success") + + self.assertEqual(1, Notification.objects.count()) + notification: Notification = Notification.objects.first() + self.assertEqual(self.sender, notification.actor) + self.assertEqual(verb, notification.verb) + self.assertEqual(target_url, notification.target_url) + self.assertEqual( + str(NotificationCategory.USER_INTERACTION), + notification.notification_category, ) self.assertTrue(notification.emailed) def test_does_not_send_duplicate_notification(self): verb = "Anne hat deinen Auftrag bewertet" target_url = "https://www.vbv.ch" - course = "Versicherungsvermittler/in" - for i in range(2): - self.notification_service.send_user_interaction_notification( - sender=self.sender, - recipient=self.recipient, - verb=verb, - target_url=target_url, - course=course, - email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, - template_data={ - "blah": 123, - "foo": "ich habe hunger", - }, - ) + result = self.notification_service._send_notification( + sender=self.sender, + recipient=self.recipient, + verb=verb, + target_url=target_url, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, + email_template=EmailTemplate.CASEWORK_SUBMITTED, + template_data={ + "blah": 123, + "foo": "ich habe hunger", + }, + ) + self.assertEqual(result, "USER_INTERACTION_CASEWORK_SUBMITTED_success") - self.assertEqual(1, Notification.objects.count()) - notification: Notification = Notification.objects.first() - self.assertEqual(self.sender, notification.actor) - self.assertEqual(verb, notification.verb) - self.assertEqual(target_url, notification.target_url) - self.assertEqual(course, notification.course) - self.assertEqual( - str(NotificationType.USER_INTERACTION), - notification.notification_type, - ) - self.assertTrue(notification.emailed) + result = self.notification_service._send_notification( + sender=self.sender, + recipient=self.recipient, + verb=verb, + target_url=target_url, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, + email_template=EmailTemplate.CASEWORK_SUBMITTED, + template_data={ + "blah": 123, + "foo": "ich habe hunger", + }, + ) + self.assertEqual(result, "USER_INTERACTION_CASEWORK_SUBMITTED_duplicate") + + self.assertEqual(1, Notification.objects.count()) + notification: Notification = Notification.objects.first() + self.assertEqual(self.sender, notification.actor) + self.assertEqual(verb, notification.verb) + self.assertEqual(target_url, notification.target_url) + self.assertEqual( + str(NotificationCategory.USER_INTERACTION), + notification.notification_category, + ) + self.assertEqual( + str(NotificationTrigger.CASEWORK_SUBMITTED), + notification.notification_trigger, + ) + self.assertFalse(notification.emailed) + + # when the email was not sent, yet it will still send it afterwards... + self.recipient.additional_json_data[ + "email_notification_categories" + ] = json.dumps(["USER_INTERACTION"]) + self.recipient.save() + + result = self.notification_service._send_notification( + sender=self.sender, + recipient=self.recipient, + verb=verb, + target_url=target_url, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, + email_template=EmailTemplate.CASEWORK_SUBMITTED, + template_data={ + "blah": 123, + "foo": "ich habe hunger", + }, + ) + self.assertEqual( + result, "USER_INTERACTION_CASEWORK_SUBMITTED_emailed_duplicate" + ) + + self.assertEqual(1, Notification.objects.count()) + notification: Notification = Notification.objects.first() + self.assertTrue(notification.emailed) def test_only_sends_email_if_enabled(self): # Assert no mail is sent if corresponding email notification type is not enabled - self.recipient.additional_json_data["email_notification_types"] = json.dumps( - ["INFORMATION"] - ) - self.recipient.save() - self.notification_service.send_user_interaction_notification( + self.notification_service._send_notification( sender=self.sender, recipient=self.recipient, verb="should not be sent", target_url="", - course="", - email_template=EmailTemplate.CASEWORK_EVALUATED, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, + email_template=EmailTemplate.CASEWORK_SUBMITTED, template_data={}, ) self.assertEqual(1, Notification.objects.count()) self.assertFalse(self._has_sent_emails()) # Assert mail is sent if corresponding email notification type is enabled - self.recipient.additional_json_data["email_notification_types"] = json.dumps( - ["USER_INTERACTION"] - ) + self.recipient.additional_json_data[ + "email_notification_categories" + ] = json.dumps(["USER_INTERACTION"]) self.recipient.save() - self.notification_service.send_user_interaction_notification( + self.notification_service._send_notification( sender=self.sender, recipient=self.recipient, verb="should be sent", target_url="", - course="", - email_template=EmailTemplate.CASEWORK_EVALUATED, + notification_category=NotificationCategory.USER_INTERACTION, + notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED, + email_template=EmailTemplate.CASEWORK_SUBMITTED, template_data={}, ) self.assertEqual(2, Notification.objects.count()) diff --git a/server/vbv_lernwelt/notify/views.py b/server/vbv_lernwelt/notify/views.py index b5e2f50b..d4ee3ee9 100644 --- a/server/vbv_lernwelt/notify/views.py +++ b/server/vbv_lernwelt/notify/views.py @@ -4,11 +4,11 @@ from rest_framework.response import Response @api_view(["POST", "GET"]) def email_notification_settings(request): - EMAIL_NOTIFICATION_TYPES = "email_notification_types" + EMAIL_NOTIFICATION_CATEGORIES = "email_notification_categories" if request.method == "POST": - request.user.additional_json_data[EMAIL_NOTIFICATION_TYPES] = request.data + request.user.additional_json_data[EMAIL_NOTIFICATION_CATEGORIES] = request.data request.user.save() return Response( status=200, - data=request.user.additional_json_data.get(EMAIL_NOTIFICATION_TYPES, []), + data=request.user.additional_json_data.get(EMAIL_NOTIFICATION_CATEGORIES, []), )