Skip to content

Commit

Permalink
feat: receiver for verified exam event
Browse files Browse the repository at this point in the history
  • Loading branch information
alangsto committed Oct 5, 2023
1 parent 0a6eb51 commit 9574690
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 1 deletion.
24 changes: 23 additions & 1 deletion lms/djangoapps/grades/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from django.dispatch import receiver
from opaque_keys.edx.keys import LearningContextKey
from openedx_events.learning.signals import EXAM_ATTEMPT_VERIFIED
from submissions.models import score_reset, score_set
from xblock.scorable import ScorableXBlockMixin, Score

Expand All @@ -25,7 +26,7 @@
from openedx.core.lib.grade_utils import is_score_higher_or_equal

from .. import events
from ..constants import ScoreDatabaseTableEnum
from ..constants import GradeOverrideFeatureEnum, ScoreDatabaseTableEnum
from ..course_grade_factory import CourseGradeFactory
from ..scores import weighted_score
from .signals import (
Expand Down Expand Up @@ -122,6 +123,10 @@ def submissions_score_reset_handler(sender, **kwargs): # pylint: disable=unused
def disconnect_submissions_signal_receiver(signal):
"""
Context manager to be used for temporarily disconnecting edx-submission's set or reset signal.
Clear Student State on ORA problems currently results in a set->reset signal pair getting fired
from submissions which leads to tasks being enqueued, one of which can never succeed. This context manager
fixes the issue by disconnecting the "set" handler during the clear_state operation.
"""
if signal == score_set:
handler = submissions_score_set_handler
Expand Down Expand Up @@ -300,3 +305,20 @@ def listen_for_course_grade_passed_first_time(sender, user_id, course_id, **kwar
"""
events.course_grade_passed_first_time(user_id, course_id)
events.fire_segment_event_on_course_grade_passed_first_time(user_id, course_id)


@receiver(EXAM_ATTEMPT_VERIFIED)
def exam_attempt_verified_event_handler(sender, signal, **kwargs): # pylint: disable=unused-argument
"""
Consume `EXAM_ATTEMPT_VERIFIED` events from the event bus. This will trigger
an undo section override, if one exists.
"""
from ..api import should_override_grade_on_rejected_exam, undo_override_subsection_grade

event_data = kwargs.get('exam_attempt')
user_data = event_data.student_user
course_key = event_data.course_key
usage_key = event_data.usage_key

if should_override_grade_on_rejected_exam(course_key):
undo_override_subsection_grade(user_data.id, course_key, usage_key, GradeOverrideFeatureEnum.proctoring)
103 changes: 103 additions & 0 deletions lms/djangoapps/grades/tests/test_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Tests for the grades handlers
"""
from datetime import datetime, timezone
from unittest import mock
from uuid import uuid4

import ddt
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx_events.data import EventsMetadata
from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData
from openedx_events.learning.signals import EXAM_ATTEMPT_VERIFIED

from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.grades.signals.handlers import exam_attempt_verified_event_handler
from ..constants import GradeOverrideFeatureEnum


@ddt.ddt
class ExamCompletionEventBusTests(TestCase):
"""
Tests for exam events from the event bus
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course_key = CourseKey.from_string('course-v1:edX+TestX+Test_Course')
cls.subsection_id = 'block-v1:edX+TestX+Test_Course+type@sequential+block@subsection'
cls.usage_key = UsageKey.from_string(cls.subsection_id)
cls.student_user = UserFactory(
username='student_user',
)

@staticmethod
def _get_exam_event_data(student_user, course_key, usage_key, exam_type, requesting_user=None):
""" create ExamAttemptData object for exam based event """
if requesting_user:
requesting_user_data = UserData(
id=requesting_user.id,
is_active=True,
pii=None
)
else:
requesting_user_data = None

return ExamAttemptData(
student_user=UserData(
id=student_user.id,
is_active=True,
pii=UserPersonalData(
username=student_user.username,
email=student_user.email,
),
),
course_key=course_key,
usage_key=usage_key,
requesting_user=requesting_user_data,
exam_type=exam_type,
)

@staticmethod
def _get_exam_event_metadata(event_signal):
""" create metadata object for event """
return EventsMetadata(
event_type=event_signal.event_type,
id=uuid4(),
minorversion=0,
source='openedx/lms/web',
sourcehost='lms.test',
time=datetime.now(timezone.utc)
)

@ddt.data(
True,
False
)
@mock.patch('lms.djangoapps.grades.api.should_override_grade_on_rejected_exam')
@mock.patch('lms.djangoapps.grades.api.undo_override_subsection_grade')
def test_exam_attempt_verified_event_handler(self, override_enabled, mock_undo_override, mock_should_override):
mock_should_override.return_value = override_enabled

exam_event_data = self._get_exam_event_data(self.student_user,
self.course_key,
self.usage_key,
exam_type='proctored')
event_metadata = self._get_exam_event_metadata(EXAM_ATTEMPT_VERIFIED)

event_kwargs = {
'exam_attempt': exam_event_data,
'metadata': event_metadata
}
exam_attempt_verified_event_handler(None, EXAM_ATTEMPT_VERIFIED, ** event_kwargs)

if override_enabled:
mock_undo_override.assert_called_once_with(
self.student_user.id,
self.course_key,
self.usage_key,
GradeOverrideFeatureEnum.proctoring
)
else:
mock_undo_override.assert_not_called()

0 comments on commit 9574690

Please sign in to comment.