diff --git a/common/djangoapps/cors_csrf/authentication.py b/common/djangoapps/cors_csrf/authentication.py
new file mode 100644
index 0000000000..723ec8eed1
--- /dev/null
+++ b/common/djangoapps/cors_csrf/authentication.py
@@ -0,0 +1,29 @@
+"""Django Rest Framework Authentication classes for cross-domain end-points."""
+from rest_framework import authentication
+from cors_csrf.helpers import is_cross_domain_request_allowed, skip_cross_domain_referer_check
+
+
+class SessionAuthenticationCrossDomainCsrf(authentication.SessionAuthentication):
+ """Session authentication that skips the referer check over secure connections.
+
+ Django Rest Framework's `SessionAuthentication` class calls Django's
+ CSRF middleware implementation directly, which bypasses the middleware
+ stack.
+
+ This version of `SessionAuthentication` performs the same workaround
+ as `CorsCSRFMiddleware` to skip the referer check for whitelisted
+ domains over a secure connection. See `cors_csrf.middleware` for
+ more information.
+
+ Since this subclass overrides only the `enforce_csrf()` method,
+ it can be mixed in with other `SessionAuthentication` subclasses.
+
+ """
+
+ def enforce_csrf(self, request):
+ """Skip the referer check if the cross-domain request is allowed. """
+ if is_cross_domain_request_allowed(request):
+ with skip_cross_domain_referer_check(request):
+ return super(SessionAuthenticationCrossDomainCsrf, self).enforce_csrf(request)
+ else:
+ return super(SessionAuthenticationCrossDomainCsrf, self).enforce_csrf(request)
diff --git a/common/djangoapps/cors_csrf/helpers.py b/common/djangoapps/cors_csrf/helpers.py
new file mode 100644
index 0000000000..c1a059c1a9
--- /dev/null
+++ b/common/djangoapps/cors_csrf/helpers.py
@@ -0,0 +1,92 @@
+"""Helper methods for CORS and CSRF checks. """
+import logging
+import urlparse
+import contextlib
+
+from django.conf import settings
+
+log = logging.getLogger(__name__)
+
+
+def is_cross_domain_request_allowed(request):
+ """Check whether we should allow the cross-domain request.
+
+ We allow a cross-domain request only if:
+
+ 1) The request is made securely and the referer has "https://" as the protocol.
+ 2) The referer domain has been whitelisted.
+
+ Arguments:
+ request (HttpRequest)
+
+ Returns:
+ bool
+
+ """
+ referer = request.META.get('HTTP_REFERER')
+ referer_parts = urlparse.urlparse(referer) if referer else None
+ referer_hostname = referer_parts.hostname if referer_parts is not None else None
+
+ # Use CORS_ALLOW_INSECURE *only* for development and testing environments;
+ # it should never be enabled in production.
+ if not getattr(settings, 'CORS_ALLOW_INSECURE', False):
+ if not request.is_secure():
+ log.debug(
+ u"Request is not secure, so we cannot send the CSRF token. "
+ u"For testing purposes, you can disable this check by setting "
+ u"`CORS_ALLOW_INSECURE` to True in the settings"
+ )
+ return False
+
+ if not referer:
+ log.debug(u"No referer provided over a secure connection, so we cannot check the protocol.")
+ return False
+
+ if not referer_parts.scheme == 'https':
+ log.debug(u"Referer '%s' must have the scheme 'https'")
+ return False
+
+ domain_is_whitelisted = (
+ getattr(settings, 'CORS_ORIGIN_ALLOW_ALL', False) or
+ referer_hostname in getattr(settings, 'CORS_ORIGIN_WHITELIST', [])
+ )
+ if not domain_is_whitelisted:
+ if referer_hostname is None:
+ # If no referer is specified, we can't check if it's a cross-domain
+ # request or not.
+ log.debug(u"Referrer hostname is `None`, so it is not on the whitelist.")
+ elif referer_hostname != request.get_host():
+ log.info(
+ (
+ u"Domain '%s' is not on the cross domain whitelist. "
+ u"Add the domain to `CORS_ORIGIN_WHITELIST` or set "
+ u"`CORS_ORIGIN_ALLOW_ALL` to True in the settings."
+ ), referer_hostname
+ )
+ else:
+ log.debug(
+ (
+ u"Domain '%s' is the same as the hostname in the request, "
+ u"so we are not going to treat it as a cross-domain request."
+ ), referer_hostname
+ )
+ return False
+
+ return True
+
+
+@contextlib.contextmanager
+def skip_cross_domain_referer_check(request):
+ """Skip the cross-domain CSRF referer check.
+
+ Django's CSRF middleware performs the referer check
+ only when the request is made over a secure connection.
+ To skip the check, we patch `request.is_secure()` to
+ False.
+ """
+ is_secure_default = request.is_secure
+ request.is_secure = lambda: False
+ try:
+ yield
+ finally:
+ request.is_secure = is_secure_default
diff --git a/common/djangoapps/cors_csrf/middleware.py b/common/djangoapps/cors_csrf/middleware.py
index 41f51b618e..dfd554d9dc 100644
--- a/common/djangoapps/cors_csrf/middleware.py
+++ b/common/djangoapps/cors_csrf/middleware.py
@@ -43,82 +43,16 @@ CSRF cookie.
"""
import logging
-import urlparse
from django.conf import settings
from django.middleware.csrf import CsrfViewMiddleware
from django.core.exceptions import MiddlewareNotUsed, ImproperlyConfigured
+from cors_csrf.helpers import is_cross_domain_request_allowed, skip_cross_domain_referer_check
+
log = logging.getLogger(__name__)
-def is_cross_domain_request_allowed(request):
- """Check whether we should allow the cross-domain request.
-
- We allow a cross-domain request only if:
-
- 1) The request is made securely and the referer has "https://" as the protocol.
- 2) The referer domain has been whitelisted.
-
- Arguments:
- request (HttpRequest)
-
- Returns:
- bool
-
- """
- referer = request.META.get('HTTP_REFERER')
- referer_parts = urlparse.urlparse(referer) if referer else None
- referer_hostname = referer_parts.hostname if referer_parts is not None else None
-
- # Use CORS_ALLOW_INSECURE *only* for development and testing environments;
- # it should never be enabled in production.
- if not getattr(settings, 'CORS_ALLOW_INSECURE', False):
- if not request.is_secure():
- log.debug(
- u"Request is not secure, so we cannot send the CSRF token. "
- u"For testing purposes, you can disable this check by setting "
- u"`CORS_ALLOW_INSECURE` to True in the settings"
- )
- return False
-
- if not referer:
- log.debug(u"No referer provided over a secure connection, so we cannot check the protocol.")
- return False
-
- if not referer_parts.scheme == 'https':
- log.debug(u"Referer '%s' must have the scheme 'https'")
- return False
-
- domain_is_whitelisted = (
- getattr(settings, 'CORS_ORIGIN_ALLOW_ALL', False) or
- referer_hostname in getattr(settings, 'CORS_ORIGIN_WHITELIST', [])
- )
- if not domain_is_whitelisted:
- if referer_hostname is None:
- # If no referer is specified, we can't check if it's a cross-domain
- # request or not.
- log.debug(u"Referrer hostname is `None`, so it is not on the whitelist.")
- elif referer_hostname != request.get_host():
- log.info(
- (
- u"Domain '%s' is not on the cross domain whitelist. "
- u"Add the domain to `CORS_ORIGIN_WHITELIST` or set "
- u"`CORS_ORIGIN_ALLOW_ALL` to True in the settings."
- ), referer_hostname
- )
- else:
- log.debug(
- (
- u"Domain '%s' is the same as the hostname in the request, "
- u"so we are not going to treat it as a cross-domain request."
- ), referer_hostname
- )
- return False
-
- return True
-
-
class CorsCSRFMiddleware(CsrfViewMiddleware):
"""
Middleware for handling CSRF checks with CORS requests
@@ -134,18 +68,8 @@ class CorsCSRFMiddleware(CsrfViewMiddleware):
log.debug("Could not disable CSRF middleware referer check for cross-domain request.")
return
- is_secure_default = request.is_secure
-
- def is_secure_patched():
- """
- Avoid triggering the additional CSRF middleware checks on the referrer
- """
- return False
- request.is_secure = is_secure_patched
-
- res = super(CorsCSRFMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
- request.is_secure = is_secure_default
- return res
+ with skip_cross_domain_referer_check(request):
+ return super(CorsCSRFMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
class CsrfCrossDomainCookieMiddleware(object):
diff --git a/common/djangoapps/cors_csrf/tests/test_authentication.py b/common/djangoapps/cors_csrf/tests/test_authentication.py
new file mode 100644
index 0000000000..7f2d78fd8a
--- /dev/null
+++ b/common/djangoapps/cors_csrf/tests/test_authentication.py
@@ -0,0 +1,56 @@
+"""Tests for the CORS CSRF version of Django Rest Framework's SessionAuthentication."""
+from mock import patch
+
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.test.client import RequestFactory
+from django.conf import settings
+
+from rest_framework.exceptions import AuthenticationFailed
+
+from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
+
+
+class CrossDomainAuthTest(TestCase):
+ """Tests for the CORS CSRF version of Django Rest Framework's SessionAuthentication. """
+
+ URL = "/dummy_url"
+ REFERER = "https://www.edx.org"
+ CSRF_TOKEN = 'abcd1234'
+
+ def setUp(self):
+ super(CrossDomainAuthTest, self).setUp()
+ self.auth = SessionAuthenticationCrossDomainCsrf()
+
+ def test_perform_csrf_referer_check(self):
+ request = self._fake_request()
+ with self.assertRaisesRegexp(AuthenticationFailed, 'CSRF'):
+ self.auth.enforce_csrf(request)
+
+ @patch.dict(settings.FEATURES, {
+ 'ENABLE_CORS_HEADERS': True,
+ 'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True
+ })
+ @override_settings(
+ CORS_ORIGIN_WHITELIST=["www.edx.org"],
+ CROSS_DOMAIN_CSRF_COOKIE_NAME="prod-edx-csrftoken",
+ CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=".edx.org"
+ )
+ def test_skip_csrf_referer_check(self):
+ request = self._fake_request()
+ result = self.auth.enforce_csrf(request)
+ self.assertIs(result, None)
+ self.assertTrue(request.is_secure())
+
+ def _fake_request(self):
+ """Construct a fake request with a referer and CSRF token over a secure connection. """
+ factory = RequestFactory()
+ factory.cookies[settings.CSRF_COOKIE_NAME] = self.CSRF_TOKEN
+
+ request = factory.post(
+ self.URL,
+ HTTP_REFERER=self.REFERER,
+ HTTP_X_CSRFTOKEN=self.CSRF_TOKEN
+ )
+ request.is_secure = lambda: True
+ return request
diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py
index c399bc51b4..a3e961a69f 100644
--- a/common/djangoapps/enrollment/tests/test_views.py
+++ b/common/djangoapps/enrollment/tests/test_views.py
@@ -6,6 +6,8 @@ import json
import unittest
from mock import patch
+from django.test import Client
+from django.core.handlers.wsgi import WSGIRequest
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
from rest_framework import status
@@ -504,3 +506,81 @@ class EnrollmentEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
url = reverse('courseenrollments')
resp = self.client.get(url)
return json.loads(resp.content)
+
+
+def cross_domain_config(func):
+ """Decorator for configuring a cross-domain request. """
+ feature_flag_decorator = patch.dict(settings.FEATURES, {
+ 'ENABLE_CORS_HEADERS': True,
+ 'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True
+ })
+ settings_decorator = override_settings(
+ CORS_ORIGIN_WHITELIST=["www.edx.org"],
+ CROSS_DOMAIN_CSRF_COOKIE_NAME="prod-edx-csrftoken",
+ CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=".edx.org"
+ )
+ is_secure_decorator = patch.object(WSGIRequest, 'is_secure', return_value=True)
+
+ return feature_flag_decorator(
+ settings_decorator(
+ is_secure_decorator(func)
+ )
+ )
+
+
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class EnrollmentCrossDomainTest(ModuleStoreTestCase):
+ """Test cross-domain calls to the enrollment end-points. """
+
+ USERNAME = "Bob"
+ EMAIL = "bob@example.com"
+ PASSWORD = "edx"
+ REFERER = "https://www.edx.org"
+
+ def setUp(self):
+ """ Create a course and user, then log in. """
+ super(EnrollmentCrossDomainTest, self).setUp()
+ self.course = CourseFactory.create()
+ self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
+
+ self.client = Client(enforce_csrf_checks=True)
+ self.client.login(username=self.USERNAME, password=self.PASSWORD)
+
+ @cross_domain_config
+ def test_cross_domain_change_enrollment(self, *args): # pylint: disable=unused-argument
+ csrf_cookie = self._get_csrf_cookie()
+ resp = self._cross_domain_post(csrf_cookie)
+
+ # Expect that the request gets through successfully,
+ # passing the CSRF checks (including the referer check).
+ self.assertEqual(resp.status_code, 200)
+
+ @cross_domain_config
+ def test_cross_domain_missing_csrf(self, *args): # pylint: disable=unused-argument
+ resp = self._cross_domain_post('invalid_csrf_token')
+ self.assertEqual(resp.status_code, 401)
+
+ def _get_csrf_cookie(self):
+ """Retrieve the cross-domain CSRF cookie. """
+ url = reverse('courseenrollment', kwargs={
+ 'course_id': unicode(self.course.id)
+ })
+ resp = self.client.get(url, HTTP_REFERER=self.REFERER)
+ self.assertEqual(resp.status_code, 200)
+ self.assertIn('prod-edx-csrftoken', resp.cookies) # pylint: disable=no-member
+ return resp.cookies['prod-edx-csrftoken'].value # pylint: disable=no-member
+
+ def _cross_domain_post(self, csrf_cookie):
+ """Perform a cross-domain POST request. """
+ url = reverse('courseenrollments')
+ params = json.dumps({
+ 'course_details': {
+ 'course_id': unicode(self.course.id),
+ },
+ 'user': self.user.username
+ })
+ return self.client.post(
+ url, params, content_type='application/json',
+ HTTP_REFERER=self.REFERER,
+ HTTP_X_CSRFTOKEN=csrf_cookie
+ )
diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py
index 4198a4a62a..bdd867dd4d 100644
--- a/common/djangoapps/enrollment/views.py
+++ b/common/djangoapps/enrollment/views.py
@@ -15,6 +15,7 @@ from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
from opaque_keys.edx.keys import CourseKey
from embargo import api as embargo_api
+from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf
from cors_csrf.decorators import ensure_csrf_cookie_cross_domain
from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser
from util.disable_rate_limit import can_disable_rate_limit
@@ -25,6 +26,11 @@ from enrollment.errors import (
)
+class EnrollmentCrossDomainSessionAuth(SessionAuthenticationAllowInactiveUser, SessionAuthenticationCrossDomainCsrf):
+ """Session authentication that allows inactive users and cross-domain requests. """
+ pass
+
+
class ApiKeyPermissionMixIn(object):
"""
This mixin is used to provide a convenience function for doing individual permission checks
@@ -277,7 +283,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
* user: The ID of the user.
"""
- authentication_classes = OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser
+ authentication_classes = OAuth2AuthenticationAllowInactiveUser, EnrollmentCrossDomainSessionAuth
permission_classes = ApiKeyHeaderPermissionIsAuthenticated,
throttle_classes = EnrollmentUserThrottle,
diff --git a/common/djangoapps/util/sandboxing.py b/common/djangoapps/util/sandboxing.py
index 5fea7ed25c..8fcfa6dfbc 100644
--- a/common/djangoapps/util/sandboxing.py
+++ b/common/djangoapps/util/sandboxing.py
@@ -25,7 +25,7 @@ def can_execute_unsafe_code(course_id):
# To others using this: the code as-is is brittle and likely to be changed in the future,
# as per the TODO, so please consider carefully before adding more values to COURSES_WITH_UNSAFE_CODE
for regex in getattr(settings, 'COURSES_WITH_UNSAFE_CODE', []):
- if re.match(regex, course_id.to_deprecated_string()):
+ if re.match(regex, unicode(course_id)):
return True
return False
diff --git a/common/djangoapps/util/tests/test_sandboxing.py b/common/djangoapps/util/tests/test_sandboxing.py
index 7a33fbbe95..21179b1dfa 100644
--- a/common/djangoapps/util/tests/test_sandboxing.py
+++ b/common/djangoapps/util/tests/test_sandboxing.py
@@ -3,6 +3,7 @@ Tests for sandboxing.py in util app
"""
from django.test import TestCase
+from opaque_keys.edx.locator import LibraryLocator
from util.sandboxing import can_execute_unsafe_code
from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey
@@ -12,12 +13,13 @@ class SandboxingTest(TestCase):
"""
Test sandbox whitelisting
"""
- @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
+ @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*', 'library:v1-edX+.*'])
def test_sandbox_exclusion(self):
"""
Test to make sure that a non-match returns false
"""
self.assertFalse(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'notful', 'empty')))
+ self.assertFalse(can_execute_unsafe_code(LibraryLocator('edY', 'test_bank')))
@override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*'])
def test_sandbox_inclusion(self):
@@ -26,10 +28,12 @@ class SandboxingTest(TestCase):
"""
self.assertTrue(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2012_Fall')))
self.assertTrue(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2013_Spring')))
+ self.assertFalse(can_execute_unsafe_code(LibraryLocator('edX', 'test_bank')))
- def test_courses_with_unsafe_code_default(self):
+ def test_courselikes_with_unsafe_code_default(self):
"""
Test that the default setting for COURSES_WITH_UNSAFE_CODE is an empty setting, e.g. we don't use @override_settings in these tests
"""
self.assertFalse(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2012_Fall')))
self.assertFalse(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2013_Spring')))
+ self.assertFalse(can_execute_unsafe_code(LibraryLocator('edX', 'test_bank')))
diff --git a/common/templates/mathjax_include.html b/common/templates/mathjax_include.html
index d3fef03ec2..bb0c14ece9 100644
--- a/common/templates/mathjax_include.html
+++ b/common/templates/mathjax_include.html
@@ -6,38 +6,33 @@
## This enables ASCIIMathJAX, and is used by js_textbox
-<%def name="mathjaxConfig()">
- %if mathjax_mode is not Undefined and mathjax_mode == 'wiki':
- MathJax.Hub.Config({
- tex2jax: {inlineMath: [ ['$','$'], ["\\(","\\)"]],
- displayMath: [ ['$$','$$'], ["\\[","\\]"]]}
- });
- %else:
- MathJax.Hub.Config({
- tex2jax: {
- inlineMath: [
- ["\\(","\\)"],
- ['[mathjaxinline]','[/mathjaxinline]']
- ],
- displayMath: [
- ["\\[","\\]"],
- ['[mathjax]','[/mathjax]']
- ]
- }
- });
- %endif
- MathJax.Hub.Configured();
- window.HUB = MathJax.Hub;
-%def>
+%if mathjax_mode is not Undefined and mathjax_mode == 'wiki':
+
+%else:
+
+%endif
-
+
diff --git a/lms/djangoapps/commerce/constants.py b/lms/djangoapps/commerce/constants.py
index fbbeae0666..a59ff3fa95 100644
--- a/lms/djangoapps/commerce/constants.py
+++ b/lms/djangoapps/commerce/constants.py
@@ -19,3 +19,4 @@ class Messages(object):
NO_SKU_ENROLLED = u'The {enrollment_mode} mode for {course_id} does not have a SKU. Enrolling {username} directly.'
ORDER_COMPLETED = u'Order {order_number} was completed.'
ORDER_INCOMPLETE_ENROLLED = u'Order {order_number} was created, but is not yet complete. User was enrolled.'
+ NO_HONOR_MODE = u'Course {course_id} does not have an honor mode.'
diff --git a/lms/djangoapps/commerce/tests.py b/lms/djangoapps/commerce/tests.py
index dcf8366545..88f6e594f5 100644
--- a/lms/djangoapps/commerce/tests.py
+++ b/lms/djangoapps/commerce/tests.py
@@ -254,7 +254,6 @@ class OrdersViewTests(EnrollmentEventTestMixin, ModuleStoreTestCase):
response = self._post_to_view()
# Validate the response
- self._mock_ecommerce_api()
self.assertEqual(response.status_code, 200)
msg = Messages.NO_ECOM_API.format(username=self.user.username, course_id=self.course.id)
self.assertResponseMessage(response, msg)
@@ -272,3 +271,29 @@ class OrdersViewTests(EnrollmentEventTestMixin, ModuleStoreTestCase):
course_mode.save()
self._test_course_without_sku()
+
+ def _test_professional_mode_only(self):
+ """ Verifies that the view behaves appropriately when the course only has a professional mode. """
+ CourseMode.objects.filter(course_id=self.course.id).delete()
+ mode = 'no-id-professional'
+ CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode,
+ sku=uuid4().hex.decode('ascii'))
+ self._mock_ecommerce_api()
+ response = self._post_to_view()
+ self.assertEqual(response.status_code, 406)
+ msg = Messages.NO_HONOR_MODE.format(course_id=self.course.id)
+ self.assertResponseMessage(response, msg)
+
+ @httpretty.activate
+ def test_course_with_professional_mode_only(self):
+ """ Verifies that the view behaves appropriately when the course only has a professional mode. """
+ self._test_professional_mode_only()
+
+ @httpretty.activate
+ @override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None)
+ def test_no_settings_and_professional_mode_only(self):
+ """
+ Verifies that the view behaves appropriately when the course only has a professional mode and
+ the E-Commerce API is not configured.
+ """
+ self._test_professional_mode_only()
diff --git a/lms/djangoapps/commerce/views.py b/lms/djangoapps/commerce/views.py
index ba4b82bdfa..e8be34f1f1 100644
--- a/lms/djangoapps/commerce/views.py
+++ b/lms/djangoapps/commerce/views.py
@@ -54,17 +54,16 @@ class OrdersView(APIView):
return True, course_key, None
- def _get_jwt(self, user):
+ def _get_jwt(self, user, ecommerce_api_signing_key):
"""
Returns a JWT object with the specified user's info.
- Raises AttributeError if settings.ECOMMERCE_API_SIGNING_KEY is not set.
"""
data = {
'username': user.username,
'email': user.email
}
- return jwt.encode(data, getattr(settings, 'ECOMMERCE_API_SIGNING_KEY'))
+ return jwt.encode(data, ecommerce_api_signing_key)
def _enroll(self, course_key, user):
""" Enroll the user in the course. """
@@ -79,40 +78,44 @@ class OrdersView(APIView):
if not valid:
return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)
+ # Ensure that the course has an honor mode with SKU
+ honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)
+ course_id = unicode(course_key)
+
+ # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS
+ # redirects to track selection.
+ if not honor_mode:
+ msg = Messages.NO_HONOR_MODE.format(course_id=course_id)
+ return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
+ elif not honor_mode.sku:
+ # If there are no course modes with SKUs, enroll the user without contacting the external API.
+ msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id,
+ username=user.username)
+ log.debug(msg)
+ self._enroll(course_key, user)
+ return DetailResponse(msg)
+
# Ensure that the E-Commerce API is setup properly
ecommerce_api_url = getattr(settings, 'ECOMMERCE_API_URL', None)
ecommerce_api_signing_key = getattr(settings, 'ECOMMERCE_API_SIGNING_KEY', None)
if not (ecommerce_api_url and ecommerce_api_signing_key):
self._enroll(course_key, user)
- msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key))
+ msg = Messages.NO_ECOM_API.format(username=user.username, course_id=course_id)
log.debug(msg)
return DetailResponse(msg)
- # Default to honor mode. In the future we may expand this view to support additional modes.
- mode = CourseMode.DEFAULT_MODE_SLUG
- course_modes = CourseMode.objects.filter(course_id=course_key, mode_slug=mode)\
- .exclude(sku__isnull=True).exclude(sku__exact='')
-
- # If there are no course modes with SKUs, enroll the user without contacting the external API.
- if not course_modes.exists():
- msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=mode, course_id=unicode(course_key),
- username=user.username)
- log.debug(msg)
- self._enroll(course_key, user)
- return DetailResponse(msg)
-
# Contact external API
headers = {
'Content-Type': 'application/json',
- 'Authorization': 'JWT {}'.format(self._get_jwt(user))
+ 'Authorization': 'JWT {}'.format(self._get_jwt(user, ecommerce_api_signing_key))
}
url = '{}/orders/'.format(ecommerce_api_url.strip('/'))
try:
timeout = getattr(settings, 'ECOMMERCE_API_TIMEOUT', 5)
- response = requests.post(url, data=json.dumps({'sku': course_modes[0].sku}), headers=headers,
+ response = requests.post(url, data=json.dumps({'sku': honor_mode.sku}), headers=headers,
timeout=timeout)
except Exception as ex: # pylint: disable=broad-except
log.exception('Call to E-Commerce API failed: %s.', ex.message)
@@ -144,7 +147,7 @@ class OrdersView(APIView):
'status': order_status,
'complete_status': OrderStatus.COMPLETE,
'username': user.username,
- 'course_id': unicode(course_key),
+ 'course_id': course_id,
}
log.error(msg, msg_kwargs)
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index 0b9f61a0e0..f5cc4daf88 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -556,3 +556,8 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})
##### CDN EXPERIMENT/MONITORING FLAGS #####
PERFORMANCE_GRAPHITE_URL = ENV_TOKENS.get('PERFORMANCE_GRAPHITE_URL', PERFORMANCE_GRAPHITE_URL)
CDN_VIDEO_URLS = ENV_TOKENS.get('CDN_VIDEO_URLS', CDN_VIDEO_URLS)
+
+##### ECOMMERCE API CONFIGURATION SETTINGS #####
+ECOMMERCE_API_URL = ENV_TOKENS.get('ECOMMERCE_API_URL', ECOMMERCE_API_URL)
+ECOMMERCE_API_SIGNING_KEY = AUTH_TOKENS.get('ECOMMERCE_API_SIGNING_KEY', ECOMMERCE_API_SIGNING_KEY)
+ECOMMERCE_API_TIMEOUT = ENV_TOKENS.get('ECOMMERCE_API_TIMEOUT', ECOMMERCE_API_TIMEOUT)
diff --git a/lms/static/require-config-lms.js b/lms/static/require-config-lms.js
index 50b3404f24..3530432f09 100644
--- a/lms/static/require-config-lms.js
+++ b/lms/static/require-config-lms.js
@@ -67,7 +67,6 @@
"ova": 'js/vendor/ova/ova',
"catch": 'js/vendor/ova/catch/js/catch',
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2',
- "mathjax": 'https://cdn.mathjax.org/mathjax/2.4-latest/MathJax.js?config=TeX-MML-AM_HTMLorMML-full'
// end of files needed by OVA
},
shim: {
diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt
index 0b71ae36f4..05bc28928e 100644
--- a/requirements/edx/github.txt
+++ b/requirements/edx/github.txt
@@ -29,7 +29,7 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a
-e git+https://github.com/edx/bok-choy.git@d62839324cbea30dda564596f20175f9d5c28516#egg=bok_choy
-e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
--e git+https://github.com/edx/edx-ora2.git@4773573b79bc530f0fe7c8f90a10491e4224dc2d#egg=edx-ora2
+-e git+https://github.com/edx/edx-ora2.git@release-2015-03-16T17.59#egg=edx-ora2
-e git+https://github.com/edx/edx-submissions.git@8fb070d2a3087dd7656d27022e550d12e3b85ba3#egg=edx-submissions
-e git+https://github.com/edx/opaque-keys.git@1254ed4d615a428591850656f39f26509b86d30a#egg=opaque-keys
-e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease