Commit e609f982 by Will Daly

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.
parent 6ba6afce
...@@ -11,6 +11,7 @@ from xmodule.modulestore.tests.django_utils import ( ...@@ -11,6 +11,7 @@ from xmodule.modulestore.tests.django_utils import (
) )
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course
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
...@@ -274,7 +275,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -274,7 +275,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
# Create a verified mode # Create a verified mode
url = reverse('create_mode', args=[unicode(self.course.id)]) 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) 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) verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None)
...@@ -282,3 +283,34 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -282,3 +283,34 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
course_modes = CourseMode.modes_for_course(self.course.id) course_modes = CourseMode.modes_for_course(self.course.id)
self.assertEquals(course_modes, expected_modes) 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)
...@@ -3,6 +3,8 @@ Views for the course_mode module ...@@ -3,6 +3,8 @@ Views for the course_mode module
""" """
import decimal import decimal
from ipware.ip import get_ip
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
...@@ -22,6 +24,8 @@ from opaque_keys.edx.keys import CourseKey ...@@ -22,6 +24,8 @@ from opaque_keys.edx.keys import CourseKey
from util.db import commit_on_success_with_read_committed from util.db import commit_on_success_with_read_committed
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from embargo import api as embargo_api
class ChooseModeView(View): class ChooseModeView(View):
"""View used when the user is asked to pick a mode. """View used when the user is asked to pick a mode.
...@@ -52,6 +56,17 @@ class ChooseModeView(View): ...@@ -52,6 +56,17 @@ class ChooseModeView(View):
""" """
course_key = CourseKey.from_string(course_id) 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) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
modes = CourseMode.modes_for_course_dict(course_key) modes = CourseMode.modes_for_course_dict(course_key)
......
...@@ -6,19 +6,18 @@ import json ...@@ -6,19 +6,18 @@ import json
import unittest import unittest
from mock import patch from mock import patch
from django.test.utils import override_settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from rest_framework import status from rest_framework import status
from django.conf import settings from django.conf import settings
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
ModuleStoreTestCase, mixed_store_config
)
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from util.testing import UrlResetMixin
from enrollment import api from enrollment import api
from enrollment.errors import CourseEnrollmentError from enrollment.errors import CourseEnrollmentError
from student.tests.factories import UserFactory, CourseModeFactory from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from embargo.test_utils import restrict_course
@ddt.ddt @ddt.ddt
...@@ -245,3 +244,66 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase): ...@@ -245,3 +244,66 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase):
) )
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("No course ", resp.content) 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)
...@@ -3,15 +3,19 @@ The Enrollment API Views should be simple, lean HTTP endpoints for API access. T ...@@ -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. 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 import status
from rest_framework.authentication import OAuth2Authentication from rest_framework.authentication import OAuth2Authentication
from rest_framework import permissions from rest_framework import permissions
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView from rest_framework.views import APIView
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError
from enrollment import api from enrollment import api
from enrollment.errors import CourseNotFoundError, CourseEnrollmentError, CourseModeNotFoundError from enrollment.errors import CourseNotFoundError, CourseEnrollmentError, CourseModeNotFoundError
from embargo import api as embargo_api
from util.authentication import SessionAuthenticationAllowInactiveUser from util.authentication import SessionAuthenticationAllowInactiveUser
from util.disable_rate_limit import can_disable_rate_limit from util.disable_rate_limit import can_disable_rate_limit
...@@ -278,7 +282,36 @@ class EnrollmentListView(APIView): ...@@ -278,7 +282,36 @@ class EnrollmentListView(APIView):
course_id = request.DATA['course_details']['course_id'] course_id = request.DATA['course_details']['course_id']
try: 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: except CourseModeNotFoundError as error:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
...@@ -305,10 +338,3 @@ class EnrollmentListView(APIView): ...@@ -305,10 +338,3 @@ class EnrollmentListView(APIView):
).format(user=user, course_id=course_id) ).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)
}
)
...@@ -5,18 +5,19 @@ import ddt ...@@ -5,18 +5,19 @@ import ddt
import unittest import unittest
from mock import patch from mock import patch
from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
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
from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course
from student.tests.factories import UserFactory, CourseModeFactory from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
@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')
class EnrollmentTest(ModuleStoreTestCase): class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
""" """
Test student enrollment, especially with different course modes. Test student enrollment, especially with different course modes.
""" """
...@@ -25,9 +26,10 @@ class EnrollmentTest(ModuleStoreTestCase): ...@@ -25,9 +26,10 @@ class EnrollmentTest(ModuleStoreTestCase):
EMAIL = "bob@example.com" EMAIL = "bob@example.com"
PASSWORD = "edx" PASSWORD = "edx"
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def setUp(self): def setUp(self):
""" Create a course and user, then log in. """ """ Create a course and user, then log in. """
super(EnrollmentTest, self).setUp() super(EnrollmentTest, self).setUp('embargo')
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD) self.client.login(username=self.USERNAME, password=self.PASSWORD)
...@@ -132,6 +134,29 @@ class EnrollmentTest(ModuleStoreTestCase): ...@@ -132,6 +134,29 @@ class EnrollmentTest(ModuleStoreTestCase):
else: else:
self.assertFalse(mock_update_email_opt_in.called) 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): def test_user_not_authenticated(self):
# Log out, so we're no longer authenticated # Log out, so we're no longer authenticated
self.client.logout() self.client.logout()
......
...@@ -9,6 +9,7 @@ import time ...@@ -9,6 +9,7 @@ import time
import json import json
from collections import defaultdict from collections import defaultdict
from pytz import UTC from pytz import UTC
from ipware.ip import get_ip
from django.conf import settings from django.conf import settings
from django.contrib.auth import logout, authenticate, login from django.contrib.auth import logout, authenticate, login
...@@ -113,6 +114,8 @@ from xmodule.error_module import ErrorDescriptor ...@@ -113,6 +114,8 @@ from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import DonationConfiguration, CourseRegistrationCode from shoppingcart.models import DonationConfiguration, CourseRegistrationCode
from openedx.core.djangoapps.user_api.api import profile as profile_api from openedx.core.djangoapps.user_api.api import profile as profile_api
from embargo import api as embargo_api
import analytics import analytics
from eventtracking import tracker from eventtracking import tracker
...@@ -876,6 +879,17 @@ def change_enrollment(request, check_access=True): ...@@ -876,6 +879,17 @@ def change_enrollment(request, check_access=True):
available_modes = CourseMode.modes_for_course_dict(course_id) 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 # Check that auto enrollment is allowed for this course
# (= the course is NOT behind a paywall) # (= the course is NOT behind a paywall)
if CourseMode.can_auto_enroll(course_id): if CourseMode.can_auto_enroll(course_id):
......
...@@ -24,12 +24,11 @@ from datetime import datetime, timedelta ...@@ -24,12 +24,11 @@ from datetime import datetime, timedelta
from mock import patch, Mock from mock import patch, Mock
import ddt import ddt
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
ModuleStoreTestCase, mixed_store_config
)
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.roles import CourseSalesAdminRole from student.roles import CourseSalesAdminRole
from util.date_utils import get_default_time_display 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.views import _can_download_report, _get_date_from_str
from shoppingcart.models import ( from shoppingcart.models import (
...@@ -42,6 +41,7 @@ from courseware.tests.factories import InstructorFactory ...@@ -42,6 +41,7 @@ from courseware.tests.factories import InstructorFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from course_modes.models import CourseMode from course_modes.models import CourseMode
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from embargo.test_utils import restrict_course
from shoppingcart.processors import render_purchase_form_html from shoppingcart.processors import render_purchase_form_html
from shoppingcart.admin import SoftDeleteCouponAdmin from shoppingcart.admin import SoftDeleteCouponAdmin
from shoppingcart.views import initialize_report from shoppingcart.views import initialize_report
...@@ -1579,6 +1579,51 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): ...@@ -1579,6 +1579,51 @@ class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
@ddt.ddt @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): class DonationViewTest(ModuleStoreTestCase):
"""Tests for making a donation. """Tests for making a donation.
......
...@@ -2,9 +2,11 @@ import logging ...@@ -2,9 +2,11 @@ import logging
import datetime import datetime
import decimal import decimal
import pytz import pytz
from ipware.ip import get_ip
from django.db.models import Q from django.db.models import Q
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.shortcuts import redirect
from django.http import ( from django.http import (
HttpResponse, HttpResponseRedirect, HttpResponseNotFound, HttpResponse, HttpResponseRedirect, HttpResponseNotFound,
HttpResponseBadRequest, HttpResponseForbidden, Http404 HttpResponseBadRequest, HttpResponseForbidden, Http404
...@@ -28,6 +30,7 @@ from config_models.decorators import require_config ...@@ -28,6 +30,7 @@ from config_models.decorators import require_config
from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport
from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, \ from student.models import CourseEnrollment, EnrollmentClosedError, CourseFullError, \
AlreadyEnrolledError AlreadyEnrolledError
from embargo import api as embargo_api
from .exceptions import ( from .exceptions import (
ItemAlreadyInCartException, AlreadyEnrolledInCourseException, ItemAlreadyInCartException, AlreadyEnrolledInCourseException,
CourseDoesNotExistException, ReportTypeDoesNotExistException, CourseDoesNotExistException, ReportTypeDoesNotExistException,
...@@ -50,6 +53,7 @@ import json ...@@ -50,6 +53,7 @@ import json
from xmodule_django.models import CourseKeyField from xmodule_django.models import CourseKeyField
from .decorators import enforce_shopping_cart_enabled from .decorators import enforce_shopping_cart_enabled
log = logging.getLogger("shoppingcart") log = logging.getLogger("shoppingcart")
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
...@@ -352,6 +356,15 @@ def register_code_redemption(request, registration_code): ...@@ -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, reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code,
request, limiter) request, limiter)
course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0) 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 = { context = {
'reg_code_already_redeemed': reg_code_already_redeemed, 'reg_code_already_redeemed': reg_code_already_redeemed,
'reg_code_is_valid': reg_code_is_valid, 'reg_code_is_valid': reg_code_is_valid,
...@@ -365,6 +378,15 @@ def register_code_redemption(request, registration_code): ...@@ -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, reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(registration_code,
request, limiter) request, limiter)
course = get_course_by_id(getattr(course_registration, 'course_id'), depth=0) 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 = { context = {
'reg_code': registration_code, 'reg_code': registration_code,
'site_name': site_name, 'site_name': site_name,
......
...@@ -32,6 +32,8 @@ from student.models import CourseEnrollment ...@@ -32,6 +32,8 @@ from student.models import CourseEnrollment
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from shoppingcart.models import Order, CertificateItem 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.views import render_to_response, PayAndVerifyView
from verify_student.models import SoftwareSecurePhotoVerification from verify_student.models import SoftwareSecurePhotoVerification
from reverification.tests.factories import MidcourseReverificationWindowFactory from reverification.tests.factories import MidcourseReverificationWindowFactory
...@@ -60,7 +62,7 @@ class StartView(TestCase): ...@@ -60,7 +62,7 @@ class StartView(TestCase):
@ddt.ddt @ddt.ddt
class TestPayAndVerifyView(ModuleStoreTestCase): class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
""" """
Tests for the payment and verification flow views. Tests for the payment and verification flow views.
""" """
...@@ -72,8 +74,9 @@ class TestPayAndVerifyView(ModuleStoreTestCase): ...@@ -72,8 +74,9 @@ class TestPayAndVerifyView(ModuleStoreTestCase):
YESTERDAY = NOW - timedelta(days=1) YESTERDAY = NOW - timedelta(days=1)
TOMORROW = NOW + timedelta(days=1) TOMORROW = NOW + timedelta(days=1)
@mock.patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def setUp(self): def setUp(self):
super(TestPayAndVerifyView, self).setUp() super(TestPayAndVerifyView, self).setUp('embargo')
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
result = self.client.login(username=self.USERNAME, password=self.PASSWORD) result = self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.assertTrue(result, msg="Could not log in") self.assertTrue(result, msg="Could not log in")
...@@ -622,6 +625,20 @@ class TestPayAndVerifyView(ModuleStoreTestCase): ...@@ -622,6 +625,20 @@ class TestPayAndVerifyView(ModuleStoreTestCase):
self.assertContains(response, "verification deadline") self.assertContains(response, "verification deadline")
self.assertContains(response, "Jan 02, 1999 at 00:00 UTC") 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): def _create_course(self, *course_modes, **kwargs):
"""Create a new course with the specified course modes. """ """Create a new course with the specified course modes. """
course = CourseFactory.create() course = CourseFactory.create()
......
...@@ -8,6 +8,7 @@ import decimal ...@@ -8,6 +8,7 @@ import decimal
import datetime import datetime
from collections import namedtuple from collections import namedtuple
from pytz import UTC from pytz import UTC
from ipware.ip import get_ip
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
...@@ -46,6 +47,8 @@ from .exceptions import WindowExpiredException ...@@ -46,6 +47,8 @@ from .exceptions import WindowExpiredException
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from microsite_configuration import microsite from microsite_configuration import microsite
from embargo import api as embargo_api
from util.json_request import JsonResponse from util.json_request import JsonResponse
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
...@@ -256,6 +259,17 @@ class PayAndVerifyView(View): ...@@ -256,6 +259,17 @@ class PayAndVerifyView(View):
log.warn(u"No course specified for verification flow request.") log.warn(u"No course specified for verification flow request.")
raise Http404 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 # Check that the course has an unexpired verified mode
course_mode, expired_course_mode = self._get_verified_modes_for_course(course_key) course_mode, expired_course_mode = self._get_verified_modes_for_course(course_key)
......
...@@ -6,7 +6,8 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], ...@@ -6,7 +6,8 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
var COURSE_KEY = 'edX/DemoX/Fall', var COURSE_KEY = 'edX/DemoX/Fall',
ENROLL_URL = '/api/enrollment/v1/enrollment', 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() { beforeEach(function() {
// Mock the redirect call // Mock the redirect call
...@@ -49,6 +50,27 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'], ...@@ -49,6 +50,27 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
expect(EnrollmentInterface.redirect).toHaveBeenCalledWith( FORWARD_URL ); 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 );
});
}); });
} }
); );
...@@ -36,7 +36,26 @@ var edx = edx || {}; ...@@ -36,7 +36,26 @@ var edx = edx || {};
data: data, data: data,
headers: this.headers, headers: this.headers,
context: this 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 ) ); this.redirect( this.trackSelectionUrl( courseKey ) );
}); });
}, },
......
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