diff --git a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue index 04af049c..b04fdaef 100644 --- a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue +++ b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationContainer.vue @@ -65,7 +65,9 @@ const assignmentDetail = computed(() => { }); const dueDate = computed(() => - dayjs(assignmentDetail.value?.evaluation_deadline?.start) + assignmentDetail.value?.evaluation_deadline?.start + ? dayjs(assignmentDetail.value?.evaluation_deadline?.start) + : undefined ); const inEvaluationTask = computed( diff --git a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue index 931fb4ba..445e2245 100644 --- a/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue +++ b/client/src/pages/cockpit/assignmentEvaluationPage/EvaluationIntro.vue @@ -11,7 +11,7 @@ const props = defineProps<{ assignmentUser: CourseSessionUser; assignment: Assignment; assignmentCompletion: AssignmentCompletion; - dueDate?: Dayjs; + dueDate?: Dayjs | undefined; }>(); const emit = defineEmits(["startEvaluation"]); @@ -101,7 +101,13 @@ async function startEvaluation() {
- {{ $t(text.evaluationInstruction) }} + {{ + $t(text.evaluationInstruction, { + name: `${ + props.assignmentUser.first_name + " " + props.assignmentUser.last_name + }`, + }) + }}
diff --git a/compose/django/docker_start.sh b/compose/django/docker_start.sh index 8da059c4..4e30ebbe 100644 --- a/compose/django/docker_start.sh +++ b/compose/django/docker_start.sh @@ -21,5 +21,8 @@ fi # sed -i "s|command=/usr/local/bin/supercronic /app/supercronic_crontab|command=/usr/local/bin/supercronic /app/supercronic_crontab -sentry-dsn '$IT_SENTRY_DSN'|" /app/supervisord.conf #fi +# Create Prüfungslehrgang +python /app/manage.py create_vermittler_pruefung + # Set the command to run supervisord /home/django/.local/bin/supervisord -c /app/supervisord.conf diff --git a/cypress/e2e/assignment/assignmentTrainer.cy.js b/cypress/e2e/assignment/assignmentTrainer.cy.js index 5902ff3d..905d05c4 100644 --- a/cypress/e2e/assignment/assignmentTrainer.cy.js +++ b/cypress/e2e/assignment/assignmentTrainer.cy.js @@ -218,7 +218,10 @@ describe("assignmentTrainer.cy.js", () => { cy.get('[data-cy="title"]').should("contain", "Feedback"); cy.get('[data-cy="evaluation-duedate]"').should("not.exist"); - cy.get('[data-cy="instruction"]').should("contain", "Intro für Feedback"); + cy.get('[data-cy="instruction"]').should( + "contain", + "Bitte unterstütze Test Student1 und gib Feedback zum Auftrag." + ); cy.get('[data-cy="start-evaluation"]').click(); cy.get('[data-cy="evaluation-task"]').should("contain", "Feedback 1 / 5"); diff --git a/server/vbv_lernwelt/course/consts.py b/server/vbv_lernwelt/course/consts.py index 5b3b30ab..39fc79c9 100644 --- a/server/vbv_lernwelt/course/consts.py +++ b/server/vbv_lernwelt/course/consts.py @@ -8,3 +8,4 @@ COURSE_UK_IT = -8 COURSE_UK_TRAINING_IT = -9 COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10 COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11 +COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12 diff --git a/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py b/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py index 48fa0bc6..2ad76a87 100644 --- a/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py +++ b/server/vbv_lernwelt/course/creators/versicherungsvermittlerin.py @@ -1,5 +1,6 @@ from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID from vbv_lernwelt.course.factories import CoursePageFactory +from vbv_lernwelt.course.models import CircleContactType from vbv_lernwelt.course.utils import get_wagtail_default_site @@ -20,6 +21,8 @@ def create_versicherungsvermittlerin_with_categories( id=course_id, title=title, category_name="Handlungsfeld", + enable_circle_documents=False, + circle_contact_type=CircleContactType.LEARNING_MENTOR.value, ) CourseCategory.objects.get_or_create(course=course, title="Allgemein", general=True) diff --git a/server/vbv_lernwelt/course/management/commands/create_default_courses.py b/server/vbv_lernwelt/course/management/commands/create_default_courses.py index ac931c2d..af21c29e 100644 --- a/server/vbv_lernwelt/course/management/commands/create_default_courses.py +++ b/server/vbv_lernwelt/course/management/commands/create_default_courses.py @@ -59,6 +59,7 @@ from vbv_lernwelt.course.consts import ( COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID, COURSE_VERSICHERUNGSVERMITTLERIN_ID, COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID, + COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID, ) from vbv_lernwelt.course.creators.test_course import ( create_edoniq_test_assignment, @@ -95,6 +96,7 @@ from vbv_lernwelt.importer.services import ( from vbv_lernwelt.learning_mentor.models import LearningMentor from vbv_lernwelt.learnpath.create_vv_new_learning_path import ( create_vv_new_learning_path, + create_vv_pruefung_learning_path, ) from vbv_lernwelt.learnpath.models import ( Circle, @@ -153,6 +155,9 @@ def command(course): course_id=COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID, language="it" ) + if COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID in course: + create_versicherungsvermittlerin_pruefung_course() + if COURSE_UK in course: create_course_uk_de() create_course_uk_de_course_sessions() @@ -276,6 +281,34 @@ def create_versicherungsvermittlerin_course( ) +def create_versicherungsvermittlerin_pruefung_course( + course_id=COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID, language="de" +): + names = { + "de": "Versicherungsvermittler/-in VBV Prüfung", + "fr": "Intermédiaire d’assurance AFA Examen", + "it": "Intermediario/a assicurativo/a AFA Esame", + } + # Versicherungsvermittler/in mit neuen Circles + course = create_versicherungsvermittlerin_with_categories( + course_id=course_id, + title=names[language], + ) + + # assignments create assignments parent page + _assignment_list_page = AssignmentListPageFactory( + parent=course.coursepage, + ) + + create_vv_new_competence_profile(course_id=course_id) + create_default_media_library(course_id=course_id) + create_vv_reflection(course_id=course_id) + + CourseSession.objects.create(course_id=course_id, title=names[language]) + + create_vv_pruefung_learning_path(course_id=course_id) + + def create_course_uk_de(course_id=COURSE_UK, lang="de"): names = { "de": "Überbetriebliche Kurse", diff --git a/server/vbv_lernwelt/course/management/commands/create_vermittler_pruefung.py b/server/vbv_lernwelt/course/management/commands/create_vermittler_pruefung.py new file mode 100644 index 00000000..c227d1b0 --- /dev/null +++ b/server/vbv_lernwelt/course/management/commands/create_vermittler_pruefung.py @@ -0,0 +1,23 @@ +import djclick as click + +from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID +from vbv_lernwelt.course.management.commands.create_default_courses import ( + create_versicherungsvermittlerin_pruefung_course, +) +from vbv_lernwelt.course.models import Course + +ADMIN_EMAILS = ["info@iterativ.ch", "admin"] + + +@click.command() +def command(): + print( + "Creating Vermittler Prüfung course", + COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID, + ) + + if Course.objects.filter(id=COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID).exists(): + print("Course already exists, skipping") + return + + create_versicherungsvermittlerin_pruefung_course() diff --git a/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py b/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py index 915b2239..29eee41e 100644 --- a/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py +++ b/server/vbv_lernwelt/learnpath/create_vv_new_learning_path.py @@ -83,6 +83,26 @@ def create_vv_new_learning_path( Page.objects.update(owner=user) +def create_vv_pruefung_learning_path( + course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, user=None +): + if user is None: + user = User.objects.get(username="info@iterativ.ch") + + course_page = CoursePage.objects.get(course_id=course_id) + lp = LearningPathFactory( + title="Lernpfad", + parent=course_page, + ) + + TopicFactory(title="Prüfung", parent=lp) + create_circle_pruefungsvorbereitung(lp) + create_circle_pruefung(lp) + + # all pages belong to 'admin' by default + Page.objects.update(owner=user) + + def create_circle_basis(lp, title="Basis"): circle = CircleFactory( title=title, diff --git a/server/vbv_lernwelt/notify/services.py b/server/vbv_lernwelt/notify/services.py index bf792511..aadde5aa 100644 --- a/server/vbv_lernwelt/notify/services.py +++ b/server/vbv_lernwelt/notify/services.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import TYPE_CHECKING import structlog +from django.core.exceptions import ValidationError +from django.core.validators import validate_email from django.db.models import Model from notifications.signals import notify @@ -38,11 +40,22 @@ class NotificationService: def send_assignment_submitted_notification( cls, recipient: User, sender: User, assignment_completion: AssignmentCompletion ): - texts = { - "de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben.", - "fr": "%(sender)s a soumis l'étude de cas dirigée «%(assignment_title)s».", - "it": "%(sender)s ha consegnato il caso di studio guidato «%(assignment_title)s».", - } + if ( + assignment_completion.assignment.assignment_type + == AssignmentType.PRAXIS_ASSIGNMENT.value + ): + texts = { + "de": "%(sender)s hat den Praxisauftrag «%(assignment_title)s» abgegeben.", + "fr": "%(sender)s a soumis la mission pratique «%(assignment_title)s».", + "it": "%(sender)s ha consegnato l'incarico pratico «%(assignment_title)s».", + } + # this was the default case before the praxis assignment was introduced + else: + texts = { + "de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» abgegeben.", + "fr": "%(sender)s a soumis l'étude de cas dirigée «%(assignment_title)s».", + "it": "%(sender)s ha consegnato il caso di studio guidato «%(assignment_title)s».", + } verb = texts.get(recipient.language, "de") % { "sender": sender.get_full_name(), "assignment_title": assignment_completion.assignment.title, @@ -68,11 +81,22 @@ class NotificationService: assignment_completion: AssignmentCompletion, target_url: str, ): - texts = { - "de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet.", - "fr": "%(sender)s a évalué l'étude de cas dirigée «%(assignment_title)s».", - "it": "%(sender)s ha valutato il caso di studio guidato «%(assignment_title)s».", - } + if ( + assignment_completion.assignment.assignment_type + == AssignmentType.PRAXIS_ASSIGNMENT.value + ): + texts = { + "de": "%(sender)s hat den Praxisauftrag «%(assignment_title)s» bewertet.", + "fr": "%(sender)s a évalué la mission pratique «%(assignment_title)s».", + "it": "%(sender)s ha valutato l'incarico pratico «%(assignment_title)s».", + } + # this was the default case before the praxis assignment was introduced + else: + texts = { + "de": "%(sender)s hat die geleitete Fallarbeit «%(assignment_title)s» bewertet.", + "fr": "%(sender)s a évalué l'étude de cas dirigée «%(assignment_title)s».", + "it": "%(sender)s ha valutato il caso di studio guidato «%(assignment_title)s».", + } verb = texts.get(recipient.language, "de") % { "sender": sender.get_full_name(), "assignment_title": assignment_completion.assignment.title, @@ -281,6 +305,13 @@ class NotificationService: template_data=template_data, ) emailed = False + + try: + validate_email(recipient.email) + except ValidationError: + log.info("Recipient email is invalid") + return f"{notification_identifier}_invalid_email" + try: notification = NotificationService._find_duplicate_notification( recipient=recipient, diff --git a/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py b/server/vbv_lernwelt/notify/tests/test_assigment_notifications.py similarity index 61% rename from server/vbv_lernwelt/notify/tests/test_assigment_reminders.py rename to server/vbv_lernwelt/notify/tests/test_assigment_notifications.py index 7c912559..d83fb977 100644 --- a/server/vbv_lernwelt/notify/tests/test_assigment_reminders.py +++ b/server/vbv_lernwelt/notify/tests/test_assigment_notifications.py @@ -6,7 +6,13 @@ 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.assignment.models import ( + Assignment, + AssignmentCompletion, + AssignmentCompletionStatus, + AssignmentType, +) +from vbv_lernwelt.core.admin import User 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 @@ -24,6 +30,7 @@ from vbv_lernwelt.notify.email.reminders.assigment import ( send_assignment_reminder_notifications, ) from vbv_lernwelt.notify.models import Notification +from vbv_lernwelt.notify.services import NotificationService EXPECTED_MEMBER_VERB = "Erinnerung: Bald ist ein Abgabetermin" EXPECTED_EXPERT_VERB = "Erinnerung: Bald ist ein Bewertungstermin" @@ -40,6 +47,7 @@ ASSIGNMENT_TYPE_LEARNING_CONTENT_LOOKUP: Dict[AssignmentType, str] = { 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", + AssignmentType.PRAXIS_ASSIGNMENT: "test-lehrgang-lp-circle-reisen-lc-mein-kundenstamm", } @@ -284,3 +292,164 @@ class TestAssignmentCourseRemindersTest(TestCase): with self.assertRaises(Notification.DoesNotExist): Notification.objects.get(recipient__username=RECIPIENT_TRAINER) + + +class TestAssignmentCourseUpdateTest(TestCase): + def setUp(self): + create_default_users() + create_test_course(with_sessions=True) + + CourseSessionAssignment.objects.all().delete() + Notification.objects.all().delete() + + self.student = User.objects.get(email=RECIPIENT_STUDENTS[0]) + self.trainer = User.objects.get(email=RECIPIENT_TRAINER) + + @freeze_time("2023-01-01") + def test_notification_title_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)), + ) + assignment = Assignment.objects.get( + slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice" + ) + + ac = AssignmentCompletion.objects.create( + completion_status=AssignmentCompletionStatus.SUBMITTED.value, + assignment_user=self.student, + assignment=assignment, + evaluation_passed=True, + course_session=casework.course_session, + completion_data={}, + evaluation_max_points=10, + evaluation_points=10, + evaluation_user=self.trainer, + ) + + # WHEN + NotificationService.send_assignment_submitted_notification( + recipient=self.trainer, + sender=ac.assignment_user, + assignment_completion=ac, + ) + + # THEN + self.assertEqual(1, len(Notification.objects.all())) + + notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER) + self.assertEqual("USER_INTERACTION", notification.notification_category) + self.assertIn("hat die geleitete Fallarbeit", notification.verb) + + def test_notification_title_praxis_assignment_for_experts(self): + # GIVEN. + casework = create_assignment( + assignment_type=AssignmentType.PRAXIS_ASSIGNMENT, + ) + assignment = Assignment.objects.get( + slug="test-lehrgang-assignment-mein-kundenstamm" + ) + + ac = AssignmentCompletion.objects.create( + completion_status=AssignmentCompletionStatus.SUBMITTED.value, + assignment_user=self.student, + assignment=assignment, + evaluation_passed=True, + course_session=casework.course_session, + completion_data={}, + evaluation_max_points=10, + evaluation_points=10, + evaluation_user=self.trainer, + ) + + # WHEN + NotificationService.send_assignment_submitted_notification( + recipient=self.trainer, + sender=ac.assignment_user, + assignment_completion=ac, + ) + + # THEN + self.assertEqual(1, len(Notification.objects.all())) + + notification = Notification.objects.get(recipient__username=RECIPIENT_TRAINER) + self.assertEqual("USER_INTERACTION", notification.notification_category) + self.assertIn("hat den Praxisauftrag", notification.verb) + + @freeze_time("2023-01-01") + def test_notification_title_casework_for_student(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)), + ) + assignment = Assignment.objects.get( + slug="test-lehrgang-assignment-überprüfen-einer-motorfahrzeugs-versicherungspolice" + ) + + ac = AssignmentCompletion.objects.create( + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, + assignment_user=self.student, + assignment=assignment, + evaluation_passed=True, + course_session=casework.course_session, + completion_data={}, + evaluation_max_points=10, + evaluation_points=10, + evaluation_user=self.trainer, + ) + + # WHEN + NotificationService.send_assignment_evaluated_notification( + recipient=ac.assignment_user, + sender=self.trainer, + assignment_completion=ac, + target_url="/some/url", + ) + + # THEN + self.assertEqual(1, len(Notification.objects.all())) + + notification = Notification.objects.get(recipient__username=self.student.email) + self.assertEqual("USER_INTERACTION", notification.notification_category) + self.assertIn("hat die geleitete Fallarbeit", notification.verb) + + @freeze_time("2023-01-01") + def test_notification_title_praxis_assignment_for_student(self): + # GIVEN + casework = create_assignment( + assignment_type=AssignmentType.PRAXIS_ASSIGNMENT, + ) + assignment = Assignment.objects.get( + slug="test-lehrgang-assignment-mein-kundenstamm" + ) + + ac = AssignmentCompletion.objects.create( + completion_status=AssignmentCompletionStatus.EVALUATION_SUBMITTED.value, + assignment_user=self.student, + assignment=assignment, + evaluation_passed=True, + course_session=casework.course_session, + completion_data={}, + evaluation_max_points=10, + evaluation_points=10, + evaluation_user=self.trainer, + ) + + # WHEN + NotificationService.send_assignment_evaluated_notification( + recipient=ac.assignment_user, + sender=self.trainer, + assignment_completion=ac, + target_url="/some/url", + ) + + # THEN + self.assertEqual(1, len(Notification.objects.all())) + + notification = Notification.objects.get(recipient__username=self.student.email) + self.assertEqual("USER_INTERACTION", notification.notification_category) + self.assertIn("hat den Praxisauftrag", notification.verb)