Commit fd2f3b69 by Peter Fogg

Prevent ordering closed courses, and give a good user message.

When Drupal attempts to enroll a user in a closed course, a 406 will
be returned. This causes the marketing site to redirect to the track
selection page for that course, which will then redirect to the
dashboard with a nice message.

ECOM-2317
parent e6fcfae9
"""
Tests for course_modes views.
"""
from datetime import datetime
import unittest import unittest
import decimal import decimal
import ddt import ddt
import freezegun
from mock import patch from mock import patch
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -9,6 +15,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -9,6 +15,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course from embargo.test_utils import restrict_course
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
...@@ -340,6 +347,21 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -340,6 +347,21 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.assertNotContains(response, "Find courses") self.assertNotContains(response, "Find courses")
self.assertNotContains(response, "Schools & Partners") self.assertNotContains(response, "Schools & Partners")
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@freezegun.freeze_time('2015-01-02')
def test_course_closed(self):
for mode in ["honor", "verified"]:
CourseModeFactory(mode_slug=mode, course_id=self.course.id)
self.course.enrollment_end = datetime(2015, 01, 01)
modulestore().update_item(self.course, self.user.id)
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
response = self.client.get(url)
# URL-encoded version of 1/1/15, 12:00 AM
redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM'
self.assertRedirects(response, redirect_url)
@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):
......
...@@ -3,14 +3,16 @@ Views for the course_mode module ...@@ -3,14 +3,16 @@ Views for the course_mode module
""" """
import decimal import decimal
import urllib
from babel.dates import format_datetime
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import get_language, to_locale, ugettext as _
from django.views.generic.base import View from django.views.generic.base import View
from ipware.ip import get_ip from ipware.ip import get_ip
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -108,6 +110,11 @@ class ChooseModeView(View): ...@@ -108,6 +110,11 @@ class ChooseModeView(View):
chosen_price = donation_for_course.get(unicode(course_key), None) chosen_price = donation_for_course.get(unicode(course_key), None)
course = modulestore().get_course(course_key) course = modulestore().get_course(course_key)
if CourseEnrollment.is_enrollment_closed(request.user, course):
locale = to_locale(get_language())
enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale)
params = urllib.urlencode({'course_closed': enrollment_end_date})
return redirect('{0}?{1}'.format(reverse('dashboard'), params))
# When a credit mode is available, students will be given the option # When a credit mode is available, students will be given the option
# to upgrade from a verified mode to a credit mode at the end of the course. # to upgrade from a verified mode to a credit mode at the end of the course.
......
...@@ -701,6 +701,10 @@ def dashboard(request): ...@@ -701,6 +701,10 @@ def dashboard(request):
redirect_message = _("The course you are looking for does not start until {date}.").format( redirect_message = _("The course you are looking for does not start until {date}.").format(
date=request.GET['notlive'] date=request.GET['notlive']
) )
elif 'course_closed' in request.GET:
redirect_message = _("The course you are looking for is closed for enrollment as of {date}.").format(
date=request.GET['course_closed']
)
else: else:
redirect_message = '' redirect_message = ''
......
...@@ -2,12 +2,13 @@ ...@@ -2,12 +2,13 @@
""" """
End-to-end tests for the LMS. End-to-end tests for the LMS.
""" """
from datetime import datetime, timedelta
from datetime import datetime
from flaky import flaky from flaky import flaky
from textwrap import dedent from textwrap import dedent
from unittest import skip from unittest import skip
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import pytz
import urllib
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
from ..helpers import ( from ..helpers import (
...@@ -1157,6 +1158,95 @@ class NotLiveRedirectTest(UniqueCourseTest): ...@@ -1157,6 +1158,95 @@ class NotLiveRedirectTest(UniqueCourseTest):
@attr('shard_1') @attr('shard_1')
class EnrollmentClosedRedirectTest(UniqueCourseTest):
"""
Test that a banner is shown when the user is redirected to the
dashboard after trying to view the track selection page for a
course after enrollment has ended.
"""
def setUp(self):
"""Create a course that is closed for enrollment, and sign in as a user."""
super(EnrollmentClosedRedirectTest, self).setUp()
course = CourseFixture(
self.course_info['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name']
)
now = datetime.now(pytz.UTC)
course.add_course_details({
'enrollment_start': (now - timedelta(days=30)).isoformat(),
'enrollment_end': (now - timedelta(days=1)).isoformat()
})
course.install()
# Add an honor mode to the course
ModeCreationPage(self.browser, self.course_id).visit()
# Add a verified mode to the course
ModeCreationPage(
self.browser,
self.course_id,
mode_slug=u'verified',
mode_display_name=u'Verified Certificate',
min_price=10,
suggested_prices='10,20'
).visit()
def _assert_dashboard_message(self):
"""
Assert that the 'closed for enrollment' text is present on the
dashboard.
"""
page = DashboardPage(self.browser)
page.wait_for_page()
self.assertIn(
'The course you are looking for is closed for enrollment',
page.banner_text
)
def test_redirect_banner(self):
"""
Navigate to the course info page, then check that we're on the
dashboard page with the appropriate message.
"""
AutoAuthPage(self.browser).visit()
url = BASE_URL + "/course_modes/choose/" + self.course_id
self.browser.get(url)
self._assert_dashboard_message()
def test_login_redirect(self):
"""
Test that the user is correctly redirected after logistration when
attempting to enroll in a closed course.
"""
url = '{base_url}/register?{params}'.format(
base_url=BASE_URL,
params=urllib.urlencode({
'course_id': self.course_id,
'enrollment_action': 'enroll',
'email_opt_in': 'false'
})
)
self.browser.get(url)
register_page = CombinedLoginAndRegisterPage(
self.browser,
start_page="register",
course_id=self.course_id
)
register_page.wait_for_page()
register_page.register(
email="email@example.com",
password="password",
username="username",
full_name="Test User",
country="US",
favorite_movie="Mad Max: Fury Road",
terms_of_service=True
)
self._assert_dashboard_message()
@attr('shard_1')
class LMSLanguageTest(UniqueCourseTest): class LMSLanguageTest(UniqueCourseTest):
""" Test suite for the LMS Language """ """ Test suite for the LMS Language """
def setUp(self): def setUp(self):
......
""" Commerce API v0 view tests. """ """ Commerce API v0 view tests. """
from datetime import datetime, timedelta
import json import json
import itertools import itertools
from uuid import uuid4 from uuid import uuid4
...@@ -10,6 +11,7 @@ from django.test import TestCase ...@@ -10,6 +11,7 @@ from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
import mock import mock
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import pytz
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -25,6 +27,7 @@ from openedx.core.lib.django_test_client_utils import get_absolute_url ...@@ -25,6 +27,7 @@ from openedx.core.lib.django_test_client_utils import get_absolute_url
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import CourseModeFactory from student.tests.factories import CourseModeFactory
from student.tests.tests import EnrollmentEventTestMixin from student.tests.tests import EnrollmentEventTestMixin
from xmodule.modulestore.django import modulestore
@attr('shard_1') @attr('shard_1')
...@@ -345,6 +348,16 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase) ...@@ -345,6 +348,16 @@ class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase)
self.assertEqual(mock_update.called, is_opt_in) self.assertEqual(mock_update.called, is_opt_in)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_closed_course(self):
"""
Ensure that the view does not attempt to create a basket for closed
courses.
"""
self.course.enrollment_end = datetime.now(pytz.UTC) - timedelta(days=1)
modulestore().update_item(self.course, self.user.id) # pylint:disable=no-member
with mock_create_basket(expect_called=False):
self.assertEqual(self._post_to_view().status_code, 406)
@attr('shard_1') @attr('shard_1')
@override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY) @override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY)
......
...@@ -100,6 +100,13 @@ class BasketsView(APIView): ...@@ -100,6 +100,13 @@ class BasketsView(APIView):
msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
return DetailResponse(msg, status=HTTP_409_CONFLICT) return DetailResponse(msg, status=HTTP_409_CONFLICT)
# Check to see if enrollment for this course is closed.
course = courses.get_course(course_key)
if CourseEnrollment.is_enrollment_closed(user, course):
msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id)
log.info(u'Unable to enroll user %s in closed course %s.', user.id, course_id)
return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
# If there is no audit or honor course mode, this most likely # If there is no audit or honor course mode, this most likely
# a Prof-Ed course. Return an error so that the JS redirects # a Prof-Ed course. Return an error so that the JS redirects
# to track selection. # to track selection.
......
...@@ -17,3 +17,4 @@ class Messages(object): ...@@ -17,3 +17,4 @@ class Messages(object):
NO_HONOR_MODE = u'Course {course_id} does not have an honor mode.' NO_HONOR_MODE = u'Course {course_id} does not have an honor mode.'
NO_DEFAULT_ENROLLMENT_MODE = u'Course {course_id} does not have an honor or audit mode.' NO_DEFAULT_ENROLLMENT_MODE = u'Course {course_id} does not have an honor or audit mode.'
ENROLLMENT_EXISTS = u'User {username} is already enrolled in {course_id}.' ENROLLMENT_EXISTS = u'User {username} is already enrolled in {course_id}.'
ENROLLMENT_CLOSED = u'Enrollment is closed for {course_id}.'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment