Merged release to master
Resolved conflicts from hotfix-2015-10-05
This commit is contained in:
@@ -86,7 +86,8 @@ class CreditCourseDashboardTest(ModuleStoreTestCase):
|
||||
def test_not_eligible_for_credit(self):
|
||||
# The user is not yet eligible for credit, so no additional information should be displayed on the dashboard.
|
||||
response = self._load_dashboard()
|
||||
self.assertNotContains(response, "credit")
|
||||
self.assertNotContains(response, "credit-eligibility-msg")
|
||||
self.assertNotContains(response, "purchase-credit-btn")
|
||||
|
||||
def test_eligible_for_credit(self):
|
||||
# Simulate that the user has completed the only requirement in the course
|
||||
|
||||
@@ -400,7 +400,7 @@ FEATURES = {
|
||||
'ENABLE_OPENBADGES': False,
|
||||
|
||||
# Credit course API
|
||||
'ENABLE_CREDIT_API': False,
|
||||
'ENABLE_CREDIT_API': True,
|
||||
|
||||
# The block types to disable need to be specified in "x block disable config" in django admin.
|
||||
'ENABLE_DISABLING_XBLOCK_TYPES': True,
|
||||
@@ -2124,7 +2124,7 @@ if FEATURES.get('CLASS_DASHBOARD'):
|
||||
INSTALLED_APPS += ('class_dashboard',)
|
||||
|
||||
################ Enable credit eligibility feature ####################
|
||||
ENABLE_CREDIT_ELIGIBILITY = False
|
||||
ENABLE_CREDIT_ELIGIBILITY = True
|
||||
FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY
|
||||
|
||||
######################## CAS authentication ###########################
|
||||
|
||||
42
lms/static/js/commerce/credit.js
Normal file
42
lms/static/js/commerce/credit.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Credit-related utilities
|
||||
*/
|
||||
var edx = edx || {};
|
||||
|
||||
(function ($, _) {
|
||||
'use strict';
|
||||
|
||||
edx.commerce = edx.commerce || {};
|
||||
edx.commerce.credit = edx.commerce.credit || {};
|
||||
|
||||
edx.commerce.credit.createCreditRequest = function (providerId, courseKey, username) {
|
||||
return $.ajax({
|
||||
url: '/api/credit/v1/providers/' + providerId + '/request/',
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': $.cookie('csrftoken')
|
||||
},
|
||||
data: JSON.stringify({
|
||||
'course_key': courseKey,
|
||||
'username': username
|
||||
}),
|
||||
context: this,
|
||||
success: function (requestData) {
|
||||
var $form = $('<form>', {
|
||||
'action': requestData.url,
|
||||
'method': 'POST',
|
||||
'accept-method': 'UTF-8'
|
||||
});
|
||||
|
||||
_.each(requestData.parameters, function (value, key) {
|
||||
$('<textarea>').attr({
|
||||
name: key,
|
||||
value: value
|
||||
}).appendTo($form);
|
||||
});
|
||||
|
||||
$form.submit();
|
||||
}
|
||||
});
|
||||
};
|
||||
})(jQuery, _);
|
||||
@@ -50,12 +50,12 @@ var edx = edx || {};
|
||||
this.getProviderData(providerId).then(this.renderProvider, this.renderError)
|
||||
}
|
||||
},
|
||||
renderCourseNamePlaceholder: function(courseId) {
|
||||
renderCourseNamePlaceholder: function (courseId) {
|
||||
// Display the course Id or name (if available) in the placeholder
|
||||
var $courseNamePlaceholder = $(".course_name_placeholder");
|
||||
$courseNamePlaceholder.text(courseId);
|
||||
|
||||
this.getCourseData(courseId).then(function(responseData) {
|
||||
this.getCourseData(courseId).then(function (responseData) {
|
||||
$courseNamePlaceholder.text(responseData.name);
|
||||
});
|
||||
},
|
||||
@@ -77,7 +77,7 @@ var edx = edx || {};
|
||||
var self = this,
|
||||
orderId = $.url('?basket_id') || $.url('?payment-order-num');
|
||||
|
||||
if (orderId && this.$el.data('is-payment-complete')==='True') {
|
||||
if (orderId && this.$el.data('is-payment-complete') === 'True') {
|
||||
// Get the order details
|
||||
self.$el.removeClass('hidden');
|
||||
self.getReceiptData(orderId).then(self.renderReceipt, self.renderError);
|
||||
@@ -168,7 +168,7 @@ var edx = edx || {};
|
||||
billedTo: null
|
||||
};
|
||||
|
||||
if (order.billing_address){
|
||||
if (order.billing_address) {
|
||||
receiptContext.billedTo = {
|
||||
firstName: order.billing_address.first_name,
|
||||
lastName: order.billing_address.last_name,
|
||||
@@ -263,8 +263,8 @@ var edx = edx || {};
|
||||
line = order.lines[0];
|
||||
if (this.useEcommerceApi) {
|
||||
attributeValues = _.find(line.product.attribute_values, function (attribute) {
|
||||
return attribute.name === 'credit_provider'
|
||||
});
|
||||
return attribute.name === 'credit_provider';
|
||||
});
|
||||
|
||||
// This method assumes that all items in the order are related to a single course.
|
||||
if (attributeValues != undefined) {
|
||||
@@ -273,7 +273,7 @@ var edx = edx || {};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
new edx.commerce.ReceiptView({
|
||||
@@ -282,16 +282,11 @@ var edx = edx || {};
|
||||
|
||||
})(jQuery, _, _.str, Backbone);
|
||||
|
||||
|
||||
function completeOrder (event) {
|
||||
function completeOrder(event) { // jshint ignore:line
|
||||
var courseKey = $(event).data("course-key"),
|
||||
username = $(event).data("username"),
|
||||
providerId = $(event).data("provider"),
|
||||
postData = {
|
||||
'course_key': courseKey,
|
||||
'username': username
|
||||
},
|
||||
errorContainer = $("#error-container");
|
||||
$errorContainer = $("#error-container");
|
||||
|
||||
analytics.track(
|
||||
"edx.bi.credit.clicked_complete_credit",
|
||||
@@ -301,34 +296,7 @@ function completeOrder (event) {
|
||||
}
|
||||
);
|
||||
|
||||
$.ajax({
|
||||
url: '/api/credit/v1/providers/' + 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");
|
||||
}
|
||||
edx.commerce.credit.createCreditRequest(providerId, courseKey, username).fail(function () {
|
||||
$errorContainer.removeClass("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
(function($, analytics) {
|
||||
/**
|
||||
* Student dashboard credit messaging.
|
||||
*/
|
||||
|
||||
var edx = edx || {};
|
||||
|
||||
(function ($, analytics) {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
var errorContainer = $(".credit-error-msg"),
|
||||
creditStatusError = errorContainer.data("credit-error");
|
||||
$(document).ready(function () {
|
||||
var $errorContainer = $(".credit-error-msg"),
|
||||
creditStatusError = $errorContainer.data("credit-error");
|
||||
|
||||
if (creditStatusError == "True"){
|
||||
errorContainer.toggleClass("is-hidden");
|
||||
if (creditStatusError === "True") {
|
||||
$errorContainer.toggleClass("is-hidden");
|
||||
}
|
||||
|
||||
// Fire analytics events when the "purchase credit" button is clicked
|
||||
$(".purchase-credit-btn").on("click", function(event) {
|
||||
$(".purchase-credit-btn").on("click", function (event) {
|
||||
var courseKey = $(event.target).data("course-key");
|
||||
analytics.track(
|
||||
"edx.bi.credit.clicked_purchase_credit",
|
||||
@@ -20,46 +26,19 @@
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// This event invokes credit request endpoint. It will initiate
|
||||
// a credit request for the credit course for the provided user.
|
||||
$(".pending-credit-btn").on("click", function(event){
|
||||
var courseKey = $(event.target).data("course-key"),
|
||||
username = $(event.target).data("user"),
|
||||
provider_id = $(event.target).data("provider"),
|
||||
postData = {
|
||||
'course_key': courseKey,
|
||||
'username': username
|
||||
};
|
||||
$.ajax({
|
||||
url: 'api/credit/v1/providers/' + provider_id + '/request/',
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': $.cookie('csrftoken')
|
||||
},
|
||||
data: JSON.stringify(postData) ,
|
||||
context: this,
|
||||
success: function(requestData){
|
||||
var form = $('#credit-pending-form');
|
||||
$(".pending-credit-btn").on("click", function (event) {
|
||||
var $target = $(event.target),
|
||||
courseKey = $target.data("course-key"),
|
||||
username = $target.data("user"),
|
||||
providerId = $target.data("provider");
|
||||
|
||||
$('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){
|
||||
$(".credit-request-pending-msg").hide("is-hidden");
|
||||
$(".pending-credit-btn").hide();
|
||||
errorContainer.toggleClass("is-hidden");
|
||||
}
|
||||
edx.commerce.credit.createCreditRequest(providerId, courseKey, username).fail(function () {
|
||||
$(".credit-action").hide();
|
||||
$errorContainer.toggleClass("is-hidden");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -548,24 +548,6 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.credit-btn{
|
||||
@include float(right);
|
||||
}
|
||||
|
||||
.denied-credit-btn{
|
||||
@include float(right);
|
||||
}
|
||||
.credit-request-pending-msg{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.credit-btn {
|
||||
@extend %btn-pl-yellow-base;
|
||||
background-image: none ;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.message {
|
||||
@extend %ui-depth1;
|
||||
border-radius: 3px;
|
||||
@@ -750,19 +732,21 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.credit-eligibility-msg {
|
||||
@include float(left);
|
||||
width: flex-grid(10, 12);
|
||||
}
|
||||
|
||||
.credit-request-pending-msg {
|
||||
@include float(left);
|
||||
margin-top: 10px;
|
||||
}
|
||||
.credit-action {
|
||||
.credit-msg {
|
||||
@include float(left);
|
||||
width: flex-grid(9, 12);
|
||||
}
|
||||
|
||||
.credit-request-approved-msg{
|
||||
width: flex-grid(10, 12);
|
||||
@include float(left);
|
||||
.credit-btn {
|
||||
@extend %btn-pl-yellow-base;
|
||||
@include float(right);
|
||||
background-image: none ;
|
||||
text-shadow: none;
|
||||
box-shadow: none;
|
||||
text-transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
||||
@@ -21,6 +21,7 @@ from django.utils.translation import ugettext as _
|
||||
<script src="${static.url('js/vendor/jquery.ajax-retry.js')}"></script>
|
||||
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
|
||||
<script src="${static.url('js/src/tooltip_manager.js')}"></script>
|
||||
<script src="${static.url('js/commerce/credit.js')}"></script>
|
||||
<script src="${static.url('js/commerce/views/receipt_view.js')}"></script>
|
||||
</%block>
|
||||
|
||||
@@ -56,7 +57,6 @@ from django.utils.translation import ugettext as _
|
||||
<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>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<%= interpolate("<img src='%s' alt='%s'></image>", [thumbnail_url, display_name]) %>
|
||||
</div>
|
||||
<div class="complete-order">
|
||||
<%= 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")]) %>
|
||||
<%= 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( "Get Credit")]) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,7 @@ import json
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script src="${static.url('js/commerce/credit.js')}"></script>
|
||||
<%static:js group='dashboard'/>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
|
||||
@@ -19,57 +19,47 @@
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
% if not credit_status["purchased"] and not credit_status["error"] :
|
||||
<p class="message-copy credit-eligibility-msg">
|
||||
${_("You have completed this course and are eligible to purchase course credit. Select <b>Learn More</b> to get started.")}
|
||||
<div class="credit-action">
|
||||
% if not credit_status["purchased"] and not credit_status["error"] :
|
||||
<p class="message-copy credit-msg credit-eligibility-msg">
|
||||
## Translators: provider_name is the name of a credit provider or university (e.g. State University)
|
||||
${_("You are now eligible to purchase course credit from {provider_name} for this course. Click <strong>Get Credit</strong> to get started.").format(
|
||||
provider_name=credit_status["provider_name"],
|
||||
)}
|
||||
</p>
|
||||
<div class="purchase_credit">
|
||||
<a class="btn credit-btn purchase-credit-btn" href="${settings.ECOMMERCE_PUBLIC_URL_ROOT}/credit/checkout/${credit_status['course_key']}" target="_blank" data-course-key="${credit_status['course_key']}">${_("Get Credit")}</a>
|
||||
</div>
|
||||
% elif credit_status["request_status"] in [None, "pending"] and not credit_status["error"]:
|
||||
<p class="message-copy credit-msg credit-request-pending-msg">
|
||||
## Translators: provider_name is the name of a credit provider or university (e.g. State University)
|
||||
${_("Thank you for your payment. To receive course credit, you must now request credit at the {provider_name} website.").format(
|
||||
provider_name=credit_status["provider_name"],
|
||||
)
|
||||
}
|
||||
</p>
|
||||
<button class="btn credit-btn pending-credit-btn" data-course-key="${credit_status['course_key']}" data-user="${user.username}" data-provider="${credit_status['provider_id']}">${_("Finalize Credit")}</button>
|
||||
|
||||
</p>
|
||||
<div class="purchase_credit">
|
||||
<a class="btn credit-btn purchase-credit-btn" href="${settings.ECOMMERCE_PUBLIC_URL_ROOT}/credit/checkout/${credit_status['course_key']}" target="_blank" data-course-key="${credit_status['course_key']}">${_("Learn More")}</a>
|
||||
</div>
|
||||
% elif credit_status["request_status"] in [None, "pending"] and not credit_status["error"] :
|
||||
% if credit_status["request_status"] == "pending":
|
||||
<p class="message-copy credit-request-pending-msg">
|
||||
## Translators: provider_name is the name of a credit provider, such as 'State University' or 'Happy Fun Company'.
|
||||
${_("{provider_name} has received your course credit request. We will update you when credit processing is complete.").format(
|
||||
% elif credit_status["request_status"] == "approved" and not credit_status["error"] :
|
||||
<p class="message-copy credit-msg credit-request-approved-msg">
|
||||
## Translators: provider_name is the name of a credit provider or university (e.g. State University)
|
||||
${_("<strong>Congratulations!</strong> {provider_name} has converted your course credit. To see your course credit, click <strong>Access Credit</strong>.").format(
|
||||
provider_name=credit_status["provider_name"],
|
||||
)
|
||||
}
|
||||
</p>
|
||||
% elif credit_status["request_status"] is None:
|
||||
<p class="message-copy credit-request-pending-msg">
|
||||
<a class="btn credit-btn access-credit-btn" href="${credit_status['provider_status_url']}" target="_blank">${_("Access Credit")}</a>
|
||||
% elif credit_status["request_status"] == "rejected" and not credit_status["error"] :
|
||||
<p class="message-copy credit-msg credit-request-rejected-msg">
|
||||
## Translators: link_to_provider_site is a link to an external webpage. The text of the link will be the name of a
|
||||
## credit provider, such as 'State University' or 'Happy Fun Company'.
|
||||
${_("Thank you for your payment. To receive course credit, you must now request credit at the {link_to_provider_site} website. Select <b>Request Credit</b> to get started.").format(
|
||||
## credit provider, such as 'State University' or 'Happy Fun Company'. provider_name is the name of credit provider.
|
||||
${_("{provider_name} did not approve your request for course credit. For more information, contact {link_to_provider_site} directly.").format(
|
||||
provider_name=credit_status["provider_name"],
|
||||
link_to_provider_site=provider_link,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
<button class="btn credit-btn pending-credit-btn" data-course-key="${credit_status['course_key']}" data-user="${user.username}" data-provider="${credit_status['provider_id']}">${_("Request Credit")}</button>
|
||||
% endif
|
||||
|
||||
<form id="credit-pending-form"> </form>
|
||||
% elif credit_status["request_status"] == "approved" and not credit_status["error"] :
|
||||
<p class="message-copy credit-request-approved-msg">
|
||||
## Translators: link_to_provider_site is a link to an external webpage. The text of the link will be the name of a
|
||||
## credit provider, such as 'State University' or 'Happy Fun Company'. provider_name is the name of credit provider.
|
||||
${_("<b>Congratulations!</b> {provider_name} has approved your request for course credit. To see your course credit, visit the {link_to_provider_site} website").format(
|
||||
provider_name=credit_status["provider_name"],
|
||||
link_to_provider_site=provider_link,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
<a class="btn credit-btn access-credit-btn" href="${credit_status['provider_status_url']}" target="_blank">${_("View Credit")}</a>
|
||||
% elif credit_status["request_status"] == "rejected" and not credit_status["error"] :
|
||||
<p class="message-copy credit-request-rejected-msg">
|
||||
## Translators: link_to_provider_site is a link to an external webpage. The text of the link will be the name of a
|
||||
## credit provider, such as 'State University' or 'Happy Fun Company'. provider_name is the name of credit provider.
|
||||
${_("{provider_name} did not approve your request for course credit. For more information, contact {link_to_provider_site} directly.").format(
|
||||
provider_name=credit_status["provider_name"],
|
||||
link_to_provider_site=provider_link,
|
||||
)
|
||||
}
|
||||
</p>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
@@ -54,10 +54,10 @@ def signature(params, shared_secret):
|
||||
str: The 32-character signature.
|
||||
|
||||
"""
|
||||
encoded_params = "".join([
|
||||
"{key}:{value}".format(key=key, value=params[key])
|
||||
encoded_params = u"".join([
|
||||
u"{key}:{value}".format(key=key, value=params[key])
|
||||
for key in sorted(params.keys())
|
||||
if key != "signature"
|
||||
if key != u"signature"
|
||||
])
|
||||
hasher = hmac.new(shared_secret, encoded_params, hashlib.sha256)
|
||||
hasher = hmac.new(shared_secret, encoded_params.encode('utf-8'), hashlib.sha256)
|
||||
return hasher.hexdigest()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
Tests for digital signatures used to validate messages to/from credit providers.
|
||||
"""
|
||||
@@ -9,14 +10,14 @@ from django.test.utils import override_settings
|
||||
from openedx.core.djangoapps.credit import signature
|
||||
|
||||
|
||||
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={
|
||||
"asu": u'abcd1234'
|
||||
})
|
||||
class SignatureTest(TestCase):
|
||||
"""
|
||||
Tests for digital signatures.
|
||||
"""
|
||||
|
||||
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={
|
||||
"asu": u'abcd1234'
|
||||
})
|
||||
def test_unicode_secret_key(self):
|
||||
# Test a key that has type `unicode` but consists of ASCII characters
|
||||
# (This can happen, for example, when loading the key from a JSON configuration file)
|
||||
@@ -35,3 +36,9 @@ class SignatureTest(TestCase):
|
||||
# so we can fix the misconfiguration.
|
||||
key = signature.get_shared_secret_key("asu")
|
||||
self.assertIs(key, None)
|
||||
|
||||
def test_unicode_data(self):
|
||||
""" Verify the signature generation method supports Unicode data. """
|
||||
key = signature.get_shared_secret_key("asu")
|
||||
sig = signature.signature({'name': u'Ed Xavíer'}, key)
|
||||
self.assertEqual(sig, "76b6c9a657000829253d7c23977b35b34ad750c5681b524d7fdfb25cd5273cec")
|
||||
|
||||
@@ -57,7 +57,7 @@ git+https://github.com/edx/edx-lint.git@c5745631d2eee4e2efe8c31fa7b42fe2c12a0755
|
||||
git+https://github.com/edx/ecommerce-api-client.git@1.1.0#egg=ecommerce-api-client==1.1.0
|
||||
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
|
||||
git+https://github.com/edx/edx-organizations.git@release-2015-09-22#egg=edx-organizations==0.1.6
|
||||
git+https://github.com/edx/edx-proctoring.git@0.9.14#egg=edx-proctoring==0.9.14
|
||||
git+https://github.com/edx/edx-proctoring.git@0.9.16#egg=edx-proctoring==0.9.16
|
||||
|
||||
# Third Party XBlocks
|
||||
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
|
||||
|
||||
Reference in New Issue
Block a user