diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index 5dfe8669d4..2833b1cf34 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -40,6 +40,8 @@ from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.services import IDVerificationService from lms.djangoapps.verify_student.tests.factories import SSOVerificationFactory from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.features.content_type_gating.models import ContentTypeGatingConfig +from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -1144,3 +1146,46 @@ class SsoRecordsTests(SupportViewTestCase): # lint-amnesty, pylint: disable=mis assert response.status_code == 200 assert len(data) == 1 self.assertContains(response, '"uid": "test@example.com"') + + +class FeatureBasedEnrollmentSupportApiViewTests(SupportViewTestCase): + """ + Test suite for FBE Support API view. + """ + def setUp(self): + super().setUp() + SupportStaffRole().add_users(self.user) + + def test_fbe_enabled_response(self): + """ + Test the response for the api view when the gating and duration configs + are enabled. + """ + for course_mode in [CourseMode.AUDIT, CourseMode.VERIFIED]: + CourseModeFactory.create(mode_slug=course_mode, course_id=self.course.id) + ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) + CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) + + response = self.client.get( + reverse("support:feature_based_enrollment_details", kwargs={'course_id': str(self.course.id)}) + ) + data = json.loads(response.content.decode('utf-8')) + gating_config = data['gating_config'] + duration_config = data['duration_config'] + + assert str(self.course.id) == data['course_id'] + assert gating_config['enabled'] + assert gating_config['enabled_as_of'] == '2018-01-01 00:00:00+00:00' + assert duration_config['enabled'] + assert duration_config['enabled_as_of'] == '2018-01-01 00:00:00+00:00' + + def test_fbe_disabled_response(self): + """ + Test the FBE support api view response to be empty when no gating and duration + config is present. + """ + response = self.client.get( + reverse("support:feature_based_enrollment_details", kwargs={'course_id': str(self.course.id)}) + ) + data = json.loads(response.content.decode('utf-8')) + assert data == {} diff --git a/lms/djangoapps/support/urls.py b/lms/djangoapps/support/urls.py index ed9c985971..6c101bffcf 100644 --- a/lms/djangoapps/support/urls.py +++ b/lms/djangoapps/support/urls.py @@ -2,14 +2,14 @@ URLs for the student support app. """ - +from django.conf import settings from django.conf.urls import url from .views.certificate import CertificatesSupportView from .views.contact_us import ContactUsView from .views.course_entitlements import EntitlementSupportView from .views.enrollments import EnrollmentSupportListView, EnrollmentSupportView -from .views.feature_based_enrollments import FeatureBasedEnrollmentsSupportView +from .views.feature_based_enrollments import FeatureBasedEnrollmentsSupportView, FeatureBasedEnrollmentSupportAPIView from .views.index import index from .views.manage_user import ManageUserDetailView, ManageUserSupportView from .views.program_enrollments import LinkProgramEnrollmentSupportView, ProgramEnrollmentsInspectorView @@ -40,6 +40,11 @@ urlpatterns = [ FeatureBasedEnrollmentsSupportView.as_view(), name="feature_based_enrollments" ), + url( + fr'^feature_based_enrollment_details/{settings.COURSE_ID_PATTERN}$', + FeatureBasedEnrollmentSupportAPIView.as_view(), + name="feature_based_enrollment_details" + ), url(r'link_program_enrollments/?$', LinkProgramEnrollmentSupportView.as_view(), name='link_program_enrollments'), url( r'program_enrollments_inspector/?$', diff --git a/lms/djangoapps/support/views/feature_based_enrollments.py b/lms/djangoapps/support/views/feature_based_enrollments.py index a5b56ff339..929c2a30ea 100644 --- a/lms/djangoapps/support/views/feature_based_enrollments.py +++ b/lms/djangoapps/support/views/feature_based_enrollments.py @@ -2,18 +2,17 @@ Support tool for viewing course duration information """ - -from django.core.exceptions import ObjectDoesNotExist +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from django.utils.decorators import method_decorator from django.views.generic import View -from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.generics import GenericAPIView from common.djangoapps.edxmako.shortcuts import render_to_response +from common.djangoapps.util.json_request import JsonResponse from lms.djangoapps.support.decorators import require_support_permission -from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.features.content_type_gating.models import ContentTypeGatingConfig -from openedx.features.course_duration_limits.models import CourseDurationLimitConfig +from lms.djangoapps.support.views.utils import get_course_duration_info class FeatureBasedEnrollmentsSupportView(View): @@ -29,7 +28,7 @@ class FeatureBasedEnrollmentsSupportView(View): course_key = request.GET.get('course_key', '') if course_key: - results = self._get_course_duration_info(course_key) + results = get_course_duration_info(course_key) else: results = {} @@ -38,35 +37,40 @@ class FeatureBasedEnrollmentsSupportView(View): 'results': results, }) - def _get_course_duration_info(self, course_key): + +class FeatureBasedEnrollmentSupportAPIView(GenericAPIView): + """ + Support-only API View for getting feature based enrollment configuration details + for a course. + """ + authentication_classes = ( + JwtAuthentication, SessionAuthentication + ) + permission_classes = (IsAuthenticated,) + + @method_decorator(require_support_permission) + def get(self, request, course_id): """ - Fetch course duration information from database + Returns the duration config information if FBE is enabled. If + FBE is not enabled, empty dict is returned. + + * Example Request: + - GET /support/feature_based_enrollment_details/ + + * Example Response: + { + "course_id": , + "course_name": "FBE course", + "gating_config": { + "enabled": true, + "enabled_as_of": "2030-01-01 00:00:00+00:00", + "reason": "Site" + }, + "duration_config": { + "enabled": true, + "enabled_as_of": "2030-01-01 00:00:00+00:00", + "reason": "Site" + } + } """ - try: - key = CourseKey.from_string(course_key) - course = CourseOverview.objects.values('display_name').get(id=key) - duration_config = CourseDurationLimitConfig.current(course_key=key) - gating_config = ContentTypeGatingConfig.current(course_key=key) - duration_enabled = CourseDurationLimitConfig.enabled_for_course(course_key=key) - gating_enabled = ContentTypeGatingConfig.enabled_for_course(course_key=key) - - gating_dict = { - 'enabled': gating_enabled, - 'enabled_as_of': str(gating_config.enabled_as_of) if gating_config.enabled_as_of else 'N/A', - 'reason': gating_config.provenances['enabled'].value - } - duration_dict = { - 'enabled': duration_enabled, - 'enabled_as_of': str(duration_config.enabled_as_of) if duration_config.enabled_as_of else 'N/A', - 'reason': duration_config.provenances['enabled'].value - } - - return { - 'course_id': course_key, - 'course_name': course.get('display_name'), - 'gating_config': gating_dict, - 'duration_config': duration_dict, - } - - except (ObjectDoesNotExist, InvalidKeyError): - return {} + return JsonResponse(get_course_duration_info(course_id)) diff --git a/lms/djangoapps/support/views/utils.py b/lms/djangoapps/support/views/utils.py new file mode 100644 index 0000000000..d93ef69366 --- /dev/null +++ b/lms/djangoapps/support/views/utils.py @@ -0,0 +1,44 @@ +""" +Various utility methods used by support app views. +""" +from django.core.exceptions import ObjectDoesNotExist +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey + +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.features.content_type_gating.models import ContentTypeGatingConfig +from openedx.features.course_duration_limits.models import CourseDurationLimitConfig + + +def get_course_duration_info(course_key): + """ + Fetch course duration information from database. + """ + try: + key = CourseKey.from_string(course_key) + course = CourseOverview.objects.values('display_name').get(id=key) + duration_config = CourseDurationLimitConfig.current(course_key=key) + gating_config = ContentTypeGatingConfig.current(course_key=key) + duration_enabled = CourseDurationLimitConfig.enabled_for_course(course_key=key) + gating_enabled = ContentTypeGatingConfig.enabled_for_course(course_key=key) + + gating_dict = { + 'enabled': gating_enabled, + 'enabled_as_of': str(gating_config.enabled_as_of) if gating_config.enabled_as_of else 'N/A', + 'reason': gating_config.provenances['enabled'].value + } + duration_dict = { + 'enabled': duration_enabled, + 'enabled_as_of': str(duration_config.enabled_as_of) if duration_config.enabled_as_of else 'N/A', + 'reason': duration_config.provenances['enabled'].value + } + + return { + 'course_id': course_key, + 'course_name': course.get('display_name'), + 'gating_config': gating_dict, + 'duration_config': duration_dict, + } + + except (ObjectDoesNotExist, InvalidKeyError): + return {}