feat: assignment reminder mails

This commit is contained in:
Livio Bieri 2023-10-09 14:10:04 +02:00
parent 3cd764ee76
commit e44dc5e31d
5 changed files with 246 additions and 48 deletions

View File

@ -6,9 +6,7 @@ from django.conf import settings
from django.utils import timezone from django.utils import timezone
from sendgrid import Mail, SendGridAPIClient from sendgrid import Mail, SendGridAPIClient
from vbv_lernwelt.course_session.models import ( from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse
CourseSessionAttendanceCourse,
)
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)

View File

@ -14,7 +14,6 @@ from vbv_lernwelt.notify.services import NotificationService
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
PRESENCE_COURSE_REMINDER_LEAD_TIME = timedelta(weeks=2)
ASSIGNMENT_REMINDER_LEAD_TIME = timedelta(days=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( for assignment in CourseSessionEdoniqTest.objects.filter(
submission_deadline__start__lte=end, deadline__start__lte=end,
submission_deadline__start__gte=start, deadline__start__gte=start,
): ):
for member in CourseSessionUser.objects.filter( for member in CourseSessionUser.objects.filter(
course_session_id=assignment.course_session.id, course_session_id=assignment.course_session.id,
@ -79,7 +78,7 @@ def assignment_reminder_members_notification_job():
class Command(LoggedCommand): 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): def handle(self, *args, **options):
results = assignment_reminder_members_notification_job() results = assignment_reminder_members_notification_job()

View File

@ -4,15 +4,28 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('notify', '0004_alter_notification_notification_trigger'), ("notify", "0004_alter_notification_notification_trigger"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='notification', model_name="notification",
name='notification_trigger', 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), 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,
),
), ),
] ]

View File

@ -22,8 +22,8 @@ from vbv_lernwelt.notify.models import (
if TYPE_CHECKING: if TYPE_CHECKING:
from vbv_lernwelt.assignment.models import AssignmentCompletion from vbv_lernwelt.assignment.models import AssignmentCompletion
from vbv_lernwelt.course_session.models import ( from vbv_lernwelt.course_session.models import (
CourseSessionAttendanceCourse,
CourseSessionAssignment, CourseSessionAssignment,
CourseSessionAttendanceCourse,
CourseSessionEdoniqTest, CourseSessionEdoniqTest,
) )
from vbv_lernwelt.feedback.models import FeedbackResponse from vbv_lernwelt.feedback.models import FeedbackResponse

View File

@ -1,53 +1,241 @@
from datetime import datetime from datetime import datetime
from typing import Dict
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from freezegun import freeze_time 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.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 from vbv_lernwelt.course.models import CourseSession
from vbv_lernwelt.course_session.models import CourseSessionAttendanceCourse from vbv_lernwelt.course_session.models import (
from vbv_lernwelt.learnpath.models import LearningContentAttendanceCourse 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): def setUp(self):
create_default_users() create_default_users()
create_test_course(with_sessions=True) create_test_course(with_sessions=True)
CourseSessionAttendanceCourse.objects.all().delete()
cs_bern = CourseSession.objects.get( CourseSessionAssignment.objects.all().delete()
id=TEST_COURSE_SESSION_BERN_ID, CourseSessionEdoniqTest.objects.all().delete()
)
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 Notification.objects.all().delete()
self.csac_future = CourseSessionAttendanceCourse.objects.create(
course_session=cs_bern, def _assert_member_assignment_notifications(
learning_content=LearningContentAttendanceCourse.objects.get( self, action_object, expected_recipients
slug="test-lehrgang-lp-circle-fahrzeug-lc-präsenzkurs-fahrzeug" ):
), for expected_recipient in expected_recipients:
location="Handelsschule BV Bern, Zimmer 122", notification = Notification.objects.get(
trainer="Thomas Berger", 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") # ...too early
def test_happy_day(self): create_edoniq_test_assignment(
assert False, "TODO" 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)