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:
parent
c35884ea6e
commit
78affdedb1
10 changed files with 116 additions and 54 deletions
|
@ -92,7 +92,7 @@ var PostsController = Ember.ArrayController.extend({
|
|||
|
||||
if (response) {
|
||||
// Get message from response
|
||||
message += ': ' + getRequestErrorMessage(response);
|
||||
message += ': ' + getRequestErrorMessage(response, true);
|
||||
} else {
|
||||
message += '.';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var ForgotValidator = Ember.Object.create({
|
||||
validate: function (model) {
|
||||
check: function (model) {
|
||||
var data = model.getProperties('email'),
|
||||
validationErrors = [];
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var PostValidator = Ember.Object.create({
|
||||
validate: function (model) {
|
||||
check: function (model) {
|
||||
var validationErrors = [],
|
||||
|
||||
title = model.get('title');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var ResetValidator = Ember.Object.create({
|
||||
validate: function (model) {
|
||||
check: function (model) {
|
||||
|
||||
var data = model.getProperties('passwords'),
|
||||
p1 = data.passwords.newPassword,
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var SigninValidator = Ember.Object.create({
|
||||
validate: function (model) {
|
||||
check: function (model) {
|
||||
var data = model.getProperties('identification', 'password'),
|
||||
validationErrors = [];
|
||||
|
||||
|
|
|
@ -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.'
|
||||
|
|
Loading…
Add table
Reference in a new issue