Merge pull request #23317 from edx/ormsbee/courseware_mfe_button_visibility
Show MFE preview to course staff (if waffle set)
This commit is contained in:
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user