Refactor Notification model

This commit is contained in:
Daniel Egger 2023-08-30 14:20:36 +02:00
parent b26ec64edb
commit da56f2a346
20 changed files with 586 additions and 337 deletions

View File

@ -56,7 +56,7 @@ function onNotificationClick(notification: Notification) {
> >
<div class="flex flex-row"> <div class="flex flex-row">
<img <img
v-if="notification.notification_type === 'USER_INTERACTION'" v-if="notification.notification_category === 'USER_INTERACTION'"
alt="Notification icon" alt="Notification icon"
class="mr-2 h-[45px] min-w-[45px] rounded-full" class="mr-2 h-[45px] min-w-[45px] rounded-full"
:src="notification.actor_avatar_url ?? undefined" :src="notification.actor_avatar_url ?? undefined"

View File

@ -491,7 +491,7 @@ export interface DocumentUploadData {
// notifications // notifications
export type NotificationType = "USER_INTERACTION" | "PROGRESS" | "INFORMATION"; export type NotificationCategory = "USER_INTERACTION" | "PROGRESS" | "INFORMATION";
export interface Notification { export interface Notification {
// given by AbstractNotification model // given by AbstractNotification model
@ -503,7 +503,7 @@ export interface Notification {
target: string | null; target: string | null;
action_object: string | null; action_object: string | null;
// given by Notification model // given by Notification model
notification_type: NotificationType; notification_category: NotificationCategory;
target_url: string | null; target_url: string | null;
actor_avatar_url: string | null; actor_avatar_url: string | null;
course: string | null; course: string | null;

View File

@ -133,38 +133,21 @@ def update_assignment_completion(
if completion_status == AssignmentCompletionStatus.SUBMITTED: if completion_status == AssignmentCompletionStatus.SUBMITTED:
ac.submitted_at = timezone.now() ac.submitted_at = timezone.now()
if evaluation_user: if evaluation_user:
verb = gettext( NotificationService.send_assignment_submitted_notification(
"%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben."
) % {
"sender": assignment_user.get_full_name(),
"assignment_title": assignment.title,
}
NotificationService.send_user_interaction_notification(
recipient=evaluation_user, recipient=evaluation_user,
verb=verb,
sender=ac.assignment_user, sender=ac.assignment_user,
course=course_session.course.title, assignment_completion=ac,
target_url=ac.get_assignment_evaluation_frontend_url(),
email_template=EmailTemplate.CASEWORK_SUBMITTED,
) )
elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED: elif completion_status == AssignmentCompletionStatus.EVALUATION_SUBMITTED:
ac.evaluation_submitted_at = timezone.now() ac.evaluation_submitted_at = timezone.now()
learning_content_assignment = assignment.learningcontentassignment_set.first() learning_content_assignment = assignment.learningcontentassignment_set.first()
if learning_content_assignment: if learning_content_assignment:
assignment_frontend_url = learning_content_assignment.get_frontend_url() assignment_frontend_url = learning_content_assignment.get_frontend_url()
verb = gettext( NotificationService.send_assignment_evaluated_notification(
"%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet."
) % {
"sender": evaluation_user.get_full_name(),
"assignment_title": assignment.title,
}
NotificationService.send_user_interaction_notification(
recipient=ac.assignment_user, recipient=ac.assignment_user,
verb=verb,
sender=evaluation_user, sender=evaluation_user,
course=course_session.course.title, assignment_completion=ac,
target_url=assignment_frontend_url, target_url=assignment_frontend_url,
email_template=EmailTemplate.CASEWORK_EVALUATED,
) )
ac.completion_status = completion_status.value ac.completion_status = completion_status.value

View File

@ -14,6 +14,7 @@ from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.utils import find_first from vbv_lernwelt.core.utils import find_first
from vbv_lernwelt.course.creators.test_course import create_test_course from vbv_lernwelt.course.creators.test_course import create_test_course
from vbv_lernwelt.course.models import CourseSession from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.notify.models import Notification
class AttendanceCourseUserMutationTestCase(GraphQLTestCase): class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
@ -153,6 +154,41 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
}, },
) )
# check notification
self.assertEqual(Notification.objects.count(), 1)
notification = Notification.objects.first()
self.assertEquals(
"Test Student1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» abgegeben.",
notification.verb,
)
self.assertEquals(
"test-trainer1@example.com",
notification.recipient.email,
)
self.assertEquals(
"test-student1@example.com",
notification.actor.email,
)
self.assertEquals(
"USER_INTERACTION",
notification.notification_category,
)
self.assertEquals(
"CASEWORK_SUBMITTED",
notification.notification_trigger,
)
self.assertEquals(
notification.action_object,
db_entry,
)
self.assertEquals(
notification.course_session,
self.course_session,
)
self.assertTrue(
f"/course/test-lehrgang/cockpit/assignment" in notification.target_url
)
# second submit will fail # second submit will fail
completion_data_string = json.dumps( completion_data_string = json.dumps(
{ {
@ -346,6 +382,42 @@ class AttendanceCourseUserMutationTestCase(GraphQLTestCase):
}, },
) )
# check notification
self.assertEqual(Notification.objects.count(), 1)
notification = Notification.objects.first()
self.assertEquals(
"Test Trainer1 hat die geleitete Fallarbeit «Überprüfen einer Motorfahrzeugs-Versicherungspolice» bewertet.",
notification.verb,
)
self.assertEquals(
"test-student1@example.com",
notification.recipient.email,
)
self.assertEquals(
"test-trainer1@example.com",
notification.actor.email,
)
self.assertEquals(
"USER_INTERACTION",
notification.notification_category,
)
self.assertEquals(
"CASEWORK_EVALUATED",
notification.notification_trigger,
)
self.assertEquals(
notification.action_object,
db_entry,
)
self.assertEquals(
notification.course_session,
self.course_session,
)
self.assertEquals(
notification.target_url,
"/course/test-lehrgang/learn/fahrzeug/überprüfen-einer-motorfahrzeug-versicherungspolice",
)
# `EVALUATION_SUBMITTED` will create a new AssignmentCompletionAuditLog # `EVALUATION_SUBMITTED` will create a new AssignmentCompletionAuditLog
acl = AssignmentCompletionAuditLog.objects.get( acl = AssignmentCompletionAuditLog.objects.get(
assignment_user=self.student, assignment_user=self.student,

View File

@ -1,14 +1,16 @@
import uuid import uuid
import structlog
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course.models import CourseSessionUser
from vbv_lernwelt.notify.email.email_services import EmailTemplate
from vbv_lernwelt.notify.services import NotificationService from vbv_lernwelt.notify.services import NotificationService
logger = structlog.get_logger(__name__)
class FeedbackIntegerField(models.IntegerField): class FeedbackIntegerField(models.IntegerField):
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -46,20 +48,31 @@ class FeedbackResponse(models.Model):
HUNDRED = 100, "100%" HUNDRED = 100, "100%"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self._state.adding: # with `id=UUIDField` it is always set...
# with `id=UUIDField` it is always set... create_new = self._state.adding
course_session_users = CourseSessionUser.objects.filter(
role="EXPERT", course_session=self.course_session, expert=self.circle
)
for csu in course_session_users:
NotificationService.send_information_notification(
recipient=csu.user,
verb=f"{_('New feedback for circle')} {self.circle.title}",
target_url=f"/course/{self.course_session.course.slug}/cockpit/feedback/{self.circle_id}/",
email_template=EmailTemplate.NEW_FEEDBACK,
)
super(FeedbackResponse, self).save(*args, **kwargs) super(FeedbackResponse, self).save(*args, **kwargs)
try:
if create_new:
# with `id=UUIDField` it is always set...
course_session_users = CourseSessionUser.objects.filter(
role="EXPERT",
course_session=self.course_session,
expert=self.circle,
)
for csu in course_session_users:
NotificationService.send_new_feedback_notification(
recipient=csu.user,
feedback_response=self,
)
except Exception:
logger.exception(
"Failed to send feedback notification",
exc_info=True,
label="feedback_notification",
)
data = models.JSONField(default=dict) data = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)

View File

@ -8,7 +8,11 @@ from vbv_lernwelt.course.models import CourseSession, CourseSessionUser
from vbv_lernwelt.feedback.factories import FeedbackResponseFactory from vbv_lernwelt.feedback.factories import FeedbackResponseFactory
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse
from vbv_lernwelt.learnpath.models import Circle from vbv_lernwelt.learnpath.models import Circle
from vbv_lernwelt.notify.models import Notification from vbv_lernwelt.notify.models import (
Notification,
NotificationCategory,
NotificationTrigger,
)
class FeedbackApiBaseTestCase(APITestCase): class FeedbackApiBaseTestCase(APITestCase):
@ -62,20 +66,28 @@ class FeedbackSummaryApiTestCase(FeedbackApiBaseTestCase):
basis_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen") basis_circle = Circle.objects.get(slug="test-lehrgang-lp-circle-reisen")
csu.expert.add(basis_circle) csu.expert.add(basis_circle)
FeedbackResponse.objects.create( feedback = FeedbackResponse.objects.create(
circle=basis_circle, course_session=csu.course_session circle=basis_circle, course_session=csu.course_session
) )
notifications = Notification.objects.all() self.assertEqual(Notification.objects.count(), 1)
self.assertEqual(len(notifications), 1) notification = Notification.objects.first()
self.assertEqual(notifications[0].recipient, expert) self.assertEqual(notification.recipient, expert)
self.assertEqual( self.assertEqual(
notifications[0].verb, f"New feedback for circle {basis_circle.title}" notification.verb, f"New feedback for circle {basis_circle.title}"
) )
self.assertEqual( self.assertEqual(
notifications[0].target_url, notification.target_url,
f"/course/{self.course_session.course.slug}/cockpit/feedback/{basis_circle.id}/", f"/course/{self.course_session.course.slug}/cockpit/feedback/{basis_circle.id}/",
) )
self.assertEqual(
notification.notification_category, NotificationCategory.INFORMATION
)
self.assertEqual(
notification.notification_trigger, NotificationTrigger.NEW_FEEDBACK
)
self.assertEqual(notification.action_object, feedback)
self.assertEqual(notification.course_session, csu.course_session)
def test_triggers_notification_only_on_create(self): def test_triggers_notification_only_on_create(self):
expert = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch") expert = User.objects.get(username="patrizia.huggel@eiger-versicherungen.ch")

View File

@ -24,7 +24,7 @@ from vbv_lernwelt.learnpath.models import (
LearningContentAttendanceCourse, LearningContentAttendanceCourse,
LearningContentEdoniqTest, LearningContentEdoniqTest,
) )
from vbv_lernwelt.notify.models import NotificationType from vbv_lernwelt.notify.models import NotificationCategory
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -713,7 +713,10 @@ def sync_students_from_t2l(data):
def update_user_json_data(user: User, data: Dict[str, Any]): def update_user_json_data(user: User, data: Dict[str, Any]):
# Set E-Mail notification settings for new users # Set E-Mail notification settings for new users
user.additional_json_data = user.additional_json_data | sanitize_json_data_input( user.additional_json_data = user.additional_json_data | sanitize_json_data_input(
{**data, "email_notification_types": [str(NotificationType.INFORMATION)]} {
**data,
"email_notification_categories": [str(NotificationCategory.INFORMATION)],
}
) )

View File

@ -52,7 +52,7 @@ class CreateOrUpdateStudentTestCase(TestCase):
"Lehrvertragsnummer": "1234", "Lehrvertragsnummer": "1234",
"Tel. Privat": "079 593 83 43", "Tel. Privat": "079 593 83 43",
"Geburtsdatum": "01.01.2000", "Geburtsdatum": "01.01.2000",
"email_notification_types": ["INFORMATION"], "email_notification_categories": ["INFORMATION"],
} }
def test_create_student(self): def test_create_student(self):

View File

@ -5,20 +5,26 @@ from vbv_lernwelt.core.admin import LogAdmin
class CustomNotificationAdmin(LogAdmin): class CustomNotificationAdmin(LogAdmin):
date_hierarchy = "timestamp" date_hierarchy = "timestamp"
raw_id_fields = ("recipient",) raw_id_fields = ("recipient",)
search_fields = (("recipient__username"),)
list_display = ( list_display = (
"timestamp",
"recipient", "recipient",
"actor", "actor",
"notification_type", "notification_category",
"notification_trigger",
"course_session",
"emailed", "emailed",
"unread", "unread",
) )
list_filter = ( list_filter = (
"notification_type", "notification_category",
"notification_trigger",
"emailed", "emailed",
"unread", "unread",
"timestamp", "timestamp",
"level", # "level",
"public", # "public",
"course_session",
) )
def get_queryset(self, request): def get_queryset(self, request):

View File

@ -7,12 +7,12 @@ class NotifyConfig(AppConfig):
name = "vbv_lernwelt.notify" name = "vbv_lernwelt.notify"
def ready(self): def ready(self):
# Move the admin import here to avoid early imports
from .admin import CustomNotificationAdmin
# Unregister the default Notification admin if it exists # Unregister the default Notification admin if it exists
from vbv_lernwelt.notify.models import Notification from vbv_lernwelt.notify.models import Notification
# Move the admin import here to avoid early imports
from .admin import CustomNotificationAdmin
if admin.site.is_registered(Notification): if admin.site.is_registered(Notification):
admin.site.unregister(Notification) admin.site.unregister(Notification)

View File

@ -3,7 +3,7 @@ from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.admin import User
from vbv_lernwelt.notify.models import NotificationType from vbv_lernwelt.notify.models import NotificationCategory
from vbv_lernwelt.notify.tests.factories import NotificationFactory from vbv_lernwelt.notify.tests.factories import NotificationFactory
@ -28,8 +28,7 @@ def create_default_notifications() -> int:
verb="Alexandra hat einen neuen Beitrag erfasst", verb="Alexandra hat einen neuen Beitrag erfasst",
actor_avatar_url=avatar_urls[0], actor_avatar_url=avatar_urls[0],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[0], timestamp=timestamps[0],
), ),
NotificationFactory( NotificationFactory(
@ -38,8 +37,7 @@ def create_default_notifications() -> int:
verb="Alexandra hat einen neuen Beitrag erfasst", verb="Alexandra hat einen neuen Beitrag erfasst",
actor_avatar_url=avatar_urls[0], actor_avatar_url=avatar_urls[0],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[1], timestamp=timestamps[1],
), ),
NotificationFactory( NotificationFactory(
@ -48,8 +46,7 @@ def create_default_notifications() -> int:
verb="Alexandra hat einen neuen Beitrag erfasst", verb="Alexandra hat einen neuen Beitrag erfasst",
actor_avatar_url=avatar_urls[0], actor_avatar_url=avatar_urls[0],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[2], timestamp=timestamps[2],
), ),
NotificationFactory( NotificationFactory(
@ -58,8 +55,7 @@ def create_default_notifications() -> int:
verb="Alexandra hat einen neuen Beitrag erfasst", verb="Alexandra hat einen neuen Beitrag erfasst",
actor_avatar_url=avatar_urls[0], actor_avatar_url=avatar_urls[0],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[3], timestamp=timestamps[3],
), ),
NotificationFactory( NotificationFactory(
@ -68,8 +64,7 @@ def create_default_notifications() -> int:
verb="Bianca hat für den Auftrag Autoversicherung 3 eine Lösung abgegeben", verb="Bianca hat für den Auftrag Autoversicherung 3 eine Lösung abgegeben",
actor_avatar_url=avatar_urls[1], actor_avatar_url=avatar_urls[1],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[4], timestamp=timestamps[4],
), ),
NotificationFactory( NotificationFactory(
@ -78,8 +73,7 @@ def create_default_notifications() -> int:
verb="Bianca hat für den Auftrag Autoversicherung 1 eine Lösung abgegeben", verb="Bianca hat für den Auftrag Autoversicherung 1 eine Lösung abgegeben",
actor_avatar_url=avatar_urls[1], actor_avatar_url=avatar_urls[1],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[5], timestamp=timestamps[5],
), ),
NotificationFactory( NotificationFactory(
@ -88,8 +82,7 @@ def create_default_notifications() -> int:
verb="Bianca hat für den Auftrag Autoversicherung 2 eine Lösung abgegeben", verb="Bianca hat für den Auftrag Autoversicherung 2 eine Lösung abgegeben",
actor_avatar_url=avatar_urls[1], actor_avatar_url=avatar_urls[1],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[6], timestamp=timestamps[6],
), ),
NotificationFactory( NotificationFactory(
@ -98,8 +91,7 @@ def create_default_notifications() -> int:
verb="Bianca hat für den Auftrag Autoversicherung 4 eine Lösung abgegeben", verb="Bianca hat für den Auftrag Autoversicherung 4 eine Lösung abgegeben",
actor_avatar_url=avatar_urls[1], actor_avatar_url=avatar_urls[1],
target_url="/", target_url="/",
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[7], timestamp=timestamps[7],
), ),
NotificationFactory( NotificationFactory(
@ -108,8 +100,7 @@ def create_default_notifications() -> int:
verb="Chantal hat eine Bewertung für den Transferauftrag 3 eingegeben", verb="Chantal hat eine Bewertung für den Transferauftrag 3 eingegeben",
target_url="/", target_url="/",
actor_avatar_url=avatar_urls[2], actor_avatar_url=avatar_urls[2],
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[8], timestamp=timestamps[8],
), ),
NotificationFactory( NotificationFactory(
@ -118,8 +109,7 @@ def create_default_notifications() -> int:
verb="Chantal hat eine Bewertung für den Transferauftrag 4 eingegeben", verb="Chantal hat eine Bewertung für den Transferauftrag 4 eingegeben",
target_url="/", target_url="/",
actor_avatar_url=avatar_urls[2], actor_avatar_url=avatar_urls[2],
notification_type=NotificationType.USER_INTERACTION, notification_category=NotificationCategory.USER_INTERACTION,
course="Versicherungsvermittler/-in",
timestamp=timestamps[9], timestamp=timestamps[9],
), ),
NotificationFactory( NotificationFactory(
@ -127,22 +117,21 @@ def create_default_notifications() -> int:
actor=user, actor=user,
verb="Super, du kommst in deinem Lernpfad gut voran. Schaue dir jetzt die verfügbaren Prüfungstermine an.", verb="Super, du kommst in deinem Lernpfad gut voran. Schaue dir jetzt die verfügbaren Prüfungstermine an.",
target_url="/", target_url="/",
notification_type=NotificationType.PROGRESS, notification_category=NotificationCategory.PROGRESS,
course="Versicherungsvermittler/-in",
timestamp=timestamps[10], timestamp=timestamps[10],
), ),
NotificationFactory( NotificationFactory(
recipient=user, recipient=user,
actor=user, actor=user,
verb="Wartungsarbeiten: 20.12.2022 08:00 - 12:00", verb="Wartungsarbeiten: 20.12.2022 08:00 - 12:00",
notification_type=NotificationType.INFORMATION, notification_category=NotificationCategory.INFORMATION,
timestamp=timestamps[11], timestamp=timestamps[11],
), ),
NotificationFactory( NotificationFactory(
recipient=user, recipient=user,
actor=user, actor=user,
verb="Wartungsarbeiten: 31.01.2023 08:00 - 12:00", verb="Wartungsarbeiten: 31.01.2023 08:00 - 12:00",
notification_type=NotificationType.INFORMATION, notification_category=NotificationCategory.INFORMATION,
timestamp=timestamps[12], timestamp=timestamps[12],
), ),
) )

