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.
This commit is contained in:
@@ -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')),
|
||||
)
|
||||
|
||||
0
lms/djangoapps/commerce/api/v0/__init__.py
Normal file
0
lms/djangoapps/commerce/api/v0/__init__.py
Normal file
0
lms/djangoapps/commerce/api/v0/tests/__init__.py
Normal file
0
lms/djangoapps/commerce/api/v0/tests/__init__.py
Normal file
333
lms/djangoapps/commerce/api/v0/tests/test_views.py
Normal file
333
lms/djangoapps/commerce/api/v0/tests/test_views.py
Normal file
@@ -0,0 +1,333 @@
|
||||
""" Commerce API v0 view tests. """
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
import mock
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from commerce.constants import Messages
|
||||
from commerce.tests import TEST_BASKET_ID, TEST_ORDER_NUMBER, TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY
|
||||
from commerce.tests.mocks import mock_basket_order, mock_create_basket
|
||||
from commerce.tests.test_views import UserMixin
|
||||
from course_modes.models import CourseMode
|
||||
from ecommerce_api_client import exceptions
|
||||
from embargo.test_utils import restrict_course
|
||||
from enrollment.api import get_enrollment
|
||||
from openedx.core.lib.django_test_client_utils import get_absolute_url
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import CourseModeFactory
|
||||
from student.tests.tests import EnrollmentEventTestMixin
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
@override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY)
|
||||
class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the commerce orders view.
|
||||
"""
|
||||
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 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 assertResponsePaymentData(self, response):
|
||||
""" Asserts correctness of a JSON body containing payment information. """
|
||||
actual_response = json.loads(response.content)
|
||||
self.assertEqual(actual_response, TEST_PAYMENT_DATA)
|
||||
|
||||
def assertValidEcommerceInternalRequestErrorResponse(self, response):
|
||||
""" Asserts the response is a valid response sent when the E-Commerce API is unavailable. """
|
||||
self.assertEqual(response.status_code, 500)
|
||||
actual = json.loads(response.content)['detail']
|
||||
self.assertIn('Call to E-Commerce API failed', actual)
|
||||
|
||||
def assertUserNotEnrolled(self):
|
||||
""" Asserts that the user is NOT enrolled in the course, and that an enrollment event was NOT fired. """
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
def setUp(self):
|
||||
super(BasketsViewTests, self).setUp()
|
||||
self.url = reverse('commerce_api:v0:baskets:create')
|
||||
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 [CourseMode.HONOR, CourseMode.VERIFIED, CourseMode.AUDIT]:
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=mode,
|
||||
mode_display_name=mode,
|
||||
sku=uuid4().hex.decode('ascii')
|
||||
)
|
||||
|
||||
# Ignore events fired from UserFactory creation
|
||||
self.reset_tracker()
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'EMBARGO': True})
|
||||
def test_embargo_restriction(self):
|
||||
"""
|
||||
The view should return HTTP 403 status if the course is embargoed.
|
||||
"""
|
||||
with restrict_course(self.course.id) as redirect_url:
|
||||
response = self._post_to_view()
|
||||
self.assertEqual(403, response.status_code)
|
||||
body = json.loads(response.content)
|
||||
self.assertEqual(get_absolute_url(redirect_url), body['user_message_url'])
|
||||
|
||||
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)
|
||||
|
||||
@ddt.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)
|
||||
|
||||
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.
|
||||
"""
|
||||
with mock_create_basket(exception=exceptions.Timeout):
|
||||
response = self._post_to_view()
|
||||
|
||||
self.assertValidEcommerceInternalRequestErrorResponse(response)
|
||||
self.assertUserNotEnrolled()
|
||||
|
||||
def test_ecommerce_api_error(self):
|
||||
"""
|
||||
If the E-Commerce API raises an error, the view should return an HTTP 503 status.
|
||||
"""
|
||||
with mock_create_basket(exception=exceptions.SlumberBaseException):
|
||||
response = self._post_to_view()
|
||||
|
||||
self.assertValidEcommerceInternalRequestErrorResponse(response)
|
||||
self.assertUserNotEnrolled()
|
||||
|
||||
def _test_successful_ecommerce_api_call(self, is_completed=True):
|
||||
"""
|
||||
Verifies that the view contacts the E-Commerce API with the correct data and headers.
|
||||
"""
|
||||
with mock.patch('commerce.api.v0.views.audit_log') as mock_audit_log:
|
||||
response = self._post_to_view()
|
||||
|
||||
# Verify that an audit message was logged
|
||||
self.assertTrue(mock_audit_log.called)
|
||||
|
||||
# Validate the response content
|
||||
if is_completed:
|
||||
msg = Messages.ORDER_COMPLETED.format(order_number=TEST_ORDER_NUMBER)
|
||||
self.assertResponseMessage(response, msg)
|
||||
else:
|
||||
self.assertResponsePaymentData(response)
|
||||
|
||||
@ddt.data(True, False)
|
||||
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
|
||||
|
||||
return_value = {'id': TEST_BASKET_ID, 'payment_data': None, 'order': {'number': TEST_ORDER_NUMBER}}
|
||||
with mock_create_basket(response=return_value):
|
||||
self._test_successful_ecommerce_api_call()
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_course_with_paid_seat_sku(self, user_is_active):
|
||||
"""
|
||||
If the course has a SKU, the view should return data that the client
|
||||
will use to redirect the user to an external payment processor.
|
||||
"""
|
||||
# Set user's active flag
|
||||
self.user.is_active = user_is_active
|
||||
self.user.save() # pylint: disable=no-member
|
||||
|
||||
return_value = {'id': TEST_BASKET_ID, 'payment_data': TEST_PAYMENT_DATA, 'order': None}
|
||||
with mock_create_basket(response=return_value):
|
||||
self._test_successful_ecommerce_api_call(False)
|
||||
|
||||
def _test_course_without_sku(self):
|
||||
"""
|
||||
Validates the view bypasses the E-Commerce API when the course has no CourseModes with SKUs.
|
||||
"""
|
||||
# Place an order
|
||||
with mock_create_basket(expect_called=False):
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
self._test_course_without_sku()
|
||||
|
||||
@override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None)
|
||||
def test_ecommerce_service_not_configured(self):
|
||||
"""
|
||||
If the E-Commerce Service is not configured, the view should enroll the user.
|
||||
"""
|
||||
with mock_create_basket(expect_called=False):
|
||||
response = self._post_to_view()
|
||||
|
||||
# Validate the response
|
||||
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))
|
||||
|
||||
def assertProfessionalModeBypassed(self):
|
||||
""" Verifies that the view returns HTTP 406 when a course with no honor mode is encountered. """
|
||||
|
||||
CourseMode.objects.filter(course_id=self.course.id).delete()
|
||||
mode = CourseMode.NO_ID_PROFESSIONAL_MODE
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode,
|
||||
sku=uuid4().hex.decode('ascii'))
|
||||
|
||||
with mock_create_basket(expect_called=False):
|
||||
response = self._post_to_view()
|
||||
|
||||
# The view should return an error status code
|
||||
self.assertEqual(response.status_code, 406)
|
||||
msg = Messages.NO_HONOR_MODE.format(course_id=self.course.id)
|
||||
self.assertResponseMessage(response, msg)
|
||||
|
||||
def test_course_with_professional_mode_only(self):
|
||||
""" Verifies that the view behaves appropriately when the course only has a professional mode. """
|
||||
self.assertProfessionalModeBypassed()
|
||||
|
||||
@override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None)
|
||||
def test_professional_mode_only_and_ecommerce_service_not_configured(self):
|
||||
"""
|
||||
Verifies that the view behaves appropriately when the course only has a professional mode and
|
||||
the E-Commerce Service is not configured.
|
||||
"""
|
||||
self.assertProfessionalModeBypassed()
|
||||
|
||||
def test_empty_sku(self):
|
||||
""" If the CourseMode has an empty string for a SKU, the API should not be used. """
|
||||
# Set SKU to empty string for all modes.
|
||||
for course_mode in CourseMode.objects.filter(course_id=self.course.id):
|
||||
course_mode.sku = ''
|
||||
course_mode.save()
|
||||
|
||||
self._test_course_without_sku()
|
||||
|
||||
def test_existing_active_enrollment(self):
|
||||
""" The view should respond with HTTP 409 if the user has an existing active enrollment for the course. """
|
||||
|
||||
# Enroll user in the course
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
response = self._post_to_view()
|
||||
self.assertEqual(response.status_code, 409)
|
||||
msg = Messages.ENROLLMENT_EXISTS.format(username=self.user.username, course_id=self.course.id)
|
||||
self.assertResponseMessage(response, msg)
|
||||
|
||||
def test_existing_inactive_enrollment(self):
|
||||
"""
|
||||
If the user has an inactive enrollment for the course, the view should behave as if the
|
||||
user has no enrollment.
|
||||
"""
|
||||
# Create an inactive enrollment
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
CourseEnrollment.unenroll(self.user, self.course.id, True)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
self.assertIsNotNone(get_enrollment(self.user.username, unicode(self.course.id)))
|
||||
|
||||
with mock_create_basket():
|
||||
self._test_successful_ecommerce_api_call(False)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY)
|
||||
class BasketOrderViewTests(UserMixin, TestCase):
|
||||
""" Tests for the basket order view. """
|
||||
view_name = 'commerce_api:v0:baskets:retrieve_order'
|
||||
MOCK_ORDER = {'number': 1}
|
||||
path = reverse(view_name, kwargs={'basket_id': 1})
|
||||
|
||||
def setUp(self):
|
||||
super(BasketOrderViewTests, self).setUp()
|
||||
self._login()
|
||||
|
||||
def test_order_found(self):
|
||||
""" If the order is located, the view should pass the data from the API. """
|
||||
|
||||
with mock_basket_order(basket_id=1, response=self.MOCK_ORDER):
|
||||
response = self.client.get(self.path)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
actual = json.loads(response.content)
|
||||
self.assertEqual(actual, self.MOCK_ORDER)
|
||||
|
||||
def test_order_not_found(self):
|
||||
""" If the order is not found, the view should return a 404. """
|
||||
with mock_basket_order(basket_id=1, exception=exceptions.HttpNotFoundError):
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_login_required(self):
|
||||
""" The view should return 403 if the user is not logged in. """
|
||||
self.client.logout()
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
16
lms/djangoapps/commerce/api/v0/urls.py
Normal file
16
lms/djangoapps/commerce/api/v0/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
""" 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')),
|
||||
)
|
||||
159
lms/djangoapps/commerce/api/v0/views.py
Normal file
159
lms/djangoapps/commerce/api/v0/views.py
Normal file
@@ -0,0 +1,159 @@
|
||||
""" 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'),
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
""" Tests for commerce views. """
|
||||
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
import mock
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from ecommerce_api_client import exceptions
|
||||
from commerce.constants import Messages
|
||||
from commerce.tests import TEST_BASKET_ID, TEST_ORDER_NUMBER, TEST_PAYMENT_DATA, TEST_API_URL, TEST_API_SIGNING_KEY
|
||||
from commerce.tests.mocks import mock_basket_order, mock_create_basket
|
||||
from course_modes.models import CourseMode
|
||||
from embargo.test_utils import restrict_course
|
||||
from openedx.core.lib.django_test_client_utils import get_absolute_url
|
||||
from enrollment.api import get_enrollment
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory, CourseModeFactory
|
||||
from student.tests.tests import EnrollmentEventTestMixin
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
class UserMixin(object):
|
||||
@@ -38,313 +23,6 @@ class UserMixin(object):
|
||||
self.client.login(username=self.user.username, password='test')
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
@override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY)
|
||||
class BasketsViewTests(EnrollmentEventTestMixin, UserMixin, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the commerce orders view.
|
||||
"""
|
||||
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 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 assertResponsePaymentData(self, response):
|
||||
""" Asserts correctness of a JSON body containing payment information. """
|
||||
actual_response = json.loads(response.content)
|
||||
self.assertEqual(actual_response, TEST_PAYMENT_DATA)
|
||||
|
||||
def assertValidEcommerceInternalRequestErrorResponse(self, response):
|
||||
""" Asserts the response is a valid response sent when the E-Commerce API is unavailable. """
|
||||
self.assertEqual(response.status_code, 500)
|
||||
actual = json.loads(response.content)['detail']
|
||||
self.assertIn('Call to E-Commerce API failed', actual)
|
||||
|
||||
def assertUserNotEnrolled(self):
|
||||
""" Asserts that the user is NOT enrolled in the course, and that an enrollment event was NOT fired. """
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
self.assert_no_events_were_emitted()
|
||||
|
||||
def setUp(self):
|
||||
super(BasketsViewTests, self).setUp()
|
||||
self.url = reverse('commerce:baskets')
|
||||
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 [CourseMode.HONOR, CourseMode.VERIFIED, CourseMode.AUDIT]:
|
||||
CourseModeFactory.create(
|
||||
course_id=self.course.id,
|
||||
mode_slug=mode,
|
||||
mode_display_name=mode,
|
||||
sku=uuid4().hex.decode('ascii')
|
||||
)
|
||||
|
||||
# Ignore events fired from UserFactory creation
|
||||
self.reset_tracker()
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'EMBARGO': True})
|
||||
def test_embargo_restriction(self):
|
||||
"""
|
||||
The view should return HTTP 403 status if the course is embargoed.
|
||||
"""
|
||||
with restrict_course(self.course.id) as redirect_url:
|
||||
response = self._post_to_view()
|
||||
self.assertEqual(403, response.status_code)
|
||||
body = json.loads(response.content)
|
||||
self.assertEqual(get_absolute_url(redirect_url), body['user_message_url'])
|
||||
|
||||
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)
|
||||
|
||||
@ddt.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)
|
||||
|
||||
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.
|
||||
"""
|
||||
with mock_create_basket(exception=exceptions.Timeout):
|
||||
response = self._post_to_view()
|
||||
|
||||
self.assertValidEcommerceInternalRequestErrorResponse(response)
|
||||
self.assertUserNotEnrolled()
|
||||
|
||||
def test_ecommerce_api_error(self):
|
||||
"""
|
||||
If the E-Commerce API raises an error, the view should return an HTTP 503 status.
|
||||
"""
|
||||
with mock_create_basket(exception=exceptions.SlumberBaseException):
|
||||
response = self._post_to_view()
|
||||
|
||||
self.assertValidEcommerceInternalRequestErrorResponse(response)
|
||||
self.assertUserNotEnrolled()
|
||||
|
||||
def _test_successful_ecommerce_api_call(self, is_completed=True):
|
||||
"""
|
||||
Verifies that the view contacts the E-Commerce API with the correct data and headers.
|
||||
"""
|
||||
with mock.patch('commerce.views.audit_log') as mock_audit_log:
|
||||
response = self._post_to_view()
|
||||
|
||||
# Verify that an audit message was logged
|
||||
self.assertTrue(mock_audit_log.called)
|
||||
|
||||
# Validate the response content
|
||||
if is_completed:
|
||||
msg = Messages.ORDER_COMPLETED.format(order_number=TEST_ORDER_NUMBER)
|
||||
self.assertResponseMessage(response, msg)
|
||||
else:
|
||||
self.assertResponsePaymentData(response)
|
||||
|
||||
@ddt.data(True, False)
|
||||
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
|
||||
|
||||
return_value = {'id': TEST_BASKET_ID, 'payment_data': None, 'order': {'number': TEST_ORDER_NUMBER}}
|
||||
with mock_create_basket(response=return_value):
|
||||
self._test_successful_ecommerce_api_call()
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_course_with_paid_seat_sku(self, user_is_active):
|
||||
"""
|
||||
If the course has a SKU, the view should return data that the client
|
||||
will use to redirect the user to an external payment processor.
|
||||
"""
|
||||
# Set user's active flag
|
||||
self.user.is_active = user_is_active
|
||||
self.user.save() # pylint: disable=no-member
|
||||
|
||||
return_value = {'id': TEST_BASKET_ID, 'payment_data': TEST_PAYMENT_DATA, 'order': None}
|
||||
with mock_create_basket(response=return_value):
|
||||
self._test_successful_ecommerce_api_call(False)
|
||||
|
||||
def _test_course_without_sku(self):
|
||||
"""
|
||||
Validates the view bypasses the E-Commerce API when the course has no CourseModes with SKUs.
|
||||
"""
|
||||
# Place an order
|
||||
with mock_create_basket(expect_called=False):
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
self._test_course_without_sku()
|
||||
|
||||
@override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None)
|
||||
def test_ecommerce_service_not_configured(self):
|
||||
"""
|
||||
If the E-Commerce Service is not configured, the view should enroll the user.
|
||||
"""
|
||||
with mock_create_basket(expect_called=False):
|
||||
response = self._post_to_view()
|
||||
|
||||
# Validate the response
|
||||
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))
|
||||
|
||||
def assertProfessionalModeBypassed(self):
|
||||
""" Verifies that the view returns HTTP 406 when a course with no honor mode is encountered. """
|
||||
|
||||
CourseMode.objects.filter(course_id=self.course.id).delete()
|
||||
mode = CourseMode.NO_ID_PROFESSIONAL_MODE
|
||||
CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode,
|
||||
sku=uuid4().hex.decode('ascii'))
|
||||
|
||||
with mock_create_basket(expect_called=False):
|
||||
response = self._post_to_view()
|
||||
|
||||
# The view should return an error status code
|
||||
self.assertEqual(response.status_code, 406)
|
||||
msg = Messages.NO_HONOR_MODE.format(course_id=self.course.id)
|
||||
self.assertResponseMessage(response, msg)
|
||||
|
||||
def test_course_with_professional_mode_only(self):
|
||||
""" Verifies that the view behaves appropriately when the course only has a professional mode. """
|
||||
self.assertProfessionalModeBypassed()
|
||||
|
||||
@override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None)
|
||||
def test_professional_mode_only_and_ecommerce_service_not_configured(self):
|
||||
"""
|
||||
Verifies that the view behaves appropriately when the course only has a professional mode and
|
||||
the E-Commerce Service is not configured.
|
||||
"""
|
||||
self.assertProfessionalModeBypassed()
|
||||
|
||||
def test_empty_sku(self):
|
||||
""" If the CourseMode has an empty string for a SKU, the API should not be used. """
|
||||
# Set SKU to empty string for all modes.
|
||||
for course_mode in CourseMode.objects.filter(course_id=self.course.id):
|
||||
course_mode.sku = ''
|
||||
course_mode.save()
|
||||
|
||||
self._test_course_without_sku()
|
||||
|
||||
def test_existing_active_enrollment(self):
|
||||
""" The view should respond with HTTP 409 if the user has an existing active enrollment for the course. """
|
||||
|
||||
# Enroll user in the course
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
|
||||
response = self._post_to_view()
|
||||
self.assertEqual(response.status_code, 409)
|
||||
msg = Messages.ENROLLMENT_EXISTS.format(username=self.user.username, course_id=self.course.id)
|
||||
self.assertResponseMessage(response, msg)
|
||||
|
||||
def test_existing_inactive_enrollment(self):
|
||||
"""
|
||||
If the user has an inactive enrollment for the course, the view should behave as if the
|
||||
user has no enrollment.
|
||||
"""
|
||||
# Create an inactive enrollment
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
CourseEnrollment.unenroll(self.user, self.course.id, True)
|
||||
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
|
||||
self.assertIsNotNone(get_enrollment(self.user.username, unicode(self.course.id)))
|
||||
|
||||
with mock_create_basket():
|
||||
self._test_successful_ecommerce_api_call(False)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY)
|
||||
class BasketOrderViewTests(UserMixin, TestCase):
|
||||
""" Tests for the basket order view. """
|
||||
view_name = 'commerce:basket_order'
|
||||
MOCK_ORDER = {'number': 1}
|
||||
path = reverse(view_name, kwargs={'basket_id': 1})
|
||||
|
||||
def setUp(self):
|
||||
super(BasketOrderViewTests, self).setUp()
|
||||
self._login()
|
||||
|
||||
def test_order_found(self):
|
||||
""" If the order is located, the view should pass the data from the API. """
|
||||
|
||||
with mock_basket_order(basket_id=1, response=self.MOCK_ORDER):
|
||||
response = self.client.get(self.path)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
actual = json.loads(response.content)
|
||||
self.assertEqual(actual, self.MOCK_ORDER)
|
||||
|
||||
def test_order_not_found(self):
|
||||
""" If the order is not found, the view should return a 404. """
|
||||
with mock_basket_order(basket_id=1, exception=exceptions.HttpNotFoundError):
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_login_required(self):
|
||||
""" The view should return 403 if the user is not logged in. """
|
||||
self.client.logout()
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
class ReceiptViewTests(UserMixin, TestCase):
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
"""
|
||||
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. """
|
||||
@@ -205,18 +68,3 @@ def checkout_receipt(request):
|
||||
'payment_support_email': payment_support_email,
|
||||
}
|
||||
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: {
|
||||
|
||||
Reference in New Issue
Block a user