diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py
index 319075aa3f..9043aa411d 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, '
')
# Should have verified pills for audit enrollments
self.assertContains(response, '
')
+
+
+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 0058f7631f..97ed26d0de 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 52bea116a8..eb0e06fd79 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 sequence_key:
+ mfe_link += '/{}'.format(sequence_key)
- if path is None:
- return redirect_url
+ if unit_key:
+ mfe_link += '/{}'.format(unit_key)
- # 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]
- )
-
- return redirect_url
+ return mfe_link
diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py
index 4a0fcdd719..86a265fd22 100644
--- a/lms/djangoapps/courseware/views/index.py
+++ b/lms/djangoapps/courseware/views/index.py
@@ -25,12 +25,15 @@ from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import View
from edx_django_utils.monitoring import set_custom_metrics_for_course_key
-from opaque_keys.edx.keys import CourseKey
+from opaque_keys import InvalidKeyError
+from opaque_keys.edx.keys import CourseKey, UsageKey
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 +417,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 +427,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 +486,32 @@ 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', '')
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:
+ try:
+ unit_key = UsageKey.from_string(request.GET.get('activate_block_id', ''))
+ # `activate_block_id` is typically a Unit (a.k.a. Vertical),
+ # but it can technically be any block type. Do a check to
+ # make sure it's really a Unit before we use it for the MFE.
+ if unit_key.block_type != 'vertical':
+ unit_key = None
+ except InvalidKeyError:
+ unit_key = None
+
+ courseware_context['microfrontend_link'] = get_microfrontend_url(
+ self.course.id, self.section.location, unit_key
+ )
+ 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 +630,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 399b59f549..071672918f 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 b440eb3841..9e01cc1357 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 a863b2d41b..cd7da9f503 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
% endif
- % if user.is_staff and mfe_link:
+ % if microfrontend_link: