From 9bb3f703d7e74bc223e2ead671edc4991c9100a0 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Sat, 24 Oct 2015 18:19:37 -0400 Subject: [PATCH] Updated receipt page to use order endpoint The receipt page now retrieves data for orders instead of baskets. Going forward baskets will be deleted after an order has been placed, so there should be no permanent references to baskets. Orders will continue to be persisted permanently. ECOM-2653 --- lms/djangoapps/commerce/api/v0/urls.py | 2 +- .../commerce/api/v1/tests/test_views.py | 41 +++++++++++++++++++ lms/djangoapps/commerce/api/v1/urls.py | 8 +++- lms/djangoapps/commerce/api/v1/views.py | 21 +++++++++- lms/djangoapps/commerce/tests/mocks.py | 14 +++++++ lms/static/js/commerce/views/receipt_view.js | 23 ++++++++--- 6 files changed, 99 insertions(+), 10 deletions(-) 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]});