Merge pull request #8867 from edx/aamir-khan/ECOM-1816-credit-info-recept-page
ECOM-1816: added the provider detail on the receipt page.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 ) {
|
||||
$('<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 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.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 _
|
||||
<script type="text/template" id="receipt-tpl">
|
||||
<%static:include path="commerce/receipt.underscore" />
|
||||
</script>
|
||||
<script type="text/template" id="provider-tpl">
|
||||
<%static:include path="commerce/provider.underscore" />
|
||||
</script>
|
||||
|
||||
</%block>
|
||||
<%block name="js_extra">
|
||||
@@ -29,17 +32,18 @@ from django.utils.translation import ugettext as _
|
||||
<div id="error" class="wrapper-msg wrapper-msg-activate">
|
||||
<div class=" msg msg-activate">
|
||||
<i class="msg-icon icon fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
|
||||
<div class="msg-content">
|
||||
<h3 class="title">
|
||||
<span class="sr">${error_summary}</span>
|
||||
${error_summary}
|
||||
</h3>
|
||||
%if error_text:
|
||||
%if error_text:
|
||||
<div class="copy">
|
||||
<p>${error_text}</p>
|
||||
<br/>
|
||||
</div>
|
||||
%endif
|
||||
%endif
|
||||
<div class="msg">
|
||||
<p>${for_help_text}</p>
|
||||
</div>
|
||||
@@ -50,10 +54,12 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
<div class="container">
|
||||
<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>
|
||||
<span>${ _("Please wait while we retrieve your order details.") }</span>
|
||||
</div>
|
||||
<form id="complete-order-form"></form>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
|
||||
13
lms/templates/commerce/provider.underscore
Normal file
13
lms/templates/commerce/provider.underscore
Normal file
@@ -0,0 +1,13 @@
|
||||
<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 @@
|
||||
</p>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="report report-receipt report-receipt-provider hidden" id="receipt-provider"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
|
||||
@@ -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": "
|
||||
<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
|
||||
def create_credit_request(course_key, provider_id, username):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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<provider_id>[^/]+)'
|
||||
@@ -10,6 +11,11 @@ PROVIDER_ID_PATTERN = r'(?P<provider_id>[^/]+)'
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
|
||||
url(
|
||||
r"^v1/providers/(?P<provider_id>[^/]+)/$",
|
||||
get_credit_provider_info,
|
||||
name="get_provider_info"
|
||||
),
|
||||
url(
|
||||
r"^v1/providers/$",
|
||||
get_providers_detail,
|
||||
|
||||
Reference in New Issue
Block a user