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/email_services.py b/server/vbv_lernwelt/notify/email/email_services.py index d62d3421..9df03411 100644 --- a/server/vbv_lernwelt/notify/email/email_services.py +++ b/server/vbv_lernwelt/notify/email/email_services.py @@ -6,7 +6,9 @@ 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__) @@ -29,6 +31,24 @@ class EmailTemplate(Enum): "fr": "d-f88d9912e5484e55a879571463e4a166", } + # FIXME: Create Sendgrid templates + # Assumption: Edonqic, CaseWork and PrepAssignment share + # the same template for now. Once the templates are created + # update the template ids here and make sure the needed template + # data is passed to _send_notification! + ASSIGNMENT_REMINDER = { + "de": "", + "it": "", + "fr": "", + } + + # FIXME: Same as above... + CASEWORK_EXPERT_EVALUATION_REMINDER = { + "de": "", + "it": "", + "fr": "", + } + # VBV - Geleitete Fallarbeit abgegeben CASEWORK_SUBMITTED = {"de": "d-599f0b35ddcd4fac99314cdf8f5446a2"} 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 new file mode 100644 index 00000000..cc79214b --- /dev/null +++ b/server/vbv_lernwelt/notify/management/commands/send_assigment_course_reminders.py @@ -0,0 +1,92 @@ +from datetime import timedelta + +import structlog +from django.utils import timezone + +from vbv_lernwelt.assignment.models import AssignmentType +from vbv_lernwelt.core.base import LoggedCommand +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__) + +PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2) +ASSIGNMENT_REMINDER_LEAD_TIME = timedelta(days=2) + + +def assignment_reminder_members_notification_job(): + 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.user, assignment + ) + ) + + # member notifications (EDONIQ) + for assignment in CourseSessionEdoniqTest.objects.filter( + submission_deadline__start__lte=end, + submission_deadline__start__gte=start, + ): + 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.user, assignment + ) + ) + + # 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( + expert.user, assignment + ) + ) + + return {"sent": sent} + + +class Command(LoggedCommand): + help = "Sends assigments course reminder notifications to participants (submission deadline) AND experts (evaluation deadline)" + + def handle(self, *args, **options): + results = assignment_reminder_members_notification_job() + self.job_log.json_data = results + self.job_log.save() + logger.info( + "Assignment course reminder notification job finished", + label="assignment_course_reminder_notification_job", + results=results, + ) 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..9d0b9565 --- /dev/null +++ b/server/vbv_lernwelt/notify/migrations/0005_alter_notification_notification_trigger.py @@ -0,0 +1,18 @@ +# 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..e30b993f 100644 --- a/server/vbv_lernwelt/notify/services.py +++ b/server/vbv_lernwelt/notify/services.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union import structlog from django.db.models import Model @@ -21,7 +21,11 @@ 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, + CourseSessionAssignment, + CourseSessionEdoniqTest, + ) from vbv_lernwelt.feedback.models import FeedbackResponse logger = structlog.get_logger(__name__) @@ -137,6 +141,62 @@ class NotificationService: ), ) + @classmethod + def send_assignment_reminder_notification( + cls, + recipient: User, + assignment: Union[CourseSessionAssignment, 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") + + 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=EmailTemplate.ASSIGNMENT_REMINDER, + template_data={ + # FIXME: Add template data as needed + # for the sendgrid email template + }, + ) + + @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") + + 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.CASEWORK_EXPERT_EVALUATION_REMINDER, + template_data={ + # FIXME: Add template data as needed + # for the sendgrid email template + }, + ) + @classmethod def _send_notification( cls, 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 new file mode 100644 index 00000000..3e4f182b --- /dev/null +++ b/server/vbv_lernwelt/notify/tests/test_send_assigment_course_reminders.py @@ -0,0 +1,53 @@ +from datetime import datetime + +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time + +from vbv_lernwelt.core.constants import TEST_COURSE_SESSION_BERN_ID +from vbv_lernwelt.core.create_default_users import create_default_users +from vbv_lernwelt.course.creators.test_course import create_test_course +from vbv_lernwelt.course.models import CourseSession +from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse +from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse + + +class TestAttendanceCourseReminders(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() + + # 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", + ) + + @freeze_time("2023-08-25 13:02:01") + def test_happy_day(self): + assert False, "TODO"