diff --git a/edx_exams/apps/core/api.py b/edx_exams/apps/core/api.py index 2b1fcdfa..046529c5 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.statuses import ExamAttemptStatus +from edx_exams.apps.core.signals.signals import emit_exam_attempt_submitted_event log = logging.getLogger(__name__) @@ -96,6 +97,14 @@ def update_attempt_status(attempt_id, to_status): 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, + usage_key + ) + attempt_obj.status = to_status attempt_obj.save() diff --git a/edx_exams/apps/core/signals/signals.py b/edx_exams/apps/core/signals/signals.py new file mode 100644 index 00000000..a6a5a151 --- /dev/null +++ b/edx_exams/apps/core/signals/signals.py @@ -0,0 +1,21 @@ +from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData +from openedx_events.learning.signals import EXAM_ATTEMPT_SUBMITTED + + +def emit_exam_attempt_submitted_event(user, course_key, usage_key): + # .. event_implemented_name: EXAM_ATTEMPT_SUBMITTED + EXAM_ATTEMPT_SUBMITTED.send_event( + exam_attempt=ExamAttemptData( + student_user=UserData( + id=user.id, + is_active=user.is_active, + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.full_name + ) + ), + course_key=course_key, + usage_key=usage_key, + ) + ) diff --git a/edx_exams/apps/core/test_utils/factories.py b/edx_exams/apps/core/test_utils/factories.py index c75a5a0b..4ec17ca1 100644 --- a/edx_exams/apps/core/test_utils/factories.py +++ b/edx_exams/apps/core/test_utils/factories.py @@ -37,6 +37,7 @@ class Meta: password = factory.PostGenerationMethodCall('set_password', _DEFAULT_PASSWORD) first_name = factory.Sequence('User{}'.format) last_name = 'Test' + full_name = "{} {}".format(first_name, last_name) is_superuser = False is_staff = False diff --git a/edx_exams/apps/core/tests/test_api.py b/edx_exams/apps/core/tests/test_api.py index 69d5155e..767b298b 100644 --- a/edx_exams/apps/core/tests/test_api.py +++ b/edx_exams/apps/core/tests/test_api.py @@ -11,6 +11,7 @@ from django.utils import timezone from freezegun import freeze_time from opaque_keys.edx.keys import CourseKey, UsageKey +from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData from edx_exams.apps.api.test_utils import ExamsAPITestCase from edx_exams.apps.core.api import ( @@ -212,7 +213,7 @@ def setUp(self): 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, @@ -298,6 +299,33 @@ 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) + + 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 + ) + mock_event_send.assert_called_with(exam_attempt=expected_data) + + def test_illegal_start(self): """ Test that an already started exam cannot be started