Merge pull request #28655 from edx/pshiu/REV-2325_TrackSelectionUnhappyPath

feat: Add screen for partial feature based enrollment
This commit is contained in:
Phillip Shiu
2021-09-15 16:13:35 -04:00
committed by GitHub
6 changed files with 242 additions and 117 deletions

View File

@@ -4,7 +4,6 @@ Tests for course_modes views.
import decimal import decimal
import mock
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import patch from unittest.mock import patch
@@ -35,6 +34,12 @@ from xmodule.modulestore.tests.factories import CourseFactory
from ..views import VALUE_PROP_TRACK_SELECTION_FLAG from ..views import VALUE_PROP_TRACK_SELECTION_FLAG
# Name of the method to mock for Content Type Gating.
GATING_METHOD_NAME = 'openedx.features.content_type_gating.models.ContentTypeGatingConfig.enabled_for_enrollment'
# Name of the method to mock for Course Duration Limits.
CDL_METHOD_NAME = 'openedx.features.course_duration_limits.models.CourseDurationLimitConfig.enabled_for_enrollment'
@ddt.ddt @ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@@ -511,44 +516,14 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM' redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM'
self.assertRedirects(response, redirect_url) self.assertRedirects(response, redirect_url)
# Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out. def _assert_fbe_page(self, response, min_price=None, **_):
# Other tests may need to be updated/removed to reflect the new page. """
# The below test can be separated into multiple tests once un-happy path is implemented. Assert fbe.html was rendered.
def test_new_track_selection(self): """
# For the new track selection template to render, FBE must be fully on (gated_content and audit_access_deadline)
# and happy path conditions must be met:
# User can upgrade, FBE is fully on, and user is not an enterprise user.
# Create the course modes and enroll the user
verified_mode = CourseModeFactory.create(
mode_slug='verified',
course_id=self.course_that_started.id,
min_price=149,
)
CourseEnrollmentFactory(
is_active=True,
course_id=self.course_that_started.id,
user=self.user
)
# Check whether new track selection template is rendered.
# This should *only* be shown when the waffle flag is on.
with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True):
with mock.patch(
'openedx.features.content_type_gating.models.ContentTypeGatingConfig.enabled_for_enrollment',
return_value=True
):
with mock.patch(
'openedx.features.course_duration_limits.models.CourseDurationLimitConfig.enabled_for_enrollment',
return_value=True
):
url = reverse('course_modes_choose', args=[str(self.course_that_started.id)])
response = self.client.get(url)
self.assertContains(response, "Choose a path for your course in") self.assertContains(response, "Choose a path for your course in")
# Check if it displays the upgrade price for verified track and "Free" for audit track # Check if it displays the upgrade price for verified track and "Free" for audit track
self.assertContains(response, verified_mode.min_price) self.assertContains(response, min_price)
self.assertContains(response, "Free") self.assertContains(response, "Free")
# Check for specific HTML elements # Check for specific HTML elements
@@ -558,6 +533,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
# Check for upgrade button ID # Check for upgrade button ID
self.assertContains(response, 'track_selection_upgrade') self.assertContains(response, 'track_selection_upgrade')
# Check for audit button ID # Check for audit button ID
self.assertContains(response, 'track_selection_audit') self.assertContains(response, 'track_selection_audit')
@@ -565,11 +541,94 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertContains(response, '<li class="collapsible-item">') self.assertContains(response, '<li class="collapsible-item">')
self.assertContains(response, 'access to all course activities') self.assertContains(response, 'access to all course activities')
self.assertContains(response, 'Full access') self.assertContains(response, 'Full access')
# Check for happy path messaging - audit # Check for happy path messaging - audit
self.assertContains(response, "discussion forums and non-graded assignments") self.assertContains(response, "discussion forums and non-graded assignments")
self.assertContains(response, "Get temporary access") self.assertContains(response, "Get temporary access")
self.assertContains(response, "Access expires and all progress will be lost") self.assertContains(response, "Access expires and all progress will be lost")
def _assert_unfbe_page(self, response, min_price=None, **_):
"""
Assert track_selection.html and unfbe.html were rendered.
"""
# Check for string unique to track_selection.html.
self.assertContains(response, "| Upgrade Now")
# This string only occurs in lms/templates/course_modes/track_selection.html
# and related theme and translation files.
# Check for string unique to unfbe.html.
self.assertContains(response, "Some graded content may be lost")
# This string only occurs in lms/templates/course_modes/unfbe.html
# and related theme and translation files.
# Check min_price was correctly passed in.
self.assertContains(response, min_price)
def _assert_legacy_page(self, response, **_):
"""
Assert choose.html was rendered.
"""
# Check for string unique to the legacy choose.html.
self.assertContains(response, "Choose Your Track")
# This string only occurs in lms/templates/course_modes/choose.html
# and related theme and translation files.
@ddt.data(
# gated_content_on, course_duration_limits_on, waffle_flag_on, expected_page_assertion_function
(True, True, True, _assert_fbe_page),
(True, False, True, _assert_unfbe_page),
(False, True, True, _assert_unfbe_page),
(False, False, True, _assert_unfbe_page),
(True, True, False, _assert_legacy_page),
(True, False, False, _assert_legacy_page),
(False, True, False, _assert_legacy_page),
(False, False, False, _assert_legacy_page),
)
@ddt.unpack
def test_track_selection_types(
self,
gated_content_on,
course_duration_limits_on,
waffle_flag_on,
expected_page_assertion_function
):
"""
Feature-based enrollment (FBE) is when gated content and course duration
limits are enabled when a user is auditing a course.
When prompted to perform track selection (choosing between the audit and
verified course modes), the learner may view 3 different pages:
1. fbe.html - full FBE
2. unfbe.html - partial or no FBE
3. choose.html - legacy track selection page
This test checks that the right template is rendered.
"""
# The active course mode already exists. Create verified course mode:
verified_mode = CourseModeFactory.create(
mode_slug='verified',
course_id=self.course_that_started.id,
min_price=149,
)
# Enroll the test user in the audit mode:
CourseEnrollmentFactory(
is_active=True,
course_id=self.course_that_started.id,
user=self.user
)
# Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out.
# Check whether new track selection template is rendered.
# This should *only* be shown when the waffle flag is on.
with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=waffle_flag_on):
with patch(GATING_METHOD_NAME, return_value=gated_content_on):
with patch(CDL_METHOD_NAME, return_value=course_duration_limits_on):
url = reverse('course_modes_choose', args=[str(self.course_that_started.id)])
response = self.client.get(url)
expected_page_assertion_function(self, response, min_price=verified_mode.min_price)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):

View File

@@ -139,14 +139,11 @@ class ChooseModeView(View):
# If there isn't a verified mode available, then there's nothing # If there isn't a verified mode available, then there's nothing
# to do on this page. Send the user to the dashboard. # to do on this page. Send the user to the dashboard.
if not CourseMode.has_verified_mode(modes): if not CourseMode.has_verified_mode(modes):
return redirect(reverse('dashboard')) return self._redirect_to_course_or_dashboard(course, course_key, request.user)
# If a user has already paid, redirect them to the dashboard. # If a user has already paid, redirect them to the dashboard.
if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]):
# If the course has started redirect to course home instead return self._redirect_to_course_or_dashboard(course, course_key, request.user)
if course.has_started():
return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key}))
return redirect(reverse('dashboard'))
donation_for_course = request.session.get("donation_for_course", {}) donation_for_course = request.session.get("donation_for_course", {})
chosen_price = donation_for_course.get(str(course_key), None) chosen_price = donation_for_course.get(str(course_key), None)
@@ -258,15 +255,18 @@ class ChooseModeView(View):
context['audit_access_deadline'] = formatted_audit_access_date context['audit_access_deadline'] = formatted_audit_access_date
fbe_is_on = deadline and gated_content fbe_is_on = deadline and gated_content
# Route to correct Track Selection page.
# REV-2133 TODO Value Prop: remove waffle flag after testing is completed # REV-2133 TODO Value Prop: remove waffle flag after testing is completed
# and happy path version is ready to be rolled out to all users. # and happy path version is ready to be rolled out to all users.
if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled(): if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled():
# First iteration of happy path does not handle errors. If there are enrollment errors for a learner that is if not error: # TODO: Remove by executing REV-2355
# technically considered happy path, old Track Selection page will be displayed. if not enterprise_customer: # TODO: Remove by executing REV-2342
if not error: if fbe_is_on:
# Happy path conditions. return render_to_response("course_modes/fbe.html", context)
if verified_mode and fbe_is_on and not enterprise_customer: else:
return render_to_response("course_modes/track_selection.html", context) return render_to_response("course_modes/unfbe.html", context)
# If error or enterprise_customer, failover to old choose.html page
return render_to_response("course_modes/choose.html", context) return render_to_response("course_modes/choose.html", context)
@method_decorator(transaction.non_atomic_requests) @method_decorator(transaction.non_atomic_requests)
@@ -309,17 +309,11 @@ class ChooseModeView(View):
# system, such as third-party discovery. These workflows result in learners arriving # system, such as third-party discovery. These workflows result in learners arriving
# directly at this screen, and they will not necessarily be pre-enrolled in the audit mode. # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode.
CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT) CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT)
# If the course has started redirect to course home instead return self._redirect_to_course_or_dashboard(course, course_key, user)
if course.has_started():
return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key}))
return redirect(reverse('dashboard'))
if requested_mode == 'honor': if requested_mode == 'honor':
CourseEnrollment.enroll(user, course_key, mode=requested_mode) CourseEnrollment.enroll(user, course_key, mode=requested_mode)
# If the course has started redirect to course home instead return self._redirect_to_course_or_dashboard(course, course_key, user)
if course.has_started():
return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key}))
return redirect(reverse('dashboard'))
mode_info = allowed_modes[requested_mode] mode_info = allowed_modes[requested_mode]
@@ -366,6 +360,24 @@ class ChooseModeView(View):
else: else:
return None return None
def _redirect_to_course_or_dashboard(self, course, course_key, user):
"""Perform a redirect to the course if the user is able to access the course.
If the user is not able to access the course, redirect the user to the dashboard.
Args:
course: modulestore object for course
course_key: course_id converted to a course_key
user: request.user, the current user for the request
Returns:
302 to the course if possible or the dashboard if not.
"""
if course.has_started() or user.is_staff:
return redirect(reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key}))
else:
return redirect(reverse('dashboard'))
def create_mode(request, course_id): def create_mode(request, course_id):
"""Add a mode to the course corresponding to the given course ID. """Add a mode to the course corresponding to the given course ID.

View File

@@ -0,0 +1,68 @@
## This template is for track selection with Feature Based Enrollment (FBE).
## This means a learner in the default audit track will have a limited time to
## complete the course (course access duration) and may have some of the content
## of the course unavailable (gated content).
<%page expression_filter="h"/>
<%inherit file="track_selection.html" />
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<%block name="track_selection_certificate_bullets">
<div class="choice-bullets track-selection-type-fbe">
<ul>
<li>${Text(_("Showcase a {link_start}verified certificate{link_end} of completion on your resumé to advance your career")).format(
link_start=HTML('<b><u><a class="verified" href="{track_verified_url}" target="_blank">').format(track_verified_url=track_links['verified_certificate']),
link_end=HTML('</a></u></b>')
)}
</li>
<li>${Text(_("Get {start_bold}access to all course activities{end_bold}, including both graded and non-graded assignments, while the course is running")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
<button class="btn btn-link collapsible">${_("Show more")}</button>
<li class="collapsible-item">
${Text(_("{start_bold}Full access{end_bold} to course content and materials, even after the course ends")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}
<span class="popover-icon">
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0212 6C12.0212 9.31444 9.33472 12 6.02124 12C2.70776 12 0.0212402 9.31444 0.0212402 6C0.0212402 2.68749 2.70776 0 6.02124 0C9.33472 0 12.0212 2.68749 12.0212 6ZM6.02124 7.20968C5.4066 7.20968 4.90834 7.70794 4.90834 8.32258C4.90834 8.93722 5.4066 9.43548 6.02124 9.43548C6.63588 9.43548 7.13414 8.93722 7.13414 8.32258C7.13414 7.70794 6.63588 7.20968 6.02124 7.20968ZM4.96464 3.20937L5.1441 6.49969C5.1525 6.65366 5.2798 6.77419 5.43399 6.77419H6.60849C6.76268 6.77419 6.88998 6.65366 6.89838 6.49969L7.07785 3.20937C7.08692 3.04306 6.95451 2.90323 6.78796 2.90323H5.2545C5.08795 2.90323 4.95556 3.04306 4.96464 3.20937Z" fill="#00262B"/>
</svg>
<span class="popover">
${Text(_("{link_start}Learn more{link_end} about course access")).format(
link_start=HTML('<u><a href="{track_comparison_url}" target="_blank">').format(track_comparison_url=track_links['learn_more']),
link_end=HTML('</a></u>')
)}
</span>
</span>
</li>
<li class="collapsible-item">${Text(_("Support our {start_bold}mission{end_bold} to increase access to high-quality education for everyone, everywhere")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
</ul>
</div>
</%block>
<%block name="track_selection_audit_bullets">
<div class="choice-bullets track-selection-type-fbe">
<ul>
<li>${Text(_("Get temporary access to {start_bold}non-graded{end_bold} activities, including discussion forums and non-graded assignments")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
<li>${Text(_("Get {start_bold}temporary access{end_bold} to the course material, including videos and readings")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
% if audit_access_deadline:
<li>${_("Access expires and all progress will be lost on")} ${audit_access_deadline}</li>
% else:
<li>${_("Access expires and all progress will be lost")}</li>
% endif
</ul>
</div>
</%block>

View File

@@ -95,43 +95,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
</span> </span>
<p class="price-display">${currency_symbol}${min_price} ${currency}</p> <p class="price-display">${currency_symbol}${min_price} ${currency}</p>
<div class="choice-title"><h4>${_("Earn a certificate")}</h4></div> <div class="choice-title"><h4>${_("Earn a certificate")}</h4></div>
<div class="choice-bullets"> <%block name="track_selection_certificate_bullets"/>
<ul>
<li>
${Text(_("Showcase a {link_start}verified certificate{link_end} of completion on your resumé to advance your career")).format(
link_start=HTML('<b><u><a class="verified" href="{track_verified_url}" target="_blank">').format(track_verified_url=track_links['verified_certificate']),
link_end=HTML('</a></u></b>')
)}
</li>
<li>${Text(_("Get {start_bold}access to all course activities{end_bold}, including both graded and non-graded assignments, while the course is running")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
<button class="btn btn-link collapsible">${_("Show more")}</button>
<li class="collapsible-item">
${Text(_("{start_bold}Full access{end_bold} to course content and materials, even after the course ends")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}
<span class="popover-icon">
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0212 6C12.0212 9.31444 9.33472 12 6.02124 12C2.70776 12 0.0212402 9.31444 0.0212402 6C0.0212402 2.68749 2.70776 0 6.02124 0C9.33472 0 12.0212 2.68749 12.0212 6ZM6.02124 7.20968C5.4066 7.20968 4.90834 7.70794 4.90834 8.32258C4.90834 8.93722 5.4066 9.43548 6.02124 9.43548C6.63588 9.43548 7.13414 8.93722 7.13414 8.32258C7.13414 7.70794 6.63588 7.20968 6.02124 7.20968ZM4.96464 3.20937L5.1441 6.49969C5.1525 6.65366 5.2798 6.77419 5.43399 6.77419H6.60849C6.76268 6.77419 6.88998 6.65366 6.89838 6.49969L7.07785 3.20937C7.08692 3.04306 6.95451 2.90323 6.78796 2.90323H5.2545C5.08795 2.90323 4.95556 3.04306 4.96464 3.20937Z" fill="#00262B"/>
</svg>
<span class="popover">
${Text(_("{link_start}Learn more{link_end} about course access")).format(
link_start=HTML('<u><a href="{track_comparison_url}" target="_blank">').format(track_comparison_url=track_links['learn_more']),
link_end=HTML('</a></u>')
)}
</span>
</span>
</li>
<li class="collapsible-item">${Text(_("Support our {start_bold}mission{end_bold} to increase access to high-quality education for everyone, everywhere")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
</ul>
</div>
<ul class="list-actions"> <ul class="list-actions">
<li class="action action-select track-selection-button"> <li class="action action-select track-selection-button">
<input type="hidden" name="contribution" value="${price_before_discount or min_price}" /> <input type="hidden" name="contribution" value="${price_before_discount or min_price}" />
@@ -167,23 +131,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
<div class="track-selection-choice track-selection-audit ml-md-3"> <div class="track-selection-choice track-selection-audit ml-md-3">
<p class="float-right text-uppercase price-display">${_("Free")}</p> <p class="float-right text-uppercase price-display">${_("Free")}</p>
<div class="choice-title"><h4>${_("Access this course")}</h4></div> <div class="choice-title"><h4>${_("Access this course")}</h4></div>
<div class="choice-bullets"> <%block name="track_selection_audit_bullets"/>
<ul>
<li>${Text(_("Get temporary access to {start_bold}non-graded{end_bold} activities, including discussion forums and non-graded assignments")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
<li>${Text(_("Get {start_bold}temporary access{end_bold} to the course material, including videos and readings")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
% if audit_access_deadline:
<li>${_("Access expires and all progress will be lost on")} ${audit_access_deadline}</li>
% else:
<li>${_("Access expires and all progress will be lost")}</li>
% endif
</ul>
</div>
<ul class="list-actions"> <ul class="list-actions">
<li class="action action-select track-selection-button"> <li class="action action-select track-selection-button">
<button id="track_selection_audit" type="submit" name="audit_mode"> <button id="track_selection_audit" type="submit" name="audit_mode">

View File

@@ -0,0 +1,38 @@
## This template is for track selection with partial or no Feature Based
## Enrollment (FBE). In other words, it is un-FBE. FBE is when the learner in
## the default audit track has a limited time to complete the course (course
## access duration) and has some of the content of the course unavailable (gated
## content). This template covers un-FBE, which is when the learner is subject
## to either: (1) gated content but not course access duration; (2) course
## access duration but not gated content; or (3) neither gated content nor
## course access duration.
<%page expression_filter="h"/>
<%inherit file="track_selection.html" />
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<%block name="track_selection_certificate_bullets">
<div class="choice-bullets track-selection-type-unfbe">
<ul>
<li>${Text(_("Showcase a {link_start}verified certificate{link_end} of completion on your resumé to advance your career")).format(
link_start=HTML('<b><u><a class="verified" href="{track_verified_url}" target="_blank">').format(track_verified_url=track_links['verified_certificate']),
link_end=HTML('</a></u></b>'),
)}</li>
<li>${Text(_("Support our {start_bold}mission{end_bold} to increase access to high-quality education for everyone, everywhere")).format(
start_bold=HTML('<b>'),
end_bold=HTML('</b>'),
)}</li>
</ul>
</div>
</%block>
<%block name="track_selection_audit_bullets">
<div class="choice-bullets track-selection-type-unfbe">
<ul>
<li>${_("Get access to the course material, including videos and readings")}</li>
<li>${_("Some graded content may be lost")}</li>
</ul>
</div>
</%block>

View File

@@ -193,7 +193,7 @@ from common.djangoapps.pipeline_mako import render_require_js_path_overrides
<div class="marketing-hero"><%block name="marketing_hero"></%block></div> <div class="marketing-hero"><%block name="marketing_hero"></%block></div>
<div class="content-wrapper main-container" id="content" dir="${static.dir_rtl()}"> <div class="content-wrapper main-container" id="content" dir="${static.dir_rtl()}">
${self.body()} ${next.body()}
<%block name="bodyextra"/> <%block name="bodyextra"/>
</div> </div>