feat: Add Track Selection error handling (#28941)

REV-2355
This commit is contained in:
julianajlk
2021-10-29 10:02:41 -04:00
committed by GitHub
parent 2b14c3157b
commit c98cdbd1f7
5 changed files with 132 additions and 28 deletions

View File

@@ -401,17 +401,6 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
assert mode == CourseMode.DEFAULT_MODE_SLUG
assert is_active is True
def test_unsupported_enrollment_mode_failure(self):
# Create the supported course modes
for mode in ('honor', 'verified'):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
# Choose an unsupported mode (POST request)
choose_track_url = reverse('course_modes_choose', args=[str(self.course.id)])
response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['unsupported'])
assert 400 == response.status_code
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_default_mode_creation(self):
# Hit the mode creation endpoint with no querystring params, to create an honor mode
@@ -518,6 +507,49 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM'
self.assertRedirects(response, redirect_url)
@ddt.data(
(False, {'audit_mode': True}, 'Enrollment is closed', 302),
(False, {'verified_mode': True, 'contribution': '1.23'}, 'Enrollment is closed', 302),
(True, {'verified_mode': True, 'contribution': 'abc'}, 'Invalid amount selected', 200),
(True, {'verified_mode': True, 'contribution': '0.1'}, 'No selected price or selected price is too low.', 200),
(True, {'unsupported_mode': True}, 'Enrollment mode not supported', 200),
)
@ddt.unpack
@patch('django.contrib.auth.models.PermissionsMixin.has_perm')
def test_errors(self, has_perm, post_params, error_msg, status_code, mock_has_perm):
"""
Test the error template is rendered on different types of errors.
When the chosen CourseMode is 'honor' or 'audit' via POST,
it redirects to dashboard, but if there's an error in the process,
it shows the error template.
If the user does not have permission to enroll, GET is called with error message,
but it also redirects to dashboard.
"""
# Create course modes
for mode in ('audit', 'honor', 'verified'):
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
# Value Prop TODO (REV-2378): remove waffle flag from tests once flag is removed.
with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True):
mock_has_perm.return_value = has_perm
url = reverse('course_modes_choose', args=[str(self.course.id)])
# Choose mode (POST request)
response = self.client.post(url, post_params)
self.assertEqual(response.status_code, status_code)
if has_perm:
self.assertContains(response, error_msg)
self.assertContains(response, 'Sorry, we were unable to enroll you')
# Check for CTA button on error page
marketing_root = settings.MKTG_URLS.get('ROOT')
search_courses_url = urljoin(marketing_root, '/search?tab=course')
self.assertContains(response, search_courses_url)
self.assertContains(response, '<span>Explore all courses</span>')
else:
self.assertTrue(CourseEnrollment.is_enrollment_closed(self.user, self.course))
def _assert_fbe_page(self, response, min_price=None, **_):
"""
Assert fbe.html was rendered.

View File

@@ -11,9 +11,10 @@ import six
import waffle # lint-amnesty, pylint: disable=invalid-django-waffle-import
from babel.dates import format_datetime
from babel.numbers import get_currency_symbol
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.decorators import method_decorator
@@ -23,6 +24,7 @@ from django.views.generic.base import View
from edx_django_utils.monitoring.utils import increment
from ipware.ip import get_client_ip
from opaque_keys.edx.keys import CourseKey
from urllib.parse import urljoin
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.helpers import get_course_final_price, get_verified_track_links
@@ -133,7 +135,6 @@ class ChooseModeView(View):
if purchase_workflow == "bulk" and professional_mode.bulk_sku:
redirect_url = ecommerce_service.get_checkout_page_url(professional_mode.bulk_sku)
return redirect(redirect_url)
course = modulestore().get_course(course_key)
# If there isn't a verified mode available, then there's nothing
@@ -152,6 +153,10 @@ class ChooseModeView(View):
locale = to_locale(get_language())
enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale)
params = six.moves.urllib.parse.urlencode({'course_closed': enrollment_end_date})
LOG.info(
'[Track Selection Check] Enrollment is closed redirect for course [%s], user [%s]',
course_id, request.user.username
)
return redirect('{}?{}'.format(reverse('dashboard'), params))
# When a credit mode is available, students will be given the option
@@ -187,6 +192,7 @@ class ChooseModeView(View):
"nav_hidden": True,
"content_gating_enabled": gated_content,
"course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment(request.user, course),
"search_courses_url": urljoin(settings.MKTG_URLS.get('ROOT'), '/search?tab=course'),
}
context.update(
get_experiment_user_metadata_context(
@@ -258,17 +264,17 @@ class ChooseModeView(View):
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
# and happy path version is ready to be rolled out to all users.
# REV-2378 TODO Value Prop: remove waffle flag after all edge cases for track selection are completed.
if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled():
if not error: # TODO: Remove by executing REV-2355
if not enterprise_customer_for_request(request): # TODO: Remove by executing REV-2342
if fbe_is_on:
return render_to_response("course_modes/fbe.html", context)
else:
return render_to_response("course_modes/unfbe.html", context)
if not enterprise_customer_for_request(request): # TODO: Remove by executing REV-2342
if error:
return render_to_response("course_modes/error.html", context)
if fbe_is_on:
return render_to_response("course_modes/fbe.html", context)
else:
return render_to_response("course_modes/unfbe.html", context)
# If error or enterprise_customer, failover to old choose.html page
# If enterprise_customer, failover to old choose.html page
return render_to_response("course_modes/choose.html", context)
@method_decorator(transaction.non_atomic_requests)
@@ -282,9 +288,10 @@ class ChooseModeView(View):
course_id (unicode): The slash-separated course key.
Returns:
Status code 400 when the requested mode is unsupported. When the honor mode
is selected, redirects to the dashboard. When the verified mode is selected,
returns error messages if the indicated contribution amount is invalid or
When the requested mode is unsupported, returns error message.
When the honor mode is selected, redirects to the dashboard.
When the verified mode is selected, returns error messages
if the indicated contribution amount is invalid or
below the minimum, otherwise redirects to the verification flow.
"""
@@ -296,7 +303,10 @@ class ChooseModeView(View):
course = modulestore().get_course(course_key)
if not user.has_perm(ENROLL_IN_COURSE, course):
error_msg = _("Enrollment is closed")
LOG.info('[Track Selection Check] Error: [%s], for course [%s]', error_msg, course_id)
LOG.info(
'[Track Selection Check] Error: [%s], for course [%s], user [%s]',
error_msg, course_id, request.user.username
)
return self.get(request, course_id, error=error_msg)
requested_mode = self._get_requested_mode(request.POST)
@@ -307,7 +317,8 @@ class ChooseModeView(View):
'[Track Selection Check] Error: requested enrollment mode [%s] is not supported for course [%s]',
requested_mode, course_id
)
return HttpResponseBadRequest(_("Enrollment mode not supported"))
error_msg = _("Enrollment mode not supported")
return self.get(request, course_id, error=error_msg)
if requested_mode == 'audit':
# If the learner has arrived at this screen via the traditional enrollment workflow,

