diff --git a/edx_exams/apps/core/api.py b/edx_exams/apps/core/api.py index 911eac6e..b08a1e77 100644 --- a/edx_exams/apps/core/api.py +++ b/edx_exams/apps/core/api.py @@ -18,6 +18,7 @@ ) from edx_exams.apps.core.models import Exam, ExamAttempt from edx_exams.apps.core.signals.signals import ( + emit_exam_attempt_errored_event, emit_exam_attempt_rejected_event, emit_exam_attempt_submitted_event, emit_exam_attempt_verified_event @@ -127,6 +128,14 @@ def update_attempt_status(attempt_id, to_status): attempt_obj.exam.exam_type ) + if to_status == ExamAttemptStatus.error: + emit_exam_attempt_errored_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 fe39bc13..4d74d575 100644 --- a/edx_exams/apps/core/signals/handlers.py +++ b/edx_exams/apps/core/signals/handlers.py @@ -3,7 +3,12 @@ """ from django.dispatch import receiver from openedx_events.event_bus import get_producer -from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, EXAM_ATTEMPT_SUBMITTED, EXAM_ATTEMPT_VERIFIED +from openedx_events.learning.signals import ( + EXAM_ATTEMPT_ERRORED, + EXAM_ATTEMPT_REJECTED, + EXAM_ATTEMPT_SUBMITTED, + EXAM_ATTEMPT_VERIFIED +) @receiver(EXAM_ATTEMPT_SUBMITTED) @@ -46,3 +51,17 @@ def listen_for_exam_attempt_rejected(sender, signal, **kwargs): # pylint: disab event_data={'exam_attempt': kwargs['exam_attempt']}, event_metadata=kwargs['metadata'], ) + + +@receiver(EXAM_ATTEMPT_ERRORED) +def listen_for_exam_attempt_errored(sender, signal, **kwargs): # pylint: disable=unused-argument + """ + Publish EXAM_ATTEMPT_ERRORED signal onto the event bus + """ + get_producer().send( + signal=EXAM_ATTEMPT_ERRORED, + topic='exam-attempt-errored', + 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 d5d7b540..df4a049a 100644 --- a/edx_exams/apps/core/signals/signals.py +++ b/edx_exams/apps/core/signals/signals.py @@ -3,7 +3,12 @@ """ from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData -from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, EXAM_ATTEMPT_SUBMITTED, EXAM_ATTEMPT_VERIFIED +from openedx_events.learning.signals import ( + EXAM_ATTEMPT_ERRORED, + EXAM_ATTEMPT_REJECTED, + EXAM_ATTEMPT_SUBMITTED, + EXAM_ATTEMPT_VERIFIED +) def emit_exam_attempt_submitted_event(user, course_key, usage_key, exam_type): @@ -80,3 +85,28 @@ def emit_exam_attempt_rejected_event(user, course_key, usage_key, exam_type): exam_type=exam_type ) ) + + +def emit_exam_attempt_errored_event(user, course_key, usage_key, exam_type): + """ + Emit the EXAM_ATTEMPT_ERRORED Open edX event. + """ + user_data = UserData( + id=user.id, + is_active=user.is_active, + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.full_name + ) + ) + + # .. event_implemented_name: EXAM_ATTEMPT_ERRORED + EXAM_ATTEMPT_ERRORED.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 4cddd5a4..fdb979e2 100644 --- a/edx_exams/apps/core/tests/test_api.py +++ b/edx_exams/apps/core/tests/test_api.py @@ -299,48 +299,23 @@ def test_submit_attempt(self): self.assertEqual(updated_attempt.status, ExamAttemptStatus.submitted) self.assertEqual(updated_attempt.end_time, timezone.now()) - @patch('edx_exams.apps.core.signals.signals.EXAM_ATTEMPT_SUBMITTED.send_event') - def test_submit_attempt_event_emitted(self, mock_event_send): - """ - Test that when an exam is submitted, the EXAM_ATTEMPT_SUBMITED Open edX event is emitted. - """ - update_attempt_status(self.exam_attempt.id, ExamAttemptStatus.submitted) - self.assertEqual(mock_event_send.call_count, 1) - - user_data = 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 = CourseKey.from_string(self.exam.course_id) - usage_key = UsageKey.from_string(self.exam.content_id) - - expected_data = ExamAttemptData( - student_user=user_data, - course_key=course_key, - usage_key=usage_key, - exam_type=self.exam.exam_type, - requesting_user=user_data, - ) - 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): + @ddt.data( + ('EXAM_ATTEMPT_SUBMITTED', ExamAttemptStatus.submitted, True), + ('EXAM_ATTEMPT_VERIFIED', ExamAttemptStatus.verified, False), + ('EXAM_ATTEMPT_REJECTED', ExamAttemptStatus.rejected, False), + ('EXAM_ATTEMPT_ERRORED', ExamAttemptStatus.error, False) + ) + @ddt.unpack + def test_attempt_event_emitted(self, event_name, status, expect_requesting_user): """ - Test that when an exam is verified, the EXAM_ATTEMPT_VERIFIED Open edX event is emitted. + Test that when an exam status is updated, the corresponding Open edX event is emitted. """ - update_attempt_status(self.exam_attempt.id, ExamAttemptStatus.verified) - self.assertEqual(mock_event_send.call_count, 1) + patch_event = 'edx_exams.apps.core.signals.signals.{event_name}.send_event'.format(event_name=event_name) + with patch(patch_event) as mock_event_send: + update_attempt_status(self.exam_attempt.id, status) + 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( + user_data = UserData( id=self.user.id, is_active=self.user.is_active, pii=UserPersonalData( @@ -348,12 +323,18 @@ def test_verified_attempt_event_emitted(self, mock_event_send): 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) + ) + course_key = CourseKey.from_string(self.exam.course_id) + usage_key = UsageKey.from_string(self.exam.content_id) + + expected_data = ExamAttemptData( + student_user=user_data, + course_key=course_key, + usage_key=usage_key, + exam_type=self.exam.exam_type, + requesting_user=user_data if expect_requesting_user else None, + ) + mock_event_send.assert_called_with(exam_attempt=expected_data) @patch('edx_exams.apps.core.signals.signals.EXAM_ATTEMPT_REJECTED.send_event') def test_reject_attempt_event_emitted(self, mock_event_send):