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:
David Ormsbee
2020-03-06 11:58:08 -05:00
committed by GitHub
7 changed files with 152 additions and 48 deletions

View File

@@ -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'
)

View File

@@ -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)
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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>