|
33 | 33 | from ..models import LearnerGroup |
34 | 34 | from ..models import Membership |
35 | 35 | from ..models import Role |
| 36 | +from .helpers import create_superuser |
36 | 37 | from .helpers import DUMMY_PASSWORD |
| 38 | +from .helpers import provision_device |
37 | 39 | from .sync_utils import multiple_kolibri_servers |
| 40 | +from kolibri.core.attendance.models import AttendanceSession |
38 | 41 | from kolibri.core.auth.constants import role_kinds |
39 | 42 | from kolibri.core.auth.management.utils import get_client_and_server_certs |
40 | 43 | from kolibri.core.auth.utils.sync import find_soud_sync_session_for_resume |
@@ -92,6 +95,106 @@ def test_deserializing_field(self): |
92 | 95 | self.fail(e.message) |
93 | 96 |
|
94 | 97 |
|
| 98 | +class CrossDatasetSuperuserDeserializationTestCase(TransactionTestCase): |
| 99 | + """ |
| 100 | + Regression test for quizzes and lessons authored by a superuser that belongs to a |
| 101 | + different dataset than the facility the content lives in (e.g. a device's own super |
| 102 | + admin creating content in a facility that was synced onto the device). |
| 103 | +
|
| 104 | + In that case the models' ``pre_save`` deliberately nulls the creator/assigned_by |
| 105 | + foreign key. The record must still deserialize on a receiving device: previously the |
| 106 | + fields were ``blank=False``, so ``clean_fields()`` (which Morango runs during |
| 107 | + deserialization) rejected the null value and the record stayed dirty in the Store, |
| 108 | + never appearing on the receiving device. |
| 109 | + """ |
| 110 | + |
| 111 | + def setUp(self): |
| 112 | + self.controller = MorangoProfileController(PROFILE_FACILITY_DATA) |
| 113 | + InstanceIDModel.get_or_create_current_instance() |
| 114 | + provision_device() |
| 115 | + # the facility the content lives in |
| 116 | + self.facility = Facility.objects.create(name="Synced") |
| 117 | + self.classroom = Classroom.objects.create(name="Class", parent=self.facility) |
| 118 | + # a superuser belonging to a different dataset |
| 119 | + self.other_facility = Facility.objects.create(name="Device") |
| 120 | + self.superuser = create_superuser(self.other_facility, username="deviceadmin") |
| 121 | + |
| 122 | + def _assert_deserializes_cleanly(self, *instance_ids): |
| 123 | + self.controller.serialize_into_store() |
| 124 | + Store.objects.update(dirty_bit=True) |
| 125 | + self.controller.deserialize_from_store() |
| 126 | + for instance_id in instance_ids: |
| 127 | + store = Store.objects.get(id=instance_id) |
| 128 | + self.assertIsNone( |
| 129 | + store.deserialization_error, |
| 130 | + msg="{} failed to deserialize: {}".format( |
| 131 | + store.model_name, store.deserialization_error |
| 132 | + ), |
| 133 | + ) |
| 134 | + self.assertFalse(store.dirty_bit) |
| 135 | + |
| 136 | + def test_exam_authored_by_cross_dataset_superuser_deserializes(self): |
| 137 | + exam = Exam.objects.create( |
| 138 | + title="Quiz", |
| 139 | + question_count=1, |
| 140 | + question_sources=[ |
| 141 | + { |
| 142 | + "exercise_id": uuid.uuid4().hex, |
| 143 | + "question_id": uuid.uuid4().hex, |
| 144 | + "title": "a", |
| 145 | + } |
| 146 | + ], |
| 147 | + collection=self.classroom, |
| 148 | + creator=self.superuser, |
| 149 | + active=True, |
| 150 | + ) |
| 151 | + assignment = ExamAssignment.objects.create( |
| 152 | + exam=exam, |
| 153 | + collection=self.classroom, |
| 154 | + assigned_by=self.superuser, |
| 155 | + ) |
| 156 | + # pre_save nulls the cross-dataset superuser foreign keys |
| 157 | + self.assertIsNone(Exam.objects.get(id=exam.id).creator_id) |
| 158 | + self.assertIsNone(ExamAssignment.objects.get(id=assignment.id).assigned_by_id) |
| 159 | + |
| 160 | + self._assert_deserializes_cleanly(exam.id, assignment.id) |
| 161 | + |
| 162 | + def test_lesson_authored_by_cross_dataset_superuser_deserializes(self): |
| 163 | + lesson = Lesson.objects.create( |
| 164 | + title="Lesson", |
| 165 | + resources=[ |
| 166 | + { |
| 167 | + "contentnode_id": uuid.uuid4().hex, |
| 168 | + "content_id": uuid.uuid4().hex, |
| 169 | + "channel_id": uuid.uuid4().hex, |
| 170 | + } |
| 171 | + ], |
| 172 | + collection=self.classroom, |
| 173 | + created_by=self.superuser, |
| 174 | + is_active=True, |
| 175 | + ) |
| 176 | + assignment = LessonAssignment.objects.create( |
| 177 | + lesson=lesson, |
| 178 | + collection=self.classroom, |
| 179 | + assigned_by=self.superuser, |
| 180 | + ) |
| 181 | + # pre_save nulls the cross-dataset superuser foreign keys |
| 182 | + self.assertIsNone(Lesson.objects.get(id=lesson.id).created_by_id) |
| 183 | + self.assertIsNone(LessonAssignment.objects.get(id=assignment.id).assigned_by_id) |
| 184 | + |
| 185 | + self._assert_deserializes_cleanly(lesson.id, assignment.id) |
| 186 | + |
| 187 | + def test_attendance_session_authored_by_cross_dataset_superuser_deserializes(self): |
| 188 | + session = AttendanceSession.objects.create( |
| 189 | + collection=self.classroom, |
| 190 | + created_by=self.superuser, |
| 191 | + ) |
| 192 | + # pre_save nulls the cross-dataset superuser foreign key |
| 193 | + self.assertIsNone(AttendanceSession.objects.get(id=session.id).created_by_id) |
| 194 | + |
| 195 | + self._assert_deserializes_cleanly(session.id) |
| 196 | + |
| 197 | + |
95 | 198 | class MultipleServerTestCase(TestCase): |
96 | 199 | """ |
97 | 200 | A test case to do special teardown handling to prevent errors from our additional databases. |
|
0 commit comments