Skip to content
Snippets Groups Projects
Unverified Commit 079f43f4 authored by Jansen Kantor's avatar Jansen Kantor Committed by GitHub
Browse files

EDUCATOR-5571: filter gradebook api by course staff role (#26994)

feat: add excluded_course_roles parameter to gradebook view to allow omitting staff
parent 25b275bc
No related branches found
No related tags found
No related merge requests found
......@@ -21,7 +21,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.models import CourseEnrollment, CourseAccessRole
from common.djangoapps.student.roles import BulkRoleCache
from common.djangoapps.track.event_transaction_utils import (
create_new_event_transaction_id,
......@@ -618,7 +618,19 @@ class GradebookView(GradeViewMixin, PaginatedAPIView):
q_object |= Q(course_grade_in_range=True)
q_objects.append(q_object)
if request.GET.get('excluded_course_roles'):
excluded_course_roles = request.GET.getlist('excluded_course_roles')
course_access_role_filters = dict(
user=OuterRef('user'),
course_id=course_key,
)
if 'all' not in excluded_course_roles:
course_access_role_filters['role__in'] = excluded_course_roles
annotations['has_excluded_role'] = Exists(
CourseAccessRole.objects.filter(**course_access_role_filters)
)
# TODO: In django 3.0+, we can directly filter on this 'exists' rather than annotating
q_objects.append(Q(has_excluded_role=False))
entries = []
related_models = ['user']
users = self._paginate_users(course_key, q_objects, related_models, annotations=annotations)
......
......@@ -5,6 +5,7 @@ Tests for the course grading API view
import json
from collections import OrderedDict, namedtuple
from contextlib import contextmanager
from datetime import datetime
from unittest.mock import MagicMock, patch
......@@ -19,6 +20,13 @@ from rest_framework import status
from rest_framework.test import APITestCase
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.roles import (
CourseBetaTesterRole,
CourseCcxCoachRole,
CourseDataResearcherRole,
CourseInstructorRole,
CourseStaffRole
)
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
from lms.djangoapps.courseware.tests.factories import InstructorFactory, StaffFactory
......@@ -1280,6 +1288,142 @@ class GradebookViewTest(GradebookViewTestBase):
assert actual_data['total_users_count'] == num_enrollments
assert actual_data['filtered_users_count'] == num_enrollments
@contextmanager
def _mock_all_course_grade_reads(self, percent=0.9):
"""
A context manager for mocking CourseGradeFactory.read and returning the same grade for all learners.
"""
# pylint: disable=unused-argument
def fake_course_grade_read(*args, **kwargs):
return self.mock_course_grade(kwargs['user'], passed=True, percent=percent)
with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_read:
mock_read.side_effect = fake_course_grade_read
yield
def _assert_usernames(self, response, expected_usernames):
""" Helper method to assert that the expected users were returned from the endpoint """
assert status.HTTP_200_OK == response.status_code
response_data = dict(response.data)
actual_usernames = [row['username'] for row in response_data['results']]
assert set(actual_usernames) == set(expected_usernames)
def test_users_with_course_roles(self):
""" Test that a staff member erolled in the course will be included in grade results. """
# This function creates and enrolls a course staff (not global staff) user
staff_user = self.login_course_staff()
with override_waffle_flag(self.waffle_flag, active=True):
with self._mock_all_course_grade_reads():
response = self.client.get(self.get_url(course_key=self.course.id))
course_students = [
self.student.username,
self.other_student.username,
self.program_student.username,
self.program_masters_student.username
]
self._assert_usernames(
response,
course_students + [staff_user.username]
)
@ddt.data(
None,
[],
['all'],
[CourseInstructorRole.ROLE, CourseBetaTesterRole.ROLE, CourseCcxCoachRole.ROLE],
[CourseInstructorRole.ROLE, 'all'],
[CourseBetaTesterRole.ROLE, 'nonexistant-role'],
[
CourseInstructorRole.ROLE,
CourseStaffRole.ROLE,
CourseBetaTesterRole.ROLE,
CourseCcxCoachRole.ROLE,
CourseDataResearcherRole.ROLE
],
)
def test_filter_course_roles(self, excluded_course_roles):
""" Test that excluded_course_roles=all filters out any user with a course role """
# Create test users, enroll them in the course, and give them roles.
role_user_usernames = dict()
course_roles_to_create = [
CourseInstructorRole,
CourseStaffRole,
CourseBetaTesterRole,
CourseCcxCoachRole,
CourseDataResearcherRole,
]
for role in course_roles_to_create:
user = UserFactory.create(username="test_filter_course_roles__" + role.ROLE)
role(self.course.id).add_users(user)
self._create_user_enrollments(user)
role_user_usernames[role.ROLE] = user.username
# This will create global staff and not enroll them in the course
self.login_staff()
with self._mock_all_course_grade_reads():
with override_waffle_flag(self.waffle_flag, active=True):
response = self.client.get(
self.get_url(course_key=self.course.id),
{'excluded_course_roles': excluded_course_roles} if excluded_course_roles is not None else {}
)
expected_usernames = [
self.student.username,
self.other_student.username,
self.program_student.username,
self.program_masters_student.username,
]
if not excluded_course_roles:
# Don't filter out any course roles
expected_usernames += list(role_user_usernames.values())
elif 'all' in excluded_course_roles:
# Filter out every course role
pass
else:
# Filter out some number of course roles
for role, username in role_user_usernames.items():
if role not in excluded_course_roles:
expected_usernames.append(username)
self._assert_usernames(response, expected_usernames)
@ddt.data(False, True)
def test_exclude_course_roles_for_another_course(self, other_student_role_in_self_course):
"""
Test for filtering errors when users have roles in other courses.
"""
# Conditionally make other_student a beta tester (arbitrary role) in self.course
if other_student_role_in_self_course:
CourseBetaTesterRole(self.course.id).add_users(self.other_student)
# Create another course, enroll other_student, and make other_student course staff in other course
another_course = CourseFactory.create(display_name='another-course', run='run-1')
CourseEnrollmentFactory(
course_id=another_course.id,
user=self.other_student,
)
CourseStaffRole(another_course.id).add_users(self.other_student)
# Query the gradebook view for self.course, excluding staff.
# other_student is staff in another-course, not self.course, so
# they should still be included.
self.login_staff()
with self._mock_all_course_grade_reads():
with override_waffle_flag(self.waffle_flag, active=True):
response = self.client.get(
self.get_url(course_key=self.course.id),
{'excluded_course_roles': CourseStaffRole.ROLE}
)
self._assert_usernames(
response,
[
self.student.username,
self.other_student.username,
self.program_student.username,
self.program_masters_student.username,
]
)
@ddt.ddt
class GradebookBulkUpdateViewTest(GradebookViewTestBase):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment