From af47f88648f0a8ab72b352e0b7e321ad745e4d4e Mon Sep 17 00:00:00 2001 From: David Arvelo Date: Fri, 20 Jun 2014 17:36:44 -0400 Subject: [PATCH] Add Validations Layer and Post Validations closes #2893, issue #2850, issue #2856 - this is a stable, but quick and dirty validations layer for the time constraints - this could be replaced with a unified server/client layer later. the infrastructure is there. - create a validation engine mixin to match validators with models - override the save method in the mixin to perform validations first - create a post validator - fixup calls to .save() to make sure they catch errors properly --- ghost/admin/controllers/post-settings-menu.js | 18 +++-- ghost/admin/controllers/posts/post.js | 4 +- ghost/admin/mixins/editor-base-controller.js | 10 ++- ghost/admin/mixins/validation-engine.js | 69 +++++++++++++++++++ ghost/admin/models/post.js | 18 ++--- ghost/admin/utils/validator-extensions.js | 15 ++++ ghost/admin/validators/post.js | 17 +++++ 7 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 ghost/admin/mixins/validation-engine.js create mode 100644 ghost/admin/utils/validator-extensions.js create mode 100644 ghost/admin/validators/post.js diff --git a/ghost/admin/controllers/post-settings-menu.js b/ghost/admin/controllers/post-settings-menu.js index 79c0ce7067..845df72167 100644 --- a/ghost/admin/controllers/post-settings-menu.js +++ b/ghost/admin/controllers/post-settings-menu.js @@ -23,7 +23,9 @@ var PostSettingsMenuController = Ember.ObjectController.extend({ self.notifications.showSuccess('Successfully converted to ' + (val ? 'static page' : 'post')); return self.get('page'); - }, this.notifications.showErrors); + }, function (errors) { + self.notifications.showErrors(errors); + }); } return this.get('page'); @@ -103,7 +105,7 @@ var PostSettingsMenuController = Ember.ObjectController.extend({ // Because the server transforms the candidate slug by stripping // certain characters and appending a number onto the end of slugs - // to enforce uniqueness, there are cases where we can get back a + // to enforce uniqueness, there are cases where we can get back a // candidate slug that is a duplicate of the original except for // the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2) @@ -135,7 +137,9 @@ var PostSettingsMenuController = Ember.ObjectController.extend({ return self.get('model').save().then(function () { self.notifications.showSuccess('Permalink successfully changed to ' + self.get('slug') + '.'); - }, self.notifications.showErrors); + }, function (errors) { + self.notifications.showErrors(errors); + }); }); }, @@ -183,12 +187,12 @@ var PostSettingsMenuController = Ember.ObjectController.extend({ //Validation complete this.set('published_at', newPublishedAt); - //@ TODO: Make sure we're saving ONLY the publish date here, - // Don't want to accidentally save text the user's been working on. - this.get('model').save('published_at').then(function () { + this.get('model').save().then(function () { self.notifications.showSuccess('Publish date successfully changed to ' + formatDate(self.get('published_at')) + '.'); - }, this.notifications.showErrors); + }, function (errors) { + self.notifications.showErrors(errors); + }); } } }); diff --git a/ghost/admin/controllers/posts/post.js b/ghost/admin/controllers/posts/post.js index d7a4f0e55b..ac30b72fd0 100644 --- a/ghost/admin/controllers/posts/post.js +++ b/ghost/admin/controllers/posts/post.js @@ -9,8 +9,8 @@ var PostController = Ember.ObjectController.extend({ this.get('model').save().then(function () { self.notifications.showSuccess('Post successfully marked as ' + (featured ? 'featured' : 'not featured') + '.'); - }).catch(function () { - self.notifications.showError('An error occured while saving the post.'); + }).catch(function (errors) { + self.notifications.showErrors(errors); }); } } diff --git a/ghost/admin/mixins/editor-base-controller.js b/ghost/admin/mixins/editor-base-controller.js index fe09e01723..f6d664ae98 100644 --- a/ghost/admin/mixins/editor-base-controller.js +++ b/ghost/admin/mixins/editor-base-controller.js @@ -109,22 +109,26 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { actions: { save: function () { var status = this.get('willPublish') ? 'published' : 'draft', + model = this.get('model'), self = this; // set markdown equal to what's in the editor, minus the image markers. this.set('markdown', this.getMarkdown().withoutMarkers); this.set('status', status); - return this.get('model').save().then(function (model) { + + return model.save().then(function () { model.updateTags(); // `updateTags` triggers `isDirty => true`. // for a saved model it would otherwise be false. self.set('isDirty', false); self.notifications.showSuccess('Post status saved as ' + - model.get('status') + '.'); + model.get('status') + '.'); return model; - }, this.notifications.showErrors); + }, function (errors) { + self.notifications.showErrors(errors); + }); }, setSaveType: function (newType) { diff --git a/ghost/admin/mixins/validation-engine.js b/ghost/admin/mixins/validation-engine.js new file mode 100644 index 0000000000..611e89c3f2 --- /dev/null +++ b/ghost/admin/mixins/validation-engine.js @@ -0,0 +1,69 @@ +import { getRequestErrorMessage } from 'ghost/utils/ajax'; + +import ValidatorExtensions from 'ghost/utils/validator-extensions'; +import PostValidator from 'ghost/validators/post'; + +ValidatorExtensions.init(); + +var ValidationEngine = Ember.Mixin.create({ + validators: { + post: PostValidator + }, + + validate: function () { + var self = this, + type = this.get('validationType'), + validator = this.get('validators.' + type); + + return new Ember.RSVP.Promise(function (resolve, reject) { + if (!type || !validator) { + return reject('The validator specified, "' + type + '", did not exist!'); + } + + var validationErrors = validator.validate(self); + + if (Ember.isEmpty(validationErrors)) { + return resolve(); + } + + return reject(validationErrors); + }); + }, + + // override save to do validation first + save: function () { + 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; + + // If validation fails, reject with validation errors. + // If save to the server fails, reject with server response. + return this.validate().then(function () { + return _super.call(self); + }).catch(function (result) { + var message = 'There was an error saving this ' + self.get('validationType'); + + if (Ember.isArray(result)) { + // get validation error messages + message += ': ' + result.mapBy('message').join(' '); + } else if (typeof result === 'object') { + // Get messages from server response + message += ': ' + getRequestErrorMessage(result); + } else if (typeof result === 'string') { + message += ': ' + result; + } else { + message += '.'; + } + + // set format for notifications.showErrors + message = [{ message: message }]; + + return new Ember.RSVP.Promise(function (resolve, reject) { + reject(message); + }); + }); + } +}); + +export default ValidationEngine; diff --git a/ghost/admin/models/post.js b/ghost/admin/models/post.js index 4e34c20895..36eacb8077 100644 --- a/ghost/admin/models/post.js +++ b/ghost/admin/models/post.js @@ -1,4 +1,8 @@ -var Post = DS.Model.extend({ +import ValidationEngine from 'ghost/mixins/validation-engine'; + +var Post = DS.Model.extend(ValidationEngine, { + validationType: 'post', + uuid: DS.attr('string'), title: DS.attr('string'), slug: DS.attr('string'), @@ -24,18 +28,6 @@ var Post = DS.Model.extend({ isPublished: Ember.computed.equal('status', 'published'), isDraft: Ember.computed.equal('status', 'draft'), - validate: function () { - var validationErrors = []; - - if (!this.get('title.length')) { - validationErrors.push({ - message: 'You must specify a title for the post.' - }); - } - - return validationErrors; - }.property('title'), - // remove client-generated tags, which have `id: null`. // Ember Data won't recognize/update them automatically // when returned from the server with ids. diff --git a/ghost/admin/utils/validator-extensions.js b/ghost/admin/utils/validator-extensions.js new file mode 100644 index 0000000000..3319034f3f --- /dev/null +++ b/ghost/admin/utils/validator-extensions.js @@ -0,0 +1,15 @@ +function init() { + // Provide a few custom validators + // + validator.extend('empty', function (str) { + return Ember.isBlank(str); + }); + + validator.extend('notContains', function (str, badString) { + return !_.contains(str, badString); + }); +} + +export default { + init: init +}; diff --git a/ghost/admin/validators/post.js b/ghost/admin/validators/post.js new file mode 100644 index 0000000000..eea96ec593 --- /dev/null +++ b/ghost/admin/validators/post.js @@ -0,0 +1,17 @@ +var PostValidator = Ember.Object.create({ + validate: function (model) { + var validationErrors = [], + + title = model.get('title'); + + if (validator.empty(title)) { + validationErrors.push({ + message: 'You must specify a title for the post.' + }); + } + + return validationErrors; + } +}); + +export default PostValidator;