0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Better handling of validation errors + more documentation

closes #3153
- this is all about the validation engine
- add a option, `opts.model`, to use a passed-in model directly if needed
- handle validators that return an array of strings, array of objects, or both
- ajax util returns either an array of errors or a single concat'd string
- remove formatErrors function from the mixin and make it private
- allow validation options to be passed into `.save()` since ember-data doesn't take params on `.save()` anyway
- streamline control flow
This commit is contained in:
David Arvelo 2014-06-29 23:43:25 -04:00
parent c35884ea6e
commit 78affdedb1
10 changed files with 116 additions and 54 deletions

View file

@ -92,7 +92,7 @@ var PostsController = Ember.ArrayController.extend({
if (response) {
// Get message from response
message += ': ' + getRequestErrorMessage(response);
message += ': ' + getRequestErrorMessage(response, true);
} else {
message += '.';
}

View file

@ -9,9 +9,60 @@ import ForgotValidator from 'ghost/validators/forgotten';
import SettingValidator from 'ghost/validators/setting';
import ResetValidator from 'ghost/validators/reset';
// our extensions to the validator library
ValidatorExtensions.init();
// format errors to be used in `notifications.showErrors`.
// result is [{ message: 'concatenated error messages' }]
function formatErrors(errors, opts) {
var message = 'There was an error';
opts = opts || {};
if (opts.wasSave && opts.validationType) {
message += ' saving this ' + opts.validationType;
}
if (Ember.isArray(errors)) {
// get the validator's error messages from the array.
// normalize array members to map to strings.
message = errors.map(function (error) {
if (typeof error === 'string') {
return error;
}
return error.message;
}).join('<br />');
} else if (errors instanceof Error) {
message += errors.message || '.';
} else if (typeof errors === 'object') {
// Get messages from server response
message += ': ' + getRequestErrorMessage(errors, true);
} else if (typeof errors === 'string') {
message += ': ' + errors;
} else {
message += '.';
}
// set format for notifications.showErrors
message = [{ message: message }];
return message;
}
/**
* The class that gets this mixin will receive these properties and functions.
* It will be able to validate any properties on itself (or the model it passes to validate())
* with the use of a declared validator.
*/
var ValidationEngine = Ember.Mixin.create({
// these validators can be passed a model to validate when the class that
// mixes in the ValidationEngine declares a validationType equal to a key on this object.
// the model is either passed in via `this.validate({ model: object })`
// or by calling `this.validate()` without the model property.
// in that case the model will be the class that the ValidationEngine
// was mixed into, i.e. the controller or Ember Data model.
validators: {
post: PostValidator,
setup: SetupValidator,
@ -22,80 +73,82 @@ var ValidationEngine = Ember.Mixin.create({
reset: ResetValidator
},
/**
* Passses the model to the validator specified by validationType.
* Returns a promise that will resolve if validation succeeds, and reject if not.
* Some options can be specified:
*
* `format: false` - doesn't use formatErrors to concatenate errors for notifications.showErrors.
* will return whatever the specified validator returns.
* since notifications are a common usecase, `format` is true by default.
*
* `model: Object` - you can specify the model to be validated, rather than pass the default value of `this`,
* the class that mixes in this mixin.
*/
validate: function (opts) {
opts = opts || {};
var self = this,
var model = opts.model || this,
type = this.get('validationType'),
validator = this.get('validators.' + type);
return new Ember.RSVP.Promise(function (resolve, reject) {
if (!type || !validator) {
return reject(self.formatErrors('The validator specified, "' + type + '", did not exist!'));
}
opts = opts || {};
opts.validationType = type;
var validationErrors = validator.validate(self);
return new Ember.RSVP.Promise(function (resolve, reject) {
var validationErrors;
if (!type || !validator) {
validationErrors = ['The validator specified, "' + type + '", did not exist!'];
} else {
validationErrors = validator.check(model);
}
if (Ember.isEmpty(validationErrors)) {
return resolve();
}
if (opts.format !== false) {
validationErrors = self.formatErrors(validationErrors);
validationErrors = formatErrors(validationErrors, opts);
}
return reject(validationErrors);
});
},
// format errors to be used in `notifications.showErrors`.
// format is [{ message: 'concatenated error messages' }]
formatErrors: function (errors) {
var message = 'There was an error saving this ' + this.get('validationType');
if (Ember.isArray(errors)) {
// get validation error messages
message = errors.mapBy('message').join('<br />');
} else if (errors instanceof Error) {
// we got some kind of error in Ember
message += ': ' + errors.message;
} else if (typeof errors === 'object') {
// Get messages from server response
message += ': ' + getRequestErrorMessage(errors);
} else if (typeof errors === 'string') {
message += ': ' + errors;
} else {
message += '.';
}
// set format for notifications.showErrors
message = [{ message: message }];
return message;
},
// override save to do validation first
save: function () {
/**
* The primary goal of this method is to override the `save` method on Ember Data models.
* This allows us to run validation before actually trying to save the model to the server.
* You can supply options to be passed into the `validate` method, since the ED `save` method takes no options.
*/
save: function (validationOpts) {
var self = this,
// this is a hack, but needed for async _super calls.
// ref: https://github.com/emberjs/ember.js/pull/4301
_super = this.__nextSuper;
validationOpts = validationOpts || {};
validationOpts.wasSave = true;
// model.destroyRecord() calls model.save() behind the scenes.
// in that case, we don't need validation checks or error propagation.
// in that case, we don't need validation checks or error propagation,
// because the model itself is being destroyed.
if (this.get('isDeleted')) {
return this._super();
}
// If validation fails, reject with validation errors.
// If save to the server fails, reject with server response.
return this.validate().then(function () {
return this.validate(validationOpts).then(function () {
return _super.call(self);
}).catch(function (result) {
// server save failed, format the errors and reject the promise.
// if validations failed, the errors will already be formatted for us.
// server save failed - validate() would have given back an array
if (! Ember.isArray(result)) {
result = self.formatErrors(result);
if (validationOpts.format !== false) {
// concatenate all errors into an array with a single object: [{ message: 'concatted message' }]
result = formatErrors(result, validationOpts);
} else {
// return the array of errors from the server
result = getRequestErrorMessage(result);
}
}
return Ember.RSVP.reject(result);

View file

@ -6,7 +6,7 @@ var ajax = window.ajax = function () {
// Used in API request fail handlers to parse a standard api error
// response json for the message to display
var getRequestErrorMessage = function (request) {
var getRequestErrorMessage = function (request, performConcat) {
var message,
msgDetail;
@ -26,7 +26,7 @@ var getRequestErrorMessage = function (request) {
message = request.responseJSON.errors.map(function (errorItem) {
return errorItem.message;
}).join('<br />');
});
} else {
message = request.responseJSON.error || 'Unknown Error';
}
@ -36,6 +36,15 @@ var getRequestErrorMessage = function (request) {
}
}
if (performConcat && Ember.isArray(message)) {
message = message.join('<br />');
}
// return an array of errors by default
if (!performConcat && typeof message === 'string') {
message = [message];
}
return message;
};

View file

@ -1,5 +1,5 @@
var ForgotValidator = Ember.Object.create({
validate: function (model) {
check: function (model) {
var data = model.getProperties('email'),
validationErrors = [];

View file

@ -1,5 +1,5 @@
var PostValidator = Ember.Object.create({
validate: function (model) {
check: function (model) {
var validationErrors = [],
title = model.get('title');

View file

@ -1,5 +1,5 @@
var ResetValidator = Ember.Object.create({
validate: function (model) {
check: function (model) {
var data = model.getProperties('passwords'),
p1 = data.passwords.newPassword,

View file

@ -1,5 +1,5 @@
var SettingValidator = Ember.Object.create({
validate: function (model) {
check: function (model) {
var validationErrors = [],
title = model.get('title'),
description = model.get('description'),

View file

@ -1,5 +1,5 @@
var SetupValidator = Ember.Object.create({
validate: function (model) {
check: function (model) {
var data = model.getProperties('blogTitle', 'name', 'email', 'password'),
validationErrors = [];
@ -8,7 +8,7 @@ var SetupValidator = Ember.Object.create({
message: 'Please enter a blog title.'
});
}
if (!validator.isLength(data.name || '', 1)) {
validationErrors.push({
message: 'Please enter a name.'

View file

@ -1,5 +1,5 @@
var SigninValidator = Ember.Object.create({
validate: function (model) {
check: function (model) {
var data = model.getProperties('identification', 'password'),
validationErrors = [];

View file

@ -1,8 +1,8 @@
var SignupValidator = Ember.Object.create({
validate: function (model) {
check: function (model) {
var data = model.getProperties('name', 'email', 'password'),
validationErrors = [];
if (!validator.isLength(data.name || '', 1)) {
validationErrors.push({
message: 'Please enter a name.'