Commit 882c2fa3 by Clinton Blackburn

Merge pull request #10030 from edx/hotfix/2015-10-05

Hotfix for 2015-10-05
parents a3f8ed3f 775ca26c
......@@ -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
......
......@@ -413,7 +413,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,
......@@ -2122,7 +2122,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 ###########################
......
/**
* 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");
});
}
(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');
$('input', form).remove();
form.attr( 'action', requestData.url );
form.attr( 'method', 'POST' );
// 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 $target = $(event.target),
courseKey = $target.data("course-key"),
username = $target.data("user"),
providerId = $target.data("provider");
_.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);
margin-top: 10px;
}
.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,61 +19,47 @@
)
)}
</p>
% if not credit_status["purchased"] and not credit_status["error"] :
<p class="message-copy credit-eligibility-msg">
${_("You are now eligible for credit. <b>Congratulations!</b>")}
<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']}">${_("Get credit")}</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: 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. Your credit is processing. Please see {link_to_provider_site} for more information.").format(
link_to_provider_site=provider_link,
% 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>
<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']}">${_("Learn more")}</button>
% 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 purchase. Please proceed to {link_to_provider_site} to finalize your credit.").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']}">${_("Finalize 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'
${_("Your credit has been processed and approved. <b>Congratulations!</b>. Please see {link_to_provider_site} for more information.").format(
link_to_provider_site=provider_link,
)
}
</p>
<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-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'
${_("Your credit has been processed but denied. Please contact {link_to_provider_site} for more information.").format(
link_to_provider_site=provider_link,
)
}
</p>
<a class="btn credit-btn denied-credit-btn" href="${credit_status['provider_status_url']}" target="_blank">
${_("Contact {provider}").format(provider=u'credit_status["provider_name"]')
}
</a>
% endif
</div>
</div>
% endif
......@@ -242,13 +242,20 @@ def create_credit_request(course_key, provider_id, username):
# Retrieve the final grade from the eligibility table
try:
final_grade = unicode(CreditRequirementStatus.objects.get(
final_grade = CreditRequirementStatus.objects.get(
username=username,
requirement__namespace="grade",
requirement__name="grade",
requirement__course__course_key=course_key,
status="satisfied"
).reason["final_grade"])
).reason["final_grade"]
# NOTE (CCB): Limiting the grade to seven characters is a hack for ASU.
if len(unicode(final_grade)) > 7:
final_grade = u'{:.5f}'.format(final_grade)
else:
final_grade = unicode(final_grade)
except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError):
log.exception(
"Could not retrieve final grade from the credit eligibility table "
......
......@@ -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()
......@@ -2,21 +2,21 @@
Tests for the API functions in the credit app.
"""
import datetime
import ddt
import json
from mock import patch
import pytz
import unittest
import ddt
from django.conf import settings
from django.core import mail
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 unittest import skipUnless
from mock import patch
from opaque_keys.edx.keys import CourseKey
import pytz
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from util.date_utils import from_timestamp
from openedx.core.djangoapps.credit import api
......@@ -36,9 +36,6 @@ from openedx.core.djangoapps.credit.models import (
CreditEligibility
)
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6"
......@@ -95,7 +92,7 @@ class CreditApiTestBase(ModuleStoreTestCase):
return credit_course
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
@ddt.ddt
class CreditRequirementApiTests(CreditApiTestBase):
"""
......@@ -432,7 +429,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
self.assertEqual(mail.outbox[0].subject, 'Course Credit Eligibility')
# Now verify them email content
email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access
email_payload_first = mail.outbox[0].attachments[0]._payload # pylint: disable=protected-access
# Test that email has two payloads [multipart (plain text and html
# content), attached image]
......@@ -446,7 +443,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
# Now check that html email content has same logo image 'Content-ID'
# as the attached logo image 'Content-ID'
email_image = email_payload_first[1]
html_content_first = email_payload_first[0]._payload[1]._payload # pylint: disable=protected-access
html_content_first = email_payload_first[0]._payload[1]._payload # pylint: disable=protected-access
# strip enclosing angle brackets from 'logo_image' cache 'Content-ID'
image_id = email_image.get('Content-ID', '')[1:-1]
......@@ -468,8 +465,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
self.assertEqual(len(mail.outbox), 2)
# Now check that on sending eligibility notification again cached
# logo image is used
email_payload_second = mail.outbox[1].attachments[0]._payload # pylint: disable=protected-access
html_content_second = email_payload_second[0]._payload[1]._payload # pylint: disable=protected-access
email_payload_second = mail.outbox[1].attachments[0]._payload # pylint: disable=protected-access
html_content_second = email_payload_second[0]._payload[1]._payload # pylint: disable=protected-access
self.assertIn(image_id, html_content_second)
# The user should remain eligible even if the requirement status is later changed
......@@ -644,6 +641,21 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
self.assertIn(param_key, parameters)
self.assertEqual(parameters[param_key], self.USER_INFO[key])
def test_create_credit_request_grade_length(self):
""" Verify the length of the final grade is limited to seven (7) characters total.
This is a hack for ASU.
"""
# Update the user's grade
status = CreditRequirementStatus.objects.get(username=self.USER_INFO["username"])
status.status = "satisfied"
status.reason = {"final_grade": 1.0 / 3.0}
status.save()
# Initiate a credit request
request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username'])
self.assertEqual(request['parameters']['final_grade'], u'0.33333')
def test_credit_request_disable_integration(self):
CreditProvider.objects.all().update(enable_integration=False)
......@@ -842,6 +854,7 @@ 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}):
......
# 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")
......@@ -58,7 +58,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
......
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