From e609f982d7322d8883ec9cda49ad9512a4df14ef Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 10 Feb 2015 13:07:51 -0500 Subject: [PATCH] Country Access: block enrollment Block users from enrolling in a course if the user is blocked by country access rules. 1) Enrollment via the login/registration page. 2) Enrollment from the marketing iframe (via student.views.change_enrollment) 3) Enrollment using 100% redeem codes. 4) Enrollment via upgrade. This does NOT cover enrollment through third party authentication, which is sufficiently complex to deserve its own commit. --- .../course_modes/tests/test_views.py | 34 ++++++++- common/djangoapps/course_modes/views.py | 15 ++++ .../djangoapps/enrollment/tests/test_views.py | 70 +++++++++++++++++-- common/djangoapps/enrollment/views.py | 44 +++++++++--- .../student/tests/test_enrollment.py | 31 +++++++- common/djangoapps/student/views.py | 14 ++++ .../shoppingcart/tests/test_views.py | 51 +++++++++++++- lms/djangoapps/shoppingcart/views.py | 22 ++++++ .../verify_student/tests/test_views.py | 21 +++++- lms/djangoapps/verify_student/views.py | 14 ++++ .../spec/student_account/enrollment_spec.js | 24 ++++++- lms/static/js/student_account/enrollment.js | 21 +++++- 12 files changed, 337 insertions(+), 24 deletions(-) diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 434eee61a9..a779f1a24b 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -11,6 +11,7 @@ from xmodule.modulestore.tests.django_utils import ( ) from util.testing import UrlResetMixin +from embargo.test_utils import restrict_course from xmodule.modulestore.tests.factories import CourseFactory from course_modes.tests.factories import CourseModeFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory @@ -274,7 +275,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): # Create a verified mode url = reverse('create_mode', args=[unicode(self.course.id)]) - response = self.client.get(url, parameters) + self.client.get(url, parameters) honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None) verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None) @@ -282,3 +283,34 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): course_modes = CourseMode.modes_for_course(self.course.id) self.assertEquals(course_modes, expected_modes) + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): + """Test embargo restrictions on the track selection page. """ + + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def setUp(self): + super(TrackSelectionEmbargoTest, self).setUp('embargo') + + # Create a course and course modes + self.course = CourseFactory.create() + CourseModeFactory(mode_slug='honor', course_id=self.course.id) + CourseModeFactory(mode_slug='verified', course_id=self.course.id, min_price=10) + + # Create a user and log in + self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx") + self.client.login(username=self.user.username, password="edx") + + # Construct the URL for the track selection page + self.url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_embargo_restrict(self): + with restrict_course(self.course.id) as redirect_url: + response = self.client.get(self.url) + self.assertRedirects(response, redirect_url) + + def test_embargo_allow(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 7d022fed31..e20542d6b9 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -3,6 +3,8 @@ Views for the course_mode module """ import decimal +from ipware.ip import get_ip + from django.core.urlresolvers import reverse from django.conf import settings from django.http import HttpResponse, HttpResponseBadRequest @@ -22,6 +24,8 @@ from opaque_keys.edx.keys import CourseKey from util.db import commit_on_success_with_read_committed from xmodule.modulestore.django import modulestore +from embargo import api as embargo_api + class ChooseModeView(View): """View used when the user is asked to pick a mode. @@ -52,6 +56,17 @@ class ChooseModeView(View): """ course_key = CourseKey.from_string(course_id) + # Check whether the user has access to this course + # based on country access rules. + embargo_redirect = embargo_api.redirect_if_blocked( + course_key, + user=request.user, + ip_address=get_ip(request), + url=request.path + ) + if embargo_redirect: + return redirect(embargo_redirect) + enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index 39175cee9b..3a68bb3ac9 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -6,19 +6,18 @@ import json import unittest from mock import patch -from django.test.utils import override_settings from django.core.urlresolvers import reverse from rest_framework.test import APITestCase from rest_framework import status from django.conf import settings -from xmodule.modulestore.tests.django_utils import ( - ModuleStoreTestCase, mixed_store_config -) +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +from util.testing import UrlResetMixin from enrollment import api from enrollment.errors import CourseEnrollmentError from student.tests.factories import UserFactory, CourseModeFactory from student.models import CourseEnrollment +from embargo.test_utils import restrict_course @ddt.ddt @@ -245,3 +244,66 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase): ) self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn("No course ", resp.content) + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class EnrollmentEmbargoTest(UrlResetMixin, ModuleStoreTestCase): + """Test that enrollment is blocked from embargoed countries. """ + + USERNAME = "Bob" + EMAIL = "bob@example.com" + PASSWORD = "edx" + + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def setUp(self): + """ Create a course and user, then log in. """ + super(EnrollmentEmbargoTest, self).setUp('embargo') + self.course = CourseFactory.create() + self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) + self.client.login(username=self.USERNAME, password=self.PASSWORD) + + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_embargo_change_enrollment_restrict(self): + url = reverse('courseenrollments') + data = json.dumps({ + 'course_details': { + 'course_id': unicode(self.course.id) + }, + 'user': self.user.username + }) + + # Attempt to enroll from a country embargoed for this course + with restrict_course(self.course.id) as redirect_url: + response = self.client.post(url, data, content_type='application/json') + + # Expect an error response + self.assertEqual(response.status_code, 403) + + # Expect that the redirect URL is included in the response + resp_data = json.loads(response.content) + self.assertEqual(resp_data['user_message_url'], redirect_url) + + # Verify that we were not enrolled + self.assertEqual(self._get_enrollments(), []) + + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_embargo_change_enrollment_allow(self): + url = reverse('courseenrollments') + data = json.dumps({ + 'course_details': { + 'course_id': unicode(self.course.id) + }, + 'user': self.user.username + }) + + response = self.client.post(url, data, content_type='application/json') + self.assertEqual(response.status_code, 200) + + # Verify that we were enrolled + self.assertEqual(len(self._get_enrollments()), 1) + + def _get_enrollments(self): + """Retrieve the enrollment list for the current user. """ + url = reverse('courseenrollments') + resp = self.client.get(url) + return json.loads(resp.content) diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index 7815f05536..b7f3bb2b4d 100644 --- a/common/djangoapps/enrollment/views.py +++ b/common/djangoapps/enrollment/views.py @@ -3,15 +3,19 @@ The Enrollment API Views should be simple, lean HTTP endpoints for API access. T consist primarily of authentication, request validation, and serialization. """ -from opaque_keys import InvalidKeyError +from ipware.ip import get_ip +from django.conf import settings from rest_framework import status from rest_framework.authentication import OAuth2Authentication from rest_framework import permissions from rest_framework.response import Response from rest_framework.throttling import UserRateThrottle from rest_framework.views import APIView +from opaque_keys.edx.keys import CourseKey +from opaque_keys import InvalidKeyError from enrollment import api from enrollment.errors import CourseNotFoundError, CourseEnrollmentError, CourseModeNotFoundError +from embargo import api as embargo_api from util.authentication import SessionAuthenticationAllowInactiveUser from util.disable_rate_limit import can_disable_rate_limit @@ -278,7 +282,36 @@ class EnrollmentListView(APIView): course_id = request.DATA['course_details']['course_id'] try: - return Response(api.add_enrollment(user, course_id)) + course_id = CourseKey.from_string(course_id) + except InvalidKeyError: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) + } + ) + + # Check whether any country access rules block the user from enrollment + # We do this at the view level (rather than the Python API level) + # because this check requires information about the HTTP request. + redirect_url = embargo_api.redirect_if_blocked( + course_id, user=request.user, + ip_address=get_ip(request), + url=request.path + ) + if redirect_url: + return Response( + status=status.HTTP_403_FORBIDDEN, + data={ + "message": ( + u"Users from this location cannot access the course '{course_id}'." + ).format(course_id=course_id), + "user_message_url": redirect_url + } + ) + + try: + return Response(api.add_enrollment(user, unicode(course_id))) except CourseModeNotFoundError as error: return Response( status=status.HTTP_400_BAD_REQUEST, @@ -305,10 +338,3 @@ class EnrollmentListView(APIView): ).format(user=user, course_id=course_id) } ) - except InvalidKeyError: - return Response( - status=status.HTTP_400_BAD_REQUEST, - data={ - "message": u"No course '{course_id}' found for enrollment".format(course_id=course_id) - } - ) diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index 7e4fcd3fa6..a256eb5aae 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -5,18 +5,19 @@ import ddt import unittest from mock import patch -from django.test.utils import override_settings from django.conf import settings from django.core.urlresolvers import reverse from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +from util.testing import UrlResetMixin +from embargo.test_utils import restrict_course from student.tests.factories import UserFactory, CourseModeFactory from student.models import CourseEnrollment @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') -class EnrollmentTest(ModuleStoreTestCase): +class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase): """ Test student enrollment, especially with different course modes. """ @@ -25,9 +26,10 @@ class EnrollmentTest(ModuleStoreTestCase): EMAIL = "bob@example.com" PASSWORD = "edx" + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) def setUp(self): """ Create a course and user, then log in. """ - super(EnrollmentTest, self).setUp() + super(EnrollmentTest, self).setUp('embargo') self.course = CourseFactory.create() self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) self.client.login(username=self.USERNAME, password=self.PASSWORD) @@ -132,6 +134,29 @@ class EnrollmentTest(ModuleStoreTestCase): else: self.assertFalse(mock_update_email_opt_in.called) + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_embargo_restrict(self): + # When accessing the course from an embargoed country, + # we should be blocked. + with restrict_course(self.course.id) as redirect_url: + response = self._change_enrollment('enroll') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, redirect_url) + + # Verify that we weren't enrolled + is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id) + self.assertFalse(is_enrolled) + + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_embargo_allow(self): + response = self._change_enrollment('enroll') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, '') + + # Verify that we were enrolled + is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id) + self.assertTrue(is_enrolled) + def test_user_not_authenticated(self): # Log out, so we're no longer authenticated self.client.logout() diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 03ce48f4c4..4d67de21c8 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -9,6 +9,7 @@ import time import json from collections import defaultdict from pytz import UTC +from ipware.ip import get_ip from django.conf import settings from django.contrib.auth import logout, authenticate, login @@ -113,6 +114,8 @@ from xmodule.error_module import ErrorDescriptor from shoppingcart.models import DonationConfiguration, CourseRegistrationCode from openedx.core.djangoapps.user_api.api import profile as profile_api +from embargo import api as embargo_api + import analytics from eventtracking import tracker @@ -876,6 +879,17 @@ def change_enrollment(request, check_access=True): available_modes = CourseMode.modes_for_course_dict(course_id) + # Check whether the user is blocked from enrolling in this course + # This can occur if the user's IP is on a global blacklist + # or if the user is enrolling in a country in which the course + # is not available. + redirect_url = embargo_api.redirect_if_blocked( + course_id, user=user, ip_address=get_ip(request), + url=request.path + ) + if redirect_url: + return HttpResponse(redirect_url) + # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index fb5606095c..337742933e 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -24,12 +24,11 @@ from datetime import datetime, timedelta from mock import patch, Mock import ddt -from xmodule.modulestore.tests.django_utils import ( - ModuleStoreTestCase, mixed_store_config -) +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from student.roles import CourseSalesAdminRole from util.date_utils import get_default_time_display +from util.testing import UrlResetMixin from shoppingcart.views import _can_download_report, _get_date_from_str from shoppingcart.models import ( @@ -42,6 +41,7 @@ from courseware.tests.factories import InstructorFactory from student.models import CourseEnrollment from course_modes.models import CourseMode from edxmako.shortcuts import render_to_response +from embargo.test_utils import restrict_course from shoppingcart.processors import render_purchase_form_html from shoppingcart.admin import SoftDeleteCouponAdmin from shoppingcart.views import initialize_report @@ -1578,6 +1578,51 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): self.assertIn(self.course.display_name, response.content) +@ddt.ddt +class RedeemCodeEmbargoTests(UrlResetMixin, ModuleStoreTestCase): + """Test blocking redeem code redemption based on country access rules. """ + + USERNAME = 'bob' + PASSWORD = 'test' + + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def setUp(self): + super(RedeemCodeEmbargoTests, self).setUp('embargo') + self.course = CourseFactory.create() + self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) + result = self.client.login(username=self.user.username, password=self.PASSWORD) + self.assertTrue(result, msg="Could not log in") + + @ddt.data('get', 'post') + @patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_registration_code_redemption_embargo(self, method): + # Create a valid registration code + reg_code = CourseRegistrationCode.objects.create( + code="abcd1234", + course_id=self.course.id, + created_by=self.user + ) + + # Try to redeem the code from a restricted country + with restrict_course(self.course.id) as redirect_url: + url = reverse( + 'register_code_redemption', + kwargs={'registration_code': 'abcd1234'} + ) + response = getattr(self.client, method)(url) + self.assertRedirects(response, redirect_url) + + # The registration code should NOT be redeemed + is_redeemed = RegistrationCodeRedemption.objects.filter( + registration_code=reg_code + ).exists() + self.assertFalse(is_redeemed) + + # The user should NOT be enrolled + is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id) + self.assertFalse(is_enrolled) + + @ddt.ddt class DonationViewTest(ModuleStoreTestCase): """Tests for making a donation. diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 383e3ce8b9..dff6d15428 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -2,9 +2,11 @@ import logging import datetime import decimal import pytz +from ipware.ip import get_ip from django.db.models import Q from django.conf import settings from django.contrib.auth.models import Group +from django.shortcuts import redirect from django.http import ( HttpResponse, HttpResponseRedirect, HttpResponseNotFound, HttpResponseBadRequest, HttpResponseForbidden, Http404 @@ -28,6 +30,7 @@ from config_models.decorators import require_config from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, \ AlreadyEnrolledError +from embargo import api as embargo_api from .exceptions import ( ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException, @@ -50,6 +53,7 @@ import json from xmodule_django.models import CourseKeyField from .decorators import enforce_shopping_cart_enabled + log = logging.getLogger("shoppingcart") AUDIT_LOG = logging.getLogger("audit") @@ -352,6 +356,15 @@ def register_code_redemption(request, registration_code): reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code, request, limiter) course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0) + + # Restrict the user from enrolling based on country access rules + embargo_redirect = embargo_api.redirect_if_blocked( + course.id, user=request.user, ip_address=get_ip(request), + url=request.path + ) + if embargo_redirect is not None: + return redirect(embargo_redirect) + context = { 'reg_code_already_redeemed': reg_code_already_redeemed, 'reg_code_is_valid': reg_code_is_valid, @@ -365,6 +378,15 @@ def register_code_redemption(request, registration_code): reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code, request, limiter) course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0) + + # Restrict the user from enrolling based on country access rules + embargo_redirect = embargo_api.redirect_if_blocked( + course.id, user=request.user, ip_address=get_ip(request), + url=request.path + ) + if embargo_redirect is not None: + return redirect(embargo_redirect) + context = { 'reg_code': registration_code, 'site_name': site_name, diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 014e9db652..4bd7108c89 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -32,6 +32,8 @@ from student.models import CourseEnrollment from course_modes.tests.factories import CourseModeFactory from course_modes.models import CourseMode from shoppingcart.models import Order, CertificateItem +from embargo.test_utils import restrict_course +from util.testing import UrlResetMixin from verify_student.views import render_to_response, PayAndVerifyView from verify_student.models import SoftwareSecurePhotoVerification from reverification.tests.factories import MidcourseReverificationWindowFactory @@ -60,7 +62,7 @@ class StartView(TestCase): @ddt.ddt -class TestPayAndVerifyView(ModuleStoreTestCase): +class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): """ Tests for the payment and verification flow views. """ @@ -72,8 +74,9 @@ class TestPayAndVerifyView(ModuleStoreTestCase): YESTERDAY = NOW - timedelta(days=1) TOMORROW = NOW + timedelta(days=1) + @mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) def setUp(self): - super(TestPayAndVerifyView, self).setUp() + super(TestPayAndVerifyView, self).setUp('embargo') self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) result = self.client.login(username=self.USERNAME, password=self.PASSWORD) self.assertTrue(result, msg="Could not log in") @@ -622,6 +625,20 @@ class TestPayAndVerifyView(ModuleStoreTestCase): self.assertContains(response, "verification deadline") self.assertContains(response, "Jan 02, 1999 at 00:00 UTC") + @mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_embargo_restrict(self): + course = self._create_course("verified") + with restrict_course(course.id) as redirect_url: + # Simulate that we're embargoed from accessing this + # course based on our IP address. + response = self._get_page('verify_student_start_flow', course.id, expected_status_code=302) + self.assertRedirects(response, redirect_url) + + @mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True}) + def test_embargo_allow(self): + course = self._create_course("verified") + self._get_page('verify_student_start_flow', course.id) + def _create_course(self, *course_modes, **kwargs): """Create a new course with the specified course modes. """ course = CourseFactory.create() diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 770efc871f..1a8f6537bf 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -8,6 +8,7 @@ import decimal import datetime from collections import namedtuple from pytz import UTC +from ipware.ip import get_ip from edxmako.shortcuts import render_to_response, render_to_string @@ -46,6 +47,8 @@ from .exceptions import WindowExpiredException from xmodule.modulestore.django import modulestore from microsite_configuration import microsite +from embargo import api as embargo_api + from util.json_request import JsonResponse from util.date_utils import get_default_time_display @@ -256,6 +259,17 @@ class PayAndVerifyView(View): log.warn(u"No course specified for verification flow request.") raise Http404 + # Check whether the user has access to this course + # based on country access rules. + redirect_url = embargo_api.redirect_if_blocked( + course_key, + user=request.user, + ip_address=get_ip(request), + url=request.path + ) + if redirect_url: + return redirect(redirect_url) + # Check that the course has an unexpired verified mode course_mode, expired_course_mode = self._get_verified_modes_for_course(course_key) diff --git a/lms/static/js/spec/student_account/enrollment_spec.js b/lms/static/js/spec/student_account/enrollment_spec.js index 712e468dbf..155519b647 100644 --- a/lms/static/js/spec/student_account/enrollment_spec.js +++ b/lms/static/js/spec/student_account/enrollment_spec.js @@ -6,7 +6,8 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], var COURSE_KEY = 'edX/DemoX/Fall', ENROLL_URL = '/api/enrollment/v1/enrollment', - FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/'; + FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/', + EMBARGO_MSG_URL = '/embargo/blocked-message/enrollment/default/'; beforeEach(function() { // Mock the redirect call @@ -49,6 +50,27 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], expect(EnrollmentInterface.redirect).toHaveBeenCalledWith( FORWARD_URL ); }); + it('redirects the user if blocked by an embargo', function() { + // Spy on Ajax requests + var requests = AjaxHelpers.requests( this ); + + // Attempt to enroll the user + EnrollmentInterface.enroll( COURSE_KEY ); + + // Simulate an error response (403) from the server + // with a "user_message_url" parameter for the redirect. + // This will redirect the user to a page with messaging + // explaining why he/she can't enroll. + AjaxHelpers.respondWithError( + requests, 403, + { 'user_message_url': EMBARGO_MSG_URL } + ); + + // Verify that the user was redirected + expect(EnrollmentInterface.redirect).toHaveBeenCalledWith( EMBARGO_MSG_URL ); + + }); + }); } ); diff --git a/lms/static/js/student_account/enrollment.js b/lms/static/js/student_account/enrollment.js index 7ec64f392b..258fcbf44f 100644 --- a/lms/static/js/student_account/enrollment.js +++ b/lms/static/js/student_account/enrollment.js @@ -36,7 +36,26 @@ var edx = edx || {}; data: data, headers: this.headers, context: this - }).always(function() { + }) + .fail(function( jqXHR ) { + var responseData = JSON.parse(jqXHR.responseText); + if ( jqXHR.status === 403 && responseData.user_message_url ) { + // Check if we've been blocked from the course + // because of country access rules. + // If so, redirect to a page explaining to the user + // why they were blocked. + this.redirect( responseData.user_message_url ); + } + else { + // Otherwise, go to the track selection page as usual. + // This can occur, for example, when a course does not + // have a free enrollment mode, so we can't auto-enroll. + this.redirect( this.trackSelectionUrl( courseKey ) ); + } + }) + .done(function() { + // If we successfully enrolled, go to the track selection + // page to allow the user to choose a paid enrollment mode. this.redirect( this.trackSelectionUrl( courseKey ) ); }); },