From 3852358ca238466566923d8b97f00d54249bc1a6 Mon Sep 17 00:00:00 2001 From: Zachary Hancock Date: Thu, 25 Apr 2024 11:41:53 -0400 Subject: [PATCH] feat: feature setting to gate courseware search to verified enrollments (#34606) Adds a Django setting that limits courseware search to users in a verified enrollment track. --- lms/djangoapps/courseware/tests/test_views.py | 40 ++++++++++++++++--- lms/djangoapps/courseware/views/views.py | 20 ++++++++-- lms/envs/common.py | 9 +++++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index ef3d4741769e..1d88cff042a8 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -3748,6 +3748,8 @@ def test_get_template_and_context(self): assert context['course'] == self.course +@ddt.ddt +@override_waffle_flag(COURSEWARE_MICROFRONTEND_SEARCH_ENABLED, active=True) class TestCoursewareMFESearchAPI(SharedModuleStoreTestCase): """ Tests the endpoint to fetch the Courseware Search waffle flag enabled status. @@ -3761,12 +3763,36 @@ def setUp(self): self.client = APIClient() self.apiUrl = reverse('courseware_search_enabled_view', kwargs={'course_id': str(self.course.id)}) - @override_waffle_flag(COURSEWARE_MICROFRONTEND_SEARCH_ENABLED, active=True) - def test_courseware_mfe_search_enabled(self): + @ddt.data( + (CourseMode.AUDIT, False), + (CourseMode.VERIFIED, True), + (CourseMode.MASTERS, True), + ) + @ddt.unpack + @patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED': True}) + def test_courseware_mfe_search_verified_only(self, mode, expected_enabled): + """ + Only verified enrollees may use Courseware Search if ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED + is enabled. + """ + user = UserFactory() + CourseEnrollmentFactory.create(user=user, course_id=self.course.id, mode=mode) + + self.client.login(username=user.username, password=TEST_PASSWORD) + response = self.client.get(self.apiUrl, content_type='application/json') + body = json.loads(response.content.decode('utf-8')) + + self.assertEqual(response.status_code, 200) + self.assertEqual(body, {'enabled': expected_enabled}) + + @patch.dict('django.conf.settings.FEATURES', {'ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED': True}) + def test_courseware_mfe_search_staff_access(self): """ - Getter to check if user is allowed to use Courseware Search. + Staff users may use Courseware Search regardless of their enrollment status. """ + user_staff = UserFactory(is_staff=True) # not enrolled + self.client.login(username=user_staff.username, password=TEST_PASSWORD) response = self.client.get(self.apiUrl, content_type='application/json') body = json.loads(response.content.decode('utf-8')) @@ -3774,11 +3800,13 @@ def test_courseware_mfe_search_enabled(self): self.assertEqual(body, {'enabled': True}) @override_waffle_flag(COURSEWARE_MICROFRONTEND_SEARCH_ENABLED, active=False) - def test_is_mfe_search_disabled(self): + def test_is_mfe_search_waffle_disabled(self): """ - Getter to check if user is allowed to use Courseware Search. + Courseware search is only available when the waffle flag is enabled. """ - + user_admin = UserFactory(is_staff=True, is_superuser=True) + CourseEnrollmentFactory.create(user=user_admin, course_id=self.course.id, mode=CourseMode.VERIFIED) + self.client.login(username=user_admin.username, password=TEST_PASSWORD) response = self.client.get(self.apiUrl, content_type='application/json') body = json.loads(response.content.decode('utf-8')) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 3d70a763582d..4c3fa982ca69 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -53,6 +53,8 @@ from common.djangoapps.course_modes.models import CourseMode, get_course_prices from common.djangoapps.edxmako.shortcuts import marketing_link, render_to_response, render_to_string +from common.djangoapps.student import auth +from common.djangoapps.student.roles import CourseStaffRole from common.djangoapps.student.models import CourseEnrollment, UserTestGroup from common.djangoapps.util.cache import cache, cache_if_anonymous from common.djangoapps.util.course import course_location_from_key @@ -2261,10 +2263,22 @@ def get_learner_username(learner_identifier): @api_view(['GET']) def courseware_mfe_search_enabled(request, course_id=None): """ - Simple GET endpoint to expose whether the course may use Courseware Search. + Simple GET endpoint to expose whether the user may use Courseware Search + for a given course. """ - + enabled = False course_key = CourseKey.from_string(course_id) if course_id else None + user = request.user + + if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED'): + enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user(user, course_key) + if ( + auth.user_has_role(user, CourseStaffRole(CourseKey.from_string(course_id))) + or (enrollment_mode in CourseMode.VERIFIED_MODES) + ): + enabled = True + else: + enabled = True - payload = {"enabled": courseware_mfe_search_is_enabled(course_key)} + payload = {"enabled": courseware_mfe_search_is_enabled(course_key) if enabled else False} return JsonResponse(payload) diff --git a/lms/envs/common.py b/lms/envs/common.py index e14d0b3311c3..f7d26734bcab 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1060,6 +1060,15 @@ # .. toggle_tickets: https://github.com/openedx/edx-platform/pull/33911 'ENABLE_GRADING_METHOD_IN_PROBLEMS': False, + # .. toggle_name: FEATURES['ENABLE_COURSEWARE_SEARCH_VERIFIED_REQUIRED'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: When enabled, the courseware search feature will only be enabled + # for users in a verified enrollment track. + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2024-04-24 + 'ENABLE_COURSEWARE_SEARCH_VERIFIED_ENROLLMENT_REQUIRED': False, + # .. toggle_name: FEATURES['ENABLE_BLAKE2B_HASHING'] # .. toggle_implementation: DjangoSetting # .. toggle_default: False