diff --git a/compose/django/supercronic_crontab b/compose/django/supercronic_crontab index daeff803..ca3a264c 100644 --- a/compose/django/supercronic_crontab +++ b/compose/django/supercronic_crontab @@ -8,4 +8,4 @@ 0 */1 * * * /usr/local/bin/python /app/manage.py edoniq_import_results # every day at 19:30 -30 19 * * * /usr/local/bin/python /app/manage.py send_attendance_course_reminders +30 19 * * * /usr/local/bin/python /app/manage.py send_email_reminders --type=all diff --git a/server/vbv_lernwelt/notify/email/__init__.py b/server/vbv_lernwelt/notify/email/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/notify/email/email_services.py b/server/vbv_lernwelt/notify/email/email_services.py index d62d3421..66cb7b97 100644 --- a/server/vbv_lernwelt/notify/email/email_services.py +++ b/server/vbv_lernwelt/notify/email/email_services.py @@ -29,6 +29,30 @@ class EmailTemplate(Enum): "fr": "d-f88d9912e5484e55a879571463e4a166", } + ASSIGNMENT_REMINDER_CASEWORK_MEMBER = { + "de": "d-8b84fd96213540a796c40d4344bc606f", + "fr": "d-ae6cd87b6b574643b4fff209148c7620", + "it": "d-1bf8b12a70324e1b91050d5fa01ed81f", + } + + ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER = { + "de": "d-cb866d8c538f4ffaab923022ef7209fa", + "fr": "d-fdc84ae0e1b7417a8ede8db4e07ee7a8", + "it": "d-39d16586341b4559b3a3df71db3d04fb", + } + + ASSIGNMENT_REMINDER_EDONIQ_MEMBER = { + "de": "d-4b26911d04834079a64ab1758ca470cc", + "fr": "d-b9f27e3e13e44f20aa5d1a40c93da00d", + "it": "d-1d3d854c5b3e4012ac3d33eeb3d6e7d1", + } + + EVALUATION_REMINDER_CASEWORK_EXPERT = { + "de": "d-6e3dd4acc7fc4ce7a2776f5147bd32fd", + "fr": "d-0104add90a354d7fb1fc9fecfa132d06", + "it": "d-630e9316960647768c0a657e175436aa", + } + # VBV - Geleitete Fallarbeit abgegeben CASEWORK_SUBMITTED = {"de": "d-599f0b35ddcd4fac99314cdf8f5446a2"} diff --git a/server/vbv_lernwelt/notify/email/reminders/__init__.py b/server/vbv_lernwelt/notify/email/reminders/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/vbv_lernwelt/notify/email/reminders/assigment.py b/server/vbv_lernwelt/notify/email/reminders/assigment.py new file mode 100644 index 00000000..50a030c3 --- /dev/null +++ b/server/vbv_lernwelt/notify/email/reminders/assigment.py @@ -0,0 +1,84 @@ +from datetime import timedelta + +import structlog +from django.utils import timezone + +from vbv_lernwelt.assignment.models import AssignmentType +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.course_session.models import ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.notify.services import NotificationService + +logger = structlog.get_logger(__name__) + +ASSIGNMENT_REMINDER_LEAD_TIME = timedelta(days=2) + + +def send_assignment_reminder_notifications(): + start = timezone.now() + end = timezone.now() + ASSIGNMENT_REMINDER_LEAD_TIME + sent = [] + + # member notifications (CASEWORK and PREP_ASSIGNMENT) + for assignment in CourseSessionAssignment.objects.filter( + submission_deadline__start__lte=end, + submission_deadline__start__gte=start, + learning_content__assignment_type__in=[ + AssignmentType.CASEWORK.value, + AssignmentType.PREP_ASSIGNMENT.value, + ], + ): + for member in CourseSessionUser.objects.filter( + course_session_id=assignment.course_session.id, + role=CourseSessionUser.Role.MEMBER, + ): + sent.append( + NotificationService.send_assignment_reminder_notification_member( + recipient=member.user, assignment=assignment + ) + ) + + # member notifications (EDONIQ_TEST) + for edoniq_test in CourseSessionEdoniqTest.objects.filter( + deadline__start__lte=end, + deadline__start__gte=start, + ): + for member in CourseSessionUser.objects.filter( + course_session_id=edoniq_test.course_session.id, + role=CourseSessionUser.Role.MEMBER, + ): + sent.append( + NotificationService.send_edoniq_test_reminder_notification_member( + recipient=member.user, edoniq_test=edoniq_test + ) + ) + + # expert notifications (CASEWORK) + for assignment in CourseSessionAssignment.objects.filter( + evaluation_deadline__start__lte=end, + evaluation_deadline__start__gte=start, + learning_content__assignment_type__in=[ + AssignmentType.CASEWORK.value, + ], + ): + for expert in CourseSessionUser.objects.filter( + course_session_id=assignment.course_session.id, + role=CourseSessionUser.Role.EXPERT, + ): + sent.append( + NotificationService.send_casework_expert_evaluation_reminder( + recipient=expert.user, assignment=assignment + ) + ) + + logger.debug( + "Sent assigment reminders", + start_time=start.isoformat(), + end_time=end.isoformat(), + label="assigment_reminders", + sent=sent, + ) + + return {"sent": sent} diff --git a/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py b/server/vbv_lernwelt/notify/email/reminders/attendance.py similarity index 72% rename from server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py rename to server/vbv_lernwelt/notify/email/reminders/attendance.py index 81e89677..aa97081d 100644 --- a/server/vbv_lernwelt/notify/management/commands/send_attendance_course_reminders.py +++ b/server/vbv_lernwelt/notify/email/reminders/attendance.py @@ -4,7 +4,6 @@ from datetime import timedelta import structlog from django.utils import timezone -from vbv_lernwelt.core.base import LoggedCommand from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.notify.services import NotificationService @@ -14,7 +13,7 @@ logger = structlog.get_logger(__name__) PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2) -def attendance_course_reminder_notification_job(): +def send_attendance_reminder_notifications(): """Checks if an attendance course is coming up and sends a reminder to the participants""" start = timezone.now() end = timezone.now() + PRESENCE_COURSE_REMINDER_LEAD_TIME @@ -26,8 +25,8 @@ def attendance_course_reminder_notification_job(): logger.info( "Querying for attendance courses in specified time range", - start_time=start, - end_time=end, + start_time=start.isoformat(), + end_time=end.isoformat(), label="attendance_course_reminder_notification_job", num_attendance_courses=len(attendance_courses), ) @@ -36,8 +35,8 @@ def attendance_course_reminder_notification_job(): csu = CourseSessionUser.objects.filter(course_session_id=cs_id) logger.info( "Sending attendance course reminder notification", - start_time=start, - end_time=end, + start_time=start.isoformat(), + end_time=end.isoformat(), label="attendance_course_reminder_notification_job", num_users=len(csu), course_session_id=cs_id, @@ -53,17 +52,3 @@ def attendance_course_reminder_notification_job(): label="attendance_course_reminder_notification_job", ) return dict(results_counter) - - -class Command(LoggedCommand): - help = "Sends attendance course reminder notifications to participants" - - def handle(self, *args, **options): - results = attendance_course_reminder_notification_job() - self.job_log.json_data = results - self.job_log.save() - logger.info( - "Attendance course reminder notification job finished", - label="attendance_course_reminder_notification_job", - results=results, - ) diff --git a/server/vbv_lernwelt/notify/management/commands/send_email_reminders.py b/server/vbv_lernwelt/notify/management/commands/send_email_reminders.py new file mode 100644 index 00000000..eec415a5 --- /dev/null +++ b/server/vbv_lernwelt/notify/management/commands/send_email_reminders.py @@ -0,0 +1,56 @@ +from enum import Enum + +import structlog + +from vbv_lernwelt.core.base import LoggedCommand +from vbv_lernwelt.notify.email.reminders.assigment import ( + send_assignment_reminder_notifications, +) +from vbv_lernwelt.notify.email.reminders.attendance import ( + send_attendance_reminder_notifications, +) + +logger = structlog.get_logger(__name__) + + +class ReminderType(Enum): + ASSIGNMENT = "assignment" + ATTENDANCE = "attendance" + + +ACTIONS = { + ReminderType.ASSIGNMENT: send_assignment_reminder_notifications, + ReminderType.ATTENDANCE: send_attendance_reminder_notifications, +} + + +class Command(LoggedCommand): + help = "Sends Email Reminder Notifications" + + def add_arguments(self, parser): + parser.add_argument( + "--type", + choices=[t.value for t in ReminderType] + ["all"], + required=True, + help="Type of reminder", + ) + + def handle(self, *args, **options): + reminder_type = options["type"] + + if reminder_type == "all": + types = [ReminderType.ASSIGNMENT, ReminderType.ATTENDANCE] + else: + types = [ReminderType(reminder_type)] + + results = {t.value: None for t in types} + + for reminder in types: + logger.info(f"Starting {reminder.name} reminder notification job") + results[reminder.value] = ACTIONS[reminder]() + logger.info(f"{reminder.name} reminder notification job finished") + + self.job_log.json_data = results + self.job_log.save() + + logger.info("Reminder notification job finished") 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 new file mode 100644 index 00000000..807f65e8 --- /dev/null +++ b/server/vbv_lernwelt/notify/migrations/0005_alter_notification_notification_trigger.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.20 on 2023-10-06 13:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("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, + ), + ), + ] diff --git a/server/vbv_lernwelt/notify/models.py b/server/vbv_lernwelt/notify/models.py index dfe6df74..4e87ffea 100644 --- a/server/vbv_lernwelt/notify/models.py +++ b/server/vbv_lernwelt/notify/models.py @@ -15,6 +15,11 @@ class NotificationTrigger(models.TextChoices): ATTENDANCE_COURSE_REMINDER = "ATTENDANCE_COURSE_REMINDER", _( "Attendance Course Reminder" ) + ASSIGNMENT_REMINDER = "ASSIGNMENT_REMINDER", _("Assignment Reminder") + CASEWORK_EXPERT_EVALUATION_REMINDER = ( + "CASEWORK_EXPERT_EVALUATION_REMINDER", + _("Casework Expert Evaluation Reminder"), + ) CASEWORK_SUBMITTED = "CASEWORK_SUBMITTED", _("Casework Submitted") CASEWORK_EVALUATED = "CASEWORK_EVALUATED", _("Casework Evaluated") NEW_FEEDBACK = "NEW_FEEDBACK", _("New Feedback") diff --git a/server/vbv_lernwelt/notify/services.py b/server/vbv_lernwelt/notify/services.py index fa2956b8..bf792511 100644 --- a/server/vbv_lernwelt/notify/services.py +++ b/server/vbv_lernwelt/notify/services.py @@ -6,11 +6,14 @@ import structlog from django.db.models import Model from notifications.signals import notify +from vbv_lernwelt.assignment.models import AssignmentType from vbv_lernwelt.core.models import User from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.models import CourseSessionAssignment from vbv_lernwelt.notify.email.email_services import ( create_template_data_from_course_session_attendance_course, EmailTemplate, + format_swiss_datetime, send_email, ) from vbv_lernwelt.notify.models import ( @@ -21,7 +24,10 @@ from vbv_lernwelt.notify.models import ( if TYPE_CHECKING: from vbv_lernwelt.assignment.models import AssignmentCompletion - from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse + from vbv_lernwelt.course_session.models import ( + CourseSessionAttendanceCourse, + CourseSessionEdoniqTest, + ) from vbv_lernwelt.feedback.models import FeedbackResponse logger = structlog.get_logger(__name__) @@ -137,6 +143,106 @@ class NotificationService: ), ) + @classmethod + def send_assignment_reminder_notification_member( + cls, + recipient: User, + assignment: CourseSessionAssignment, + ): + texts = { + "de": "Erinnerung: Bald ist ein Abgabetermin", + "fr": "Rappel: Une date limite approche", + "it": "Promemoria: Una scadenza si avvicina", + } + + templates = { + AssignmentType.CASEWORK: EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER, + AssignmentType.PREP_ASSIGNMENT: EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER, + } + + verb = texts.get(recipient.language, "de") + circle = assignment.learning_content.get_parent_circle().title + due_at = assignment.submission_deadline.start + + return cls._send_notification( + recipient=recipient, + verb=verb, + notification_category=NotificationCategory.INFORMATION, + notification_trigger=NotificationTrigger.ASSIGNMENT_REMINDER, + target_url=assignment.learning_content.get_frontend_url(), + action_object=assignment, + course_session=assignment.course_session, + email_template=templates[ + AssignmentType(assignment.learning_content.assignment_type) + ], + template_data={ + "circle": circle, + "due_date": format_swiss_datetime(due_at), + }, + ) + + @classmethod + def send_edoniq_test_reminder_notification_member( + cls, + recipient: User, + edoniq_test: CourseSessionEdoniqTest, + ): + texts = { + "de": "Erinnerung: Bald ist ein Abgabetermin", + "fr": "Rappel: Une date limite approche", + "it": "Promemoria: Una scadenza si avvicina", + } + + verb = texts.get(recipient.language, "de") + circle = edoniq_test.learning_content.get_parent_circle().title + due_at = edoniq_test.deadline.start + + return cls._send_notification( + recipient=recipient, + verb=verb, + notification_category=NotificationCategory.INFORMATION, + notification_trigger=NotificationTrigger.ASSIGNMENT_REMINDER, + target_url=edoniq_test.learning_content.get_frontend_url(), + action_object=edoniq_test, + course_session=edoniq_test.course_session, + email_template=EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER, + template_data={ + "circle": circle, + "due_date": format_swiss_datetime(due_at), + }, + ) + + @classmethod + def send_casework_expert_evaluation_reminder( + cls, + recipient: User, + assignment: CourseSessionAssignment, + ): + texts = { + "de": "Erinnerung: Bald ist ein Bewertungstermin", + "fr": "Rappel: Une date limite approche", + "it": "Promemoria: Una scadenza si avvicina", + } + + verb = texts.get(recipient.language, "de") + circle = assignment.learning_content.get_parent_circle().title + due_at = assignment.evaluation_deadline.start + + return cls._send_notification( + recipient=recipient, + verb=verb, + notification_category=NotificationCategory.INFORMATION, + notification_trigger=NotificationTrigger.CASEWORK_EXPERT_EVALUATION_REMINDER, + target_url=assignment.evaluation_deadline.url_expert, + action_object=assignment, + course_session=assignment.course_session, + email_template=EmailTemplate.EVALUATION_REMINDER_CASEWORK_EXPERT, + template_data={ + "circle": circle, + "due_date": format_swiss_datetime(due_at), + }, + ) + @classmethod def _send_notification( cls, diff --git a/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py b/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py new file mode 100644 index 00000000..020baf08 --- /dev/null +++ b/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py @@ -0,0 +1,287 @@ +import re +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 ( + CourseSessionAssignment, + CourseSessionEdoniqTest, +) +from vbv_lernwelt.learnpath.models import ( + LearningContentAssignment, + LearningContentEdoniqTest, +) +from vbv_lernwelt.notify.email.email_services import EmailTemplate +from vbv_lernwelt.notify.email.reminders.assigment import ( + send_assignment_reminder_notifications, +) +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", +] + + +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) + + CourseSessionAssignment.objects.all().delete() + CourseSessionEdoniqTest.objects.all().delete() + + 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) + + template_data = notification.data["template_data"] + + self.assertEquals( + action_object.learning_content.get_parent_circle().title, + template_data["circle"], + ) + + self.assertEquals( + action_object.learning_content.get_frontend_url(), + notification.target_url, + ) + + match = re.fullmatch( + r"\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}", template_data["due_date"] + ) + + self.assertIsNotNone( + match, f"due_date format is incorrect: {template_data['due_date']}" + ) + + email_template = notification.data["email_template"] + + # make sure we have the correct email template + if type(action_object) == CourseSessionAssignment: + assignment_type = AssignmentType( + action_object.learning_content.assignment_type + ) + + if assignment_type == AssignmentType.CASEWORK: + self.assertEquals( + EmailTemplate.ASSIGNMENT_REMINDER_CASEWORK_MEMBER.name, + email_template, + ) + elif assignment_type == AssignmentType.PREP_ASSIGNMENT: + self.assertEquals( + EmailTemplate.ASSIGNMENT_REMINDER_PREP_ASSIGNMENT_MEMBER.name, + email_template, + ) + elif type(action_object) == CourseSessionEdoniqTest: + self.assertEquals( + EmailTemplate.ASSIGNMENT_REMINDER_EDONIQ_MEMBER.name, + email_template, + ) + + @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)) + ) + + # ...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 + send_assignment_reminder_notifications() + + # 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 + send_assignment_reminder_notifications() + + # 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 + send_assignment_reminder_notifications() + + # 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 + send_assignment_reminder_notifications() + + # 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) diff --git a/server/vbv_lernwelt/notify/tests/test_send_attendance_course_reminders.py b/server/vbv_lernwelt/notify/tests/test_attendance_reminders.py similarity index 95% rename from server/vbv_lernwelt/notify/tests/test_send_attendance_course_reminders.py rename to server/vbv_lernwelt/notify/tests/test_attendance_reminders.py index 81b3c9b3..3a0a032c 100644 --- a/server/vbv_lernwelt/notify/tests/test_send_attendance_course_reminders.py +++ b/server/vbv_lernwelt/notify/tests/test_attendance_reminders.py @@ -10,8 +10,8 @@ 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.notify.management.commands.send_attendance_course_reminders import ( - attendance_course_reminder_notification_job, +from vbv_lernwelt.notify.email.reminders.attendance import ( + send_attendance_reminder_notifications, ) from vbv_lernwelt.notify.models import Notification @@ -63,7 +63,7 @@ class TestAttendanceCourseReminders(TestCase): ) self.csac_future.due_date.save() - attendance_course_reminder_notification_job() + send_attendance_reminder_notifications() self.assertEquals(4, len(Notification.objects.all())) notification = Notification.objects.get( diff --git a/trufflehog-exclude-patterns.txt b/trufflehog-exclude-patterns.txt index e15192fe..bee94b37 100644 --- a/trufflehog-exclude-patterns.txt +++ b/trufflehog-exclude-patterns.txt @@ -3,6 +3,7 @@ env_secrets/ env/bitbucket/Dockerfile env/docker_local.env server/vbv_lernwelt/assignment/creators/create_assignments.py +server/vbv_lernwelt/notify/email/email_services.py server/vbv_lernwelt/static/ server/vbv_lernwelt/media/ server/vbv_lernwelt/edoniq_test/certificates/test.key