Commit cff9a5aa by Clinton Blackburn

Merge pull request #7318 from edx/clintonb/register-with-oscar

Enrolling via E-Commerce API for combined login-registration page
parents a7e1f9ca 935323a8
""" 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.'
""" 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)
"""
This file is intentionally empty. Django 1.6 and below require a models.py file for all apps.
"""
"""
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"),
)
""" 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
......
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