def test_columns_not_duplicated_during_init(self, mock_graded_subsections): """ Tests that GradeCSVProcessor.__init__() does not cause column names to be duplicated. """ mock_graded_subsections.return_value = self._mock_graded_subsections() processor_1 = api.GradeCSVProcessor(course_id=self.course_id) # pretend that we serialize the processor data to some "state" state = deepcopy(processor_1.__dict__) processor_2 = api.GradeCSVProcessor(**state) assert processor_1.columns == processor_2.columns expected_columns = [ *self.default_headers, 'name-homework', 'grade-homework', 'original_grade-homework', 'previous_override-homework', 'new_override-homework', 'name-lab_ques', 'grade-lab_ques', 'original_grade-lab_ques', 'previous_override-lab_ques', 'new_override-lab_ques', ] assert expected_columns == processor_1.columns
def test_course_grade_filters(self, course_grade_factory_mock): course_grade_factory_mock.side_effect = cycle((Mock(percent=0.50), Mock(percent=0.70), Mock(percent=0.90))) processor = api.GradeCSVProcessor(course_id=self.course_id, max_points=100, course_grade_min=50) rows = list(processor.get_iterator()) self.assertEqual(len(rows), self.NUM_USERS+1) processor = api.GradeCSVProcessor(course_id=self.course_id, max_points=100, course_grade_min=60, course_grade_max=80) rows = list(processor.get_iterator()) self.assertEqual(len(rows), (self.NUM_USERS - 2)+1)
def test_repeat_user(self): processor = api.GradeCSVProcessor(course_id=self.course_id) username = '******' row = { 'block_id': self.usage_key, 'new_override-85bb02db': '1', 'user_id': self.learner.id, 'username': username, 'Previous Points': '', } operation = processor.preprocess_row(row) assert operation row2 = { 'block_id': self.usage_key, 'new_override-123402db': '2', 'user_id': self.learner.id, 'username': username, 'Previous Points': '' } # different row with the same user id throw error with self.assertRaisesMessage(ValidationError, 'Repeated user'): processor.preprocess_row(row2) # there should be 2 errors for the first repeat user error self.assertCountEqual(len(processor.error_messages), 2) with self.assertRaisesMessage(ValidationError, 'Repeated user'): processor.preprocess_row(row2) # there should be 3 errors for the second repeat user error self.assertCountEqual(len(processor.error_messages), 3)
def test_filter_course_roles__role_in_another_course(self): role_to_filter = 'role-to-filter' another_course = "course-v1:testX+sg201+2021" # Give audit_learner the role `role_to_filter` and verified_learner the role `some_other_role` CourseAccessRole.objects.create( user=self.audit_learner, course_id=self.course_id, role=role_to_filter, ) CourseAccessRole.objects.create( user=self.verified_learner, course_id=self.course_id, role="some_other_role", ) # Enroll verified_learner in `another_course` and assign them `role_to_filter` in that course CourseEnrollment.objects.create(course_id=another_course, user=self.verified_learner, mode='audit') CourseAccessRole.objects.create( user=self.verified_learner, course_id=another_course, role=role_to_filter, ) with patch('lms.djangoapps.grades.api.graded_subsections_for_course_id') as mock_subsections: with patch('lms.djangoapps.grades.api.CourseGradeFactory.read') as mock_course_grade: mock_subsections.return_value = self._mock_graded_subsections() mock_course_grade.return_value = Mock(percent=0.50) processor = api.GradeCSVProcessor(course_id=self.course_id, excluded_course_roles=[role_to_filter]) data = self._process_iterator(processor.get_iterator()) # Verified learner should still be present, because their excluded role is for a different course usernames = {row['username'] for row in data} self.assertFalse(self.audit_learner.username in usernames) self.assertTrue(self.verified_learner.username in usernames)
def test_filter_course_roles( self, excluded_course_roles, expect_role_a, expect_role_b, ): processor = api.GradeCSVProcessor(course_id=self.course_id, excluded_course_roles=excluded_course_roles) # Give audit_learner the role role_a and verified_learner the role role_b CourseAccessRole.objects.create( user=self.audit_learner, course_id=self.course_id, role="role_a", ) CourseAccessRole.objects.create( user=self.verified_learner, course_id=self.course_id, role="role_b", ) with patch('lms.djangoapps.grades.api.graded_subsections_for_course_id') as mock_subsections: with patch('lms.djangoapps.grades.api.CourseGradeFactory.read') as mock_course_grade: mock_subsections.return_value = self._mock_graded_subsections() mock_course_grade.return_value = Mock(percent=0.50) data = self._process_iterator(processor.get_iterator()) usernames = {row['username'] for row in data} self.assertEqual(self.audit_learner.username in usernames, expect_role_a) self.assertEqual(self.verified_learner.username in usernames, expect_role_b)
def test_filter_override_history_limited_columns(self, mocked_graded_subsections): # Given a set of overrides mocked_graded_subsections.return_value = self._mock_graded_subsections() processor = api.GradeCSVProcessor(course_id=self.course_id) processor.result_data = self._mock_result_data() processor.result_data[0].update({'new_override-lab_ques': '1', 'status': 'Success'}) processor.result_data[2].update({'new_override-lab_ques': '2', 'status': 'Success'}) # Where some subsection were not included in the original override for row in processor.result_data: row.pop('name-homework') row.pop('original_grade-homework') row.pop('previous_override-homework') row.pop('new_override-homework') # When columns are filtered and I request a copy of the report processor.columns = processor.filtered_column_headers() rows = list(processor.get_iterator(error_data='1')) # Then my headers include the correct subsections (and don't crash like they used to) headers = rows[0].strip().split(',') expected_headers = [ *self.default_headers, 'name-lab_ques', 'grade-lab_ques', 'original_grade-lab_ques', 'previous_override-lab_ques', 'new_override-lab_ques', 'status', 'error' ] assert headers == expected_headers assert len(rows) == self.NUM_USERS + 1
def test_assignment_grade(self, mocked_graded_subsections, mocked_get_subsection_grades, mocked_course_grade): # Two mock graded subsections mock_graded_subsections = self._mock_graded_subsections() mocked_graded_subsections.return_value = mock_graded_subsections # For each user, we want a subsection with: # - an original_grade, but no override # - an original_grade and an override mocked_get_subsection_grades.side_effect = mock_subsection_grade( cycle([ make_mock_grade(earned_graded=3, possible_graded=5), make_mock_grade(earned_graded=3, possible_graded=5, override=Mock(earned_graded_override=5)) ]) ) # We need to mock the course grade or everything will explode mocked_course_grade.return_value = Mock(percent=1) processor = api.GradeCSVProcessor(course_id=self.course_id) rows = list(processor.get_iterator()) headers = rows[0].strip().split(',') # Massage data into a list of dicts, keyed on column header table = [ {header: user_row_val for header, user_row_val in zip(headers, user_row.strip().split(','))} for user_row in rows[1:] ] # If there's an override, use that, if not, use the original grade for learner_data_row in table: assert learner_data_row['grade-homework'] == '3' assert learner_data_row['original_grade-homework'] == '3' assert learner_data_row['previous_override-homework'] == '' assert learner_data_row['grade-lab_ques'] == '5' assert learner_data_row['original_grade-lab_ques'] == '3' assert learner_data_row['previous_override-lab_ques'] == '5'
def test_filter_override_history_columns(self, mocked_graded_subsections): # Given 2 graded subsections ... mocked_graded_subsections.return_value = self._mock_graded_subsections() processor = api.GradeCSVProcessor(course_id=self.course_id) processor.result_data = self._mock_result_data() # One of which, "homework", was overridden for 2 students processor.result_data[0].update({'new_override-homework': '1', 'status': 'Success'}) processor.result_data[2].update({'new_override-homework': '2', 'status': 'Success'}) # When columns are filtered and I request a copy of the report processor.columns = processor.filtered_column_headers() rows = list(processor.get_iterator(error_data='1')) # Then my headers include the modified subsection headers, and exclude the unmodified section headers = rows[0].strip().split(',') expected_headers = [ *self.default_headers, 'name-homework', 'grade-homework', 'original_grade-homework', 'previous_override-homework', 'new_override-homework', 'status', 'error'] assert headers == expected_headers assert len(rows) == self.NUM_USERS + 1
def test_export__inactive_learner(self, active_only, course_grade_factory_mock): # pylint: disable=unused-argument # Create a learner, then get her PCE and deactivate it, which will deactivate the CourseEnrollment as well inactive_learner = User.objects.create(username='******') Profile.objects.create(user=inactive_learner, name="Ina Ctive-Learner") course_enrollment = CourseEnrollment.objects.create( course_id=self.course_id, user=inactive_learner, mode='masters' ) ProgramCourseEnrollment.objects.create(course_enrollment=course_enrollment) course_enrollment.is_active = False course_enrollment.save() # Get csv file and grab row processor = api.GradeCSVProcessor(course_id=self.course_id, active_only=active_only) rows = list(processor.get_iterator()) inactive_row = [row for row in rows if 'inactive_learner' in row] if active_only: # Assert that inactive learner is not present is CSV assert len(inactive_row) == 0 else: # Assert that inactive learner is still present in the CSV export assert len(inactive_row) == 1 assert 'inactive_learner,ext:6' in inactive_row[0]
def test_subsection_max_min_no_subsection_grade(self, mock_graded_subsections): mock_graded_subsections.return_value = self._mock_graded_subsections() processor = api.GradeCSVProcessor(course_id=self.course_id, subsection=self.usage_key, subsection_grade_max=101) with patch('lms.djangoapps.grades.api.get_subsection_grades') as mock_subsection_grades: with patch('lms.djangoapps.grades.api.CourseGradeFactory.read') as mock_course_grade: mock_course_grade.return_value = Mock(percent=1) mock_subsection_grades.side_effect = mock_subsection_grade(chain([None], repeat(make_mock_grade()))) rows = list(processor.get_iterator()) assert len(rows) == self.NUM_USERS
def test_empty_grade(self): processor = api.GradeCSVProcessor(course_id=self.course_id) row = { 'block_id': self.usage_key, 'new_override-123402db': ' ', 'new_override-85bb02db': ' ', 'user_id': self.learner.id, 'Previous Points': '', } operation = processor.preprocess_row(row) assert len(operation['new_override_grades']) == 0
def test_subsection_max_min(self, mock_graded_subsections): mock_graded_subsections.return_value = self._mock_graded_subsections() # should filter out everything; all grades are 1 from mock_apps grades api processor = api.GradeCSVProcessor(course_id=self.course_id, subsection=self.usage_key, subsection_grade_max=50) rows = list(processor.get_iterator()) assert len(rows) != self.NUM_USERS + 1 # should filter out all but 1 processor = api.GradeCSVProcessor(course_id=self.course_id, subsection=self.usage_key, subsection_grade_max=80) with patch('lms.djangoapps.grades.api.get_subsection_grades') as mock_subsection_grades: with patch('lms.djangoapps.grades.api.CourseGradeFactory.read') as mock_course_grade: mock_course_grade.return_value = Mock(percent=1) mock_subsection_grades.side_effect = mock_subsection_grade( chain([make_mock_grade(earned_graded=0.5)], repeat(make_mock_grade())) ) rows = list(processor.get_iterator()) assert len(rows) == 2 processor = api.GradeCSVProcessor(course_id=self.course_id, subsection=self.usage_key, subsection_grade_min=200) rows = list(processor.get_iterator()) assert len(rows) != self.NUM_USERS + 1
def test_preprocess_nan_error(self): processor = api.GradeCSVProcessor(course_id=self.course_id) row = { 'block_id': self.usage_key, 'new_override-123402db': '1', 'new_override-85bb02db': 'not a number', 'user_id': self.learner.id, 'csum': '07ec', 'Previous Points': '', } with self.assertRaisesMessage(ValueError, 'Grade must be a number'): processor.preprocess_row(row)
def test_export(self, course_grade_factory_mock): # pylint: disable=unused-argument processor = api.GradeCSVProcessor(course_id=self.course_id) rows = list(processor.get_iterator()) # tests that there a 'student_key' column present assert any('student_key' in row for row in rows) # tests that a masters student has student_key populated masters_row = [row for row in rows if 'masters' in row] assert '[email protected],ext:5,' in masters_row[0] # tests that a non-masters (verified) student does NOT have a student key populated verified_row = [row for row in rows if 'verified' in row] # note the null between the two commas, in place where student_key is supposed to be assert '[email protected],,' in verified_row[0] assert len(rows) == self.NUM_USERS + 1
def test_validate_row(self): processor = api.GradeCSVProcessor(course_id=self.course_id) row = { 'block_id': self.usage_key, 'new_override-123402db': '2', 'new_override-85bb02db': '1', 'user_id': self.learner.id, 'Previous Points': '', 'course_id': self.course_id, } processor.validate_row(row) row['course_id'] = 'something else' with self.assertRaisesMessage(ValidationError, 'Wrong course id'): processor.validate_row(row)\
def test_multiple_subsection_override(self): processor = api.GradeCSVProcessor(course_id=self.course_id) row = { 'block_id': self.usage_key, 'new_override-12f402db': '3', 'new_override-123402db': '2', 'new_override-85bb02db': '1', 'user_id': self.learner.id, 'Previous Points': '', } operation = processor.preprocess_row(row) assert operation # 3 grades are getting override assert len(operation['new_override_grades']) == 3
def test_no_subsection_grade(self, mock_graded_subsections): mock_graded_subsections.return_value = self._mock_graded_subsections() processor = api.GradeCSVProcessor(course_id=self.course_id, subsection=self.usage_key) with patch('lms.djangoapps.grades.api.get_subsection_grades') as mock_subsection_grades: with patch('lms.djangoapps.grades.api.CourseGradeFactory.read') as mock_course_grade: mock_course_grade.return_value = Mock(percent=1) mock_subsection_grades.side_effect = mock_subsection_grade(chain([None], repeat(make_mock_grade()))) rows = list(processor.get_iterator()) assert len(rows) == self.NUM_USERS +1 grade_column_index = rows[0].split(',').index('original_grade-homework') row = rows[1].split(',') assert row[grade_column_index] == '' for i in (2, 3): row = rows[i].split(',') assert row[grade_column_index] == '1'
def test_filter_override_history_noop(self, mocked_graded_subsections): # Given no overrides for a given report mocked_graded_subsections.return_value = self._mock_graded_subsections() processor = api.GradeCSVProcessor(course_id=self.course_id) processor.result_data = self._mock_result_data() # When columns are filtered and I request a copy of the report processor.columns = processor.filtered_column_headers() rows = list(processor.get_iterator(error_data='1')) # Then my headers don't include any subsections headers = rows[0].strip().split(',') expected_headers = [ *self.default_headers, 'status', 'error'] assert headers == expected_headers assert len(rows) == self.NUM_USERS + 1
def test_process_file(self, mock_graded_subsections): mock_graded_subsections.return_value = self._mock_graded_subsections() processor = api.GradeCSVProcessor(course_id=self.course_id) mock_csv_data = { 'user_id': self.learner.id, 'username': self.learner.username, 'course_id': self.course_id, 'track': None, 'cohort': None, 'name-homework': 'Homework', 'original_grade-homework': 0, 'previous_override-homework': None, 'new_override-homework': 1, 'name-lab_ques': 'Lab', 'original_grade-lab_ques': 0, 'previous_override-lab_ques': None, 'new_override-lab_ques': 2, } mock_csv = ','.join(mock_csv_data.keys()) mock_csv += '\n' mock_csv += ','.join('' if v is None else str(v) for v in mock_csv_data.values()) buf = ContentFile(mock_csv.encode('utf-8')) processor.process_file(buf)