From 6a3973c16dba7ed0ec0972de2ffd172788123e24 Mon Sep 17 00:00:00 2001 From: alangsto <46360176+alangsto@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:21:12 -0400 Subject: [PATCH] feat: add errored event producer (#189) --- .coveragerc | 1 + edx_exams/apps/core/api.py | 9 +++ edx_exams/apps/core/signals/handlers.py | 21 +++++- edx_exams/apps/core/signals/signals.py | 57 +++++++++------ edx_exams/apps/core/tests/test_api.py | 97 +++++++------------------ 5 files changed, 91 insertions(+), 94 deletions(-) diff --git a/.coveragerc b/.coveragerc index 66133179..c077a22a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ omit = edx_exams/settings/* edx_exams/conf* edx_exams/docker_gunicorn_configuration.py + edx_exams/apps/core/signals/handlers.py *wsgi.py *migrations* *admin.py 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..508da4b5 100644 --- a/edx_exams/apps/core/signals/signals.py +++ b/edx_exams/apps/core/signals/signals.py @@ -3,12 +3,17 @@ """ 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): +def _create_user_data(user): """ - Emit the EXAM_ATTEMPT_SUBMITTED Open edX event. + Helper function to create a UserData object. """ user_data = UserData( id=user.id, @@ -20,6 +25,15 @@ def emit_exam_attempt_submitted_event(user, course_key, usage_key, exam_type): ) ) + return user_data + + +def emit_exam_attempt_submitted_event(user, course_key, usage_key, exam_type): + """ + Emit the EXAM_ATTEMPT_SUBMITTED Open edX event. + """ + user_data = _create_user_data(user) + # .. event_implemented_name: EXAM_ATTEMPT_SUBMITTED EXAM_ATTEMPT_SUBMITTED.send_event( exam_attempt=ExamAttemptData( @@ -36,15 +50,7 @@ def emit_exam_attempt_verified_event(user, course_key, usage_key, exam_type): """ Emit the EXAM_ATTEMPT_VERIFIED 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 - ) - ) + user_data = _create_user_data(user) # .. event_implemented_name: EXAM_ATTEMPT_VERIFIED EXAM_ATTEMPT_VERIFIED.send_event( @@ -61,15 +67,7 @@ def emit_exam_attempt_rejected_event(user, course_key, usage_key, exam_type): """ Emit the EXAM_ATTEMPT_REJECTED 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 - ) - ) + user_data = _create_user_data(user) # .. event_implemented_name: EXAM_ATTEMPT_REJECTED EXAM_ATTEMPT_REJECTED.send_event( @@ -80,3 +78,20 @@ 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 = _create_user_data(user) + + # .. 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..c11dde76 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,40 +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) - - @patch('edx_exams.apps.core.signals.signals.EXAM_ATTEMPT_REJECTED.send_event') - def test_reject_attempt_event_emitted(self, mock_event_send): - """ - Test that when an exam is rejected, the EXAM_ATTEMPT_REJECTED Open edX event is emitted. - """ - update_attempt_status(self.exam_attempt.id, ExamAttemptStatus.rejected) - 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, - ) - 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) def test_illegal_start(self): """