From da4a6d6103df7f76565f77152ac3f4518f90c357 Mon Sep 17 00:00:00 2001 From: Demid Date: Fri, 22 Apr 2022 16:24:23 +0300 Subject: [PATCH] feat: Implement feature flag to disable students un-enrollment (#29326) Implements a feature flag DISABLE_UNENROLLMENT that is used to disable students un-enrollment for all courses. The Unenrollment option should be disabled when this feature is set to True. ref: BB-4951 Co-authored-by: tinumide Co-authored-by: Tim McCormack --- cms/envs/common.py | 12 ++++++++++++ .../djangoapps/student/tests/test_enrollment.py | 16 ++++++++++++++++ common/djangoapps/student/views/dashboard.py | 5 +++++ common/djangoapps/student/views/management.py | 6 ++++++ lms/envs/common.py | 12 ++++++++++++ lms/templates/dashboard.html | 7 ++++++- .../dashboard/_dashboard_course_listing.html | 4 ++-- 7 files changed, 59 insertions(+), 3 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index b620644f61..b21e07ee99 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -504,6 +504,18 @@ FEATURES = { # .. toggle_warnings: For consistency in user-experience, keep the value in sync with the setting of the same name # in the LMS and CMS. 'MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW': False, + + # .. toggle_name: FEATURES['DISABLE_UNENROLLMENT'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Set to True to disable self-unenrollments via REST API. + # This also hides the "Unenroll" button on the Learner Dashboard. + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2021-10-11 + # .. toggle_warnings: For consistency in user experience, keep the value in sync with the setting of the same name + # in the LMS and CMS. + # .. toggle_tickets: 'https://github.com/open-craft/edx-platform/pull/429' + 'DISABLE_UNENROLLMENT': False, } # .. toggle_name: ENABLE_COPPA_COMPLIANCE diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index bc0264322a..520f5a594e 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -357,6 +357,22 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase, OpenEdxEventsTestMixin) resp = self._change_enrollment('unenroll', course_id="edx/") assert resp.status_code == 400 + @patch.dict(settings.FEATURES, {'DISABLE_UNENROLLMENT': True}) + def test_unenroll_when_unenrollment_disabled(self): + """ + Tests that a user cannot unenroll when unenrollment has been disabled. + """ + # Enroll the student in the course + CourseEnrollment.enroll(self.user, self.course.id, mode="honor") + + # Attempt to unenroll + resp = self._change_enrollment('unenroll') + assert resp.status_code == 400 + + # Verify that user is still enrolled + is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id) + assert is_enrolled + def test_enrollment_limit(self): """ Assert that in a course with max student limit set to 1, we can enroll staff and instructor along with diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 6f6cff7fb4..1c738704ed 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -530,6 +530,10 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem empty_dashboard_message = configuration_helpers.get_value( 'EMPTY_DASHBOARD_MESSAGE', None ) + disable_unenrollment = configuration_helpers.get_value( + 'DISABLE_UNENROLLMENT', + settings.FEATURES.get('DISABLE_UNENROLLMENT') + ) disable_course_limit = request and 'course_limit' in request.GET course_limit = get_dashboard_course_limit() if not disable_course_limit else None @@ -808,6 +812,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem # TODO START: clean up as part of REVEM-199 (START) 'course_info': get_dashboard_course_info(user, course_enrollments), # TODO START: clean up as part of REVEM-199 (END) + 'disable_unenrollment': disable_unenrollment, } # Include enterprise learner portal metadata and messaging diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index 261d814ba3..66b9d61094 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -403,6 +403,12 @@ def change_enrollment(request, check_access=True): # Otherwise, there is only one mode available (the default) return HttpResponse() elif action == "unenroll": + if configuration_helpers.get_value( + "DISABLE_UNENROLLMENT", + settings.FEATURES.get("DISABLE_UNENROLLMENT") + ): + return HttpResponseBadRequest(_("Unenrollment is currently disabled")) + enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: return HttpResponseBadRequest(_("You are not enrolled in this course")) diff --git a/lms/envs/common.py b/lms/envs/common.py index e9a8dc1862..da7fd16b2c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -998,6 +998,18 @@ FEATURES = { # .. toggle_warnings: For consistency in user-experience, keep the value in sync with the setting of the same name # in the LMS and CMS. 'MARK_LIBRARY_CONTENT_BLOCK_COMPLETE_ON_VIEW': False, + + # .. toggle_name: FEATURES['DISABLE_UNENROLLMENT'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Set to True to disable self-unenrollments via REST API. + # This also hides the "Unenroll" button on the Learner Dashboard. + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2021-10-11 + # .. toggle_warnings: For consistency in user experience, keep the value in sync with the setting of the same name + # in the LMS and CMS. + # .. toggle_tickets: 'https://github.com/open-craft/edx-platform/pull/429' + 'DISABLE_UNENROLLMENT': False, } # Specifies extra XBlock fields that should available when requested via the Course Blocks API diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 59c5a77e58..c7fa05bde9 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -209,7 +209,12 @@ from common.djangoapps.student.models import CourseEnrollment cert_status = cert_statuses.get(session_id) can_refund_entitlement = entitlement and entitlement.is_entitlement_refundable() partner_managed_enrollment = enrollment.mode == 'masters' - can_unenroll = False if partner_managed_enrollment else (not cert_status) or cert_status.get('can_unenroll') if not unfulfilled_entitlement else False + # checks if we can unenroll based on the value of partner_managed_enrollment + can_unenroll_partner_managed_enrollment = False if partner_managed_enrollment else (not cert_status) + # checks if we can unenroll based on the value of unfulfilled_entitlement + can_unenroll_unfulfilled_entitlement = cert_status.get('can_unenroll') if cert_status and not unfulfilled_entitlement else False + # compares the three different parameters by which we can unenroll + can_unenroll = (can_unenroll_partner_managed_enrollment or can_unenroll_unfulfilled_entitlement) and not disable_unenrollment credit_status = credit_statuses.get(session_id) course_mode_info = all_course_modes.get(session_id) is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid) diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index b80ec5d77f..b58479ef81 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -255,11 +255,11 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG % endif ## We should only show the gear dropdown if the user is able to refund/unenroll from their entitlement - ## and/or if they have selected a course run and email_settings are enabled + ## and/or if they have selected a course run, unenrollment is not disabled, and email_settings are enabled ## as these are the only actions currently available % if entitlement and (can_refund_entitlement or show_email_settings): <%include file='_dashboard_entitlement_actions.html' args='course_overview=course_overview,entitlement=entitlement,dashboard_index=dashboard_index, can_refund_entitlement=can_refund_entitlement, show_email_settings=show_email_settings'/> - % elif not entitlement: + % elif not entitlement and (can_unenroll or partner_managed_enrollment or show_email_settings):