Commit f967cfce by Clinton Blackburn

Merged release to master

Resolved conflicts from hotfix-2015-10-05
parents b7cf3920 882c2fa3
...@@ -86,7 +86,8 @@ class CreditCourseDashboardTest(ModuleStoreTestCase): ...@@ -86,7 +86,8 @@ class CreditCourseDashboardTest(ModuleStoreTestCase):
def test_not_eligible_for_credit(self): 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. # The user is not yet eligible for credit, so no additional information should be displayed on the dashboard.
response = self._load_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): def test_eligible_for_credit(self):
# Simulate that the user has completed the only requirement in the course # Simulate that the user has completed the only requirement in the course
......
...@@ -400,7 +400,7 @@ FEATURES = { ...@@ -400,7 +400,7 @@ FEATURES = {
'ENABLE_OPENBADGES': False, 'ENABLE_OPENBADGES': False,
# Credit course API # 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. # The block types to disable need to be specified in "x block disable config" in django admin.
'ENABLE_DISABLING_XBLOCK_TYPES': True, 'ENABLE_DISABLING_XBLOCK_TYPES': True,
...@@ -2124,7 +2124,7 @@ if FEATURES.get('CLASS_DASHBOARD'): ...@@ -2124,7 +2124,7 @@ if FEATURES.get('CLASS_DASHBOARD'):
INSTALLED_APPS += ('class_dashboard',) INSTALLED_APPS += ('class_dashboard',)
################ Enable credit eligibility feature #################### ################ Enable credit eligibility feature ####################
ENABLE_CREDIT_ELIGIBILITY = False ENABLE_CREDIT_ELIGIBILITY = True
FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY
######################## CAS authentication ########################### ######################## CAS authentication ###########################
......
/**
* 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 || {}; ...@@ -50,12 +50,12 @@ var edx = edx || {};
this.getProviderData(providerId).then(this.renderProvider, this.renderError) 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 // Display the course Id or name (if available) in the placeholder
var $courseNamePlaceholder = $(".course_name_placeholder"); var $courseNamePlaceholder = $(".course_name_placeholder");
$courseNamePlaceholder.text(courseId); $courseNamePlaceholder.text(courseId);
this.getCourseData(courseId).then(function(responseData) { this.getCourseData(courseId).then(function (responseData) {
$courseNamePlaceholder.text(responseData.name); $courseNamePlaceholder.text(responseData.name);
}); });
}, },
...@@ -77,7 +77,7 @@ var edx = edx || {}; ...@@ -77,7 +77,7 @@ var edx = edx || {};
var self = this, var self = this,
orderId = $.url('?basket_id') || $.url('?payment-order-num'); 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 // Get the order details
self.$el.removeClass('hidden'); self.$el.removeClass('hidden');
self.getReceiptData(orderId).then(self.renderReceipt, self.renderError); self.getReceiptData(orderId).then(self.renderReceipt, self.renderError);
...@@ -168,7 +168,7 @@ var edx = edx || {}; ...@@ -168,7 +168,7 @@ var edx = edx || {};
billedTo: null billedTo: null
}; };
if (order.billing_address){ if (order.billing_address) {
receiptContext.billedTo = { receiptContext.billedTo = {
firstName: order.billing_address.first_name, firstName: order.billing_address.first_name,
lastName: order.billing_address.last_name, lastName: order.billing_address.last_name,
...@@ -263,8 +263,8 @@ var edx = edx || {}; ...@@ -263,8 +263,8 @@ var edx = edx || {};
line = order.lines[0]; line = order.lines[0];
if (this.useEcommerceApi) { if (this.useEcommerceApi) {
attributeValues = _.find(line.product.attribute_values, function (attribute) { 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. // This method assumes that all items in the order are related to a single course.
if (attributeValues != undefined) { if (attributeValues != undefined) {
...@@ -273,7 +273,7 @@ var edx = edx || {}; ...@@ -273,7 +273,7 @@ var edx = edx || {};
} }
return null; return null;
}, }
}); });
new edx.commerce.ReceiptView({ new edx.commerce.ReceiptView({
...@@ -282,16 +282,11 @@ var edx = edx || {}; ...@@ -282,16 +282,11 @@ var edx = edx || {};
})(jQuery, _, _.str, Backbone); })(jQuery, _, _.str, Backbone);
function completeOrder(event) { // jshint ignore:line
function completeOrder (event) {
var courseKey = $(event).data("course-key"), var courseKey = $(event).data("course-key"),
username = $(event).data("username"), username = $(event).data("username"),
providerId = $(event).data("provider"), providerId = $(event).data("provider"),
postData = { $errorContainer = $("#error-container");
'course_key': courseKey,
'username': username
},
errorContainer = $("#error-container");
analytics.track( analytics.track(
"edx.bi.credit.clicked_complete_credit", "edx.bi.credit.clicked_complete_credit",
...@@ -301,34 +296,7 @@ function completeOrder (event) { ...@@ -301,34 +296,7 @@ function completeOrder (event) {
} }
); );
$.ajax({ edx.commerce.credit.createCreditRequest(providerId, courseKey, username).fail(function () {
url: '/api/credit/v1/providers/' + providerId + '/request/', $errorContainer.removeClass("hidden");
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");
}
}); });
} }
(function($, analytics) { /**
* Student dashboard credit messaging.
*/
var edx = edx || {};
(function ($, analytics) {
'use strict'; 'use strict';
$(document).ready(function() { $(document).ready(function () {
var errorContainer = $(".credit-error-msg"), var $errorContainer = $(".credit-error-msg"),
creditStatusError = errorContainer.data("credit-error"); creditStatusError = $errorContainer.data("credit-error");
if (creditStatusError == "True"){ if (creditStatusError === "True") {
errorContainer.toggleClass("is-hidden"); $errorContainer.toggleClass("is-hidden");
} }
// Fire analytics events when the "purchase credit" button is clicked // 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"); var courseKey = $(event.target).data("course-key");
analytics.track( analytics.track(
"edx.bi.credit.clicked_purchase_credit", "edx.bi.credit.clicked_purchase_credit",
...@@ -20,46 +26,19 @@ ...@@ -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');
$('input', form).remove();
form.attr( 'action', requestData.url ); // This event invokes credit request endpoint. It will initiate
form.attr( 'method', 'POST' ); // a credit request for the credit course for the provided user.
$(".pending-credit-btn").on("click", function (event) {
var $target = $(event.target),
courseKey = $target.data("course-key"),
username = $target.data("user"),
providerId = $target.data("provider");
_.each( requestData.parameters, function( value, key ) { edx.commerce.credit.createCreditRequest(providerId, courseKey, username).fail(function () {
$('<input>').attr({ $(".credit-action").hide();
type: 'hidden', $errorContainer.toggleClass("is-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");
}
}); });
}); });
}); });
......
...@@ -548,24 +548,6 @@ ...@@ -548,24 +548,6 @@
padding: 0; 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 { .message {
@extend %ui-depth1; @extend %ui-depth1;
border-radius: 3px; border-radius: 3px;
...@@ -750,19 +732,21 @@ ...@@ -750,19 +732,21 @@
font-weight: bold; font-weight: bold;
} }
} }
.credit-eligibility-msg {
@include float(left);
width: flex-grid(10, 12);
}
.credit-request-pending-msg { .credit-action {
@include float(left); .credit-msg {
margin-top: 10px; @include float(left);
} width: flex-grid(9, 12);
}
.credit-request-approved-msg{ .credit-btn {
width: flex-grid(10, 12); @extend %btn-pl-yellow-base;
@include float(left); @include float(right);
background-image: none ;
text-shadow: none;
box-shadow: none;
text-transform: none;
}
} }
.actions { .actions {
......
...@@ -21,6 +21,7 @@ from django.utils.translation import ugettext as _ ...@@ -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/jquery.ajax-retry.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.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/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> <script src="${static.url('js/commerce/views/receipt_view.js')}"></script>
</%block> </%block>
...@@ -56,7 +57,6 @@ from django.utils.translation import ugettext as _ ...@@ -56,7 +57,6 @@ from django.utils.translation import ugettext as _
<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>
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
<%= interpolate("<img src='%s' alt='%s'></image>", [thumbnail_url, display_name]) %> <%= interpolate("<img src='%s' alt='%s'></image>", [thumbnail_url, display_name]) %>
</div> </div>
<div class="complete-order"> <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>
</div> </div>
...@@ -35,6 +35,7 @@ import json ...@@ -35,6 +35,7 @@ import json
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
<script src="${static.url('js/commerce/credit.js')}"></script>
<%static:js group='dashboard'/> <%static:js group='dashboard'/>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
......
...@@ -19,57 +19,47 @@ ...@@ -19,57 +19,47 @@
) )
)} )}
</p> </p>
% if not credit_status["purchased"] and not credit_status["error"] : <div class="credit-action">
<p class="message-copy credit-eligibility-msg"> % if not credit_status["purchased"] and not credit_status["error"] :
${_("You have completed this course and are eligible to purchase course credit. Select <b>Learn More</b> to get started.")} <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> % elif credit_status["request_status"] == "approved" and not credit_status["error"] :
<div class="purchase_credit"> <p class="message-copy credit-msg credit-request-approved-msg">
<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> ## Translators: provider_name is the name of a credit provider or university (e.g. State University)
</div> ${_("<strong>Congratulations!</strong> {provider_name} has converted your course credit. To see your course credit, click <strong>Access Credit</strong>.").format(
% 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(
provider_name=credit_status["provider_name"], provider_name=credit_status["provider_name"],
) )
} }
</p> </p>
% elif credit_status["request_status"] is None: <a class="btn credit-btn access-credit-btn" href="${credit_status['provider_status_url']}" target="_blank">${_("Access Credit")}</a>
<p class="message-copy credit-request-pending-msg"> % 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 ## 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'. ## credit provider, such as 'State University' or 'Happy Fun Company'. provider_name is the name of credit provider.
${_("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( ${_("{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, link_to_provider_site=provider_link,
) )
} }
</p> </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 % endif
</div>
<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 % endif
...@@ -54,10 +54,10 @@ def signature(params, shared_secret): ...@@ -54,10 +54,10 @@ def signature(params, shared_secret):
str: The 32-character signature. str: The 32-character signature.
""" """
encoded_params = "".join([ encoded_params = u"".join([
"{key}:{value}".format(key=key, value=params[key]) u"{key}:{value}".format(key=key, value=params[key])
for key in sorted(params.keys()) 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() return hasher.hexdigest()
# coding=utf-8
""" """
Tests for digital signatures used to validate messages to/from credit providers. Tests for digital signatures used to validate messages to/from credit providers.
""" """
...@@ -9,14 +10,14 @@ from django.test.utils import override_settings ...@@ -9,14 +10,14 @@ from django.test.utils import override_settings
from openedx.core.djangoapps.credit import signature from openedx.core.djangoapps.credit import signature
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={
"asu": u'abcd1234'
})
class SignatureTest(TestCase): class SignatureTest(TestCase):
""" """
Tests for digital signatures. Tests for digital signatures.
""" """
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={
"asu": u'abcd1234'
})
def test_unicode_secret_key(self): def test_unicode_secret_key(self):
# Test a key that has type `unicode` but consists of ASCII characters # 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) # (This can happen, for example, when loading the key from a JSON configuration file)
...@@ -35,3 +36,9 @@ class SignatureTest(TestCase): ...@@ -35,3 +36,9 @@ class SignatureTest(TestCase):
# so we can fix the misconfiguration. # so we can fix the misconfiguration.
key = signature.get_shared_secret_key("asu") key = signature.get_shared_secret_key("asu")
self.assertIs(key, None) 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 ...@@ -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 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 -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-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 # Third Party XBlocks
-e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga -e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga
......
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