Commit f7c47204 by Clinton Blackburn

Merge pull request #293 from edx/clintonb/product-validation

Added validation for all nested products on the course model
parents a4268389 662f0361
define([ define([
'backbone' 'backbone',
'utils/utils'
], ],
function (Backbone) { function (Backbone,
Utils) {
'use strict'; 'use strict';
return Backbone.Collection.extend({}); return Backbone.Collection.extend({
/**
* Validates the collection by iterating over the nested models.
*
* @return {Boolean} Boolean indicating if this Collection is valid.
*/
isValid: function () {
return Utils.areModelsValid(this.models);
}
});
} }
); );
require([ require([
'jquery', 'jquery',
'backbone', 'backbone',
'backbone.validation',
'bootstrap', 'bootstrap',
'bootstrap_accessibility', 'bootstrap_accessibility',
'underscore' 'underscore'
...@@ -12,5 +13,14 @@ require([ ...@@ -12,5 +13,14 @@ require([
// Activate all pre-rendered tooltips. // Activate all pre-rendered tooltips.
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
}); });
// NOTE (CCB): Even if validation fails, force the model to be updated. This will ensure calls
// to model.isValid(true) return false when we validate models before saving. Without forceUpdate,
// our models would always be valid, and we'd have to add additional code to check the form fields
// before saving.
Backbone.Validation.configure({
forceUpdate: true,
labelFormatter: 'label'
});
} }
); );
...@@ -20,6 +20,7 @@ require.config({ ...@@ -20,6 +20,7 @@ require.config({
'requirejs': 'bower_components/requirejs/require', 'requirejs': 'bower_components/requirejs/require',
'routers': 'js/routers', 'routers': 'js/routers',
'templates': 'templates', 'templates': 'templates',
'test': 'js/test',
'text': 'bower_components/text/text', 'text': 'bower_components/text/text',
'underscore': 'bower_components/underscore/underscore', 'underscore': 'bower_components/underscore/underscore',
'underscore.string': 'bower_components/underscore.string/dist/underscore.string', 'underscore.string': 'bower_components/underscore.string/dist/underscore.string',
......
...@@ -30,10 +30,6 @@ define([ ...@@ -30,10 +30,6 @@ define([
Utils) { Utils) {
'use strict'; 'use strict';
Backbone.Validation.configure({
labelFormatter: 'label'
});
_.extend(Backbone.Model.prototype, Backbone.Validation.mixin); _.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
_.extend(Backbone.Validation.patterns, { _.extend(Backbone.Validation.patterns, {
...@@ -83,6 +79,11 @@ define([ ...@@ -83,6 +79,11 @@ define([
if (invalid) { if (invalid) {
return gettext('The verification deadline must occur AFTER the upgrade deadline.'); return gettext('The verification deadline must occur AFTER the upgrade deadline.');
} }
},
products: function (value) {
if (!value.isValid()) {
return gettext('Product validation failed.');
}
} }
}, },
......
define([
'backbone'
],
function (Backbone) {
'use strict';
return {
/**
* Returns a Backbone model that can be used to test validation.
*
* @param {Boolean} valid - Indicates if the model should pass or fail validation.
*/
getModelForValidation: function (valid) {
return Backbone.Model.extend({
isValid: function () {
return valid;
},
validate: function () {
if (valid) {
return {};
}
return {error: 'Test model: validate always fails.'};
}
});
}
};
}
);
define([
'collections/product_collection',
'test/spec-utils'
],
function (ProductCollection,
SpecUtils) {
'use strict';
var collection;
describe('Product collection', function () {
beforeEach(function () {
collection = new ProductCollection();
});
describe('isValid', function () {
it('should return true if the collection is empty', function () {
collection.reset();
expect(collection.isValid()).toEqual(true);
});
it('should return true if all models are valid', function () {
var ModelClass = SpecUtils.getModelForValidation(true);
collection.reset([new ModelClass(), new ModelClass()]);
expect(collection.isValid()).toEqual(true);
});
it('should return false if any of the models is NOT valid', function () {
var ModelClass = SpecUtils.getModelForValidation(false);
collection.reset([new ModelClass(), new ModelClass()]);
expect(collection.isValid()).toEqual(false);
// A mixture of validation statuses should always return false.
collection.add(new (SpecUtils.getModelForValidation(true))());
expect(collection.isValid()).toEqual(false);
});
});
});
}
);
...@@ -94,16 +94,23 @@ define([ ...@@ -94,16 +94,23 @@ define([
] ]
}; };
beforeEach(function () {
model = Course.findOrCreate(data, {parse: true});
});
describe('Course model', function () { describe('Course model', function () {
beforeEach(function () {
model = Course.findOrCreate(data, {parse: true});
// Remove the parent products
model.removeParentProducts();
});
describe('removeParentProducts', function () { describe('removeParentProducts', function () {
it('should remove all parent products from the products collection', function () { it('should remove all parent products from the products collection', function () {
var products = model.get('products'); var products;
// Re-initialize the model since the beforeEach removes the parents
model = Course.findOrCreate(data, {parse: true});
// Sanity check to ensure the products were properly parsed // Sanity check to ensure the products were properly parsed
products = model.get('products');
expect(products.length).toEqual(3); expect(products.length).toEqual(3);
// Remove the parent products // Remove the parent products
...@@ -167,6 +174,7 @@ define([ ...@@ -167,6 +174,7 @@ define([
spyOn($, 'ajax'); spyOn($, 'ajax');
$.cookie('ecommerce_csrftoken', cookie); $.cookie('ecommerce_csrftoken', cookie);
expect(model.validate()).toBeFalsy();
model.save(); model.save();
// $.ajax should have been called // $.ajax should have been called
...@@ -239,6 +247,19 @@ define([ ...@@ -239,6 +247,19 @@ define([
expect(model.isValid(true)).toBeFalsy(); expect(model.isValid(true)).toBeFalsy();
}); });
}); });
describe('products validation', function () {
it('should return an error message if any product is invalid', function () {
var msg = 'Product validation failed.',
products = model.get('products');
// Add an invalid product
products.push(new ProfessionalSeat({price: null}));
expect(model.validate().products).toEqual(msg);
expect(model.isValid(true)).toBeFalsy();
});
});
}); });
} }
); );
define([ define([
'backbone',
'test/spec-utils',
'utils/utils' 'utils/utils'
], ],
function (Utils) { function (Backbone,
SpecUtils,
Utils) {
'use strict'; 'use strict';
describe('Utils', function () { describe('Utils', function () {
...@@ -30,6 +34,26 @@ define([ ...@@ -30,6 +34,26 @@ define([
expect(Utils.restoreTimezone(dt)).toEqual(dt + '+00:00'); expect(Utils.restoreTimezone(dt)).toEqual(dt + '+00:00');
}); });
}); });
describe('areModelsValid', function () {
it('should return true if all models are valid', function () {
var ModelClass = SpecUtils.getModelForValidation(true),
models = [new ModelClass(), new ModelClass()];
expect(Utils.areModelsValid(models)).toEqual(true);
});
it('should return false if any of the models is NOT valid', function () {
var ModelClass = SpecUtils.getModelForValidation(false),
models = [new ModelClass(), new ModelClass()];
expect(Utils.areModelsValid(models)).toEqual(false);
// A mixture of validation statuses should always return false.
models.push(new (SpecUtils.getModelForValidation(true))());
expect(Utils.areModelsValid(models)).toEqual(false);
});
});
}); });
} }
); );
...@@ -67,6 +67,21 @@ define([ ...@@ -67,6 +67,21 @@ define([
datetime = moment.utc(datetime + 'Z').format(); datetime = moment.utc(datetime + 'Z').format();
} }
return datetime; return datetime;
},
/**
* Indicates if all models in the array are valid.
*
* Calls isValid() on every model in the array.
*
* @param {Backbone.Model[]} models
* @returns {Boolean} indicates if ALL models are valid.
*/
areModelsValid: function (models) {
return _.every(models, function (model) {
return model.isValid(true);
});
} }
}; };
}); }
);
...@@ -144,7 +144,7 @@ define([ ...@@ -144,7 +144,7 @@ define([
} }
// Enable validation // Enable validation
Backbone.Validation.bind(this, {forceUpdate: !this.editing}); Backbone.Validation.bind(this);
}, },
remove: function () { remove: function () {
...@@ -389,6 +389,12 @@ define([ ...@@ -389,6 +389,12 @@ define([
if (response.responseJSON && response.responseJSON.error) { if (response.responseJSON && response.responseJSON.error) {
message = response.responseJSON.error; message = response.responseJSON.error;
// Log the error to the console for debugging purposes
console.error(message);
} else {
// Log the error to the console for debugging purposes
console.error(response.responseText);
} }
self.clearAlerts(); self.clearAlerts();
......
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