Commit 1e7d41ed by Vedran Karacic Committed by Vedran Karačić

Client side basket checkout fixes.

* Display form errors from the server on the UI
* Mobile design alignment fix
* Alt attributes for all images
* Catch the ValueError for when a user with empty basket
  opens the basket page. A basket is carried in user
  sessions as a SimpleLazyObject, if it's filled it will
  be commited to the database, but when it's empty, our
  check for vouchers will return a ValueError instead.
parent 5fb27e88
...@@ -387,11 +387,21 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, CourseCatalogMockMixin, Lms ...@@ -387,11 +387,21 @@ class BasketSummaryViewTests(CourseCatalogTestMixin, CourseCatalogMockMixin, Lms
self.assertFalse(line_data['enrollment_code']) self.assertFalse(line_data['enrollment_code'])
self.assertEqual(response.context['payment_processors'][0].NAME, DummyProcessor.NAME) self.assertEqual(response.context['payment_processors'][0].NAME, DummyProcessor.NAME)
def test_no_basket_response(self): def assert_emtpy_basket(self):
""" Verify there are no form and line data in the context for a non-existing basket. """ """ Assert that the basket is empty on visiting the basket summary page. """
response = self.client.get(self.path) response = self.client.get(self.path)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['formset_lines_data'], []) self.assertEqual(response.context['formset_lines_data'], [])
self.assertEqual(response.context['total_benefit'], None)
def test_no_basket_response(self):
""" Verify there are no form, line and benefit data in the context for a non-existing basket. """
self.assert_emtpy_basket()
def test_anonymous_basket(self):
""" Verify there are no form, line and benefit data in the context for anonymous user. """
self.client.logout()
self.assert_emtpy_basket()
def test_line_item_discount_data(self): def test_line_item_discount_data(self):
""" Verify that line item has correct discount data. """ """ Verify that line item has correct discount data. """
......
...@@ -215,11 +215,14 @@ class BasketSummaryView(BasketView): ...@@ -215,11 +215,14 @@ class BasketSummaryView(BasketView):
# Total benefit displayed in price summary. # Total benefit displayed in price summary.
# Currently only one voucher per basket is supported. # Currently only one voucher per basket is supported.
applied_voucher = basket.vouchers.first() try:
total_benefit = ( applied_voucher = basket.vouchers.first()
format_benefit_value(applied_voucher.offers.first().benefit) total_benefit = (
if applied_voucher else None format_benefit_value(applied_voucher.offers.first().benefit)
) if applied_voucher else None
)
except ValueError:
total_benefit = None
context.update({ context.update({
'total_benefit': total_benefit, 'total_benefit': total_benefit,
......
...@@ -68,12 +68,18 @@ define([ ...@@ -68,12 +68,18 @@ define([
$form.appendTo('body').submit(); $form.appendTo('body').submit();
}, },
appendValidationErrorMsg = function(event, field, msg) { appendCardValidationErrorMsg = function(event, field, msg) {
field.find('~.help-block').append('<span>' + msg + '</span>'); field.find('~.help-block').append('<span>' + msg + '</span>');
field.focus(); field.focus();
event.preventDefault(); event.preventDefault();
}, },
appendCardHolderValidationErrorMsg = function(field, msg) {
field.parentsUntil('form-item').find('~.help-block').append(
'<span>' + msg + '</span>'
);
},
cardHolderInfoValidation = function (event) { cardHolderInfoValidation = function (event) {
var requiredFields = [ var requiredFields = [
'input[name=first_name]', 'input[name=first_name]',
...@@ -86,9 +92,7 @@ define([ ...@@ -86,9 +92,7 @@ define([
_.each(requiredFields, function(field) { _.each(requiredFields, function(field) {
if ($(field).val() === '') { if ($(field).val() === '') {
event.preventDefault(); event.preventDefault();
$(field).parentsUntil('form-item').find('~.help-block').append( appendCardHolderValidationErrorMsg($(field), 'This field is required');
'<span>This field is required</span>'
);
} }
}); });
...@@ -112,20 +116,20 @@ define([ ...@@ -112,20 +116,20 @@ define([
cardType = CreditCardUtils.getCreditCardType(cardNumber); cardType = CreditCardUtils.getCreditCardType(cardNumber);
if (!CreditCardUtils.isValidCardNumber(cardNumber)) { if (!CreditCardUtils.isValidCardNumber(cardNumber)) {
appendValidationErrorMsg(event, cardNumberField, 'Invalid card number'); appendCardValidationErrorMsg(event, cardNumberField, 'Invalid card number');
} else if (typeof cardType === 'undefined') { } else if (typeof cardType === 'undefined') {
appendValidationErrorMsg(event, cardNumberField, 'Unsupported card type'); appendCardValidationErrorMsg(event, cardNumberField, 'Unsupported card type');
} else if (cvnNumber.length !== cardType.cvnLength || !Number.isInteger(Number(cvnNumber))) { } else if (cvnNumber.length !== cardType.cvnLength || !Number.isInteger(Number(cvnNumber))) {
appendValidationErrorMsg(event, cvnNumberField, 'Invalid CVN'); appendCardValidationErrorMsg(event, cvnNumberField, 'Invalid CVN');
} }
if (!Number.isInteger(Number(cardExpiryMonth)) || if (!Number.isInteger(Number(cardExpiryMonth)) ||
Number(cardExpiryMonth) > 12 || Number(cardExpiryMonth) < 1) { Number(cardExpiryMonth) > 12 || Number(cardExpiryMonth) < 1) {
appendValidationErrorMsg(event, cardExpiryMonthField, 'Invalid month'); appendCardValidationErrorMsg(event, cardExpiryMonthField, 'Invalid month');
} else if (!Number.isInteger(Number(cardExpiryYear)) || Number(cardExpiryYear) < currentYear) { } else if (!Number.isInteger(Number(cardExpiryYear)) || Number(cardExpiryYear) < currentYear) {
appendValidationErrorMsg(event, cardExpiryYearField, 'Invalid year'); appendCardValidationErrorMsg(event, cardExpiryYearField, 'Invalid year');
} else if (Number(cardExpiryMonth) < currentMonth && Number(cardExpiryYear) === currentYear) { } else if (Number(cardExpiryMonth) < currentMonth && Number(cardExpiryYear) === currentYear) {
appendValidationErrorMsg(event, cardExpiryMonthField, 'Card expired'); appendCardValidationErrorMsg(event, cardExpiryMonthField, 'Card expired');
} }
}, },
onReady = function() { onReady = function() {
...@@ -155,10 +159,10 @@ define([ ...@@ -155,10 +159,10 @@ define([
$('.card-type-icon').attr( $('.card-type-icon').attr(
'src', 'src',
iconPath + card.name + '.png' iconPath + card.name + '.png'
); ).removeClass('hidden');
$('input[name=card_type]').val(card.type); $('input[name=card_type]').val(card.type);
} else { } else {
$('.card-type-icon').attr('src', ''); $('.card-type-icon').attr('src', '').addClass('hidden');
$('input[name=card_type]').val(''); $('input[name=card_type]').val('');
} }
} }
...@@ -217,6 +221,7 @@ define([ ...@@ -217,6 +221,7 @@ define([
}; };
return { return {
appendCardHolderValidationErrorMsg: appendCardHolderValidationErrorMsg,
appendToForm: appendToForm, appendToForm: appendToForm,
cardInfoValidation: cardInfoValidation, cardInfoValidation: cardInfoValidation,
checkoutPayment: checkoutPayment, checkoutPayment: checkoutPayment,
......
/** /**
* CyberSource payment processor specific actions. * CyberSource payment processor specific actions.
*/ */
require(['jquery'], function($) { require([
'jquery',
'pages/basket_page'
], function(
$,
BasketPage
) {
'use strict'; 'use strict';
function initializePaymentForm() { function initializePaymentForm() {
...@@ -44,15 +50,28 @@ require(['jquery'], function($) { ...@@ -44,15 +50,28 @@ require(['jquery'], function($) {
} }
} }
}, },
error: function (jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus) {
// TODO Handle errors. Ideally the form should be validated in JavaScript
// before it is submitted.
console.log(jqXHR);
console.log(textStatus);
console.log(errorThrown);
// Don't allow the form to submit. // Don't allow the form to submit.
event.preventDefault();
event.stopPropagation(); event.stopPropagation();
var cardHolderFields = [
'first_name', 'last_name', 'address_line1', 'address_line2',
'state', 'city', 'country', 'postal_code'
];
if (textStatus === 'error') {
var error = JSON.parse(jqXHR.responseText);
if (error.field_errors) {
for (var k in error.field_errors) {
if (cardHolderFields.indexOf(k) !== -1) {
var field = $('input[name=' + k + ']');
BasketPage.appendCardHolderValidationErrorMsg(field, error.field_errors[k]);
field.focus();
}
}
}
}
} }
}); });
}); });
......
...@@ -254,7 +254,9 @@ ...@@ -254,7 +254,9 @@
} }
.help-block { .help-block {
height: 10px; margin-top: 0;
margin-bottom: 0;
min-height: 22px;
color: red; color: red;
} }
...@@ -355,7 +357,7 @@ ...@@ -355,7 +357,7 @@
#expiration-label { #expiration-label {
width: 100%; width: 100%;
margin-left: 15px; padding-left: 15px;
text-align: left; text-align: left;
} }
...@@ -527,15 +529,9 @@ ...@@ -527,15 +529,9 @@
} }
.payment-form #payment-information { .payment-form #payment-information {
border-left: none;
}
.payment-methods {
padding-left: 0; padding-left: 0;
padding-right: 0;
#payment-method { border-left: none;
padding-left: 0;
}
} }
#summary #basket-information .product { #summary #basket-information .product {
......
...@@ -108,7 +108,7 @@ ...@@ -108,7 +108,7 @@
<legend>{% trans "SELECT PAYMENT METHOD" %}</legend> <legend>{% trans "SELECT PAYMENT METHOD" %}</legend>
<div class="col-sm-12 payment-methods"> <div class="col-sm-12 payment-methods">
<div id="payment-method" class="col-md-4 col-sm-7 col-xs-6"> <div id="payment-method" class="col-md-4 col-sm-7 col-xs-6">
<a href="#payment-information" id="payment-method-image" aria-label={% trans "Pay with a Credit Card" %} role="button"><img aria-hidden="true" src="/static/images/credit_card_options.png"></a> <a href="#payment-information" id="payment-method-image" aria-label={% trans "Pay with a Credit Card" %} role="button"><img aria-hidden="true" src="/static/images/credit_card_options.png" alt="Credit cards"></a>
</div> </div>
<div id="payment-processor" class="payment-buttons col-md-3 col-sm-5 col-xs-6" data-basket-id="{{ basket.id }}"> <div id="payment-processor" class="payment-buttons col-md-3 col-sm-5 col-xs-6" data-basket-id="{{ basket.id }}">
{# Translators: Do NOT translate the name PayPal. #} {# Translators: Do NOT translate the name PayPal. #}
...@@ -140,14 +140,14 @@ ...@@ -140,14 +140,14 @@
{% if not free_basket %} {% if not free_basket %}
<legend aria-label={% trans "Billing information" %}>{% trans "BILLING INFORMATION" %}</legend> <legend aria-label={% trans "Billing information" %}>{% trans "BILLING INFORMATION" %}</legend>
{% if not paypal_enabled %} {% if not paypal_enabled %}
<img aria-hidden="true" class="credit-card-list" src="/static/images/credit_card_options.png"> <img aria-hidden="true" class="credit-card-list" src="/static/images/credit_card_options.png" alt="Credit cards">
{% endif %} {% endif %}
<div class="pci-fields row"> <div class="pci-fields row">
<div id="card-number" class="form-group col-sm-6"> <div id="card-number" class="form-group col-sm-6">
<label for="card-number-input" class="control-label">{% trans "Card Number (required)" %}<span class="sr">, {% trans "Secure" %}</span></label> <label for="card-number-input" class="control-label">{% trans "Card Number (required)" %}<span class="sr">, {% trans "Secure" %}</span></label>
<input id="card-number-input" name="card_number" class="form-control pci-field" maxlength="20" required aria-required="true"/> <input id="card-number-input" name="card_number" class="form-control pci-field" maxlength="20" required aria-required="true"/>
<i class="fa fa-lock" aria-hidden="true"></i> <i class="fa fa-lock" aria-hidden="true"></i>
<img class="card-type-icon"> <img class="card-type-icon hidden" alt="Credit card icon">
<p class="help-block"></p> <p class="help-block"></p>
</div> </div>
<input type="hidden" name="card_type" class="pci-field"> <input type="hidden" name="card_type" class="pci-field">
......
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