Merge pull request #7318 from edx/clintonb/register-with-oscar
Enrolling via E-Commerce API for combined login-registration page
This commit is contained in:
1
lms/djangoapps/commerce/__init__.py
Normal file
1
lms/djangoapps/commerce/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
""" Commerce app. """
|
||||
21
lms/djangoapps/commerce/constants.py
Normal file
21
lms/djangoapps/commerce/constants.py
Normal file
@@ -0,0 +1,21 @@
|
||||
""" Constants for this app as well as the external API. """
|
||||
|
||||
|
||||
class OrderStatus(object):
|
||||
"""Constants representing all known order statuses. """
|
||||
OPEN = 'Open'
|
||||
ORDER_CANCELLED = 'Order Cancelled'
|
||||
BEING_PROCESSED = 'Being Processed'
|
||||
PAYMENT_CANCELLED = 'Payment Cancelled'
|
||||
PAID = 'Paid'
|
||||
FULFILLMENT_ERROR = 'Fulfillment Error'
|
||||
COMPLETE = 'Complete'
|
||||
REFUNDED = 'Refunded'
|
||||
|
||||
|
||||
class Messages(object):
|
||||
""" Strings used to populate response messages. """
|
||||
NO_ECOM_API = u'E-Commerce API not setup. Enrolled {username} in {course_id} directly.'
|
||||
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.'
|
||||
21
lms/djangoapps/commerce/http.py
Normal file
21
lms/djangoapps/commerce/http.py
Normal file
@@ -0,0 +1,21 @@
|
||||
""" HTTP-related entities. """
|
||||
|
||||
from rest_framework.status import HTTP_503_SERVICE_UNAVAILABLE, HTTP_200_OK
|
||||
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
|
||||
class DetailResponse(JsonResponse):
|
||||
""" JSON response that simply contains a detail field. """
|
||||
|
||||
def __init__(self, message, status=HTTP_200_OK):
|
||||
data = {'detail': message}
|
||||
super(DetailResponse, self).__init__(object=data, status=status)
|
||||
|
||||
|
||||
class ApiErrorResponse(DetailResponse):
|
||||
""" Response returned when calls to the E-Commerce API fail or the returned data is invalid. """
|
||||
|
||||
def __init__(self):
|
||||
message = 'Call to E-Commerce API failed. Order creation failed.'
|
||||
super(ApiErrorResponse, self).__init__(message=message, status=HTTP_503_SERVICE_UNAVAILABLE)
|
||||
3
lms/djangoapps/commerce/models.py
Normal file
3
lms/djangoapps/commerce/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
This file is intentionally empty. Django 1.6 and below require a models.py file for all apps.
|
||||
"""
|
||||
247
lms/djangoapps/commerce/tests.py
Normal file
247
lms/djangoapps/commerce/tests.py
Normal file
@@ -0,0 +1,247 @@
|
||||
""" Tests for commerce views. """
|
||||
|
||||
import json
|
||||
from uuid import uuid4
|
||||
|
||||
from ddt import ddt, data
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
import httpretty
|
||||
from httpretty.core import HTTPrettyRequestEmpty
|
||||
import jwt
|
||||
from requests import Timeout
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from commerce.constants import OrderStatus, Messages
|
||||
from course_modes.models import CourseMode
|
||||
from enrollment.api import add_enrollment
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
|
||||
|
||||
ECOMMERCE_API_URL = 'http://example.com/api'
|
||||
ECOMMERCE_API_SIGNING_KEY = 'edx'
|
||||
ORDER_NUMBER = "100004"
|
||||
ECOMMERCE_API_SUCCESSFUL_BODY = json.dumps({'status': OrderStatus.COMPLETE, 'number': ORDER_NUMBER})
|
||||
|
||||
|
||||
@ddt
|
||||
@override_settings(ECOMMERCE_API_URL=ECOMMERCE_API_URL, ECOMMERCE_API_SIGNING_KEY=ECOMMERCE_API_SIGNING_KEY)
|
||||
class OrdersViewTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the commerce orders view.
|
||||
"""
|
||||
|
||||
def _login(self):
|
||||
""" Log into LMS. """
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
def _post_to_view(self, course_id=None):
|
||||
"""
|
||||
POST to the view being tested.
|
||||
|
||||
Arguments
|
||||
course_id (str) -- ID of course for which a seat should be ordered.
|
||||
|
||||
:return: Response
|
||||
"""
|
||||
course_id = unicode(course_id or self.course.id)
|
||||
return self.client.post(self.url, {'course_id': course_id})
|
||||
|
||||
def _mock_ecommerce_api(self, status=200, body=None):
|
||||
"""
|
||||
Mock calls to the E-Commerce API.
|
||||
|
||||
The calling test should be decorated with @httpretty.activate.
|
||||
"""
|
||||
self.assertTrue(httpretty.is_enabled(), 'Test is missing @httpretty.activate decorator.')
|
||||
|
||||
url = ECOMMERCE_API_URL + '/orders/'
|
||||
body = body or ECOMMERCE_API_SUCCESSFUL_BODY
|
||||
httpretty.register_uri(httpretty.POST, url, status=status, body=body)
|
||||
|
||||
def assertResponseMessage(self, response, expected_msg):
|
||||
""" Asserts the detail field in the response's JSON body equals the expected message. """
|
||||
actual = json.loads(response.content)['detail']
|
||||
self.assertEqual(actual, expected_msg)
|
||||
|
||||
def assertValidEcommerceApiErrorResponse(self, response):
|
||||
""" Asserts the response is a valid response sent when the E-Commerce API is unavailable. """
|
||||
self.assertEqual(response.status_code, 503)
|
||||
self.assertResponseMessage(response, 'Call to E-Commerce API failed. Order creation failed.')
|
||||
|
||||
def setUp(self):
|
||||
super(OrdersViewTests, self).setUp()
|
||||
self.url = reverse('commerce:orders')
|
||||
self.user = UserFactory()
|
||||
self._login()
|
||||
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
# TODO Verify this is the best method to create CourseMode objects.
|
||||
# TODO Find/create constants for the modes.
|
||||
for mode in ['honor', 'verified', 'audit']:
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=mode,
|
||||
mode_display_name=mode,
|
||||
sku=uuid4().hex.decode('ascii')
|
||||
)
|
||||
|
||||
def test_login_required(self):
|
||||
"""
|
||||
The view should return HTTP 403 status if the user is not logged in.
|
||||
"""
|
||||
self.client.logout()
|
||||
self.assertEqual(403, self._post_to_view().status_code)
|
||||
|
||||
@data('delete', 'get', 'put')
|
||||
def test_post_required(self, method):
|
||||
"""
|
||||
Verify that the view only responds to POST operations.
|
||||
"""
|
||||
response = getattr(self.client, method)(self.url)
|
||||
self.assertEqual(405, response.status_code)
|
||||
|
||||
def test_invalid_course(self):
|
||||
"""
|
||||
If the course does not exist, the view should return HTTP 406.
|
||||
"""
|
||||
# TODO Test inactive courses, and those not open for enrollment.
|
||||
self.assertEqual(406, self._post_to_view('aaa/bbb/ccc').status_code)
|
||||
|
||||
def test_invalid_request_data(self):
|
||||
"""
|
||||
If invalid data is supplied with the request, the view should return HTTP 406.
|
||||
"""
|
||||
self.assertEqual(406, self.client.post(self.url, {}).status_code)
|
||||
self.assertEqual(406, self.client.post(self.url, {'not_course_id': ''}).status_code)
|
||||
|
||||
@httpretty.activate
|
||||
@data(400, 401, 405, 406, 429, 500, 503)
|
||||
def test_ecommerce_api_bad_status(self, status):
|
||||
"""
|
||||
If the E-Commerce API returns an HTTP status not equal to 200, the view should log an error and return
|
||||
an HTTP 503 status.
|
||||
"""
|
||||
self._mock_ecommerce_api(status=status, body=json.dumps({'user_message': 'FAIL!'}))
|
||||
response = self._post_to_view()
|
||||
self.assertValidEcommerceApiErrorResponse(response)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
@httpretty.activate
|
||||
def test_ecommerce_api_timeout(self):
|
||||
"""
|
||||
If the call to the E-Commerce API times out, the view should log an error and return an HTTP 503 status.
|
||||
"""
|
||||
# Verify that the view responds appropriately if calls to the E-Commerce API timeout.
|
||||
def request_callback(_request, _uri, _headers):
|
||||
""" Simulates API timeout """
|
||||
raise Timeout
|
||||
|
||||
self._mock_ecommerce_api(body=request_callback)
|
||||
response = self._post_to_view()
|
||||
self.assertValidEcommerceApiErrorResponse(response)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
@httpretty.activate
|
||||
def test_ecommerce_api_bad_data(self):
|
||||
"""
|
||||
If the E-Commerce API returns data that is not JSON, the view should return an HTTP 503 status.
|
||||
"""
|
||||
self._mock_ecommerce_api(body='TOTALLY NOT JSON!')
|
||||
response = self._post_to_view()
|
||||
self.assertValidEcommerceApiErrorResponse(response)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
@data(True, False)
|
||||
@httpretty.activate
|
||||
def test_course_with_honor_seat_sku(self, user_is_active):
|
||||
"""
|
||||
If the course has a SKU, the view should get authorization from the E-Commerce API before enrolling
|
||||
the user in the course. If authorization is approved, the user should be redirected to the user dashboard.
|
||||
"""
|
||||
|
||||
# Set user's active flag
|
||||
self.user.is_active = user_is_active
|
||||
self.user.save() # pylint: disable=no-member
|
||||
|
||||
def request_callback(_method, _uri, headers):
|
||||
""" Mock the E-Commerce API's call to the enrollment API. """
|
||||
add_enrollment(self.user.username, unicode(self.course.id), 'honor')
|
||||
return 200, headers, ECOMMERCE_API_SUCCESSFUL_BODY
|
||||
|
||||
self._mock_ecommerce_api(body=request_callback)
|
||||
response = self._post_to_view()
|
||||
|
||||
# Validate the response content
|
||||
msg = Messages.ORDER_COMPLETED.format(order_number=ORDER_NUMBER)
|
||||
self.assertResponseMessage(response, msg)
|
||||
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
# Verify the correct information was passed to the E-Commerce API
|
||||
request = httpretty.last_request()
|
||||
sku = CourseMode.objects.filter(course_id=self.course.id, mode_slug='honor', sku__isnull=False)[0].sku
|
||||
self.assertEqual(request.body, 'sku={}'.format(sku))
|
||||
self.assertEqual(request.headers['Content-Type'], 'application/json')
|
||||
|
||||
# Verify the JWT is correct
|
||||
expected_jwt = jwt.encode({'username': self.user.username, 'email': self.user.email},
|
||||
ECOMMERCE_API_SIGNING_KEY)
|
||||
self.assertEqual(request.headers['Authorization'], 'JWT {}'.format(expected_jwt))
|
||||
|
||||
@httpretty.activate
|
||||
def test_order_not_complete(self):
|
||||
self._mock_ecommerce_api(body=json.dumps({'status': OrderStatus.OPEN, 'number': ORDER_NUMBER}))
|
||||
response = self._post_to_view()
|
||||
self.assertEqual(response.status_code, 202)
|
||||
msg = Messages.ORDER_INCOMPLETE_ENROLLED.format(order_number=ORDER_NUMBER)
|
||||
self.assertResponseMessage(response, msg)
|
||||
|
||||
# TODO Eventually we should NOT be enrolling users directly from this view.
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
@httpretty.activate
|
||||
def test_course_without_sku(self):
|
||||
"""
|
||||
If the course does NOT have a SKU, the user should be enrolled in the course (under the honor mode) and
|
||||
redirected to the user dashboard.
|
||||
"""
|
||||
# Remove SKU from all course modes
|
||||
for course_mode in CourseMode.objects.filter(course_id=self.course.id):
|
||||
course_mode.sku = None
|
||||
course_mode.save()
|
||||
|
||||
# Place an order
|
||||
self._mock_ecommerce_api()
|
||||
response = self._post_to_view()
|
||||
|
||||
# Validate the response content
|
||||
self.assertEqual(response.status_code, 200)
|
||||
msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode='honor', course_id=self.course.id,
|
||||
username=self.user.username)
|
||||
self.assertResponseMessage(response, msg)
|
||||
|
||||
# The user should be enrolled, and no calls made to the E-Commerce API
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
self.assertIsInstance(httpretty.last_request(), HTTPrettyRequestEmpty)
|
||||
|
||||
@httpretty.activate
|
||||
@override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None)
|
||||
def test_no_settings(self):
|
||||
"""
|
||||
If no settings exist to define the E-Commerce API URL or signing key, the view should enroll the user.
|
||||
"""
|
||||
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)
|
||||
|
||||
# Ensure that the user is not enrolled and that no calls were made to the E-Commerce API
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
self.assertIsInstance(httpretty.last_request(), HTTPrettyRequestEmpty)
|
||||
12
lms/djangoapps/commerce/urls.py
Normal file
12
lms/djangoapps/commerce/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Defines the URL routes for this app.
|
||||
"""
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from .views import OrdersView
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^orders/$', OrdersView.as_view(), name="orders"),
|
||||
)
|
||||
159
lms/djangoapps/commerce/views.py
Normal file
159
lms/djangoapps/commerce/views.py
Normal file
@@ -0,0 +1,159 @@
|
||||
""" Commerce views. """
|
||||
|
||||
import logging
|
||||
from simplejson import JSONDecodeError
|
||||
|
||||
from django.conf import settings
|
||||
import jwt
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
import requests
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.status import HTTP_406_NOT_ACCEPTABLE, HTTP_202_ACCEPTED, HTTP_200_OK
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from commerce.constants import OrderStatus, Messages
|
||||
from commerce.http import DetailResponse, ApiErrorResponse
|
||||
from course_modes.models import CourseMode
|
||||
from courseware import courses
|
||||
from enrollment.api import add_enrollment
|
||||
from util.authentication import SessionAuthenticationAllowInactiveUser
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrdersView(APIView):
|
||||
""" Creates an order with a course seat and enrolls users. """
|
||||
|
||||
# LMS utilizes User.user_is_active to indicate email verification, not whether an account is active. Sigh!
|
||||
authentication_classes = (SessionAuthenticationAllowInactiveUser,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def _is_data_valid(self, request):
|
||||
"""
|
||||
Validates the data posted to the view.
|
||||
|
||||
Arguments
|
||||
request -- HTTP request
|
||||
|
||||
Returns
|
||||
Tuple (data_is_valid, course_key, error_msg)
|
||||
"""
|
||||
course_id = request.DATA.get('course_id')
|
||||
|
||||
if not course_id:
|
||||
return False, None, u'Field course_id is missing.'
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
courses.get_course(course_key)
|
||||
except (InvalidKeyError, ValueError)as ex:
|
||||
log.exception(u'Unable to locate course matching %s.', course_id)
|
||||
return False, None, ex.message
|
||||
|
||||
return True, course_key, None
|
||||
|
||||
def _get_jwt(self, user):
|
||||
"""
|
||||
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'))
|
||||
|
||||
def _enroll(self, course_key, user):
|
||||
""" Enroll the user in the course. """
|
||||
add_enrollment(user.username, unicode(course_key))
|
||||
|
||||
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Attempt to create the order and enroll the user.
|
||||
"""
|
||||
user = request.user
|
||||
valid, course_key, error = self._is_data_valid(request)
|
||||
if not valid:
|
||||
return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)
|
||||
|
||||
# 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))
|
||||
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, sku__isnull=False)
|
||||
|
||||
# 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))
|
||||
}
|
||||
|
||||
url = '{}/orders/'.format(ecommerce_api_url.strip('/'))
|
||||
|
||||
try:
|
||||
timeout = getattr(settings, 'ECOMMERCE_API_TIMEOUT', 5)
|
||||
response = requests.post(url, data={'sku': course_modes[0].sku}, headers=headers, timeout=timeout)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.exception('Call to E-Commerce API failed: %s.', ex.message)
|
||||
return ApiErrorResponse()
|
||||
|
||||
status_code = response.status_code
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except JSONDecodeError:
|
||||
log.error('E-Commerce API response is not valid JSON.')
|
||||
return ApiErrorResponse()
|
||||
|
||||
if status_code == HTTP_200_OK:
|
||||
order_number = data.get('number')
|
||||
order_status = data.get('status')
|
||||
if order_status == OrderStatus.COMPLETE:
|
||||
msg = Messages.ORDER_COMPLETED.format(order_number=order_number)
|
||||
log.debug(msg)
|
||||
return DetailResponse(msg)
|
||||
else:
|
||||
# TODO Before this functionality is fully rolled-out, this branch should be updated to NOT enroll the
|
||||
# user. Enrollments must be initiated by the E-Commerce API only.
|
||||
self._enroll(course_key, user)
|
||||
msg = u'Order %(order_number)s was received with %(status)s status. Expected %(complete_status)s. ' \
|
||||
u'User %(username)s was enrolled in %(course_id)s by LMS.'
|
||||
msg_kwargs = {
|
||||
'order_number': order_number,
|
||||
'status': order_status,
|
||||
'complete_status': OrderStatus.COMPLETE,
|
||||
'username': user.username,
|
||||
'course_id': unicode(course_key),
|
||||
}
|
||||
log.error(msg, msg_kwargs)
|
||||
|
||||
msg = Messages.ORDER_INCOMPLETE_ENROLLED.format(order_number=order_number)
|
||||
return DetailResponse(msg, status=HTTP_202_ACCEPTED)
|
||||
else:
|
||||
msg = u'Response from E-Commerce API was invalid: (%(status)d) - %(msg)s'
|
||||
msg_kwargs = {
|
||||
'status': status_code,
|
||||
'msg': data.get('user_message'),
|
||||
}
|
||||
log.error(msg, msg_kwargs)
|
||||
|
||||
return ApiErrorResponse()
|
||||
@@ -916,7 +916,7 @@ class EdxNotesViewsTest(ModuleStoreTestCase):
|
||||
response = self.client.get(self.get_token_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
client = Client.objects.get(name='edx-notes')
|
||||
jwt.decode(response.content, client.client_secret)
|
||||
jwt.decode(response.content, client.client_secret, audience=client.client_id)
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True})
|
||||
def test_get_id_token_anonymous(self):
|
||||
|
||||
@@ -1654,7 +1654,9 @@ INSTALLED_APPS = (
|
||||
|
||||
# CORS and cross-domain CSRF
|
||||
'corsheaders',
|
||||
'cors_csrf'
|
||||
'cors_csrf',
|
||||
|
||||
'commerce',
|
||||
)
|
||||
|
||||
######################### CSRF #########################################
|
||||
@@ -2095,3 +2097,8 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
|
||||
'profile_image',
|
||||
],
|
||||
}
|
||||
|
||||
# E-Commerce API Configuration
|
||||
ECOMMERCE_API_URL = None
|
||||
ECOMMERCE_API_SIGNING_KEY = None
|
||||
ECOMMERCE_API_TIMEOUT = 5
|
||||
|
||||
@@ -5,7 +5,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
|
||||
describe( 'edx.student.account.EnrollmentInterface', function() {
|
||||
|
||||
var COURSE_KEY = 'edX/DemoX/Fall',
|
||||
ENROLL_URL = '/api/enrollment/v1/enrollment',
|
||||
ENROLL_URL = '/commerce/orders/',
|
||||
FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/',
|
||||
EMBARGO_MSG_URL = '/embargo/blocked-message/enrollment/default/';
|
||||
|
||||
@@ -26,7 +26,7 @@ define(['js/common_helpers/ajax_helpers', 'js/student_account/enrollment'],
|
||||
requests,
|
||||
'POST',
|
||||
ENROLL_URL,
|
||||
'{"course_details":{"course_id":"edX/DemoX/Fall"}}'
|
||||
'{"course_id":"edX/DemoX/Fall"}'
|
||||
);
|
||||
|
||||
// Simulate a successful response from the server
|
||||
|
||||
@@ -9,7 +9,7 @@ var edx = edx || {};
|
||||
edx.student.account.EnrollmentInterface = {
|
||||
|
||||
urls: {
|
||||
enrollment: '/api/enrollment/v1/enrollment',
|
||||
orders: '/commerce/orders/',
|
||||
trackSelection: '/course_modes/choose/'
|
||||
},
|
||||
|
||||
@@ -23,14 +23,11 @@ var edx = edx || {};
|
||||
* @param {string} courseKey Slash-separated course key.
|
||||
*/
|
||||
enroll: function( courseKey ) {
|
||||
var data_obj = {
|
||||
course_details: {
|
||||
course_id: courseKey
|
||||
}
|
||||
};
|
||||
var data = JSON.stringify(data_obj);
|
||||
var data_obj = {course_id: courseKey},
|
||||
data = JSON.stringify(data_obj);
|
||||
|
||||
$.ajax({
|
||||
url: this.urls.enrollment,
|
||||
url: this.urls.orders,
|
||||
type: 'POST',
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
data: data,
|
||||
|
||||
@@ -499,6 +499,7 @@ if settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD'):
|
||||
# Shopping cart
|
||||
urlpatterns += (
|
||||
url(r'^shoppingcart/', include('shoppingcart.urls')),
|
||||
url(r'^commerce/', include('commerce.urls', namespace='commerce')),
|
||||
)
|
||||
|
||||
# Embargo
|
||||
|
||||
@@ -66,6 +66,7 @@ polib==1.0.3
|
||||
pycrypto>=2.6
|
||||
pygments==2.0.1
|
||||
pygraphviz==1.1
|
||||
PyJWT==0.4.3
|
||||
pymongo==2.7.2
|
||||
pyparsing==2.0.1
|
||||
python-memcached==1.48
|
||||
|
||||
@@ -34,7 +34,7 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a
|
||||
-e git+https://github.com/edx/opaque-keys.git@1254ed4d615a428591850656f39f26509b86d30a#egg=opaque-keys
|
||||
-e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease
|
||||
-e git+https://github.com/edx/i18n-tools.git@193cebd9aa784f8899ef496f2aa050b08eff402b#egg=i18n-tools
|
||||
-e git+https://github.com/edx/edx-oauth2-provider.git@0.4.1#egg=oauth2-provider
|
||||
-e git+https://github.com/edx/edx-oauth2-provider.git@0.4.2#egg=oauth2-provider
|
||||
-e git+https://github.com/edx/edx-val.git@fbec6efc86abb36f55de947baacc2092881dcde2#egg=edx-val
|
||||
-e git+https://github.com/pmitros/RecommenderXBlock.git@9b07e807c89ba5761827d0387177f71aa57ef056#egg=recommender-xblock
|
||||
-e git+https://github.com/edx/edx-milestones.git@547f2250ee49e73ce8d7ff4e78ecf1b049892510#egg=edx-milestones
|
||||
|
||||
Reference in New Issue
Block a user