View File

@ -3,16 +3,10 @@ from datetime import timedelta
import structlog import structlog
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from vbv_lernwelt.core.base import LoggedCommand 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.models import CourseSessionUser
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse 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 from vbv_lernwelt.notify.services import NotificationService
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@ -20,20 +14,6 @@ logger = structlog.get_logger(__name__)
PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2) 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(): def attendance_course_reminder_notification_job():
"""Checks if an attendance course is coming up and sends a reminder to the participants""" """Checks if an attendance course is coming up and sends a reminder to the participants"""
start = timezone.now() start = timezone.now()
@ -63,7 +43,7 @@ def attendance_course_reminder_notification_job():
course_session_id=cs_id, course_session_id=cs_id,
) )
for user in csu: for user in csu:
result = send_attendance_course_reminder_notification( result = NotificationService.send_attendance_course_reminder_notification(
user.user, attendance_course user.user, attendance_course
) )
results_counter[result] += 1 results_counter[result] += 1

View File

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

View File

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

View File

@ -2,25 +2,43 @@ from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from notifications.base.models import AbstractNotification 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") USER_INTERACTION = "USER_INTERACTION", _("User Interaction")
PROGRESS = "PROGRESS", _("Progress") PROGRESS = "PROGRESS", _("Progress")
INFORMATION = "INFORMATION", _("Information") 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): class Notification(AbstractNotification):
# UUIDs are not supported by the notifications app... # UUIDs are not supported by the notifications app...
# id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) # id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
notification_type = models.CharField( notification_category = models.CharField(
max_length=32, max_length=255,
choices=NotificationType.choices, choices=NotificationCategory.choices,
default=NotificationType.INFORMATION, 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): class Meta(AbstractNotification.Meta):
abstract = False abstract = False

View File

@ -1,74 +1,121 @@
from typing import Optional from __future__ import annotations
from gettext import gettext
from typing import TYPE_CHECKING
import structlog import structlog
from django.db.models import Model
from notifications.signals import notify from notifications.signals import notify
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.notify.email.email_services import EmailTemplate, send_email from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.notify.models import Notification, NotificationType 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__) logger = structlog.get_logger(__name__)
class NotificationService: class NotificationService:
@classmethod @classmethod
def send_user_interaction_notification( def send_assignment_submitted_notification(
cls, cls, recipient: User, sender: User, assignment_completion: AssignmentCompletion
recipient: User, ):
verb: str, verb = gettext(
sender: User, "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben."
course: str, ) % {
target_url: str, "sender": sender.get_full_name(),
email_template: EmailTemplate, "assignment_title": assignment_completion.assignment.title,
template_data: dict = None, }
) -> str:
return cls._send_notification( return cls._send_notification(
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
notification_category=NotificationCategory.USER_INTERACTION,
notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED,
sender=sender, sender=sender,
course=course, target_url=assignment_completion.get_assignment_evaluation_frontend_url(),
target_url=target_url, course_session=assignment_completion.course_session,
notification_type=NotificationType.USER_INTERACTION, action_object=assignment_completion,
email_template=email_template, email_template=EmailTemplate.CASEWORK_SUBMITTED,
template_data=template_data,
) )
@classmethod @classmethod
def send_progress_notification( def send_assignment_evaluated_notification(
cls, cls,
recipient: User, recipient: User,
verb: str, sender: User,
course: str, assignment_completion: AssignmentCompletion,
target_url: str, target_url: str,
email_template: EmailTemplate, ):
template_data: dict = None, verb = gettext(
) -> str: "%(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( return cls._send_notification(
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
course=course, notification_category=NotificationCategory.USER_INTERACTION,
notification_trigger=NotificationTrigger.CASEWORK_EVALUATED,
sender=sender,
target_url=target_url, target_url=target_url,
notification_type=NotificationType.PROGRESS, course_session=assignment_completion.course_session,
email_template=email_template, action_object=assignment_completion,
template_data=template_data, email_template=EmailTemplate.CASEWORK_EVALUATED,
) )
@classmethod @classmethod
def send_information_notification( def send_new_feedback_notification(
cls, cls,
recipient: User, recipient: User,
verb: str, feedback_response: FeedbackResponse,
target_url: str, ):
email_template: EmailTemplate, verb = f"New feedback for circle {feedback_response.circle.title}"
template_data: dict = None,
) -> str:
return cls._send_notification( return cls._send_notification(
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
target_url=target_url, notification_category=NotificationCategory.INFORMATION,
notification_type=NotificationType.INFORMATION, notification_trigger=NotificationTrigger.NEW_FEEDBACK,
email_template=email_template, target_url=f"/course/{feedback_response.course_session.course.slug}/cockpit/feedback/{feedback_response.circle_id}/",
template_data=template_data, 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 @classmethod
@ -76,20 +123,24 @@ class NotificationService:
cls, cls,
recipient: User, recipient: User,
verb: str, verb: str,
notification_type: NotificationType, notification_category: NotificationCategory,
email_template: EmailTemplate, notification_trigger: NotificationTrigger,
template_data: dict | None = None,
sender: User | None = None, sender: User | None = None,
course: str | None = None, action_object: Model | None = None,
target_url: str | 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, fail_silently: bool = True,
) -> str: ) -> str:
if template_data is None: if template_data is None:
template_data = {} 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: if not sender:
sender = User.objects.get(email="admin") sender = User.objects.get(email="admin")
else: else:
@ -98,8 +149,9 @@ class NotificationService:
recipient=recipient.email, recipient=recipient.email,
sender=sender.email, sender=sender.email,
verb=verb, verb=verb,
notification_type=notification_type, notification_category=notification_category,
course=course, notification_trigger=notification_trigger,
course_session=course_session.title if course_session else "",
target_url=target_url, target_url=target_url,
template_data=template_data, template_data=template_data,
) )
@ -108,16 +160,20 @@ class NotificationService:
notification = NotificationService._find_duplicate_notification( notification = NotificationService._find_duplicate_notification(
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
notification_type=notification_type, notification_category=notification_category,
notification_trigger=notification_trigger,
target_url=target_url, target_url=target_url,
template_name=email_template.name, course_session=course_session,
template_data=template_data,
) )
emailed = False emailed = False
if notification and notification.emailed: if notification and notification.emailed:
emailed = True 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") log.debug("Try to send email")
try: try:
emailed = cls._send_email( emailed = cls._send_email(
@ -153,16 +209,18 @@ class NotificationService:
sender=sender, sender=sender,
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
action_object=action_object,
emailed=emailed, emailed=emailed,
# The metadata is saved in the 'data' member of the AbstractNotification model # The extra arguments are saved in the 'data' member
email_template=email_template.name, email_template=email_template.name if email_template else "",
template_data=template_data, template_data=template_data,
) )
sent_notification: Notification = response[0][1][0] # 🫨 sent_notification: Notification = response[0][1][0] # 🫨
sent_notification.target_url = target_url sent_notification.target_url = target_url
sent_notification.notification_type = notification_type sent_notification.notification_category = notification_category
sent_notification.course = course sent_notification.notification_trigger = notification_trigger
sent_notification.course_session = course_session
sent_notification.actor_avatar_url = actor_avatar_url sent_notification.actor_avatar_url = actor_avatar_url
sent_notification.save() sent_notification.save()
log.info("Notification sent successfully", emailed=emailed) log.info("Notification sent successfully", emailed=emailed)
@ -181,10 +239,10 @@ class NotificationService:
@staticmethod @staticmethod
def _should_send_email( def _should_send_email(
notification_type: NotificationType, recipient: User notification_category: NotificationCategory, recipient: User
) -> bool: ) -> bool:
return str(notification_type) in recipient.additional_json_data.get( return str(notification_category) in recipient.additional_json_data.get(
"email_notification_types", [] "email_notification_categories", []
) )
@staticmethod @staticmethod
@ -206,10 +264,10 @@ class NotificationService:
def _find_duplicate_notification( def _find_duplicate_notification(
recipient: User, recipient: User,
verb: str, verb: str,
notification_type: NotificationType, notification_category: NotificationCategory,
template_name: str, notification_trigger: NotificationTrigger,
template_data: dict,
target_url: str | None, target_url: str | None,
course_session: CourseSession | None,
) -> Notification | None: ) -> Notification | None:
"""Check if a notification with the same parameters has already been sent to the recipient. """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. 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( return Notification.objects.filter(
recipient=recipient, recipient=recipient,
verb=verb, verb=verb,
notification_type=notification_type, notification_category=notification_category,
notification_trigger=notification_trigger,
target_url=target_url, target_url=target_url,
data={ course_session=course_session,
"email_template": template_name,
"template_data": template_data,
},
).first() ).first()

View File

@ -4,7 +4,7 @@ from rest_framework.test import APITestCase
from vbv_lernwelt.core.admin import User from vbv_lernwelt.core.admin import User
from vbv_lernwelt.core.tests.factories import UserFactory 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 from vbv_lernwelt.notify.tests.factories import NotificationFactory
@ -115,7 +115,7 @@ class TestNotificationSettingsApi(APITestCase):
def test_store_retrieve_settings(self): def test_store_retrieve_settings(self):
notification_settings = json.dumps( notification_settings = json.dumps(
[NotificationType.INFORMATION, NotificationType.PROGRESS] [NotificationCategory.INFORMATION, NotificationCategory.PROGRESS]
) )
api_path = "/api/notify/email_notification_settings/" api_path = "/api/notify/email_notification_settings/"
@ -128,7 +128,7 @@ class TestNotificationSettingsApi(APITestCase):
self.assertEqual(response.json(), notification_settings) self.assertEqual(response.json(), notification_settings)
self.user.refresh_from_db() self.user.refresh_from_db()
self.assertEqual( self.assertEqual(
self.user.additional_json_data["email_notification_types"], self.user.additional_json_data["email_notification_categories"],
notification_settings, notification_settings,
) )

View File

@ -1,45 +1,21 @@
from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch
from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from freezegun import freeze_time from freezegun import freeze_time
from vbv_lernwelt.core.constants import TEST_COURSE_SESSION_BERN_ID 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.core.create_default_users import create_default_users
from vbv_lernwelt.course.creators.test_course import create_test_course 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.course_session.models import CourseSessionAttendanceCourse
from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse 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 ( from vbv_lernwelt.notify.management.commands.send_attendance_course_reminders import (
attendance_course_reminder_notification_job, 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): class TestAttendanceCourseReminders(TestCase):
def setUp(self): def setUp(self):
create_default_users() create_default_users()
@ -57,13 +33,12 @@ class TestAttendanceCourseReminders(TestCase):
location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern",
trainer="Roland Grossenbacher", trainer="Roland Grossenbacher",
) )
in_one_week = datetime.now() + timedelta(weeks=1) ref_date_time = timezone.make_aware(datetime(2023, 8, 29))
self.csac.due_date.start = timezone.make_aware( self.csac.due_date.start = ref_date_time.replace(
in_one_week.replace(hour=7, minute=30, second=0, microsecond=0) hour=7, minute=30, second=0, microsecond=0
) )
self.csac.due_date.end = ref_date_time.replace(
self.csac.due_date.end = timezone.make_aware( hour=16, minute=15, second=0, microsecond=0
in_one_week.replace(hour=16, minute=15, second=0, microsecond=0)
) )
self.csac.due_date.save() self.csac.due_date.save()
@ -89,41 +64,53 @@ class TestAttendanceCourseReminders(TestCase):
self.csac_future.due_date.save() self.csac_future.due_date.save()
attendance_course_reminder_notification_job() attendance_course_reminder_notification_job()
self.assertEquals(3, len(sent_notifications))
recipients = CourseSessionUser.objects.filter( self.assertEquals(3, len(Notification.objects.all()))
course_session_id=self.csac.course_session.id notification = Notification.objects.get(
) recipient__username="test-student1@example.com"
self.assertEquals(
set(map(lambda n: n.recipient, sent_notifications)),
set(map(lambda csu: csu.user, recipients)),
) )
for notification in sent_notifications: self.assertEquals(
self.assertEquals( "Erinnerung: Bald findet ein Präsenzkurs statt",
_("Erinnerung: Bald findet ein Präsenzkurs statt"), notification.verb,
notification.verb, )
) self.assertEquals(
self.assertEquals( "INFORMATION",
"/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug", notification.notification_category,
notification.target_url, )
) self.assertEquals(
self.assertEquals( "ATTENDANCE_COURSE_REMINDER",
self.csac.learning_content.title, notification.notification_trigger,
notification.template_data["attendance_course"], )
) self.assertEquals(
self.assertEquals( self.csac,
self.csac.location, notification.action_object,
notification.template_data["location"], )
) self.assertEquals(
self.assertEquals( self.csac.course_session,
self.csac.trainer, notification.course_session,
notification.template_data["trainer"], )
) self.assertEquals(
self.assertEquals( "/course/test-lehrgang/learn/fahrzeug/präsenzkurs-fahrzeug",
self.csac.due_date.start.strftime("%d.%m.%Y %H:%M"), notification.target_url,
notification.template_data["start"], )
) self.assertEquals(
self.assertEquals( self.csac.learning_content.title,
self.csac.due_date.end.strftime("%d.%m.%Y %H:%M"), notification.data["template_data"]["attendance_course"],
notification.template_data["end"], )
) 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"],
)