View File

@@ -219,6 +219,32 @@
content: '';
}
section.error-container {
height: 42rem;
}
.error-button {
margin-top: 2.8rem;
margin-bottom: 1.5rem;
padding: 10px 16px;
box-shadow: none;
border-radius: 0px;
position: relative;
}
.error-button span {
font-weight: 500;
font-size: 18px;
color: #fff;
}
.error-button:hover {
background: #2D494E;
border-color: transparent;
color: #fff;
text-decoration: none;
}
@media (max-width: map-get($grid-breakpoints, 'sm')) {
.collapsible {
margin-bottom: 1.3rem;

View File

@@ -0,0 +1,36 @@
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
from django.urls import reverse
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%namespace name='static' file='/static_content.html'/>
<%block name="bodyclass">step-select-track verification-process</%block>
<%block name="pagetitle">
${_("Unable to enroll in {course_name}").format(course_name=course_name)}
</%block>
<%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('paragon/static/paragon.min.css')}" />
</%block>
<%block name="content">
<div class="track-selection-container mx-auto">
<section class="wrapper m-4 error-container">
<header class="page-header mt-5">
<h3>${_("Sorry, we were unable to enroll you in")} ${_(course_name)}.</h3>
</header>
<p class="text-center p-1">${_("Error:")} ${_(error)}</p>
<ul class="list-actions">
<li class="text-center">
<a href="${search_courses_url}" class="button error-button" title="${_('Explore all courses on edX')}">
<span>${_("Explore all courses")}</span>
</a>
</li>
</ul>
</section>
</div>
</%block>

View File

@@ -1,6 +1,5 @@
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text