From d5dda6470eb4237db22b97a5e847936d68690613 Mon Sep 17 00:00:00 2001 From: aamir-khan Date: Fri, 10 Jul 2015 01:19:35 +0500 Subject: [PATCH] ECOM-1816: added the provider detail on the receipt page. --- lms/djangoapps/commerce/views.py | 1 + lms/static/js/commerce/views/receipt_view.js | 120 +++++++++++++++-- lms/static/sass/views/_verification.scss | 36 ++++++ lms/templates/commerce/checkout_receipt.html | 12 +- lms/templates/commerce/provider.underscore | 13 ++ lms/templates/commerce/receipt.underscore | 3 + .../core/djangoapps/credit/api/provider.py | 46 ++++++- .../core/djangoapps/credit/tests/test_api.py | 121 ++++++++++++++++++ openedx/core/djangoapps/credit/urls.py | 6 + 9 files changed, 344 insertions(+), 14 deletions(-) create mode 100644 lms/templates/commerce/provider.underscore diff --git a/lms/djangoapps/commerce/views.py b/lms/djangoapps/commerce/views.py index f74038b8a2..9c50487833 100644 --- a/lms/djangoapps/commerce/views.py +++ b/lms/djangoapps/commerce/views.py @@ -66,5 +66,6 @@ def checkout_receipt(request): 'error_text': error_text, 'for_help_text': for_help_text, 'payment_support_email': payment_support_email, + 'username': request.user.username } return render_to_response('commerce/checkout_receipt.html', context) diff --git a/lms/static/js/commerce/views/receipt_view.js b/lms/static/js/commerce/views/receipt_view.js index 0c4067b42c..4dfd5728f8 100644 --- a/lms/static/js/commerce/views/receipt_view.js +++ b/lms/static/js/commerce/views/receipt_view.js @@ -13,7 +13,7 @@ var edx = edx || {}; initialize: function () { this.useEcommerceApi = !!($.url('?basket_id')); - _.bindAll(this, 'renderReceipt', 'renderError'); + _.bindAll(this, 'renderReceipt', 'renderError', 'getProviderData', 'renderProvider'); /* Mix non-conflicting functions from underscore.string (all but include, contains, and reverse) into * the Underscore namespace. @@ -28,17 +28,31 @@ var edx = edx || {}; context = { platformName: this.$el.data('platform-name'), verified: this.$el.data('verified').toLowerCase() === 'true' - }; + }, + providerId; // Add the receipt info to the template context + this.course_key = this.getOrderCourseKey(data) + this.username = this.$el.data('username'); _.extend(context, { receipt: this.receiptContext(data), - courseKey: this.getOrderCourseKey(data) + courseKey: this.course_key }); this.$el.html(_.template(templateHtml, context)); this.trackLinks(); + providerId = this.getCreditProviderId(data); + if (providerId) { + this.getProviderData(providerId).then(this.renderProvider, this.renderError) + } + }, + renderProvider: function (context) { + var templateHtml = $("#provider-tpl").html(), + providerDiv = this.$el.find("#receipt-provider"); + context.course_key = this.course_key; + context.username = this.username; + providerDiv.html(_.template(templateHtml, context)).removeClass('hidden'); }, renderError: function () { @@ -80,7 +94,7 @@ var edx = edx || {}; /** * Retrieve receipt data from Oscar (via LMS). * @param {int} basketId The basket that was purchased. - * @return {object} JQuery Promise. + * @return {object} JQuery Promise. */ getReceiptData: function (basketId) { var urlFormat = this.useEcommerceApi ? '/api/commerce/v0/baskets/%s/order/' : '/shoppingcart/receipt/%s/'; @@ -91,13 +105,27 @@ var edx = edx || {}; dataType: 'json' }).retry({times: 5, timeout: 2000, statusCodes: [404]}); }, + /** + * Retrieve credit provider data from LMS. + * @param {string} provider_id The provider_id of the credit provider. + * @return {object} JQuery Promise. + */ + getProviderData: function (providerId) { + var providerUrl = '/api/credit/v1/providers/%s/'; + + return $.ajax({ + url: _.sprintf(providerUrl, providerId), + type: 'GET', + dataType: 'json' + }).retry({times: 5, timeout: 2000, statusCodes: [404]}); + }, /** * Construct the template context from data received * from the E-Commerce API. * * @param {object} order Receipt data received from the server - * @return {object} Receipt template context. + * @return {object} Receipt template context. */ receiptContext: function (order) { var self = this, @@ -172,13 +200,13 @@ var edx = edx || {}; length = order.lines.length; for (var i = 0; i < length; i++) { var line = order.lines[i], - attribute_values = _.filter(line.product.attribute_values, function (attribute) { + attributeValues = _.find(line.product.attribute_values, function (attribute) { return attribute.name === 'course_key' }); // This method assumes that all items in the order are related to a single course. - if (attribute_values.length > 0) { - return attribute_values[0]['value']; + if (attributeValues != undefined) { + return attributeValues['value']; } } } else { @@ -196,7 +224,30 @@ var edx = edx || {}; formatMoney: function (moneyStr) { return Number(moneyStr).toFixed(2); - } + }, + + /** + * Check whether the payment is for the credit course or not. + * + * @param {object} order Receipt data received from the server + * @return {string} String of the provider_id or null. + */ + getCreditProviderId: function (order) { + var attributeValues, + line = order.lines[0]; + if (this.useEcommerceApi) { + attributeValues = _.find(line.product.attribute_values, function (attribute) { + return attribute.name === 'credit_provider' + }); + + // This method assumes that all items in the order are related to a single course. + if (attributeValues != undefined) { + return attributeValues['value']; + } + } + + return null; + }, }); new edx.commerce.ReceiptView({ @@ -204,3 +255,54 @@ var edx = edx || {}; }); })(jQuery, _, _.str, Backbone); + + +function completeOrder (event) { + var courseKey = $(event).data("course-key"), + username = $(event).data("username"), + providerId = $(event).data("provider"), + postData = { + 'course_key': courseKey, + 'username': username + }, + errorContainer = $("#error-container"); + + analytics.track( + "edx.bi.credit.clicked_complete_credit", + { + category: "credit", + label: courseKey + } + ); + + $.ajax({ + url: '/api/credit/v1/provider/' + providerId + '/request/', + type: 'POST', + headers: { + 'X-CSRFToken': $.cookie('csrftoken') + }, + data: JSON.stringify(postData) , + context: this, + success: function(requestData){ + var form = $('#complete-order-form'); + + $('input', form).remove(); + + form.attr( 'action', requestData.url ); + form.attr( 'method', 'POST' ); + + _.each( requestData.parameters, function( value, key ) { + $('').attr({ + type: 'hidden', + name: key, + value: value + }).appendTo(form); + }); + form.submit(); + }, + error: function(xhr){ + errorContainer.removeClass("is-hidden"); + errorContainer.removeClass("hidden"); + } + }); +} diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 03d939f3ca..49c434cd70 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -244,6 +244,42 @@ } } } + .report-receipt-provider { + @extend %ui-window; + border-top:3px solid $credit-color-base !important; + p { + padding: 0 $baseline $baseline/2 $baseline; + overflow: auto; + } + .bold_param { + padding: $baseline*.75 $baseline 0 $baseline; + font-weight: 600; + } + .bold_param span { + @include float(right); + } + div { + padding: 10px 20px; + margin: 0 0 15px; + overflow: auto; + } + div span { + @include float(right); + padding: 0; + margin: 0; + } + .complete-course { + @extend %btn-pl-primary-base; + @include float(right); + + &.archived { + @extend %btn-pl-default-base; + } + } + .custom_instructions div { + @include float(left); + } + } // ==================== diff --git a/lms/templates/commerce/checkout_receipt.html b/lms/templates/commerce/checkout_receipt.html index fc5bdf39cc..12ea45a9de 100644 --- a/lms/templates/commerce/checkout_receipt.html +++ b/lms/templates/commerce/checkout_receipt.html @@ -12,6 +12,9 @@ from django.utils.translation import ugettext as _ + <%block name="js_extra"> @@ -29,17 +32,18 @@ from django.utils.translation import ugettext as _
+

${error_summary} ${error_summary}

-%if error_text: + %if error_text:

${error_text}


-%endif + %endif

${for_help_text}

@@ -50,10 +54,12 @@ from django.utils.translation import ugettext as _
diff --git a/lms/templates/commerce/provider.underscore b/lms/templates/commerce/provider.underscore new file mode 100644 index 0000000000..5c8017e74d --- /dev/null +++ b/lms/templates/commerce/provider.underscore @@ -0,0 +1,13 @@ +

+ <%= interpolate(gettext("You still need to visit %s to complete the credit process."), [display_name]) %> + <%= interpolate("%s", ["", display_name]) %> +

+

+ <%= interpolate(gettext("In order to learn credit, %s requires learner to:"), [display_name]) %> +

+
+
+ <%= fulfillment_instructions %> + <%= interpolate('', [provider_id, course_key, username, gettext( "Complete Order")]) %> +
+
diff --git a/lms/templates/commerce/receipt.underscore b/lms/templates/commerce/receipt.underscore index 5abccd313c..a660efc9d0 100644 --- a/lms/templates/commerce/receipt.underscore +++ b/lms/templates/commerce/receipt.underscore @@ -73,6 +73,9 @@

<% } %> + + +
<% } else { %> diff --git a/openedx/core/djangoapps/credit/api/provider.py b/openedx/core/djangoapps/credit/api/provider.py index 9b6cd8532e..00aa1c680e 100644 --- a/openedx/core/djangoapps/credit/api/provider.py +++ b/openedx/core/djangoapps/credit/api/provider.py @@ -8,6 +8,7 @@ import pytz import uuid from django.db import transaction +from lms.djangoapps.django_comment_client.utils import JsonResponse from openedx.core.djangoapps.credit.exceptions import ( UserIsNotEligible, @@ -39,7 +40,6 @@ def get_credit_providers(providers_list=None): Returns: list of credit providers represented as dictionaries - Response Values: >>> get_credit_providers(['hogwarts']) [ @@ -60,10 +60,52 @@ def get_credit_providers(providers_list=None): ... ] """ - return CreditProvider.get_credit_providers(providers_list=providers_list) +def get_credit_provider_info(request, provider_id): # pylint: disable=unused-argument + """Retrieve the 'CreditProvider' model data against provided + credit provider. + + Args: + provider_id (str): The identifier for the credit provider + + Returns: 'CreditProvider' data dictionary + + Example Usage: + >>> get_credit_provider_info("hogwarts") + { + "provider_id": "hogwarts", + "display_name": "Hogwarts School of Witchcraft and Wizardry", + "provider_url": "https://credit.example.com/", + "provider_status_url": "https://credit.example.com/status/", + "provider_description: "A new model for the Witchcraft and Wizardry School System.", + "enable_integration": False, + "fulfillment_instructions": " +

In order to fulfill credit, Hogwarts School of Witchcraft and Wizardry requires learners to:

+ ", + } + + """ + credit_provider = CreditProvider.get_credit_provider(provider_id=provider_id) + credit_provider_data = {} + if credit_provider: + credit_provider_data = { + "provider_id": credit_provider.provider_id, + "display_name": credit_provider.display_name, + "provider_url": credit_provider.provider_url, + "provider_status_url": credit_provider.provider_status_url, + "provider_description": credit_provider.provider_description, + "enable_integration": credit_provider.enable_integration, + "fulfillment_instructions": credit_provider.fulfillment_instructions + } + + return JsonResponse(credit_provider_data) + + @transaction.commit_on_success def create_credit_request(course_key, provider_id, username): """ diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 40db25e66c..7a2c7f6e8a 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -3,11 +3,16 @@ Tests for the API functions in the credit app. """ import datetime import ddt +import json +from mock import patch import pytz +import unittest +from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from django.db import connection, transaction +from django.core.urlresolvers import reverse, NoReverseMatch from opaque_keys.edx.keys import CourseKey @@ -33,6 +38,8 @@ from student.tests.factories import UserFactory TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6" +from util.testing import UrlResetMixin + @override_settings(CREDIT_PROVIDER_SECRET_KEYS={ "hogwarts": TEST_CREDIT_PROVIDER_SECRET_KEY, @@ -691,3 +698,117 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase): """Check the user's credit status. """ statuses = api.get_credit_requests_for_user(self.USER_INFO["username"]) self.assertEqual(statuses[0]["status"], expected_status) + + +class CreditApiFeatureFlagTest(UrlResetMixin, TestCase): + """ + Base class to test the credit api urls. + """ + def setUp(self, **kwargs): + enable_credit_api = kwargs.get('enable_credit_api', False) + with patch.dict('django.conf.settings.FEATURES', {'ENABLE_CREDIT_API': enable_credit_api}): + super(CreditApiFeatureFlagTest, self).setUp('lms.urls') + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class CreditApiFeatureFlagDisabledTests(CreditApiFeatureFlagTest): + """ + Test Python API for credit provider api with feature flag + 'ENABLE_CREDIT_API' disabled. + """ + PROVIDER_ID = "hogwarts" + + def setUp(self): + super(CreditApiFeatureFlagDisabledTests, self).setUp(enable_credit_api=False) + + def test_get_credit_provider_details(self): + """ + Test that 'get_provider_info' api url not found. + """ + with self.assertRaises(NoReverseMatch): + reverse('credit:get_provider_info', args=[self.PROVIDER_ID]) + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class CreditApiFeatureFlagEnabledTests(CreditApiFeatureFlagTest, CreditApiTestBase): + """ + Test Python API for credit provider api with feature flag + 'ENABLE_CREDIT_API' enabled. + """ + USER_INFO = { + "username": "bob", + "email": "bob@example.com", + "full_name": "Bob", + "mailing_address": "123 Fake Street, Cambridge MA", + "country": "US", + } + + FINAL_GRADE = 0.95 + + def setUp(self): + super(CreditApiFeatureFlagEnabledTests, self).setUp(enable_credit_api=True) + self.user = UserFactory( + username=self.USER_INFO['username'], + email=self.USER_INFO['email'], + ) + + self.user.profile.name = self.USER_INFO['full_name'] + self.user.profile.mailing_address = self.USER_INFO['mailing_address'] + self.user.profile.country = self.USER_INFO['country'] + self.user.profile.save() + + # By default, configure the database so that there is a single + # credit requirement that the user has satisfied (minimum grade) + self._configure_credit() + + def test_get_credit_provider_details(self): + """Test that credit api method 'test_get_credit_provider_details' + returns dictionary data related to provided credit provider. + """ + expected_result = { + "provider_id": self.PROVIDER_ID, + "display_name": self.PROVIDER_NAME, + "provider_url": self.PROVIDER_URL, + "provider_status_url": self.PROVIDER_STATUS_URL, + "provider_description": self.PROVIDER_DESCRIPTION, + "enable_integration": self.ENABLE_INTEGRATION, + "fulfillment_instructions": self.FULFILLMENT_INSTRUCTIONS, + } + path = reverse('credit:get_provider_info', kwargs={'provider_id': self.PROVIDER_ID}) + result = self.client.get(path) + result = json.loads(result.content) + self.assertEqual(result, expected_result) + + # now test that user gets empty dict for non existent credit provider + path = reverse('credit:get_provider_info', kwargs={'provider_id': 'fake_provider_id'}) + result = self.client.get(path) + result = json.loads(result.content) + self.assertEqual(result, {}) + + def _configure_credit(self): + """ + Configure a credit course and its requirements. + + By default, add a single requirement (minimum grade) + that the user has satisfied. + + """ + credit_course = self.add_credit_course() + requirement = CreditRequirement.objects.create( + course=credit_course, + namespace="grade", + name="grade", + active=True + ) + status = CreditRequirementStatus.objects.create( + username=self.USER_INFO["username"], + requirement=requirement, + ) + status.status = "satisfied" + status.reason = {"final_grade": self.FINAL_GRADE} + status.save() + + CreditEligibility.objects.create( + username=self.USER_INFO['username'], + course=CreditCourse.objects.get(course_key=self.course_key) + ) diff --git a/openedx/core/djangoapps/credit/urls.py b/openedx/core/djangoapps/credit/urls.py index 1c02e9e916..01efc405fe 100644 --- a/openedx/core/djangoapps/credit/urls.py +++ b/openedx/core/djangoapps/credit/urls.py @@ -3,6 +3,7 @@ URLs for the credit app. """ from django.conf.urls import patterns, url +from .api.provider import get_credit_provider_info from .views import create_credit_request, credit_provider_callback, get_providers_detail, get_eligibility_for_user PROVIDER_ID_PATTERN = r'(?P[^/]+)' @@ -10,6 +11,11 @@ PROVIDER_ID_PATTERN = r'(?P[^/]+)' urlpatterns = patterns( '', + url( + r"^v1/providers/(?P[^/]+)/$", + get_credit_provider_info, + name="get_provider_info" + ), url( r"^v1/providers/$", get_providers_detail,