feat: assignment reminder mails
This commit is contained in:
parent
3cd764ee76
commit
e44dc5e31d
|
|
@ -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__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue