diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 319075aa3f379bbbf84012ac2e77aae25813a19c..9043aa411d8c19350a5843e676a7188a1140293c 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -60,6 +60,9 @@ from lms.djangoapps.certificates.models import ( from lms.djangoapps.certificates.tests.factories import CertificateInvalidationFactory, GeneratedCertificateFactory from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService +from lms.djangoapps.courseware.views.index import show_courseware_mfe_link +from lms.djangoapps.courseware.toggles import REDIRECT_TO_COURSEWARE_MICROFRONTEND +from lms.djangoapps.courseware.url_helpers import get_microfrontend_url from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT from lms.djangoapps.grades.config.waffle import waffle as grades_waffle from lms.djangoapps.verify_student.models import VerificationDeadline @@ -3213,3 +3216,59 @@ class DatesTabTestCase(ModuleStoreTestCase): self.assertNotContains(response, '<div class="pill due">') # Should have verified pills for audit enrollments self.assertContains(response, '<div class="pill verified">') + + +class TestShowCoursewareMFE(TestCase): + """ + Make sure we're showing the Courseware MFE link when appropriate. + """ + def test_when_to_show(self): + course_key = CourseKey.from_string("course-v1:OpenEdX+MFE+2020") + global_staff_user = UserFactory(username="global_staff", is_staff=True) + user = UserFactory(username="normal", is_staff=False) + + # We never show when the feature is entirely disabled. + with patch.dict(settings.FEATURES, {'ENABLE_COURSEWARE_MICROFRONTEND': False}): + self.assertFalse(show_courseware_mfe_link(global_staff_user, True, course_key)) + self.assertFalse(show_courseware_mfe_link(user, True, course_key)) + self.assertFalse(show_courseware_mfe_link(user, False, course_key)) + + # If it's enabled at the platform level, what we do depends on the + # CourseWaffleFlag and type of user... + with patch.dict(settings.FEATURES, {'ENABLE_COURSEWARE_MICROFRONTEND': True}): + # If the feature is enabled at the platform level, we always display + # the MFE link to global staff. But course staff only see it if the + # CourseWaffleFlag is also enabled for that course. Regular users + # never see the link. + with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, False): + self.assertTrue(show_courseware_mfe_link(global_staff_user, True, course_key)) + self.assertFalse(show_courseware_mfe_link(user, True, course_key)) + self.assertFalse(show_courseware_mfe_link(user, False, course_key)) + + # If both the feature flag and CourseWaffleFlag are enabled, we should show + # to global and course staff, but not normal users. + with override_waffle_flag(REDIRECT_TO_COURSEWARE_MICROFRONTEND, True): + self.assertTrue(show_courseware_mfe_link(global_staff_user, True, course_key)) + self.assertTrue(show_courseware_mfe_link(user, True, course_key)) + self.assertFalse(show_courseware_mfe_link(user, False, course_key)) + + @override_settings(LEARNING_MICROFRONTEND_URL='https://learningmfe.openedx.org') + def test_url_generation(self): + course_key = CourseKey.from_string("course-v1:OpenEdX+MFE+2020") + section_key = UsageKey.from_string("block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction") + unit_id = "block-v1:OpenEdX+MFE+2020+type@vertical+block@Getting_To_Know_You" + assert get_microfrontend_url(course_key) == ( + 'https://learningmfe.openedx.org' + '/course/course-v1:OpenEdX+MFE+2020' + ) + assert get_microfrontend_url(course_key, section_key, '') == ( + 'https://learningmfe.openedx.org' + '/course/course-v1:OpenEdX+MFE+2020' + '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' + ) + assert get_microfrontend_url(course_key, section_key, unit_id) == ( + 'https://learningmfe.openedx.org' + '/course/course-v1:OpenEdX+MFE+2020' + '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' + '/block-v1:OpenEdX+MFE+2020+type@vertical+block@Getting_To_Know_You' + ) diff --git a/lms/djangoapps/courseware/toggles.py b/lms/djangoapps/courseware/toggles.py index 0058f7631f3b9681be31f47d30441c769cdc1ded..97ed26d0dea6981428cd6b89d75d3ec5c8e7f255 100644 --- a/lms/djangoapps/courseware/toggles.py +++ b/lms/djangoapps/courseware/toggles.py @@ -2,7 +2,7 @@ Toggles for courseware in-course experience. """ -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from django.conf import settings from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace # Namespace for courseware waffle flags. @@ -25,6 +25,6 @@ REDIRECT_TO_COURSEWARE_MICROFRONTEND = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, ' def should_redirect_to_courseware_microfrontend(course_key): return ( - configuration_helpers.get_value('ENABLE_COURSEWARE_MICROFRONTEND') and + settings.FEATURES.get('ENABLE_COURSEWARE_MICROFRONTEND') and REDIRECT_TO_COURSEWARE_MICROFRONTEND.is_enabled(course_key) ) diff --git a/lms/djangoapps/courseware/url_helpers.py b/lms/djangoapps/courseware/url_helpers.py index 52bea116a8d83867e69e71ce23bc1ac57084a9f3..eb0e06fd79f5b1f4cc5e45b0decfa95f1c83406d 100644 --- a/lms/djangoapps/courseware/url_helpers.py +++ b/lms/djangoapps/courseware/url_helpers.py @@ -55,44 +55,36 @@ def get_redirect_url(course_key, usage_key, request=None): return redirect_url -def get_microfrontend_redirect_url(course_key, path=None): +def get_microfrontend_url(course_key, sequence_key=None, unit_key=None): """ - The micro-frontend determines the user's position in the vertical via - a separate API call, so all we need here is the course_key, section, and vertical - IDs to format it's URL. + Return a str with the URL for the specified content in the Courseware MFE. - It is also capable of determining our section and vertical if they're not present. Fully - specifying it all is preferable, though, as the micro-frontend can save itself some work, - resulting in a better user experience. + The micro-frontend determines the user's position in the vertical via + a separate API call, so all we need here is the course_key, section, and + vertical IDs to format it's URL. For simplicity and performance reasons, + this method does not inspect the modulestore to try to figure out what + Unit/Vertical a sequence is in. If you try to pass in a unit_key without + a sequence_key, the value will just be ignored and you'll get a URL pointing + to just the course_key. + + It is also capable of determining our section and vertical if they're not + present. Fully specifying it all is preferable, though, as the + micro-frontend can save itself some work, resulting in a better user + experience. We're building a URL like this: http://localhost:2000/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76 + + `course_key`, `sequence_key`, and `unit_key` can be either OpaqueKeys or + strings. They're only ever used to concatenate a URL string. """ + mfe_link = '{}/course/{}'.format(settings.LEARNING_MICROFRONTEND_URL, course_key) - redirect_url = '{base_url}/{prefix}/{course_key}'.format( - base_url=settings.LEARNING_MICROFRONTEND_URL, - prefix='course', - course_key=course_key - ) - - if path is None: - return redirect_url - - # The first four elements of the path list are the ones we care about here: - # - course - # - chapter - # - sequence - # - vertical - # We skip course because we already have it from our argument above, and we skip chapter - # because the micro-frontend URL doesn't include it. - if len(path) > 2: - redirect_url += '/{sequence_key}'.format( - sequence_key=path[2] - ) - if len(path) > 3: - redirect_url += '/{vertical_key}'.format( - vertical_key=path[3] - ) + if sequence_key: + mfe_link += '/{}'.format(sequence_key) - return redirect_url + if unit_key: + mfe_link += '/{}'.format(unit_key) + + return mfe_link diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index 4a0fcdd7195e69562e20fccf692c1a9cf051f983..7dc0eeb5ecd2a80dfec4f923d31e781e59d5f72b 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -31,6 +31,8 @@ from web_fragments.fragment import Fragment from edxmako.shortcuts import render_to_response, render_to_string from lms.djangoapps.courseware.courses import allow_public_access from lms.djangoapps.courseware.exceptions import CourseAccessRedirect +from lms.djangoapps.courseware.toggles import should_redirect_to_courseware_microfrontend +from lms.djangoapps.courseware.url_helpers import get_microfrontend_url from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from lms.djangoapps.gating.api import get_entrance_exam_score_ratio, get_entrance_exam_usage_key from lms.djangoapps.grades.api import CourseGradeFactory @@ -414,6 +416,7 @@ class CoursewareIndex(View): settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH') or (settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF') and self.is_staff) ) + staff_access = self.is_staff courseware_context = { 'csrf': csrf(self.request)['csrf_token'], @@ -423,7 +426,7 @@ class CoursewareIndex(View): 'section': self.section, 'init': '', 'fragment': Fragment(), - 'staff_access': self.is_staff, + 'staff_access': staff_access, 'can_masquerade': self.can_masquerade, 'masquerade': self.masquerade, 'supports_preview_menu': True, @@ -482,12 +485,24 @@ class CoursewareIndex(View): table_of_contents['previous_of_active_section'], table_of_contents['next_of_active_section'], ) - courseware_context['unit'] = section_context.get('activate_block_id', '') + unit_id = section_context.get('activate_block_id', '') + courseware_context['unit'] = unit_id courseware_context['fragment'] = self.section.render(self.view, section_context) if self.section.position and self.section.has_children: self._add_sequence_title_to_context(courseware_context) + # Courseware MFE link + if show_courseware_mfe_link(request.user, staff_access, self.course.id): + if self.section: + courseware_context['microfrontend_link'] = get_microfrontend_url( + self.course.id, self.section.location, unit_id + ) + else: + courseware_context['microfrontend_link'] = get_microfrontend_url(self.course.id) + else: + courseware_context['microfrontend_link'] = None + return courseware_context def _add_sequence_title_to_context(self, courseware_context): @@ -606,3 +621,26 @@ def save_positions_recursively_up(user, request, field_data_cache, xmodule, cour save_child_position(parent, current_module.location.block_id) current_module = parent + + +def show_courseware_mfe_link(user, staff_access, course_key): + """ + Return whether to display the button to switch to the Courseware MFE. + """ + # The MFE isn't enabled at all, so don't show the button. + if not settings.FEATURES.get('ENABLE_COURSEWARE_MICROFRONTEND'): + return False + + # Global staff members always get to see the courseware MFE button if + # the basic feature is enabled at all, regardless of whether a course + # has enabled it via flag. + if user.is_staff: + return True + + # If you have course staff access, you see this link only if your + # students would be redirected to the new experience (course staff are + # never automatically redirected). + if staff_access and should_redirect_to_courseware_microfrontend(course_key): + return True + + return False diff --git a/lms/envs/common.py b/lms/envs/common.py index 399b59f549b214eb4df5c2d293289aff933d3142..071672918fe3482e3575687c6ec3c7819ec6673a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -416,6 +416,19 @@ FEATURES = { # .. toggle_status: supported # .. toggle_warnings: None 'ENABLE_CHANGE_USER_PASSWORD_ADMIN': False, + + # .. toggle_name: ENABLE_COURSEWARE_MICROFRONTEND + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Set to True to enable the Courseware MFE at the platform level for global staff (see REDIRECT_TO_COURSEWARE_MICROFRONTEND for course rollout) + # .. toggle_category: admin + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2020-03-05 + # .. toggle_expiration_date: None + # .. toggle_tickets: 'https://github.com/edx/edx-platform/pull/23317' + # .. toggle_status: supported + # .. toggle_warnings: Also set settings.LEARNING_MICROFRONTEND_URL and see REDIRECT_TO_COURSEWARE_MICROFRONTEND for rollout. + 'ENABLE_COURSEWARE_MICROFRONTEND': False, } # Settings for the course reviews tool template and identification key, set either to None to disable course reviews diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index b440eb38410bf99c58caa4c0e70184b7b148f136..9e01cc1357815f3485ed445684e91302b23f8cc5 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -219,6 +219,9 @@ FEATURES['ENABLE_COSMETIC_DISPLAY_PRICE'] = True ######################### Program Enrollments ##################### FEATURES['ENABLE_ENROLLMENT_RESET'] = True +######################### New Courseware MFE ##################### +FEATURES['ENABLE_COURSEWARE_MICROFRONTEND'] = True + ########################## Third Party Auth ####################### if FEATURES.get('ENABLE_THIRD_PARTY_AUTH') and 'third_party_auth.dummy.DummyBackend' not in AUTHENTICATION_BACKENDS: diff --git a/lms/templates/preview_menu.html b/lms/templates/preview_menu.html index a863b2d41b74afa5844e1f84163acbdacd957262..cd7da9f5031027ef3afd07cd4345cec33f988e5d 100644 --- a/lms/templates/preview_menu.html +++ b/lms/templates/preview_menu.html @@ -19,16 +19,6 @@ show_preview_menu = course and can_masquerade and supports_preview_menu def selected(is_selected): return "selected" if is_selected else "" - def get_mfe_link(): - if section: - mfe_link = '{}/course/{}/{}'.format(settings.LEARNING_MICROFRONTEND_URL, course.id, section.location) - if unit: - mfe_link += '/' + unit - else: - mfe_link = None - return mfe_link - - mfe_link = get_mfe_link() course_partitions = get_all_partitions_for_course(course) masquerade_user_name = masquerade.user_name if masquerade else None masquerade_group_id = masquerade.group_id if masquerade else None @@ -76,9 +66,9 @@ show_preview_menu = course and can_masquerade and supports_preview_menu </p> </div> % endif - % if user.is_staff and mfe_link: + % if microfrontend_link: <div style="flex-grow: 1; text-align: right;"> - <a class="btn btn-primary" style="border: solid 1px white;" href="${mfe_link}"> + <a class="btn btn-primary" style="border: solid 1px white;" href="${microfrontend_link}"> ${_("View this unit in the new experience")} </a> </div>