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 ...@@ -3,5 +3,6 @@ from django.conf.urls import patterns, url, include
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^v0/', include('commerce.api.v0.urls', namespace='v0')),
url(r'^v1/', include('commerce.api.v1.urls', namespace='v1')), 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 ...@@ -4,6 +4,7 @@ from django.conf.urls import patterns, url, include
from commerce.api.v1 import views from commerce.api.v1 import views
COURSE_URLS = patterns( COURSE_URLS = patterns(
'', '',
url(r'^$', views.CourseListView.as_view(), name='list'), url(r'^$', views.CourseListView.as_view(), name='list'),
......
""" """
Defines the URL routes for this app. Defines the URL routes for this app.
""" """
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
from commerce import views from commerce import views
BASKET_ID_PATTERN = r'(?P<basket_id>[\w]+)'
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^baskets/$', views.BasketsView.as_view(), name="baskets"), url(r'^checkout/cancel/$', views.checkout_cancel, name='checkout_cancel'),
url(r'^baskets/{}/order/$'.format(BASKET_ID_PATTERN), views.BasketOrderView.as_view(), name="basket_order"), 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 ...@@ -4,29 +4,9 @@ import logging
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt 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 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 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 verify_student.models import SoftwareSecurePhotoVerification
from shoppingcart.processors.CyberSource2 import is_user_payment_error from shoppingcart.processors.CyberSource2 import is_user_payment_error
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -35,123 +15,6 @@ from django.utils.translation import ugettext as _ ...@@ -35,123 +15,6 @@ from django.utils.translation import ugettext as _
log = logging.getLogger(__name__) 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 @csrf_exempt
def checkout_cancel(_request): def checkout_cancel(_request):
""" Checkout/payment cancellation view. """ """ Checkout/payment cancellation view. """
...@@ -206,18 +69,3 @@ def checkout_receipt(request): ...@@ -206,18 +69,3 @@ def checkout_receipt(request):
'nav_hidden': True, 'nav_hidden': True,
} }
return render_to_response('commerce/checkout_receipt.html', context) 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 || {}; ...@@ -83,7 +83,7 @@ var edx = edx || {};
* @return {object} JQuery Promise. * @return {object} JQuery Promise.
*/ */
getReceiptData: function (basketId) { 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({ return $.ajax({
url: _.sprintf(urlFormat, basketId), url: _.sprintf(urlFormat, basketId),
......
...@@ -5,7 +5,7 @@ define(['common/js/spec_helpers/ajax_helpers', 'js/student_account/enrollment'], ...@@ -5,7 +5,7 @@ define(['common/js/spec_helpers/ajax_helpers', 'js/student_account/enrollment'],
describe( 'edx.student.account.EnrollmentInterface', function() { describe( 'edx.student.account.EnrollmentInterface', function() {
var COURSE_KEY = 'edX/DemoX/Fall', var COURSE_KEY = 'edX/DemoX/Fall',
ENROLL_URL = '/commerce/baskets/', ENROLL_URL = '/api/commerce/v0/baskets/',
FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/', FORWARD_URL = '/course_modes/choose/edX/DemoX/Fall/',
EMBARGO_MSG_URL = '/embargo/blocked-message/enrollment/default/'; EMBARGO_MSG_URL = '/embargo/blocked-message/enrollment/default/';
......
...@@ -9,7 +9,7 @@ var edx = edx || {}; ...@@ -9,7 +9,7 @@ var edx = edx || {};
edx.student.account.EnrollmentInterface = { edx.student.account.EnrollmentInterface = {
urls: { urls: {
baskets: '/commerce/baskets/', baskets: '/api/commerce/v0/baskets/',
}, },
headers: { 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