Пример #1
0
    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
Пример #2
0
    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)
Пример #3
0
    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)
Пример #4
0
    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)
Пример #5
0
    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)
Пример #6
0
    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
Пример #7
0
    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'
Пример #8
0
    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
Пример #9
0
    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]
Пример #10
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
Пример #11
0
 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
Пример #12
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
Пример #13
0
 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)
Пример #14
0
 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
Пример #15
0
 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)\
Пример #16
0
 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
Пример #17
0
    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'
Пример #18
0
    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
Пример #19
0
 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)