From 77dc0fa9f0898dd45a08d70692d71a5581eb4292 Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Thu, 18 Jul 2019 19:47:12 -0400 Subject: [PATCH] add number of total users and number of filtered users to the gradebook API --- .../grades/rest_api/v1/gradebook_views.py | 45 ++++++++++- .../rest_api/v1/tests/test_gradebook_views.py | 74 ++++++++++++------- lms/djangoapps/grades/rest_api/v1/utils.py | 11 +++ openedx/core/lib/api/view_utils.py | 4 +- 4 files changed, 103 insertions(+), 31 deletions(-) diff --git a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py index a19d601b4d..8024c3f1de 100644 --- a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py +++ b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py @@ -32,7 +32,6 @@ from lms.djangoapps.grades.grade_utils import are_grades_frozen from lms.djangoapps.grades.models import ( PersistentSubsectionGrade, PersistentSubsectionGradeOverride, - PersistentSubsectionGradeOverrideHistory ) from lms.djangoapps.grades.rest_api.serializers import ( StudentGradebookEntrySerializer, @@ -406,10 +405,12 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): } **Paginated GET response** When requesting gradebook entries for all users, the response is paginated and contains the following values: - * count: The total number of user gradebook entries for this course. * next: The URL containing the next page of data. * previous: The URL containing the previous page of data. * results: A list of user gradebook entries, structured as above. + * total_users_count: The total number of active users in the course. + * filtered_users_count: The total number of active users that match + the filter associated with the provided query parameters. Note: It's important that `GradeViewMixin` is the first inherited class here, so that self.api_error returns error responses as expected. @@ -553,6 +554,8 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): related_models = ['user'] users = self._paginate_users(course_key, q_objects, related_models) + users_counts = self._get_users_counts(course_key, q_objects) + with bulk_gradebook_view_context(course_key, users): for user, course_grade, exc in CourseGradeFactory().iter( users, course_key=course_key, collected_block_structure=course_data.collected_structure @@ -562,7 +565,43 @@ class GradebookView(GradeViewMixin, PaginatedAPIView): entries.append(entry) serializer = StudentGradebookEntrySerializer(entries, many=True) - return self.get_paginated_response(serializer.data) + return self.get_paginated_response(serializer.data, **users_counts) + + def _get_users_counts(self, course_key, course_enrollment_filters): + """ + Return a dictionary containing data about the total number of users and total number + of users matching a given filter in a given course. + + Arguments: + course_key: the opaque key for the course + course_enrollment_filters: a list of Q objects representing filters to be applied to CourseEnrollments + + Returns: + dict: + total_users_count: the number of total active users in the course + filtered_users_count: the number of active users in the course that match + the given course_enrollment_filters + """ + + filter_args = [ + Q(course_id=course_key) & Q(is_active=True) + ] + + total_users_count = CourseEnrollment.objects.filter(*filter_args).count() + + filter_args.extend(course_enrollment_filters or []) + + # if course_enrollment_filters is empty, then the number of filtered users will equal the total number of users + filtered_users_count = ( + total_users_count + if not course_enrollment_filters + else CourseEnrollment.objects.filter(*filter_args).count() + ) + + return { + 'total_users_count': total_users_count, + 'filtered_users_count': filtered_users_count, + } GradebookUpdateResponseItem = namedtuple('GradebookUpdateResponseItem', ['user_id', 'usage_id', 'success', 'reason']) 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 5361155226..2de9aaacc5 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 @@ -696,11 +696,12 @@ class GradebookViewTest(GradebookViewTestBase): self.assertEqual(expected_results, actual_data) @ddt.data( - 'login_staff', - 'login_course_admin', - 'login_course_staff', + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] ) - def test_gradebook_data_filter_username_contains(self, login_method): + @ddt.unpack + def test_gradebook_data_filter_username_contains(self, login_method, num_enrollments): with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: mock_grade.return_value = self.mock_course_grade( self.program_student, passed=True, percent=0.75 @@ -738,12 +739,16 @@ class GradebookViewTest(GradebookViewTestBase): self.assertIsNone(actual_data['previous']) 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', - 'login_course_admin', - 'login_course_staff', + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] ) - def test_gradebook_data_filter_masters_track_username_contains(self, login_method): + @ddt.unpack + def test_gradebook_data_filter_masters_track_username_contains(self, login_method, num_enrollments): with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: mock_grade.return_value = self.mock_course_grade( self.program_masters_student, passed=True, percent=0.75 @@ -782,12 +787,16 @@ class GradebookViewTest(GradebookViewTestBase): self.assertIsNone(actual_data['previous']) 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', - 'login_course_admin', - 'login_course_staff', + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] ) - def test_gradebook_data_filter_email_contains(self, login_method): + @ddt.unpack + def test_gradebook_data_filter_email_contains(self, login_method, num_enrollments): with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: mock_grade.return_value = self.mock_course_grade( self.other_student, passed=True, percent=0.85 @@ -816,12 +825,16 @@ class GradebookViewTest(GradebookViewTestBase): self.assertIsNone(actual_data['previous']) 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', - 'login_course_admin', - 'login_course_staff', + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] ) - def test_gradebook_data_filter_external_user_key_contains(self, login_method): + @ddt.unpack + def test_gradebook_data_filter_external_user_key_contains(self, login_method, num_enrollments): with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: mock_grade.return_value = self.mock_course_grade( self.program_student, passed=True, percent=0.75 @@ -858,8 +871,9 @@ class GradebookViewTest(GradebookViewTestBase): actual_data = dict(resp.data) self.assertIsNone(actual_data['next']) self.assertIsNone(actual_data['previous']) - #self.assertEqual(expected_results, actual_data['results']) - assert expected_results == actual_data['results'] + 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', @@ -880,11 +894,12 @@ class GradebookViewTest(GradebookViewTestBase): self._assert_empty_response(resp) @ddt.data( - 'login_staff', - 'login_course_admin', - 'login_course_staff', + ['login_staff', 4], + ['login_course_admin', 5], + ['login_course_staff', 5] ) - def test_filter_cohort_id_and_enrollment_mode(self, login_method): + @ddt.unpack + def test_filter_cohort_id_and_enrollment_mode(self, login_method, num_enrollments): with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: mock_grade.return_value = self.mock_course_grade(self.student, passed=True, percent=0.85) @@ -913,6 +928,8 @@ class GradebookViewTest(GradebookViewTestBase): self.assertIsNone(actual_data['next']) self.assertIsNone(actual_data['previous']) 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', @@ -932,11 +949,12 @@ class GradebookViewTest(GradebookViewTestBase): self._assert_empty_response(resp) @ddt.data( - 'login_staff', - 'login_course_admin', - 'login_course_staff', + ['login_staff', 5, 3], + ['login_course_admin', 6, 4], + ['login_course_staff', 6, 4], ) - def test_filter_enrollment_mode(self, login_method): + @ddt.unpack + def test_filter_enrollment_mode(self, login_method, num_enrollments, num_filtered_enrollments): with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_grade: mock_grade.side_effect = [ self.mock_course_grade(self.student, passed=True, percent=0.85), @@ -959,6 +977,10 @@ class GradebookViewTest(GradebookViewTestBase): ) self._assert_data_all_users(resp) + actual_data = dict(resp.data) + + self.assertEqual(actual_data['total_users_count'], num_enrollments) + self.assertEqual(actual_data['filtered_users_count'], num_filtered_enrollments) @ddt.data( 'login_staff', diff --git a/lms/djangoapps/grades/rest_api/v1/utils.py b/lms/djangoapps/grades/rest_api/v1/utils.py index 0a7d26cb77..c162f4bf84 100644 --- a/lms/djangoapps/grades/rest_api/v1/utils.py +++ b/lms/djangoapps/grades/rest_api/v1/utils.py @@ -40,6 +40,17 @@ class CourseEnrollmentPagination(CursorPagination): return self.page_size + def get_paginated_response(self, data, **kwargs): # pylint: disable=arguments-differ + """ + Return a response given serialized page data and kwargs. Each key-value pair is added to the response. + """ + resp = super(CourseEnrollmentPagination, self).get_paginated_response(data) + + for (key, value) in kwargs.items(): + resp.data[key] = value + + return resp + class GradeViewMixin(DeveloperErrorViewMixin): """ diff --git a/openedx/core/lib/api/view_utils.py b/openedx/core/lib/api/view_utils.py index 5308cdb383..fe6a0418f0 100644 --- a/openedx/core/lib/api/view_utils.py +++ b/openedx/core/lib/api/view_utils.py @@ -337,12 +337,12 @@ class PaginatedAPIView(APIView): return None return self.paginator.paginate_queryset(queryset, self.request, view=self) - def get_paginated_response(self, data): + def get_paginated_response(self, data, *args, **kwargs): """ Return a paginated style `Response` object for the given output data. """ assert self.paginator is not None - return self.paginator.get_paginated_response(data) + return self.paginator.get_paginated_response(data, *args, **kwargs) def get_course_key(request, course_id=None):