Commit d4a5ad48 by Renzo Lucioni Committed by jsa

Mark basket creation and order retrieval endpoints as v0

These endpoints are currently for internal use only, but should be versioned nonetheless; Drupal will begin using the basket creation endpoint soon. No functionality has been changed. XCOM-494.
parent 7c5cc14a
......@@ -3,5 +3,6 @@ from django.conf.urls import patterns, url, include
urlpatterns = patterns(
'',
url(r'^v0/', include('commerce.api.v0.urls', namespace='v0')),
url(r'^v1/', include('commerce.api.v1.urls', namespace='v1')),
)
""" API v0 URLs. """
from django.conf.urls import patterns, url, include
from commerce.api.v0 import views
BASKET_URLS = patterns(
'',
url(r'^$', views.BasketsView.as_view(), name='create'),
url(r'^{}/order/$'.format(r'(?P<basket_id>[\w]+)'), views.BasketOrderView.as_view(), name='retrieve_order'),
)
urlpatterns = patterns(
'',
url(r'^baskets/', include(BASKET_URLS, namespace='baskets')),
)
""" API v0 views. """
import logging
from ecommerce_api_client import exceptions
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.status import HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
from rest_framework.views import APIView
from commerce import ecommerce_api_client
from commerce.constants import Messages
from commerce.exceptions import InvalidResponseError
from commerce.http import DetailResponse, InternalRequestErrorResponse
from commerce.utils import audit_log
from course_modes.models import CourseMode
from courseware import courses
from embargo import api as embargo_api
from enrollment.api import add_enrollment
from enrollment.views import EnrollmentCrossDomainSessionAuth
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from student.models import CourseEnrollment
from util.json_request import JsonResponse
log = logging.getLogger(__name__)
class BasketsView(APIView):
""" Creates a basket 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 = (EnrollmentCrossDomainSessionAuth, OAuth2AuthenticationAllowInactiveUser)
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 _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 basket 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)
embargo_response = embargo_api.get_embargo_response(request, course_key, user)
if embargo_response:
return embargo_response
# Don't do anything if an enrollment already exists
course_id = unicode(course_key)
enrollment = CourseEnrollment.get_enrollment(user, course_key)
if enrollment and enrollment.is_active:
msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
return DetailResponse(msg, status=HTTP_409_CONFLICT)
# 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.
honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)
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)
# Setup the API
try:
api = ecommerce_api_client(user)
except ValueError:
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)
# Make the API call
try:
response_data = api.baskets.post({
'products': [{'sku': honor_mode.sku}],
'checkout': True,
})
payment_data = response_data["payment_data"]
if payment_data:
# Pass data to the client to begin the payment flow.
return JsonResponse(payment_data)
elif response_data['order']:
# The order was completed immediately because there is no charge.
msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
log.debug(msg)
return DetailResponse(msg)
else:
msg = u'Unexpected response from basket endpoint.'
log.error(
msg + u' Could not enroll user %(username)s in course %(course_id)s.',
{'username': user.id, 'course_id': course_id},
)
raise InvalidResponseError(msg)
except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
log.exception(ex.message)
return InternalRequestErrorResponse(ex.message)
finally:
audit_log(
'checkout_requested',
course_id=course_id,
mode=honor_mode.slug,
processor_name=None,
user_id=user.id
)
class BasketOrderView(APIView):
""" Retrieve the order associated with a basket. """
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, *_args, **kwargs):
""" HTTP handler. """
try:
order = ecommerce_api_client(request.user).baskets(kwargs['basket_id']).order.get()
return JsonResponse(order)
except exceptions.HttpNotFoundError:
return JsonResponse(status=404)
......@@ -4,6 +4,7 @@ from django.conf.urls import patterns, url, include
from commerce.api.v1 import views
COURSE_URLS = patterns(
'',
url(r'^$', views.CourseListView.as_view(), name='list'),
......
"""
Defines the URL routes for this app.
"""
from django.conf.urls import patterns, url, include
from commerce import views
BASKET_ID_PATTERN = r'(?P<basket_id>[\w]+)'
urlpatterns = patterns(
'',
url(r'^baskets/$', views.BasketsView.as_view(), name="baskets"),
url(r'^baskets/{}/order/$'.format(BASKET_ID_PATTERN), views.BasketOrderView.as_view(), name="basket_order"),
url(r'^checkout/cancel/$', views.checkout_cancel, name="checkout_cancel"),
url(r'^checkout/receipt/$', views.checkout_receipt, name="checkout_receipt"),
url(r'^checkout/cancel/$', views.checkout_cancel, name='checkout_cancel'),
url(r'^checkout/receipt/$', views.checkout_receipt, name='checkout_receipt'),
)
......@@ -4,29 +4,9 @@ import logging
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from ecommerce_api_client import exceptions
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.status import HTTP_406_NOT_ACCEPTABLE, HTTP_409_CONFLICT
from rest_framework.views import APIView
from commerce import ecommerce_api_client
from commerce.constants import Messages
from commerce.exceptions import InvalidResponseError
from commerce.http import DetailResponse, InternalRequestErrorResponse
from commerce.utils import audit_log
from course_modes.models import CourseMode
from courseware import courses
from edxmako.shortcuts import render_to_response
from enrollment.api import add_enrollment
from enrollment.views import EnrollmentCrossDomainSessionAuth
from embargo import api as embargo_api
from microsite_configuration import microsite
from student.models import CourseEnrollment
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
from util.json_request import JsonResponse
from verify_student.models import SoftwareSecurePhotoVerification
from shoppingcart.processors.CyberSource2 import is_user_payment_error
from django.utils.translation import ugettext as _
......@@ -35,123 +15,6 @@ from django.utils.translation import ugettext as _
log = logging.getLogger(__name__)
class BasketsView(APIView):
""" Creates a basket 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 = (EnrollmentCrossDomainSessionAuth, OAuth2AuthenticationAllowInactiveUser)
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 _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 basket 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)
embargo_response = embargo_api.get_embargo_response(request, course_key, user)
if embargo_response:
return embargo_response
# Don't do anything if an enrollment already exists
course_id = unicode(course_key)
enrollment = CourseEnrollment.get_enrollment(user, course_key)
if enrollment and enrollment.is_active:
msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
return DetailResponse(msg, status=HTTP_409_CONFLICT)
# 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.
honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)
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)
# Setup the API
try:
api = ecommerce_api_client(user)
except ValueError:
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)
# Make the API call
try:
response_data = api.baskets.post({
'products': [{'sku': honor_mode.sku}],
'checkout': True,
})
payment_data = response_data["payment_data"]
if payment_data:
# Pass data to the client to begin the payment flow.
return JsonResponse(payment_data)
elif response_data['order']:
# The order was completed immediately because there is no charge.
msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
log.debug(msg)
return DetailResponse(msg)
else:
msg = u'Unexpected response from basket endpoint.'
log.error(
msg + u' Could not enroll user %(username)s in course %(course_id)s.',
{'username': user.id, 'course_id': course_id},
)
raise InvalidResponseError(msg)
except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
log.exception(ex.message)
return InternalRequestErrorResponse(ex.message)
finally:
audit_log(
'checkout_requested',
course_id=course_id,
mode=honor_mode.slug,
processor_name=None,
user_id=user.id
)
@csrf_exempt
def checkout_cancel(_request):
""" Checkout/payment cancellation view. """
......@@ -206,18 +69,3 @@ def checkout_receipt(request):
'nav_hidden': True,
}
return render_to_response('commerce/checkout_receipt.html', context)
class BasketOrderView(APIView):
""" Retrieve the order associated with a basket. """
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, *_args, **kwargs):
""" HTTP handler. """
try:
order = ecommerce_api_client(request.user).baskets(kwargs['basket_id']).order.get()
return JsonResponse(order)
except exceptions.HttpNotFoundError:
return JsonResponse(status=404)
......@@ -83,7 +83,7 @@ var edx = edx || {};
* @return {object} JQuery Promise.
*/
getReceiptData: function (basketId) {
var urlFormat = this.useEcommerceApi ? '/commerce/baskets/%s/order/' : '/shoppingcart/receipt/%s/';
var urlFormat = this.useEcommerceApi ? '/api/commerce/v0/baskets/%s/order/' : '/shoppingcart/receipt/%s/';
return $.ajax({
url: _.sprintf(urlFormat, basketId),
......
......@@ -5,7 +5,7 @@ define(['common/js/spec_helpers/ajax_helpers', 'js/student_account/enrollment'],
describe( 'edx.student.account.EnrollmentInterface', function() {
var COURSE_KEY = 'edX/DemoX/Fall',
ENROLL_URL = '/commerce/baskets/',
ENROLL_URL = '/api/commerce/v0/baskets/',
FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/',
EMBARGO_MSG_URL = '/embargo/blocked-message/enrollment/default/';
......
......@@ -9,7 +9,7 @@ var edx = edx || {};
edx.student.account.EnrollmentInterface = {
urls: {
baskets: '/commerce/baskets/',
baskets: '/api/commerce/v0/baskets/',
},
headers: {
......
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