From e44dc5e31d720abf7b170321e203e16ef5a61648 Mon Sep 17 00:00:00 2001 From: Livio Bieri Date: Mon, 9 Oct 2023 14:10:04 +0200 Subject: [PATCH] feat: assignment reminder mails --- .../notify/email/email_services.py | 4 +- .../send_assigment_course_reminders.py | 9 +- ...alter_notification_notification_trigger.py | 23 +- server/vbv_lernwelt/notify/services.py | 2 +- .../test_send_assigment_course_reminders.py | 256 +++++++++++++++--- 5 files changed, 246 insertions(+), 48 deletions(-) diff --git a/server/vbv_lernwelt/notify/email/email_services.py b/server/vbv_lernwelt/notify/email/email_services.py index 9df03411..525133d5 100644 --- a/server/vbv_lernwelt/notify/email/email_services.py +++ b/server/vbv_lernwelt/notify/email/email_services.py @@ -6,9 +6,7 @@ from django.conf import settings from django.utils import timezone from sendgrid import Mail, SendGridAPIClient -from vbv_lernwelt.course_session.models import ( - CourseSessionAttendanceCourse, -) +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse logger = structlog.get_logger(__name__) diff --git a/server/vbv_lernwelt/notify/management/commands/send_assigment_course_reminders.py b/server/vbv_lernwelt/notify/management/commands/send_assigment_course_reminders.py index cc79214b..5cb5c59f 100644 --- a/server/vbv_lernwelt/notify/management/commands/send_assigment_course_reminders.py +++ b/server/vbv_lernwelt/notify/management/commands/send_assigment_course_reminders.py @@ -14,7 +14,6 @@ from vbv_lernwelt.notify.services import NotificationService logger = structlog.get_logger(__name__) -PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2) ASSIGNMENT_REMINDER_LEAD_TIME = timedelta(days=2) @@ -42,10 +41,10 @@ def assignment_reminder_members_notification_job(): ) ) - # member notifications (EDONIQ) + # member notifications (EDONIQ_TEST) for assignment in CourseSessionEdoniqTest.objects.filter( - submission_deadline__start__lte=end, - submission_deadline__start__gte=start, + deadline__start__lte=end, + deadline__start__gte=start, ): for member in CourseSessionUser.objects.filter( course_session_id=assignment.course_session.id, @@ -79,7 +78,7 @@ def assignment_reminder_members_notification_job(): class Command(LoggedCommand): - help = "Sends assigments course reminder notifications to participants (submission deadline) AND experts (evaluation deadline)" + help = "Sends assignments course reminder notifications to participants and experts" def handle(self, *args, **options): results = assignment_reminder_members_notification_job() diff --git a/server/vbv_lernwelt/notify/migrations/0005_alter_notification_notification_trigger.py b/server/vbv_lernwelt/notify/migrations/0005_alter_notification_notification_trigger.py index 9d0b9565..807f65e8 100644 --- a/server/vbv_lernwelt/notify/migrations/0005_alter_notification_notification_trigger.py +++ b/server/vbv_lernwelt/notify/migrations/0005_alter_notification_notification_trigger.py @@ -4,15 +4,28 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('notify', '0004_alter_notification_notification_trigger'), + ("notify", "0004_alter_notification_notification_trigger"), ] operations = [ migrations.AlterField( - model_name='notification', - name='notification_trigger', - field=models.CharField(choices=[('ATTENDANCE_COURSE_REMINDER', 'Attendance Course Reminder'), ('ASSIGNMENT_REMINDER', 'Assignment Reminder'), ('CASEWORK_EXPERT_EVALUATION_REMINDER', 'Casework Expert Evaluation Reminder'), ('CASEWORK_SUBMITTED', 'Casework Submitted'), ('CASEWORK_EVALUATED', 'Casework Evaluated'), ('NEW_FEEDBACK', 'New Feedback')], default='', max_length=255), + model_name="notification", + name="notification_trigger", + field=models.CharField( + choices=[ + ("ATTENDANCE_COURSE_REMINDER", "Attendance Course Reminder"), + ("ASSIGNMENT_REMINDER", "Assignment Reminder"), + ( + "CASEWORK_EXPERT_EVALUATION_REMINDER", + "Casework Expert Evaluation Reminder", + ), + ("CASEWORK_SUBMITTED", "Casework Submitted"), + ("CASEWORK_EVALUATED", "Casework Evaluated"), + ("NEW_FEEDBACK", "New Feedback"), + ], + default="", + max_length=255, + ), ), ] diff --git a/server/vbv_lernwelt/notify/services.py b/server/vbv_lernwelt/notify/services.py index e30b993f..3239754e 100644 --- a/server/vbv_lernwelt/notify/services.py +++ b/server/vbv_lernwelt/notify/services.py @@ -22,8 +22,8 @@ from vbv_lernwelt.notify.models import ( if TYPE_CHECKING: from vbv_lernwelt.assignment.models import AssignmentCompletion from vbv_lernwelt.course_session.models import ( - CourseSessionAttendanceCourse, CourseSessionAssignment, + CourseSessionAttendanceCourse, CourseSessionEdoniqTest, ) from vbv_lernwelt.feedback.models import FeedbackResponse diff --git a/server/vbv_lernwelt/notify/tests/test_send_assigment_course_reminders.py b/server/vbv_lernwelt/notify/tests/test_send_assigment_course_reminders.py index 3e4f182b..c8fef4b1 100644 --- a/server/vbv_lernwelt/notify/tests/test_send_assigment_course_reminders.py +++ b/server/vbv_lernwelt/notify/tests/test_send_assigment_course_reminders.py @@ -1,53 +1,241 @@ from datetime import datetime +from typing import Dict from django.test import TestCase from django.utils import timezone from freezegun import freeze_time +from vbv_lernwelt.assignment.models import AssignmentType 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 -from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse -from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.learnpath.models import ( + LearningContentAssignment, + LearningContentEdoniqTest, +) +from vbv_lernwelt.notify.management.commands.send_assigment_course_reminders import ( + assignment_reminder_members_notification_job, +) +from vbv_lernwelt.notify.models import Notification + +EXPECTED_MEMBER_VERB = "Erinnerung: Bald ist ein Abgabetermin" +EXPECTED_EXPERT_VERB = "Erinnerung: Bald ist ein Bewertungstermin" + +RECIPIENT_TRAINER = "test-trainer1@example.com" +RECIPIENT_STUDENTS = [ + "test-student1@example.com", + "test-student2@example.com", + "test-student3@example.com", +] -class TestAttendanceCourseReminders(TestCase): +ASSIGNMENT_TYPE_LEARNING_CONTENT_LOOKUP: Dict[AssignmentType, str] = { + AssignmentType.CONDITION_ACCEPTANCE: "test-lehrgang-lp-circle-fahrzeug-lc-redlichkeitserklärung", + AssignmentType.PREP_ASSIGNMENT: "test-lehrgang-lp-circle-fahrzeug-lc-fahrzeug-mein-erstes-auto", + AssignmentType.REFLECTION: "test-lehrgang-lp-circle-fahrzeug-lc-reflexion", + AssignmentType.CASEWORK: "test-lehrgang-lp-circle-fahrzeug-lc-überprüfen-einer-motorfahrzeug-versicherungspolice", +} + + +def create_assignment( + assignment_type: AssignmentType, + submission_deadline=None, + evaluation_deadline=None, +): + assignment = CourseSessionAssignment.objects.create( + course_session=CourseSession.objects.get(id=TEST_COURSE_SESSION_BERN_ID), + learning_content=LearningContentAssignment.objects.get( + slug=ASSIGNMENT_TYPE_LEARNING_CONTENT_LOOKUP[assignment_type] + ), + ) + + assert ( + AssignmentType(assignment.learning_content.assignment_type) == assignment_type + ) + + if submission_deadline: + assignment.submission_deadline.start = submission_deadline + assignment.submission_deadline.end = None + assignment.submission_deadline.save() + + if evaluation_deadline: + assignment.evaluation_deadline.start = evaluation_deadline + assignment.evaluation_deadline.end = None + assignment.evaluation_deadline.save() + + return assignment + + +def create_edoniq_test_assignment(deadline_start): + edoniq_test = CourseSessionEdoniqTest.objects.create( + course_session=CourseSession.objects.get( + id=TEST_COURSE_SESSION_BERN_ID, + ), + learning_content=LearningContentEdoniqTest.objects.get( + slug="test-lehrgang-lp-circle-fahrzeug-lc-wissens-und-verständnisfragen" + ), + ) + + edoniq_test.deadline.start = deadline_start + edoniq_test.deadline.end = None + edoniq_test.deadline.save() + + return edoniq_test + + +class TestAssignmentCourseRemindersTest(TestCase): def setUp(self): create_default_users() create_test_course(with_sessions=True) - CourseSessionAttendanceCourse.objects.all().delete() - cs_bern = CourseSession.objects.get( - id=TEST_COURSE_SESSION_BERN_ID, - ) - self.csac = CourseSessionAttendanceCourse.objects.create( - course_session=cs_bern, - learning_content=LearningContentAttendanceCourse.objects.get( - slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" - ), - location="Handelsschule KV Bern, Zimmer 123, Eigerstrasse 16, 3012 Bern", - trainer="Roland Grossenbacher", - ) - 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 = ref_date_time.replace( - hour=16, minute=15, second=0, microsecond=0 - ) - self.csac.due_date.save() + CourseSessionAssignment.objects.all().delete() + CourseSessionEdoniqTest.objects.all().delete() - # Attendance course more than two weeks in the future - self.csac_future = CourseSessionAttendanceCourse.objects.create( - course_session=cs_bern, - learning_content=LearningContentAttendanceCourse.objects.get( - slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" - ), - location="Handelsschule BV Bern, Zimmer 122", - trainer="Thomas Berger", + Notification.objects.all().delete() + + def _assert_member_assignment_notifications( + self, action_object, expected_recipients + ): + for expected_recipient in expected_recipients: + notification = Notification.objects.get( + recipient__username=expected_recipient + ) + self.assertEquals(action_object, notification.action_object) + self.assertEquals("ASSIGNMENT_REMINDER", notification.notification_trigger) + self.assertEquals("INFORMATION", notification.notification_category) + self.assertEquals(EXPECTED_MEMBER_VERB, notification.verb) + + @freeze_time("2023-01-01") + def test_notification_edoniq(self): + # GIVEN + should_be_sent = create_edoniq_test_assignment( + deadline_start=timezone.make_aware(datetime(2023, 1, 2)) ) - @freeze_time("2023-08-25 13:02:01") - def test_happy_day(self): - assert False, "TODO" + # ...too early + create_edoniq_test_assignment( + deadline_start=timezone.make_aware(datetime(2023, 1, 4)) + ) + + # ...too late + create_edoniq_test_assignment( + deadline_start=timezone.make_aware(datetime(2022, 1, 1)) + ) + + # WHEN + assignment_reminder_members_notification_job() + + # THEN + self.assertEquals(3, len(Notification.objects.all())) + self._assert_member_assignment_notifications( + action_object=should_be_sent, + expected_recipients=RECIPIENT_STUDENTS, + ) + + with self.assertRaises(Notification.DoesNotExist): + Notification.objects.get(recipient__username=RECIPIENT_TRAINER) + + @freeze_time("2023-01-01") + def test_notification_casework_for_members(self): + # GIVEN + casework = create_assignment( + assignment_type=AssignmentType.CASEWORK, + # has a submission deadline within range -> member notification + submission_deadline=timezone.make_aware(datetime(2023, 1, 2)), + # but no evaluation deadline within range -> no expert notification + evaluation_deadline=timezone.make_aware(datetime(2023, 2, 2)), + ) + + # ...too early + create_assignment( + assignment_type=AssignmentType.CASEWORK, + submission_deadline=timezone.make_aware(datetime(2023, 1, 4)), + evaluation_deadline=timezone.make_aware(datetime(2023, 2, 2)), + ) + + # ...too late + create_assignment( + assignment_type=AssignmentType.CASEWORK, + submission_deadline=timezone.make_aware(datetime(2022, 1, 1)), + evaluation_deadline=timezone.make_aware(datetime(2022, 2, 2)), + ) + + # WHEN + assignment_reminder_members_notification_job() + + # THEN + self.assertEquals(3, len(Notification.objects.all())) + self._assert_member_assignment_notifications( + action_object=casework, + expected_recipients=RECIPIENT_STUDENTS, + ) + + with self.assertRaises(Notification.DoesNotExist): + Notification.objects.get(recipient__username=RECIPIENT_TRAINER) + + @freeze_time("2023-01-01") + def test_notification_casework_for_experts(self): + # GIVEN + casework = create_assignment( + assignment_type=AssignmentType.CASEWORK, + submission_deadline=timezone.make_aware(datetime(2022, 12, 12)), + evaluation_deadline=timezone.make_aware(datetime(2023, 1, 2)), + ) + + # WHEN + assignment_reminder_members_notification_job() + + # THEN + self.assertEquals(1, len(Notification.objects.all())) + + notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER) + self.assertEquals(casework, notification.action_object) + self.assertEquals("INFORMATION", notification.notification_category) + self.assertEquals(EXPECTED_EXPERT_VERB, notification.verb) + self.assertEquals( + casework.evaluation_deadline.url_expert, notification.target_url + ) + self.assertEquals( + "CASEWORK_EXPERT_EVALUATION_REMINDER", notification.notification_trigger + ) + + @freeze_time("2023-01-01") + def test_notification_prep_assignment(self): + # GIVEN + prep_assignment = create_assignment( + assignment_type=AssignmentType.PREP_ASSIGNMENT, + submission_deadline=timezone.make_aware(datetime(2023, 1, 2)), + evaluation_deadline=None, + ) + + # ...too early + create_assignment( + assignment_type=AssignmentType.PREP_ASSIGNMENT, + submission_deadline=timezone.make_aware(datetime(2023, 1, 4)), + evaluation_deadline=None, + ) + + # ...too late + create_assignment( + assignment_type=AssignmentType.PREP_ASSIGNMENT, + submission_deadline=timezone.make_aware(datetime(2022, 1, 1)), + evaluation_deadline=None, + ) + + # WHEN + assignment_reminder_members_notification_job() + + # THEN + self.assertEquals(3, len(Notification.objects.all())) + self._assert_member_assignment_notifications( + action_object=prep_assignment, + expected_recipients=RECIPIENT_STUDENTS, + ) + + with self.assertRaises(Notification.DoesNotExist): + Notification.objects.get(recipient__username=RECIPIENT_TRAINER)