From df634ab7e35d69c7df4f3dc21a57825f1f5b1c73 Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Mon, 12 Aug 2019 16:47:27 -0400 Subject: [PATCH] add course level min and max filters to gradebook API --- .../grades/rest_api/v1/gradebook_views.py | 34 ++++ .../rest_api/v1/tests/test_gradebook_views.py | 181 +++++++++++++++++- 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py index 42390ee3ab..d265074e4a 100644 --- a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py +++ b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py @@ -33,6 +33,7 @@ from lms.djangoapps.grades.grade_utils import are_grades_frozen from lms.djangoapps.grades.models import ( PersistentSubsectionGrade, PersistentSubsectionGradeOverride, + PersistentCourseGrade, ) from lms.djangoapps.grades.rest_api.serializers import ( StudentGradebookEntrySerializer, @@ -579,6 +580,39 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): ) ) q_objects.append(Q(selected_assignment_grade_in_range=True)) + if request.GET.get('course_grade_min') or request.GET.get('course_grade_max'): + grade_conditions = {} + q_object = Q() + if request.GET.get('course_grade_min'): + course_grade_min = float(request.GET.get('course_grade_min')) / 100 + grade_conditions['percent_grade__gte'] = course_grade_min + + if course_grade_min == 0: + subquery_grade_absent = ~Exists( + PersistentCourseGrade.objects.filter( + course_id=OuterRef('course'), + user_id=OuterRef('user_id'), + ) + ) + + annotations['course_grade_absent'] = subquery_grade_absent + q_object |= Q(course_grade_absent=True) + + if request.GET.get('course_grade_max'): + course_grade_max = float(request.GET.get('course_grade_max')) / 100 + grade_conditions['percent_grade__lte'] = course_grade_max + + subquery_grade_in_range = Exists( + PersistentCourseGrade.objects.filter( + course_id=OuterRef('course'), + user_id=OuterRef('user_id'), + **grade_conditions + ) + ) + annotations['course_grade_in_range'] = subquery_grade_in_range + q_object |= Q(course_grade_in_range=True) + + q_objects.append(q_object) entries = [] related_models = ['user'] diff --git a/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py index 2de9aaacc5..4637c77f56 100644 --- a/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py +++ b/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py @@ -29,7 +29,8 @@ from lms.djangoapps.grades.models import ( BlockRecordList, PersistentSubsectionGrade, PersistentSubsectionGradeOverride, - PersistentSubsectionGradeOverrideHistory + PersistentSubsectionGradeOverrideHistory, + PersistentCourseGrade, ) from lms.djangoapps.grades.rest_api.v1.tests.mixins import GradeViewTestMixin from lms.djangoapps.grades.rest_api.v1.views import CourseEnrollmentPagination @@ -1031,6 +1032,184 @@ class GradebookViewTest(GradebookViewTestBase): expected_page_size = user_size self.assertEqual(len(actual_data['results']), expected_page_size) + @ddt.data( + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] + ) + @ddt.unpack + def test_filter_course_grade_min(self, login_method, num_enrollments): + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: + # even though we're creating actual PersistentCourseGrades below, we still need + # mocked subsection grades + mock_grade.side_effect = [ + self.mock_course_grade(self.student, passed=True, percent=0.85), + self.mock_course_grade(self.program_student, passed=True, percent=0.75), + ] + + PersistentCourseGrade( + user_id=self.student.id, + course_id=self.course_key, + percent_grade=0.85 + ).save() + PersistentCourseGrade( + user_id=self.other_student.id, + course_id=self.course_key, + percent_grade=0.45 + ).save() + PersistentCourseGrade( + user_id=self.program_student.id, + course_id=self.course_key, + percent_grade=0.75 + ).save() + + with override_waffle_flag(self.waffle_flag, active=True): + getattr(self, login_method)() + resp = self.client.get( + self.get_url(course_key=self.course.id) + '?course_grade_min=50' + ) + + expected_results = [ + OrderedDict([ + ('user_id', self.student.id), + ('username', self.student.username), + ('email', ''), + ('percent', 0.85), + ('section_breakdown', self.expected_subsection_grades()), + ]), + OrderedDict([ + ('user_id', self.program_student.id), + ('username', self.program_student.username), + ('email', ''), + ('external_user_key', 'program_user_key_0'), + ('percent', 0.75), + ('section_breakdown', self.expected_subsection_grades()), + ]) + ] + + self.assertEqual(status.HTTP_200_OK, resp.status_code) + actual_data = dict(resp.data) + self.assertEqual(expected_results, actual_data['results']) + self.assertEqual(actual_data['total_users_count'], num_enrollments) + self.assertEqual(actual_data['filtered_users_count'], 2) + + @ddt.data( + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] + ) + @ddt.unpack + def test_filter_course_grade_min_and_max(self, login_method, num_enrollments): + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: + # even though we're creating actual PersistentCourseGrades below, we still need + # mocked subsection grades + mock_grade.side_effect = [ + self.mock_course_grade(self.program_student, passed=True, percent=0.75), + ] + + PersistentCourseGrade( + user_id=self.student.id, + course_id=self.course_key, + percent_grade=0.85 + ).save() + PersistentCourseGrade( + user_id=self.other_student.id, + course_id=self.course_key, + percent_grade=0.45 + ).save() + PersistentCourseGrade( + user_id=self.program_student.id, + course_id=self.course_key, + percent_grade=0.75 + ).save() + + with override_waffle_flag(self.waffle_flag, active=True): + getattr(self, login_method)() + resp = self.client.get( + self.get_url(course_key=self.course.id) + '?course_grade_min=50&course_grade_max=80' + ) + + expected_results = [ + OrderedDict([ + ('user_id', self.program_student.id), + ('username', self.program_student.username), + ('email', ''), + ('external_user_key', 'program_user_key_0'), + ('percent', 0.75), + ('section_breakdown', self.expected_subsection_grades()), + ]), + ] + + self.assertEqual(status.HTTP_200_OK, resp.status_code) + actual_data = dict(resp.data) + self.assertEqual(expected_results, actual_data['results']) + self.assertEqual(actual_data['total_users_count'], num_enrollments) + self.assertEqual(actual_data['filtered_users_count'], 1) + + @ddt.data( + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] + ) + @ddt.unpack + def test_filter_course_grade_absent(self, login_method, num_enrollments): + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: + # even though we're creating actual PersistentCourseGrades below, we still need + # mocked subsection grades + mock_grade.side_effect = [ + self.mock_course_grade(self.student, passed=True, percent=0.0), + self.mock_course_grade(self.other_student, passed=False, percent=0.45), + self.mock_course_grade(self.program_student, passed=True, percent=0.75), + ] + + PersistentCourseGrade( + user_id=self.other_student.id, + course_id=self.course_key, + percent_grade=0.45 + ).save() + PersistentCourseGrade( + user_id=self.program_student.id, + course_id=self.course_key, + percent_grade=0.75 + ).save() + + with override_waffle_flag(self.waffle_flag, active=True): + getattr(self, login_method)() + resp = self.client.get( + self.get_url(course_key=self.course.id) + '?course_grade_min=0' + ) + + expected_results = [ + OrderedDict([ + ('user_id', self.student.id), + ('username', self.student.username), + ('email', ''), + ('percent', 0.0), + ('section_breakdown', self.expected_subsection_grades()), + ]), + OrderedDict([ + ('user_id', self.other_student.id), + ('username', self.other_student.username), + ('email', ''), + ('percent', 0.45), + ('section_breakdown', self.expected_subsection_grades()), + ]), + OrderedDict([ + ('user_id', self.program_student.id), + ('username', self.program_student.username), + ('email', ''), + ('external_user_key', 'program_user_key_0'), + ('percent', 0.75), + ('section_breakdown', self.expected_subsection_grades()), + ]) + ] + + self.assertEqual(status.HTTP_200_OK, resp.status_code) + actual_data = dict(resp.data) + self.assertEqual(expected_results, actual_data['results']) + self.assertEqual(actual_data['total_users_count'], num_enrollments) + self.assertEqual(actual_data['filtered_users_count'], num_enrollments) + @ddt.ddt class GradebookBulkUpdateViewTest(GradebookViewTestBase):