-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Emit the ExamAttemptSubmitted Open edX event when an exam is submitted. #178
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
""" | ||
Core Application Configuration | ||
""" | ||
|
||
from django.apps import AppConfig | ||
|
||
|
||
class CoreConfig(AppConfig): | ||
""" | ||
Application configuration for core application. | ||
""" | ||
name = 'edx_exams.apps.core' | ||
|
||
def ready(self): | ||
""" | ||
Connect handlers to signals. | ||
""" | ||
from .signals import handlers # pylint: disable=unused-import,import-outside-toplevel |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
""" | ||
Signal handlers for the edx-exams application. | ||
""" | ||
from django.dispatch import receiver | ||
from openedx_events.event_bus import get_producer | ||
from openedx_events.learning.signals import EXAM_ATTEMPT_SUBMITTED | ||
|
||
|
||
@receiver(EXAM_ATTEMPT_SUBMITTED) | ||
def listen_for_exam_attempt_submitted(sender, signal, **kwargs): # pylint: disable=unused-argument | ||
""" | ||
Publish EXAM_ATTEMPT_SUBMITTED signals onto the event bus. | ||
""" | ||
get_producer().send( | ||
signal=EXAM_ATTEMPT_SUBMITTED, | ||
topic='exam-attempt-submitted', | ||
event_key_field='exam_attempt.course_key', | ||
event_data={'exam_attempt': kwargs['exam_attempt']}, | ||
event_metadata=kwargs['metadata'], | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
""" | ||
Signal definitions and functions to send those signals for the edx-exams application. | ||
""" | ||
|
||
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, exam_type): | ||
""" | ||
Emit the EXAM_ATTEMPT_SUBMITTED 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_SUBMITTED | ||
EXAM_ATTEMPT_SUBMITTED.send_event( | ||
exam_attempt=ExamAttemptData( | ||
student_user=user_data, | ||
course_key=course_key, | ||
usage_key=usage_key, | ||
exam_type=exam_type, | ||
requesting_user=user_data | ||
) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is necessary because the |
||
exam_name='test_exam', | ||
exam_type='proctored', | ||
time_limit_mins=30, | ||
|
@@ -298,6 +299,35 @@ 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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be in a separate file for testing signals specifically? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The signal is emitted from the API, so I think that this test makes the most sense in this file, because that's what I'm testing. I could move it into a separate class specifically for testing events emitted by the API functions, if you'd prefer? If I were to have tests in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see what you're saying, yeah it makes sense to do that test on the API level. I guess I'm moreso trying to figure out if we need to do any testing on the level of the signal itself. I'm doing some digging now to see if that's necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me know what you find! I don't think we need to test the actual Django signal API. All the plumbing that funnels Django signals to the event bus is all behind the scenes and not something we could test, I believe. But we could test whether calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I found an example, there's a test here in edx-platform certificates that seems similar to yours. That tests seems to go about as deep as yours does so I think it's good as is. |
||
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) | ||
|
||
def test_illegal_start(self): | ||
""" | ||
Test that an already started exam cannot be started | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there also supposed to be a
requesting_user
passed along? Looking at https://github.com/openedx/openedx-events/blob/fec19895eed1c37e4da6305bfa9849665a832372/openedx_events/learning/data.py#L316There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or I guess it defaults to None, and only a learner can submit their own exam
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. The plan was to reserve this attribute for cases where a user other than the learner has made the change, like an instructor resetting an exam in the Instructor Dashboard or overriding an exam attempt status on manual review, for example. I don't believe that submission can be triggered by anyone but the learner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went back on this and decided to add in the
requesting_user
whenever there is one. I would prefer to be explicit about therequesting_user
than having an implicit understanding that it should be empty whenever the learner is the requester.