Commit d5dda647 by aamir-khan Committed by Awais

ECOM-1816: added the provider detail on the receipt page.

parent ba06ab19
...@@ -66,5 +66,6 @@ def checkout_receipt(request): ...@@ -66,5 +66,6 @@ def checkout_receipt(request):
'error_text': error_text, 'error_text': error_text,
'for_help_text': for_help_text, 'for_help_text': for_help_text,
'payment_support_email': payment_support_email, 'payment_support_email': payment_support_email,
'username': request.user.username
} }
return render_to_response('commerce/checkout_receipt.html', context) return render_to_response('commerce/checkout_receipt.html', context)
...@@ -13,7 +13,7 @@ var edx = edx || {}; ...@@ -13,7 +13,7 @@ var edx = edx || {};
initialize: function () { initialize: function () {
this.useEcommerceApi = !!($.url('?basket_id')); 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 /* Mix non-conflicting functions from underscore.string (all but include, contains, and reverse) into
* the Underscore namespace. * the Underscore namespace.
...@@ -28,17 +28,31 @@ var edx = edx || {}; ...@@ -28,17 +28,31 @@ var edx = edx || {};
context = { context = {
platformName: this.$el.data('platform-name'), platformName: this.$el.data('platform-name'),
verified: this.$el.data('verified').toLowerCase() === 'true' verified: this.$el.data('verified').toLowerCase() === 'true'
}; },
providerId;
// Add the receipt info to the template context // Add the receipt info to the template context
this.course_key = this.getOrderCourseKey(data)
this.username = this.$el.data('username');
_.extend(context, { _.extend(context, {
receipt: this.receiptContext(data), receipt: this.receiptContext(data),
courseKey: this.getOrderCourseKey(data) courseKey: this.course_key
}); });
this.$el.html(_.template(templateHtml, context)); this.$el.html(_.template(templateHtml, context));
this.trackLinks(); 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 () { renderError: function () {
...@@ -80,7 +94,7 @@ var edx = edx || {}; ...@@ -80,7 +94,7 @@ var edx = edx || {};
/** /**
* Retrieve receipt data from Oscar (via LMS). * Retrieve receipt data from Oscar (via LMS).
* @param {int} basketId The basket that was purchased. * @param {int} basketId The basket that was purchased.
* @return {object} JQuery Promise. * @return {object} JQuery Promise.
*/ */
getReceiptData: function (basketId) { getReceiptData: function (basketId) {
var urlFormat = this.useEcommerceApi ? '/api/commerce/v0/baskets/%s/order/' : '/shoppingcart/receipt/%s/'; var urlFormat = this.useEcommerceApi ? '/api/commerce/v0/baskets/%s/order/' : '/shoppingcart/receipt/%s/';
...@@ -91,13 +105,27 @@ var edx = edx || {}; ...@@ -91,13 +105,27 @@ var edx = edx || {};
dataType: 'json' dataType: 'json'
}).retry({times: 5, timeout: 2000, statusCodes: [404]}); }).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 * Construct the template context from data received
* from the E-Commerce API. * from the E-Commerce API.
* *
* @param {object} order Receipt data received from the server * @param {object} order Receipt data received from the server
* @return {object} Receipt template context. * @return {object} Receipt template context.
*/ */
receiptContext: function (order) { receiptContext: function (order) {
var self = this, var self = this,
...@@ -172,13 +200,13 @@ var edx = edx || {}; ...@@ -172,13 +200,13 @@ var edx = edx || {};
length = order.lines.length; length = order.lines.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
var line = order.lines[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' return attribute.name === 'course_key'
}); });
// This method assumes that all items in the order are related to a single course. // This method assumes that all items in the order are related to a single course.
if (attribute_values.length > 0) { if (attributeValues != undefined) {
return attribute_values[0]['value']; return attributeValues['value'];
} }
} }
} else { } else {
...@@ -196,7 +224,30 @@ var edx = edx || {}; ...@@ -196,7 +224,30 @@ var edx = edx || {};
formatMoney: function (moneyStr) { formatMoney: function (moneyStr) {
return Number(moneyStr).toFixed(2); 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({ new edx.commerce.ReceiptView({
...@@ -204,3 +255,54 @@ var edx = edx || {}; ...@@ -204,3 +255,54 @@ var edx = edx || {};
}); });
})(jQuery, _, _.str, Backbone); })(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 ) {
$('<input>').attr({
type: 'hidden',
name: key,
value: value
}).appendTo(form);
});
form.submit();
},
error: function(xhr){
errorContainer.removeClass("is-hidden");
errorContainer.removeClass("hidden");
}
});
}
...@@ -244,6 +244,42 @@ ...@@ -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);
}
}
// ==================== // ====================
......
...@@ -12,6 +12,9 @@ from django.utils.translation import ugettext as _ ...@@ -12,6 +12,9 @@ from django.utils.translation import ugettext as _
<script type="text/template" id="receipt-tpl"> <script type="text/template" id="receipt-tpl">
<%static:include path="commerce/receipt.underscore" /> <%static:include path="commerce/receipt.underscore" />
</script> </script>
<script type="text/template" id="provider-tpl">
<%static:include path="commerce/provider.underscore" />
</script>
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
...@@ -29,17 +32,18 @@ from django.utils.translation import ugettext as _ ...@@ -29,17 +32,18 @@ from django.utils.translation import ugettext as _
<div id="error" class="wrapper-msg wrapper-msg-activate"> <div id="error" class="wrapper-msg wrapper-msg-activate">
<div class=" msg msg-activate"> <div class=" msg msg-activate">
<i class="msg-icon icon fa fa-exclamation-triangle" aria-hidden="true"></i> <i class="msg-icon icon fa fa-exclamation-triangle" aria-hidden="true"></i>
<div class="msg-content"> <div class="msg-content">
<h3 class="title"> <h3 class="title">
<span class="sr">${error_summary}</span> <span class="sr">${error_summary}</span>
${error_summary} ${error_summary}
</h3> </h3>
%if error_text: %if error_text:
<div class="copy"> <div class="copy">
<p>${error_text}</p> <p>${error_text}</p>
<br/> <br/>
</div> </div>
%endif %endif
<div class="msg"> <div class="msg">
<p>${for_help_text}</p> <p>${for_help_text}</p>
</div> </div>
...@@ -50,10 +54,12 @@ from django.utils.translation import ugettext as _ ...@@ -50,10 +54,12 @@ from django.utils.translation import ugettext as _
<div class="container"> <div class="container">
<section class="wrapper carousel"> <section class="wrapper carousel">
<div id="receipt-container" class="pay-and-verify hidden" data-is-payment-complete='${is_payment_complete}' data-platform-name='${platform_name}' data-verified='${verified}'> <div id="receipt-container" class="pay-and-verify hidden" data-is-payment-complete='${is_payment_complete}'
data-platform-name='${platform_name}' data-verified='${verified}' data-username='${username}'>
<h1>${_("Loading Order Data...")}</h1> <h1>${_("Loading Order Data...")}</h1>
<span>${ _("Please wait while we retrieve your order details.") }</span> <span>${ _("Please wait while we retrieve your order details.") }</span>
</div> </div>
<form id="complete-order-form"></form>
</section> </section>
</div> </div>
</%block> </%block>
<p class="bold_param">
<%= interpolate(gettext("You still need to visit %s to complete the credit process."), [display_name]) %>
<span><%= interpolate("<img src='%s' alt='%s'></image>", ["", display_name]) %></span>
</p>
<p>
<%= interpolate(gettext("In order to learn credit, %s requires learner to:"), [display_name]) %>
</p>
<div class="custom_instructions">
<div>
<%= fulfillment_instructions %>
<%= interpolate('<button data-provider="%s" data-course-key="%s" data-username="%s" class="complete-course" onClick=completeOrder(this)>%s</button>', [provider_id, course_key, username, gettext( "Complete Order")]) %>
</div>
</div>
...@@ -73,6 +73,9 @@ ...@@ -73,6 +73,9 @@
</p> </p>
</div> </div>
<% } %> <% } %>
<div class="report report-receipt report-receipt-provider hidden" id="receipt-provider"></div>
</div> </div>
</div> </div>
<% } else { %> <% } else { %>
......
...@@ -8,6 +8,7 @@ import pytz ...@@ -8,6 +8,7 @@ import pytz
import uuid import uuid
from django.db import transaction from django.db import transaction
from lms.djangoapps.django_comment_client.utils import JsonResponse
from openedx.core.djangoapps.credit.exceptions import ( from openedx.core.djangoapps.credit.exceptions import (
UserIsNotEligible, UserIsNotEligible,
...@@ -39,7 +40,6 @@ def get_credit_providers(providers_list=None): ...@@ -39,7 +40,6 @@ def get_credit_providers(providers_list=None):
Returns: Returns:
list of credit providers represented as dictionaries list of credit providers represented as dictionaries
Response Values: Response Values:
>>> get_credit_providers(['hogwarts']) >>> get_credit_providers(['hogwarts'])
[ [
...@@ -60,10 +60,52 @@ def get_credit_providers(providers_list=None): ...@@ -60,10 +60,52 @@ def get_credit_providers(providers_list=None):
... ...
] ]
""" """
return CreditProvider.get_credit_providers(providers_list=providers_list) 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": "
<p>In order to fulfill credit, Hogwarts School of Witchcraft and Wizardry requires learners to:</p>
<ul>
<li>Sample instruction abc</li>
<li>Sample instruction xyz</li>
</ul>",
}
"""
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 @transaction.commit_on_success
def create_credit_request(course_key, provider_id, username): def create_credit_request(course_key, provider_id, username):
""" """
......
...@@ -3,11 +3,16 @@ Tests for the API functions in the credit app. ...@@ -3,11 +3,16 @@ Tests for the API functions in the credit app.
""" """
import datetime import datetime
import ddt import ddt
import json
from mock import patch
import pytz import pytz
import unittest
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.db import connection, transaction from django.db import connection, transaction
from django.core.urlresolvers import reverse, NoReverseMatch
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -33,6 +38,8 @@ from student.tests.factories import UserFactory ...@@ -33,6 +38,8 @@ from student.tests.factories import UserFactory
TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6" TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6"
from util.testing import UrlResetMixin
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={ @override_settings(CREDIT_PROVIDER_SECRET_KEYS={
"hogwarts": TEST_CREDIT_PROVIDER_SECRET_KEY, "hogwarts": TEST_CREDIT_PROVIDER_SECRET_KEY,
...@@ -691,3 +698,117 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase): ...@@ -691,3 +698,117 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
"""Check the user's credit status. """ """Check the user's credit status. """
statuses = api.get_credit_requests_for_user(self.USER_INFO["username"]) statuses = api.get_credit_requests_for_user(self.USER_INFO["username"])
self.assertEqual(statuses[0]["status"], expected_status) 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)
)
...@@ -3,6 +3,7 @@ URLs for the credit app. ...@@ -3,6 +3,7 @@ URLs for the credit app.
""" """
from django.conf.urls import patterns, url 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 from .views import create_credit_request, credit_provider_callback, get_providers_detail, get_eligibility_for_user
PROVIDER_ID_PATTERN = r'(?P<provider_id>[^/]+)' PROVIDER_ID_PATTERN = r'(?P<provider_id>[^/]+)'
...@@ -11,6 +12,11 @@ urlpatterns = patterns( ...@@ -11,6 +12,11 @@ urlpatterns = patterns(
'', '',
url( url(
r"^v1/providers/(?P<provider_id>[^/]+)/$",
get_credit_provider_info,
name="get_provider_info"
),
url(
r"^v1/providers/$", r"^v1/providers/$",
get_providers_detail, get_providers_detail,
name="providers_detail" name="providers_detail"
......
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