diff --git a/lms/djangoapps/commerce/api/v0/urls.py b/lms/djangoapps/commerce/api/v0/urls.py index e802738db9..8d0765085a 100644 --- a/lms/djangoapps/commerce/api/v0/urls.py +++ b/lms/djangoapps/commerce/api/v0/urls.py @@ -7,7 +7,7 @@ from commerce.api.v0 import views BASKET_URLS = patterns( '', url(r'^$', views.BasketsView.as_view(), name='create'), - url(r'^{}/order/$'.format(r'(?P[\w]+)'), views.BasketOrderView.as_view(), name='retrieve_order'), + url(r'^(?P[\w]+)/order/$', views.BasketOrderView.as_view(), name='retrieve_order'), ) urlpatterns = patterns( diff --git a/lms/djangoapps/commerce/api/v1/tests/test_views.py b/lms/djangoapps/commerce/api/v1/tests/test_views.py index e0c33563d3..d86ddad5b2 100644 --- a/lms/djangoapps/commerce/api/v1/tests/test_views.py +++ b/lms/djangoapps/commerce/api/v1/tests/test_views.py @@ -7,13 +7,19 @@ import ddt from django.conf import settings from django.contrib.auth.models import Permission from django.core.urlresolvers import reverse +from django.test import TestCase from django.test.utils import override_settings +from edx_rest_api_client import exceptions from flaky import flaky +from nose.plugins.attrib import attr import pytz from rest_framework.utils.encoders import JSONEncoder from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory +from commerce.tests import TEST_API_URL, TEST_API_SIGNING_KEY +from commerce.tests.mocks import mock_order_endpoint +from commerce.tests.test_views import UserMixin from course_modes.models import CourseMode from student.tests.factories import UserFactory from verify_student.models import VerificationDeadline @@ -307,3 +313,38 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) ] } self.assertDictEqual(expected_dict, json.loads(response.content)) + + +@attr('shard_1') +@override_settings(ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY) +class OrderViewTests(UserMixin, TestCase): + """ Tests for the basket order view. """ + view_name = 'commerce_api:v1:orders:detail' + ORDER_NUMBER = 'EDX-100001' + MOCK_ORDER = {'number': ORDER_NUMBER} + path = reverse(view_name, kwargs={'number': ORDER_NUMBER}) + + def setUp(self): + super(OrderViewTests, 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_order_endpoint(order_number=self.ORDER_NUMBER, 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_order_endpoint(order_number=self.ORDER_NUMBER, 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) diff --git a/lms/djangoapps/commerce/api/v1/urls.py b/lms/djangoapps/commerce/api/v1/urls.py index b62dc4947b..d4ed3a7def 100644 --- a/lms/djangoapps/commerce/api/v1/urls.py +++ b/lms/djangoapps/commerce/api/v1/urls.py @@ -4,15 +4,19 @@ 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'), url(r'^{}/$'.format(settings.COURSE_ID_PATTERN), views.CourseRetrieveUpdateView.as_view(), name='retrieve_update'), ) +ORDER_URLS = patterns( + '', + url(r'^(?P[-\w]+)/$', views.OrderView.as_view(), name='detail'), +) + urlpatterns = patterns( '', url(r'^courses/', include(COURSE_URLS, namespace='courses')), - + url(r'^orders/', include(ORDER_URLS, namespace='orders')), ) diff --git a/lms/djangoapps/commerce/api/v1/views.py b/lms/djangoapps/commerce/api/v1/views.py index 1c1b32ccad..d57ae1fe2f 100644 --- a/lms/djangoapps/commerce/api/v1/views.py +++ b/lms/djangoapps/commerce/api/v1/views.py @@ -2,16 +2,20 @@ import logging from django.http import Http404 +from edx_rest_api_client import exceptions from rest_framework.authentication import SessionAuthentication -from rest_framework_oauth.authentication import OAuth2Authentication +from rest_framework.views import APIView from rest_framework.generics import RetrieveUpdateAPIView, ListAPIView from rest_framework.permissions import IsAuthenticated +from rest_framework_oauth.authentication import OAuth2Authentication +from commerce import ecommerce_api_client from commerce.api.v1.models import Course from commerce.api.v1.permissions import ApiKeyOrModelPermission from commerce.api.v1.serializers import CourseSerializer from course_modes.models import CourseMode from openedx.core.lib.api.mixins import PutAsCreateMixin +from util.json_request import JsonResponse log = logging.getLogger(__name__) @@ -54,3 +58,18 @@ class CourseRetrieveUpdateView(PutAsCreateMixin, RetrieveUpdateAPIView): # There is nothing to pre-save. The default behavior changes the Course.id attribute from # a CourseKey to a string, which is not desired. pass + + +class OrderView(APIView): + """ Retrieve order details. """ + + authentication_classes = (SessionAuthentication,) + permission_classes = (IsAuthenticated,) + + def get(self, request, number): # pylint:disable=unused-argument + """ HTTP handler. """ + try: + order = ecommerce_api_client(request.user).orders(number).get() + return JsonResponse(order) + except exceptions.HttpNotFoundError: + return JsonResponse(status=404) diff --git a/lms/djangoapps/commerce/tests/mocks.py b/lms/djangoapps/commerce/tests/mocks.py index 1eee94b056..983bff21fd 100644 --- a/lms/djangoapps/commerce/tests/mocks.py +++ b/lms/djangoapps/commerce/tests/mocks.py @@ -103,3 +103,17 @@ class mock_create_refund(mock_ecommerce_api_endpoint): # pylint: disable=invali def get_uri(self): return TEST_API_URL + '/refunds/' + + +class mock_order_endpoint(mock_ecommerce_api_endpoint): # pylint: disable=invalid-name + """ Mocks calls to E-Commerce API client basket order method. """ + + default_response = {'number': 'EDX-100001'} + method = httpretty.GET + + def __init__(self, order_number, **kwargs): + super(mock_order_endpoint, self).__init__(**kwargs) + self.order_number = order_number + + def get_uri(self): + return TEST_API_URL + '/orders/{}/'.format(self.order_number) diff --git a/lms/static/js/commerce/views/receipt_view.js b/lms/static/js/commerce/views/receipt_view.js index 5eeaa38487..8e0a5b9e62 100644 --- a/lms/static/js/commerce/views/receipt_view.js +++ b/lms/static/js/commerce/views/receipt_view.js @@ -10,9 +10,13 @@ var edx = edx || {}; edx.commerce.ReceiptView = Backbone.View.extend({ useEcommerceApi: true, + ecommerceBasketId: null, + ecommerceOrderNumber: null, initialize: function () { - this.useEcommerceApi = !!($.url('?basket_id')); + this.ecommerceBasketId = $.url('?basket_id'); + this.ecommerceOrderNumber = $.url('?orderNum'); + this.useEcommerceApi = this.ecommerceBasketId || this.ecommerceOrderNumber; _.bindAll(this, 'renderReceipt', 'renderError', 'getProviderData', 'renderProvider', 'getCourseData'); /* Mix non-conflicting functions from underscore.string (all but include, contains, and reverse) into @@ -75,7 +79,7 @@ var edx = edx || {}; render: function () { var self = this, - orderId = $.url('?basket_id') || $.url('?payment-order-num'); + orderId = this.ecommerceOrderNumber || this.ecommerceBasketId || $.url('?payment-order-num'); if (orderId && this.$el.data('is-payment-complete') === 'True') { // Get the order details @@ -106,14 +110,21 @@ var edx = edx || {}; /** * Retrieve receipt data from Oscar (via LMS). - * @param {int} basketId The basket that was purchased. + * @param {string} orderId Identifier of the order that was purchased. * @return {object} JQuery Promise. */ - getReceiptData: function (basketId) { - var urlFormat = this.useEcommerceApi ? '/api/commerce/v0/baskets/%s/order/' : '/shoppingcart/receipt/%s/'; + getReceiptData: function (orderId) { + var urlFormat = '/shoppingcart/receipt/%s/'; + + if (this.ecommerceOrderNumber) { + urlFormat = '/api/commerce/v1/orders/%s/'; + } else if (this.ecommerceBasketId){ + urlFormat = '/api/commerce/v0/baskets/%s/order/'; + } + return $.ajax({ - url: _.sprintf(urlFormat, basketId), + url: _.sprintf(urlFormat, orderId), type: 'GET', dataType: 'json' }).retry({times: 5, timeout: 2000, statusCodes: [404]});