View File

@ -5,7 +5,11 @@ from django.test import TestCase
from vbv_lernwelt.core.models import User from vbv_lernwelt.core.models import User
from vbv_lernwelt.core.tests.factories import UserFactory from vbv_lernwelt.core.tests.factories import UserFactory
from vbv_lernwelt.notify.email.email_services import EmailTemplate 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 from vbv_lernwelt.notify.services import NotificationService
@ -24,142 +28,178 @@ class TestNotificationService(TestCase):
self.admin = UserFactory(username="admin", email="admin") self.admin = UserFactory(username="admin", email="admin")
self.sender_username = "Bob" 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.sender = User.objects.get(username=self.sender_username)
self.recipient_username = "Alice" 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 = User.objects.get(username=self.recipient_username)
self.recipient.additional_json_data["email_notification_types"] = json.dumps(
["USER_INTERACTION", "INFORMATION"]
)
self.recipient.save() self.recipient.save()
self.client.login(username=self.recipient, password="pw") self.client.login(username=self.recipient, password="pw")
def test_send_information_notification(self): def test_send_notification_without_email(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):
verb = "Anne hat deinen Auftrag bewertet" verb = "Anne hat deinen Auftrag bewertet"
target_url = "https://www.vbv.ch" target_url = "https://www.vbv.ch"
course = "Versicherungsvermittler/in" result = self.notification_service._send_notification(
self.notification_service.send_user_interaction_notification(
sender=self.sender, sender=self.sender,
recipient=self.recipient, recipient=self.recipient,
verb=verb, verb=verb,
target_url=target_url, target_url=target_url,
course=course, notification_category=NotificationCategory.USER_INTERACTION,
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED,
fail_silently=False,
) )
self.assertEqual(result, "USER_INTERACTION_CASEWORK_SUBMITTED_success")
self.assertEqual(1, Notification.objects.count()) self.assertEqual(1, Notification.objects.count())
notification: Notification = Notification.objects.first() notification: Notification = Notification.objects.first()
self.assertEqual(self.sender, notification.actor) self.assertEqual(self.sender, notification.actor)
self.assertEqual(verb, notification.verb) self.assertEqual(verb, notification.verb)
self.assertEqual(target_url, notification.target_url) self.assertEqual(target_url, notification.target_url)
self.assertEqual(course, notification.course)
self.assertEqual( 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) self.assertTrue(notification.emailed)
def test_does_not_send_duplicate_notification(self): def test_does_not_send_duplicate_notification(self):
verb = "Anne hat deinen Auftrag bewertet" verb = "Anne hat deinen Auftrag bewertet"
target_url = "https://www.vbv.ch" target_url = "https://www.vbv.ch"
course = "Versicherungsvermittler/in"
for i in range(2): result = self.notification_service._send_notification(
self.notification_service.send_user_interaction_notification( sender=self.sender,
sender=self.sender, recipient=self.recipient,
recipient=self.recipient, verb=verb,
verb=verb, target_url=target_url,
target_url=target_url, notification_category=NotificationCategory.USER_INTERACTION,
course=course, notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED,
email_template=EmailTemplate.ATTENDANCE_COURSE_REMINDER, email_template=EmailTemplate.CASEWORK_SUBMITTED,
template_data={ template_data={
"blah": 123, "blah": 123,
"foo": "ich habe hunger", "foo": "ich habe hunger",
}, },
) )
self.assertEqual(result, "USER_INTERACTION_CASEWORK_SUBMITTED_success")
self.assertEqual(1, Notification.objects.count()) result = self.notification_service._send_notification(
notification: Notification = Notification.objects.first() sender=self.sender,
self.assertEqual(self.sender, notification.actor) recipient=self.recipient,
self.assertEqual(verb, notification.verb) verb=verb,
self.assertEqual(target_url, notification.target_url) target_url=target_url,
self.assertEqual(course, notification.course) notification_category=NotificationCategory.USER_INTERACTION,
self.assertEqual( notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED,
str(NotificationType.USER_INTERACTION), email_template=EmailTemplate.CASEWORK_SUBMITTED,
notification.notification_type, template_data={
) "blah": 123,
self.assertTrue(notification.emailed) "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): def test_only_sends_email_if_enabled(self):
# Assert no mail is sent if corresponding email notification type is not enabled # Assert no mail is sent if corresponding email notification type is not enabled
self.recipient.additional_json_data["email_notification_types"] = json.dumps( self.notification_service._send_notification(
["INFORMATION"]
)
self.recipient.save()
self.notification_service.send_user_interaction_notification(
sender=self.sender, sender=self.sender,
recipient=self.recipient, recipient=self.recipient,
verb="should not be sent", verb="should not be sent",
target_url="", target_url="",
course="", notification_category=NotificationCategory.USER_INTERACTION,
email_template=EmailTemplate.CASEWORK_EVALUATED, notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED,
email_template=EmailTemplate.CASEWORK_SUBMITTED,
template_data={}, template_data={},
) )
self.assertEqual(1, Notification.objects.count()) self.assertEqual(1, Notification.objects.count())
self.assertFalse(self._has_sent_emails()) self.assertFalse(self._has_sent_emails())
# Assert mail is sent if corresponding email notification type is enabled # Assert mail is sent if corresponding email notification type is enabled
self.recipient.additional_json_data["email_notification_types"] = json.dumps( self.recipient.additional_json_data[
["USER_INTERACTION"] "email_notification_categories"
) ] = json.dumps(["USER_INTERACTION"])
self.recipient.save() self.recipient.save()
self.notification_service.send_user_interaction_notification( self.notification_service._send_notification(
sender=self.sender, sender=self.sender,
recipient=self.recipient, recipient=self.recipient,
verb="should be sent", verb="should be sent",
target_url="", target_url="",
course="", notification_category=NotificationCategory.USER_INTERACTION,
email_template=EmailTemplate.CASEWORK_EVALUATED, notification_trigger=NotificationTrigger.CASEWORK_SUBMITTED,
email_template=EmailTemplate.CASEWORK_SUBMITTED,
template_data={}, template_data={},
) )
self.assertEqual(2, Notification.objects.count()) self.assertEqual(2, Notification.objects.count())

View File

@ -4,11 +4,11 @@ from rest_framework.response import Response
@api_view(["POST", "GET"]) @api_view(["POST", "GET"])
def email_notification_settings(request): def email_notification_settings(request):
EMAIL_NOTIFICATION_TYPES = "email_notification_types" EMAIL_NOTIFICATION_CATEGORIES = "email_notification_categories"
if request.method == "POST": 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() request.user.save()
return Response( return Response(
status=200, status=200,
data=request.user.additional_json_data.get(EMAIL_NOTIFICATION_TYPES, []), data=request.user.additional_json_data.get(EMAIL_NOTIFICATION_CATEGORIES, []),
) )