diff --git a/edx_exams/apps/core/api.py b/edx_exams/apps/core/api.py index b6173fef..db201ecc 100644 --- a/edx_exams/apps/core/api.py +++ b/edx_exams/apps/core/api.py @@ -17,7 +17,7 @@ ExamIllegalStatusTransition ) from edx_exams.apps.core.models import Exam, ExamAttempt -from edx_exams.apps.core.signals.signals import emit_exam_attempt_submitted_event +from edx_exams.apps.core.signals.signals import emit_exam_attempt_submitted_event, emit_exam_attempt_verified_event from edx_exams.apps.core.statuses import ExamAttemptStatus log = logging.getLogger(__name__) @@ -94,11 +94,12 @@ def update_attempt_status(attempt_id, to_status): attempt_obj.start_time = datetime.now(pytz.UTC) attempt_obj.allowed_time_limit_mins = _calculate_allowed_mins(attempt_obj.exam) + course_key = CourseKey.from_string(attempt_obj.exam.course_id) + usage_key = UsageKey.from_string(attempt_obj.exam.content_id) + if to_status == ExamAttemptStatus.submitted: attempt_obj.end_time = datetime.now(pytz.UTC) - course_key = CourseKey.from_string(attempt_obj.exam.course_id) - usage_key = UsageKey.from_string(attempt_obj.exam.content_id) emit_exam_attempt_submitted_event( attempt_obj.user, course_key, @@ -106,6 +107,14 @@ def update_attempt_status(attempt_id, to_status): attempt_obj.exam.exam_type ) + if to_status == ExamAttemptStatus.verified: + emit_exam_attempt_verified_event( + attempt_obj.user, + course_key, + usage_key, + attempt_obj.exam.exam_type + ) + attempt_obj.status = to_status attempt_obj.save() diff --git a/edx_exams/apps/core/signals/handlers.py b/edx_exams/apps/core/signals/handlers.py index 8e4fdf35..9510c256 100644 --- a/edx_exams/apps/core/signals/handlers.py +++ b/edx_exams/apps/core/signals/handlers.py @@ -3,7 +3,7 @@ """ from django.dispatch import receiver from openedx_events.event_bus import get_producer -from openedx_events.learning.signals import EXAM_ATTEMPT_SUBMITTED +from openedx_events.learning.signals import EXAM_ATTEMPT_SUBMITTED, EXAM_ATTEMPT_VERIFIED @receiver(EXAM_ATTEMPT_SUBMITTED) @@ -18,3 +18,17 @@ def listen_for_exam_attempt_submitted(sender, signal, **kwargs): # pylint: disa event_data={'exam_attempt': kwargs['exam_attempt']}, event_metadata=kwargs['metadata'], ) + + +@receiver(EXAM_ATTEMPT_VERIFIED) +def listen_for_exam_attempt_verified(sender, signal, **kwargs): # pylint: disable=unused-argument + """ + Publish EXAM_ATTEMPT_VERIFIED signal onto the event bus + """ + get_producer().send( + signal=EXAM_ATTEMPT_VERIFIED, + topic='exam-attempt-verified', + event_key_field='exam_attempt.course_key', + event_data={'exam_attempt': kwargs['exam_attempt']}, + event_metadata=kwargs['metadata'], + ) diff --git a/edx_exams/apps/core/signals/signals.py b/edx_exams/apps/core/signals/signals.py index 3ddeb515..09c5c7ce 100644 --- a/edx_exams/apps/core/signals/signals.py +++ b/edx_exams/apps/core/signals/signals.py @@ -3,7 +3,7 @@ """ from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData -from openedx_events.learning.signals import EXAM_ATTEMPT_SUBMITTED +from openedx_events.learning.signals import EXAM_ATTEMPT_SUBMITTED, EXAM_ATTEMPT_VERIFIED def emit_exam_attempt_submitted_event(user, course_key, usage_key, exam_type): @@ -30,3 +30,29 @@ def emit_exam_attempt_submitted_event(user, course_key, usage_key, exam_type): requesting_user=user_data ) ) + + +def emit_exam_attempt_verified_event(user, course_key, usage_key, exam_type): + """ + Emit the EXAM_ATTEMPT_VERIFIED Open edX event. + """ + name = user.full_name or '' + user_data = UserData( + id=user.id, + is_active=user.is_active, + pii=UserPersonalData( + username=user.username, + email=user.email, + name=name + ) + ) + + # .. event_implemented_name: EXAM_ATTEMPT_VERIFIED + EXAM_ATTEMPT_VERIFIED.send_event( + exam_attempt=ExamAttemptData( + student_user=user_data, + course_key=course_key, + usage_key=usage_key, + exam_type=exam_type + ) + ) diff --git a/edx_exams/apps/core/tests/test_api.py b/edx_exams/apps/core/tests/test_api.py index 49553449..f6073c2f 100644 --- a/edx_exams/apps/core/tests/test_api.py +++ b/edx_exams/apps/core/tests/test_api.py @@ -268,7 +268,7 @@ def test_start_attempt_with_due_date(self, is_due_during_exam): resource_id=str(uuid.uuid4()), course_id=self.course_id, provider=self.test_provider, - content_id='abcd1234', + content_id='block-v1:edX+test+2023+type@sequential+block@1111111111', exam_name='test_exam', exam_type='proctored', time_limit_mins=30, @@ -328,6 +328,33 @@ def test_submit_attempt_event_emitted(self, mock_event_send): ) mock_event_send.assert_called_with(exam_attempt=expected_data) + @patch('edx_exams.apps.core.signals.signals.EXAM_ATTEMPT_VERIFIED.send_event') + def test_verified_attempt_event_emitted(self, mock_event_send): + """ + Test that when an exam is verified, the EXAM_ATTEMPT_VERIFIED Open edX event is emitted. + """ + update_attempt_status(self.exam_attempt.id, ExamAttemptStatus.verified) + self.assertEqual(mock_event_send.call_count, 1) + + usage_key = UsageKey.from_string(self.exam.content_id) + course_key = CourseKey.from_string(self.exam.course_id) + + expected_data = ExamAttemptData( + student_user=UserData( + id=self.user.id, + is_active=self.user.is_active, + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.full_name + ) + ), + course_key=course_key, + usage_key=usage_key, + exam_type=self.exam.exam_type, + ) + mock_event_send.assert_called_with(exam_attempt=expected_data) + def test_illegal_start(self): """ Test that an already started exam cannot be started diff --git a/edx_exams/apps/lti/signals/tests/test_handlers.py b/edx_exams/apps/lti/signals/tests/test_handlers.py index c24a6d8c..3f1437eb 100644 --- a/edx_exams/apps/lti/signals/tests/test_handlers.py +++ b/edx_exams/apps/lti/signals/tests/test_handlers.py @@ -36,7 +36,7 @@ def setUp(self): ) self.course_id = 'course-v1:edx+test+f19' - self.content_id = '11111111' + self.content_id = 'block-v1:edX+test+2023+type@sequential+block@1111111111' self.exam = Exam.objects.create( resource_id=str(uuid.uuid4()), diff --git a/edx_exams/apps/lti/tests/test_views.py b/edx_exams/apps/lti/tests/test_views.py index 0817a8cb..7cbf675d 100644 --- a/edx_exams/apps/lti/tests/test_views.py +++ b/edx_exams/apps/lti/tests/test_views.py @@ -365,7 +365,7 @@ def setUp(self): super().setUp() self.course_id = 'course-v1:edx+test+f19' - self.content_id = '11111111' + self.content_id = 'block-v1:edX+test+2023+type@sequential+block@1111111111' self.exam = ExamFactory( course_id=self.course_id,