@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
36
lms/templates/course_modes/error.html
Normal file
36
lms/templates/course_modes/error.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user