|
12 | 12 | CourseData, |
13 | 13 | CoursePassingStatusData, |
14 | 14 | PersistentCourseGradeData, |
| 15 | + PersistentSubsectionGradeData, |
15 | 16 | UserData, |
16 | | - UserPersonalData |
| 17 | + UserPersonalData, |
| 18 | + XBlockWithScoringData, |
17 | 19 | ) |
18 | 20 | from openedx_events.learning.signals import ( |
19 | 21 | CCX_COURSE_PASSING_STATUS_UPDATED, |
20 | 22 | COURSE_PASSING_STATUS_UPDATED, |
21 | | - PERSISTENT_GRADE_SUMMARY_CHANGED |
| 23 | + PERSISTENT_GRADE_SUMMARY_CHANGED, |
| 24 | + PERSISTENT_SUBSECTION_GRADE_CHANGED, |
22 | 25 | ) |
23 | 26 | from openedx_events.tests.utils import OpenEdxEventsTestMixin |
| 27 | +from opaque_keys.edx.locator import BlockUsageLocator |
24 | 28 |
|
25 | 29 | from common.djangoapps.student.tests.factories import AdminFactory, UserFactory |
26 | 30 | from lms.djangoapps.ccx.models import CustomCourseForEdX |
27 | 31 | from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory |
28 | | -from lms.djangoapps.grades.models import PersistentCourseGrade |
| 32 | +from lms.djangoapps.grades.models import ( |
| 33 | + BlockRecord, |
| 34 | + BlockRecordList, |
| 35 | + PersistentCourseGrade, |
| 36 | + PersistentSubsectionGrade, |
| 37 | +) |
29 | 38 | from lms.djangoapps.grades.tests.utils import mock_passing_grade |
| 39 | +from lms.djangoapps.grades.transformer import GradesTransformer |
30 | 40 | from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase |
31 | 41 | from xmodule.modulestore.tests.factories import CourseFactory |
32 | 42 | from common.test.utils import assert_dict_contains_subset |
@@ -113,6 +123,132 @@ def test_persistent_grade_event_emitted(self): |
113 | 123 | ) |
114 | 124 |
|
115 | 125 |
|
| 126 | +class PersistentSubsectionGradeEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): |
| 127 | + """ |
| 128 | + Tests for the Open edX Events associated with the persistent subsection grade process. |
| 129 | +
|
| 130 | + This class guarantees that the following events are sent during the user updates their grade, with |
| 131 | + the exact Data Attributes as the event definition stated: |
| 132 | +
|
| 133 | + - PERSISTENT_SUBSECTION_GRADE_CHANGED: sent after the user updates or creates the grade. |
| 134 | + """ |
| 135 | + ENABLED_OPENEDX_EVENTS = [ |
| 136 | + "org.openedx.learning.course.persistent_subsection_grade.changed.v1", |
| 137 | + ] |
| 138 | + |
| 139 | + @classmethod |
| 140 | + def setUpClass(cls): |
| 141 | + """ |
| 142 | + Set up class method for the Test class. |
| 143 | +
|
| 144 | + This method starts manually events isolation. Explanation here: |
| 145 | + openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 |
| 146 | + """ |
| 147 | + super().setUpClass() |
| 148 | + cls.start_events_isolation() |
| 149 | + |
| 150 | + def setUp(self): # pylint: disable=arguments-differ |
| 151 | + super().setUp() |
| 152 | + self.course = CourseFactory.create() |
| 153 | + self.user = UserFactory.create() |
| 154 | + self.subsection_usage_key = BlockUsageLocator( |
| 155 | + course_key=self.course.id, |
| 156 | + block_type='sequential', |
| 157 | + block_id='subsection_12345', |
| 158 | + ) |
| 159 | + self.problem_locator_a = BlockUsageLocator( |
| 160 | + course_key=self.course.id, |
| 161 | + block_type='problem', |
| 162 | + block_id='problem_abc', |
| 163 | + ) |
| 164 | + self.problem_locator_b = BlockUsageLocator( |
| 165 | + course_key=self.course.id, |
| 166 | + block_type='problem', |
| 167 | + block_id='problem_def', |
| 168 | + ) |
| 169 | + self.record_a = BlockRecord(locator=self.problem_locator_a, weight=1, raw_possible=10, graded=False) |
| 170 | + self.record_b = BlockRecord(locator=self.problem_locator_b, weight=1, raw_possible=10, graded=True) |
| 171 | + self.block_records = BlockRecordList([self.record_a, self.record_b], self.course.id) |
| 172 | + self.params = { |
| 173 | + "user_id": self.user.id, |
| 174 | + "usage_key": self.subsection_usage_key, |
| 175 | + "course_version": self.course.number, |
| 176 | + "subtree_edited_timestamp": now(), |
| 177 | + "earned_all": 6.0, |
| 178 | + "possible_all": 12.0, |
| 179 | + "earned_graded": 6.0, |
| 180 | + "possible_graded": 8.0, |
| 181 | + "visible_blocks": self.block_records, |
| 182 | + "first_attempted": now(), |
| 183 | + } |
| 184 | + self.receiver_called = False |
| 185 | + |
| 186 | + def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument |
| 187 | + """ |
| 188 | + Used show that the Open edX Event was called by the Django signal handler. |
| 189 | + """ |
| 190 | + self.receiver_called = True |
| 191 | + |
| 192 | + def test_persistent_subsection_grade_event_emitted(self): |
| 193 | + """ |
| 194 | + Test whether the persistent subsection grade updated event is sent after the user updates creates or |
| 195 | + updates their grade. |
| 196 | +
|
| 197 | + Expected result: |
| 198 | + - PERSISTENT_SUBSECTION_GRADE_CHANGED is sent and received by the mocked receiver. |
| 199 | + - The arguments that the receiver gets are the arguments sent by the event |
| 200 | + except the metadata generated on the fly. |
| 201 | + """ |
| 202 | + event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect) |
| 203 | + |
| 204 | + PERSISTENT_SUBSECTION_GRADE_CHANGED.connect(event_receiver) |
| 205 | + grade = PersistentSubsectionGrade.update_or_create_grade(**self.params) |
| 206 | + self.assertTrue(self.receiver_called) |
| 207 | + |
| 208 | + grading_policy_hash = GradesTransformer.grading_policy_hash(self.course) |
| 209 | + visible_blocks = [ |
| 210 | + XBlockWithScoringData( |
| 211 | + usage_key=self.record_a.locator, |
| 212 | + block_type=self.record_a.locator.block_type, |
| 213 | + graded=self.record_a.graded, |
| 214 | + raw_possible=self.record_a.raw_possible, |
| 215 | + weight=self.record_a.weight, |
| 216 | + ), |
| 217 | + XBlockWithScoringData( |
| 218 | + usage_key=self.record_b.locator, |
| 219 | + block_type=self.record_b.locator.block_type, |
| 220 | + graded=self.record_b.graded, |
| 221 | + raw_possible=self.record_b.raw_possible, |
| 222 | + weight=self.record_b.weight, |
| 223 | + ), |
| 224 | + ] |
| 225 | + |
| 226 | + assert_dict_contains_subset( |
| 227 | + self, |
| 228 | + { |
| 229 | + "signal": PERSISTENT_SUBSECTION_GRADE_CHANGED, |
| 230 | + "sender": None, |
| 231 | + "grade": PersistentSubsectionGradeData( |
| 232 | + user_id=self.params["user_id"], |
| 233 | + course=CourseData( |
| 234 | + course_key=self.course.id, |
| 235 | + ), |
| 236 | + subsection_edited_timestamp=self.params["subtree_edited_timestamp"], |
| 237 | + grading_policy_hash=grading_policy_hash, |
| 238 | + usage_key=self.subsection_usage_key, |
| 239 | + weighted_graded_earned=self.params["earned_graded"], |
| 240 | + weighted_graded_possible=self.params["possible_graded"], |
| 241 | + weighted_total_earned=self.params["earned_all"], |
| 242 | + weighted_total_possible=self.params["possible_all"], |
| 243 | + first_attempted=self.params["first_attempted"], |
| 244 | + visible_blocks=visible_blocks, |
| 245 | + visible_blocks_hash=str(grade.visible_blocks_id), |
| 246 | + ) |
| 247 | + }, |
| 248 | + event_receiver.call_args.kwargs, |
| 249 | + ) |
| 250 | + |
| 251 | + |
116 | 252 | class CoursePassingStatusEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): |
117 | 253 | """ |
118 | 254 | Tests for Open edX passing status update event. |
|
0 commit comments