Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
// # Post Model
|
2016-09-26 16:23:49 +03:00
|
|
|
var _ = require('lodash'),
|
|
|
|
uuid = require('node-uuid'),
|
|
|
|
moment = require('moment'),
|
|
|
|
Promise = require('bluebird'),
|
|
|
|
sequence = require('../utils/sequence'),
|
|
|
|
errors = require('../errors'),
|
|
|
|
Showdown = require('showdown-ghost'),
|
|
|
|
legacyConverter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']}),
|
|
|
|
Mobiledoc = require('mobiledoc-html-renderer').default,
|
Upgraded editor (#7516)
refs #7429
Finally it's starting to feel like a real editor, although there will be another version bump over the weekend which improves the toolbar behaviour and usability, and enables image uploading.
- Added the start of a new toolbar, what we're (well I am) calling the Owesome bar, not to be confused with the Firefox Awesome bar. It's a cultural thing. (google "O for awesome").
- The idea of dragging and dropping cards has been removed for now, although the code will still be in there as we will support dragging cards around fairly shortly. When apps are included a better card interface will be required for a larger amount of app created content cards (Oh yeah!)
- Ghost Server now pulls in it's configuration from Ghost-Editor, this allows Ghost-Editor to a) keep cards up to date, and b) define what happens if a card is missing.
- The whole cards in admin written in ember and cards in server written in javascript thing is still very much a work in progress, it's kind of messy as we find the optimum solution (which isn't the current sollution).
So yeah, this is a WIP not the final styling, not the final interactions, not the final anything... :)
Adds a new mobile doc editor which has:
- A new toolbar
- Basic image uploading capability
2016-10-10 09:09:32 +03:00
|
|
|
mobileDocOptions = require('ghost-editor').htmlOptions,
|
|
|
|
converter = new Mobiledoc(mobileDocOptions),
|
2016-09-26 16:23:49 +03:00
|
|
|
ghostBookshelf = require('./base'),
|
|
|
|
events = require('../events'),
|
|
|
|
config = require('../config'),
|
|
|
|
utils = require('../utils'),
|
|
|
|
baseUtils = require('./base/utils'),
|
|
|
|
i18n = require('../i18n'),
|
2014-02-19 17:57:26 +04:00
|
|
|
Post,
|
2014-02-22 01:54:56 +04:00
|
|
|
Posts;
|
2013-06-25 15:43:15 +04:00
|
|
|
|
2013-09-23 02:20:08 +04:00
|
|
|
Post = ghostBookshelf.Model.extend({
|
2013-06-25 15:43:15 +04:00
|
|
|
|
|
|
|
tableName: 'posts',
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
emitChange: function emitChange(event, usePreviousResourceType) {
|
2015-03-24 23:23:23 +03:00
|
|
|
var resourceType = this.get('page') ? 'page' : 'post';
|
|
|
|
if (usePreviousResourceType) {
|
|
|
|
resourceType = this.updated('page') ? 'page' : 'post';
|
|
|
|
}
|
|
|
|
events.emit(resourceType + '.' + event, this);
|
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
defaults: function defaults() {
|
2013-06-25 15:43:15 +04:00
|
|
|
return {
|
|
|
|
uuid: uuid.v4(),
|
2013-09-14 23:01:46 +04:00
|
|
|
status: 'draft'
|
2013-06-25 15:43:15 +04:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
onSaved: function onSaved(model, response, options) {
|
|
|
|
return this.updateTags(model, response, options);
|
|
|
|
},
|
2014-04-27 20:58:34 +04:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
onCreated: function onCreated(model) {
|
|
|
|
var status = model.get('status');
|
|
|
|
model.emitChange('added');
|
2014-04-27 20:58:34 +04:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (['published', 'scheduled'].indexOf(status) !== -1) {
|
|
|
|
model.emitChange(status);
|
|
|
|
}
|
|
|
|
},
|
2014-10-28 03:41:18 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
onUpdated: function onUpdated(model) {
|
|
|
|
model.statusChanging = model.get('status') !== model.updated('status');
|
|
|
|
model.isPublished = model.get('status') === 'published';
|
|
|
|
model.isScheduled = model.get('status') === 'scheduled';
|
|
|
|
model.wasPublished = model.updated('status') === 'published';
|
|
|
|
model.wasScheduled = model.updated('status') === 'scheduled';
|
|
|
|
model.resourceTypeChanging = model.get('page') !== model.updated('page');
|
|
|
|
model.publishedAtHasChanged = model.hasDateChanged('published_at');
|
|
|
|
model.needsReschedule = model.publishedAtHasChanged && model.isScheduled;
|
|
|
|
|
|
|
|
// Handle added and deleted for post -> page or page -> post
|
|
|
|
if (model.resourceTypeChanging) {
|
|
|
|
if (model.wasPublished) {
|
|
|
|
model.emitChange('unpublished', true);
|
|
|
|
}
|
2016-04-14 14:22:38 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (model.wasScheduled) {
|
|
|
|
model.emitChange('unscheduled', true);
|
|
|
|
}
|
|
|
|
|
|
|
|
model.emitChange('deleted', true);
|
2015-03-24 23:23:23 +03:00
|
|
|
model.emitChange('added');
|
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (model.isPublished) {
|
|
|
|
model.emitChange('published');
|
2014-10-28 03:41:18 +03:00
|
|
|
}
|
2015-03-24 23:23:23 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (model.isScheduled) {
|
|
|
|
model.emitChange('scheduled');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (model.statusChanging) {
|
|
|
|
// CASE: was published before and is now e.q. draft or scheduled
|
|
|
|
if (model.wasPublished) {
|
|
|
|
model.emitChange('unpublished');
|
2016-04-14 14:22:38 +03:00
|
|
|
}
|
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
// CASE: was draft or scheduled before and is now e.q. published
|
2015-03-24 23:23:23 +03:00
|
|
|
if (model.isPublished) {
|
|
|
|
model.emitChange('published');
|
|
|
|
}
|
2016-04-14 14:22:38 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
// CASE: was draft or published before and is now e.q. scheduled
|
2016-04-14 14:22:38 +03:00
|
|
|
if (model.isScheduled) {
|
|
|
|
model.emitChange('scheduled');
|
|
|
|
}
|
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
// CASE: from scheduled to something
|
|
|
|
if (model.wasScheduled && !model.isScheduled && !model.isPublished) {
|
|
|
|
model.emitChange('unscheduled');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (model.isPublished) {
|
|
|
|
model.emitChange('published.edited');
|
2015-03-24 23:23:23 +03:00
|
|
|
}
|
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (model.needsReschedule) {
|
|
|
|
model.emitChange('rescheduled');
|
|
|
|
}
|
2014-10-28 03:41:18 +03:00
|
|
|
}
|
2015-03-24 23:23:23 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
// Fire edited if this wasn't a change between resourceType
|
|
|
|
model.emitChange('edited');
|
|
|
|
}
|
|
|
|
},
|
2016-09-19 16:45:36 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
onDestroying: function onDestroying(model, options) {
|
|
|
|
return model.load('tags', options)
|
|
|
|
.then(function (response) {
|
|
|
|
if (!response.related || !response.related('tags') || !response.related('tags').length) {
|
|
|
|
return;
|
|
|
|
}
|
2016-09-19 16:45:36 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
return Promise.mapSeries(response.related('tags').models, function (tag) {
|
|
|
|
return baseUtils.tagUpdate.detachTagFromPost(model, tag, options)();
|
2016-09-19 16:45:36 +03:00
|
|
|
});
|
2016-10-14 15:37:01 +03:00
|
|
|
})
|
|
|
|
.then(function () {
|
|
|
|
if (model.previous('status') === 'published') {
|
|
|
|
model.emitChange('unpublished');
|
|
|
|
}
|
|
|
|
|
|
|
|
model.emitChange('deleted');
|
|
|
|
});
|
2013-08-20 22:52:44 +04:00
|
|
|
},
|
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
onSaving: function onSaving(model, attr, options) {
|
2016-04-14 14:22:38 +03:00
|
|
|
options = options || {};
|
|
|
|
|
2014-03-24 22:08:06 +04:00
|
|
|
var self = this,
|
2015-10-27 14:15:09 +03:00
|
|
|
title,
|
2016-03-23 16:17:01 +03:00
|
|
|
i,
|
|
|
|
// Variables to make the slug checking more readable
|
|
|
|
newTitle = this.get('title'),
|
2016-04-14 14:22:38 +03:00
|
|
|
newStatus = this.get('status'),
|
2016-05-19 14:49:22 +03:00
|
|
|
olderStatus = this.previous('status'),
|
2016-03-23 16:17:01 +03:00
|
|
|
prevTitle = this._previousAttributes.title,
|
|
|
|
prevSlug = this._previousAttributes.slug,
|
2016-04-14 14:22:38 +03:00
|
|
|
tagsToCheck = this.get('tags'),
|
2016-06-02 20:11:54 +03:00
|
|
|
publishedAt = this.get('published_at'),
|
2016-05-19 14:49:22 +03:00
|
|
|
publishedAtHasChanged = this.hasDateChanged('published_at'),
|
2016-09-26 16:23:49 +03:00
|
|
|
mobiledoc = this.get('mobiledoc'),
|
2016-06-02 20:11:54 +03:00
|
|
|
tags = [];
|
2013-08-25 14:49:31 +04:00
|
|
|
|
2016-05-19 14:49:22 +03:00
|
|
|
// CASE: disallow published -> scheduled
|
|
|
|
// @TODO: remove when we have versioning based on updated_at
|
|
|
|
if (newStatus !== olderStatus && newStatus === 'scheduled' && olderStatus === 'published') {
|
2016-10-06 15:27:35 +03:00
|
|
|
return Promise.reject(new errors.ValidationError({
|
|
|
|
message: i18n.t('errors.models.post.isAlreadyPublished', {key: 'status'})
|
|
|
|
}));
|
2016-05-19 14:49:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// CASE: both page and post can get scheduled
|
2016-04-14 14:22:38 +03:00
|
|
|
if (newStatus === 'scheduled') {
|
|
|
|
if (!publishedAt) {
|
2016-10-06 15:27:35 +03:00
|
|
|
return Promise.reject(new errors.ValidationError({
|
|
|
|
message: i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'})
|
|
|
|
}));
|
2016-04-14 14:22:38 +03:00
|
|
|
} else if (!moment(publishedAt).isValid()) {
|
2016-10-06 15:27:35 +03:00
|
|
|
return Promise.reject(new errors.ValidationError({
|
|
|
|
message: i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'})
|
|
|
|
}));
|
2016-05-19 14:49:22 +03:00
|
|
|
// CASE: to schedule/reschedule a post, a minimum diff of x minutes is needed (default configured is 2minutes)
|
2016-09-13 18:41:14 +03:00
|
|
|
} else if (publishedAtHasChanged && moment(publishedAt).isBefore(moment().add(config.get('times').cannotScheduleAPostBeforeInMinutes, 'minutes'))) {
|
2016-10-06 15:27:35 +03:00
|
|
|
return Promise.reject(new errors.ValidationError({
|
|
|
|
message: i18n.t('errors.models.post.expectedPublishedAtInFuture', {
|
2016-09-13 18:41:14 +03:00
|
|
|
cannotScheduleAPostBeforeInMinutes: config.get('times').cannotScheduleAPostBeforeInMinutes
|
2016-05-19 14:49:22 +03:00
|
|
|
})
|
2016-10-06 15:27:35 +03:00
|
|
|
}));
|
2016-04-14 14:22:38 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-02 20:11:54 +03:00
|
|
|
// If we have a tags property passed in
|
|
|
|
if (!_.isUndefined(tagsToCheck) && !_.isNull(tagsToCheck)) {
|
|
|
|
// and deduplicate upper/lowercase tags
|
|
|
|
_.each(tagsToCheck, function each(item) {
|
|
|
|
for (i = 0; i < tags.length; i = i + 1) {
|
|
|
|
if (tags[i].name.toLocaleLowerCase() === item.name.toLocaleLowerCase()) {
|
|
|
|
return;
|
|
|
|
}
|
2014-03-24 22:08:06 +04:00
|
|
|
}
|
2014-05-23 23:32:14 +04:00
|
|
|
|
2016-06-02 20:11:54 +03:00
|
|
|
tags.push(item);
|
|
|
|
});
|
|
|
|
|
|
|
|
// keep tags for 'saved' event
|
|
|
|
this.tagsToSave = tags;
|
|
|
|
}
|
2014-02-19 17:57:26 +04:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
ghostBookshelf.Model.prototype.onSaving.call(this, model, attr, options);
|
2013-08-25 14:49:31 +04:00
|
|
|
|
2016-09-26 16:23:49 +03:00
|
|
|
if (mobiledoc) {
|
|
|
|
this.set('html', converter.render(JSON.parse(mobiledoc)).result);
|
|
|
|
} else {
|
|
|
|
// legacy showdown mode
|
|
|
|
this.set('html', legacyConverter.makeHtml(_.toString(this.get('markdown'))));
|
|
|
|
}
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2014-01-07 00:17:20 +04:00
|
|
|
// disabling sanitization until we can implement a better version
|
2015-11-12 15:29:45 +03:00
|
|
|
title = this.get('title') || i18n.t('errors.models.post.untitled');
|
2016-06-03 11:06:18 +03:00
|
|
|
this.set('title', _.toString(title).trim());
|
2013-08-30 08:18:55 +04:00
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
// ### Business logic for published_at and published_by
|
|
|
|
// If the current status is 'published' and published_at is not set, set it to now
|
2016-04-14 14:22:38 +03:00
|
|
|
if (newStatus === 'published' && !publishedAt) {
|
2015-09-25 19:11:22 +03:00
|
|
|
this.set('published_at', new Date());
|
|
|
|
}
|
2015-02-22 00:04:00 +03:00
|
|
|
|
2015-09-25 19:11:22 +03:00
|
|
|
// If the current status is 'published' and the status has just changed ensure published_by is set correctly
|
2016-04-14 14:22:38 +03:00
|
|
|
if (newStatus === 'published' && this.hasChanged('status')) {
|
2015-02-22 00:04:00 +03:00
|
|
|
// unless published_by is set and we're importing, set published_by to contextUser
|
|
|
|
if (!(this.get('published_by') && options.importing)) {
|
2014-08-09 23:16:54 +04:00
|
|
|
this.set('published_by', this.contextUser(options));
|
|
|
|
}
|
2015-09-25 19:11:22 +03:00
|
|
|
} else {
|
|
|
|
// In any other case (except import), `published_by` should not be changed
|
|
|
|
if (this.hasChanged('published_by') && !options.importing) {
|
|
|
|
this.set('published_by', this.previous('published_by'));
|
|
|
|
}
|
2013-06-25 15:43:15 +04:00
|
|
|
}
|
2013-06-15 20:11:15 +04:00
|
|
|
|
2016-03-23 16:17:01 +03:00
|
|
|
// If a title is set, not the same as the old title, a draft post, and has never been published
|
2016-04-14 14:22:38 +03:00
|
|
|
if (prevTitle !== undefined && newTitle !== prevTitle && newStatus === 'draft' && !publishedAt) {
|
2013-09-14 23:01:46 +04:00
|
|
|
// Pass the new slug through the generator to strip illegal characters, detect duplicates
|
2016-03-23 16:17:01 +03:00
|
|
|
return ghostBookshelf.Model.generateSlug(Post, this.get('title'),
|
2015-09-23 13:54:56 +03:00
|
|
|
{status: 'all', transacting: options.transacting, importing: options.importing})
|
2015-06-14 18:58:49 +03:00
|
|
|
.then(function then(slug) {
|
2016-03-23 16:17:01 +03:00
|
|
|
// After the new slug is found, do another generate for the old title to compare it to the old slug
|
|
|
|
return ghostBookshelf.Model.generateSlug(Post, prevTitle).then(function then(prevTitleSlug) {
|
|
|
|
// If the old slug is the same as the slug that was generated from the old title
|
|
|
|
// then set a new slug. If it is not the same, means was set by the user
|
|
|
|
if (prevTitleSlug === prevSlug) {
|
|
|
|
self.set({slug: slug});
|
|
|
|
}
|
|
|
|
});
|
2013-09-14 23:01:46 +04:00
|
|
|
});
|
2016-03-23 16:17:01 +03:00
|
|
|
} else {
|
|
|
|
// If any of the attributes above were false, set initial slug and check to see if slug was changed by the user
|
|
|
|
if (this.hasChanged('slug') || !this.get('slug')) {
|
|
|
|
// Pass the new slug through the generator to strip illegal characters, detect duplicates
|
|
|
|
return ghostBookshelf.Model.generateSlug(Post, this.get('slug') || this.get('title'),
|
|
|
|
{status: 'all', transacting: options.transacting, importing: options.importing})
|
|
|
|
.then(function then(slug) {
|
|
|
|
self.set({slug: slug});
|
|
|
|
});
|
|
|
|
}
|
2013-09-14 23:01:46 +04:00
|
|
|
}
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
onCreating: function onCreating(model, attr, options) {
|
2014-04-03 17:03:09 +04:00
|
|
|
options = options || {};
|
2013-06-18 02:59:08 +04:00
|
|
|
|
2014-02-19 17:57:26 +04:00
|
|
|
// set any dynamic default properties
|
2013-06-25 15:43:15 +04:00
|
|
|
if (!this.get('author_id')) {
|
2014-07-15 15:03:12 +04:00
|
|
|
this.set('author_id', this.contextUser(options));
|
2013-06-25 15:43:15 +04:00
|
|
|
}
|
2013-06-26 05:42:51 +04:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
ghostBookshelf.Model.prototype.onCreating.call(this, model, attr, options);
|
2013-06-25 15:43:15 +04:00
|
|
|
},
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2014-12-10 17:03:39 +03:00
|
|
|
/**
|
2014-05-10 08:55:46 +04:00
|
|
|
* ### updateTags
|
|
|
|
* Update tags that are attached to a post. Create any tags that don't already exist.
|
2014-12-21 21:45:30 +03:00
|
|
|
* @param {Object} savedModel
|
|
|
|
* @param {Object} response
|
2014-05-10 08:55:46 +04:00
|
|
|
* @param {Object} options
|
|
|
|
* @return {Promise(ghostBookshelf.Models.Post)} Updated Post model
|
|
|
|
*/
|
2015-06-14 18:58:49 +03:00
|
|
|
updateTags: function updateTags(savedModel, response, options) {
|
2016-06-02 20:11:54 +03:00
|
|
|
if (_.isUndefined(this.tagsToSave)) {
|
|
|
|
// The tag property was not set, so we shouldn't be doing any playing with tags on this request
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
var newTags = this.tagsToSave,
|
2015-09-01 03:51:56 +03:00
|
|
|
TagModel = ghostBookshelf.model('Tag');
|
2013-09-14 00:38:53 +04:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
options = options || {};
|
2013-08-21 16:55:58 +04:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
function doTagUpdates(options) {
|
|
|
|
return Promise.props({
|
|
|
|
currentPost: baseUtils.tagUpdate.fetchCurrentPost(Post, savedModel.id, options),
|
|
|
|
existingTags: baseUtils.tagUpdate.fetchMatchingTags(TagModel, newTags, options)
|
|
|
|
}).then(function fetchedData(results) {
|
|
|
|
var currentTags = results.currentPost.related('tags').toJSON(options),
|
|
|
|
existingTags = results.existingTags ? results.existingTags.toJSON(options) : [],
|
|
|
|
tagOps = [],
|
|
|
|
tagsToRemove,
|
|
|
|
tagsToCreate;
|
|
|
|
|
|
|
|
if (baseUtils.tagUpdate.tagSetsAreEqual(newTags, currentTags)) {
|
|
|
|
return;
|
|
|
|
}
|
2013-08-21 16:55:58 +04:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
// Tags from the current tag array which don't exist in the new tag array should be removed
|
|
|
|
tagsToRemove = _.reject(currentTags, function (currentTag) {
|
|
|
|
if (newTags.length === 0) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-06-11 21:23:27 +03:00
|
|
|
return _.some(newTags, function (newTag) {
|
2015-09-01 03:51:56 +03:00
|
|
|
return baseUtils.tagUpdate.tagsAreEqual(currentTag, newTag);
|
|
|
|
});
|
|
|
|
});
|
2014-01-13 18:29:40 +04:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
// Tags from the new tag array which don't exist in the DB should be created
|
2016-06-11 21:23:27 +03:00
|
|
|
tagsToCreate = _.map(_.reject(newTags, function (newTag) {
|
|
|
|
return _.some(existingTags, function (existingTag) {
|
2015-09-01 03:51:56 +03:00
|
|
|
return baseUtils.tagUpdate.tagsAreEqual(existingTag, newTag);
|
2013-09-14 00:38:53 +04:00
|
|
|
});
|
2015-09-01 03:51:56 +03:00
|
|
|
}), 'name');
|
|
|
|
|
|
|
|
// Remove any tags which don't exist anymore
|
|
|
|
_.each(tagsToRemove, function (tag) {
|
|
|
|
tagOps.push(baseUtils.tagUpdate.detachTagFromPost(savedModel, tag, options));
|
2014-05-10 08:55:46 +04:00
|
|
|
});
|
2013-08-21 16:55:58 +04:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
// Loop through the new tags and either add them, attach them, or update them
|
|
|
|
_.each(newTags, function (newTag, index) {
|
|
|
|
var tag;
|
|
|
|
|
|
|
|
if (tagsToCreate.indexOf(newTag.name) > -1) {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
tagOps.push(baseUtils.tagUpdate.createTagThenAttachTagToPost(Post, TagModel, savedModel, newTag, index, options));
|
2015-09-01 03:51:56 +03:00
|
|
|
} else {
|
|
|
|
// try to find a tag on the current post which matches
|
|
|
|
tag = _.find(currentTags, function (currentTag) {
|
|
|
|
return baseUtils.tagUpdate.tagsAreEqual(currentTag, newTag);
|
2014-12-21 21:45:30 +03:00
|
|
|
});
|
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
if (tag) {
|
|
|
|
tagOps.push(baseUtils.tagUpdate.updateTagOrderForPost(savedModel, tag, index, options));
|
|
|
|
return;
|
|
|
|
}
|
2014-01-13 18:29:40 +04:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
// else finally, find the existing tag which matches
|
|
|
|
tag = _.find(existingTags, function (existingTag) {
|
|
|
|
return baseUtils.tagUpdate.tagsAreEqual(existingTag, newTag);
|
|
|
|
});
|
2014-12-21 21:45:30 +03:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
if (tag) {
|
✨ replace auto increment id's by object id (#7495)
* 🛠 bookshelf tarball, bson-objectid
* 🎨 schema changes
- change increment type to string
- add a default fallback for string length 191 (to avoid adding this logic to every single column which uses an ID)
- remove uuid, because ID now represents a global resource identifier
- keep uuid for post, because we are using this as preview id
- keep uuid for clients for now - we are using this param for Ghost-Auth
* ✨ base model: generate ObjectId on creating event
- each new resource get's a auto generate ObjectId
- this logic won't work for attached models, this commit comes later
* 🎨 centralised attach method
When attaching models there are two things important two know
1. To be able to attach an ObjectId, we need to register the `onCreating` event the fetched model!This is caused by the Bookshelf design in general. On this target model we are attaching the new model.
2. We need to manually fetch the target model, because Bookshelf has a weird behaviour (which is known as a bug, see see https://github.com/tgriesser/bookshelf/issues/629). The most important property when attaching a model is `parentFk`, which is the foreign key. This can be null when fetching the model with the option `withRelated`. To ensure quality and consistency, the custom attach wrapper always fetches the target model manual. By fetching the target model (again) is a little performance decrease, but it also has advantages: we can register the event, and directly unregister the event again. So very clean code.
Important: please only use the custom attach wrapper in the future.
* 🎨 token model had overriden the onCreating function because of the created_at field
- we need to ensure that the base onCreating hook get's triggered for ALL models
- if not, they don't get an ObjectId assigned
- in this case: be smart and check if the target model has a created_at field
* 🎨 we don't have a uuid field anymore, remove the usages
- no default uuid creation in models
- i am pretty sure we have some more definitions in our tests (for example in the export json files), but that is too much work to delete them all
* 🎨 do not parse ID to Number
- we had various occurances of parsing all ID's to numbers
- we don't need this behaviour anymore
- ID is string
- i will adapt the ID validation in the next commit
* 🎨 change ID regex for validation
- we only allow: ID as ObjectId, ID as 1 and ID as me
- we need to keep ID 1, because our whole software relies on ID 1 (permissions etc)
* 🎨 owner fixture
- roles: [4] does not work anymore
- 4 means -> static id 4
- this worked in an auto increment system (not even in a system with distributed writes)
- with ObjectId we generate each ID automatically (for static and dynamic resources)
- it is possible to define all id's for static resources still, but that means we need to know which ID is already used and for consistency we have to define ObjectId's for these static resources
- so no static id's anymore, except of: id 1 for owner and id 0 for external usage (because this is required from our permission system)
- NOTE: please read through the comment in the user model
* 🎨 tests: DataGenerator and test utils
First of all: we need to ensure using ObjectId's in the tests. When don't, we can't ensure that ObjectId's work properly.
This commit brings lot's of dynamic into all the static defined id's.
In one of the next commits, i will adapt all the tests.
* 🚨 remove counter in Notification API
- no need to add a counter
- we simply generate ObjectId's (they are auto incremental as well)
- our id validator does only allow ObjectId as id,1 and me
* 🎨 extend contextUser in Base Model
- remove isNumber check, because id's are no longer numbers, except of id 0/1
- use existing isExternalUser
- support id 0/1 as string or number
* ✨ Ghost Owner has id 1
- ensure we define this id in the fixtures.json
- doesn't matter if number or string
* 🎨 functional tests adaptions
- use dynamic id's
* 🎨 fix unit tests
* 🎨 integration tests adaptions
* 🎨 change importer utils
- all our export examples (test/fixtures/exports) contain id's as numbers
- fact: but we ignore them anyway when inserting into the database, see https://github.com/TryGhost/Ghost/blob/master/core/server/data/import/utils.js#L249
- in https://github.com/TryGhost/Ghost/pull/7495/commits/0e6ed957cd54dc02a25cf6fb1ab7d7e723295e2c#diff-70f514a06347c048648be464819503c4L67 i removed parsing id's to integers
- i realised that this ^ check just existed, because the userIdToMap was an object key and object keys are always strings!
- i think this logic is a little bit complicated, but i don't want to refactor this now
- this commit ensures when trying to find the user, the id comparison works again
- i've added more documentation to understand this logic ;)
- plus i renamed an attribute to improve readability
* 🎨 Data-Generator: add more defaults to createUser
- if i use the function DataGenerator.forKnex.createUser i would like to get a full set of defaults
* 🎨 test utils: change/extend function set for functional tests
- functional tests work a bit different
- they boot Ghost and seed the database
- some functional tests have mis-used the test setup
- the test setup needs two sections: integration/unit and functional tests
- any functional test is allowed to either add more data or change data in the existing Ghost db
- but what it should not do is: add test fixtures like roles or users from our DataGenerator and cross fingers it will work
- this commit adds a clean method for functional tests to add extra users
* 🎨 functional tests adaptions
- use last commit to insert users for functional tests clean
- tidy up usage of testUtils.setup or testUtils.doAuth
* 🐛 test utils: reset database before init
- ensure we don't have any left data from other tests in the database when starting ghost
* 🐛 fix test (unrelated to this PR)
- fixes a random failure
- return statement was missing
* 🎨 make changes for invites
2016-11-17 12:09:11 +03:00
|
|
|
tagOps.push(baseUtils.tagUpdate.attachTagToPost(Post, savedModel.id, tag, index, options));
|
2015-09-01 03:51:56 +03:00
|
|
|
}
|
|
|
|
}
|
2013-09-14 00:38:53 +04:00
|
|
|
});
|
2013-08-21 16:55:58 +04:00
|
|
|
|
2015-09-01 03:51:56 +03:00
|
|
|
return sequence(tagOps);
|
2014-05-10 08:55:46 +04:00
|
|
|
});
|
2015-09-01 03:51:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Handle updating tags in a transaction, unless we're already in one
|
|
|
|
if (options.transacting) {
|
|
|
|
return doTagUpdates(options);
|
|
|
|
} else {
|
|
|
|
return ghostBookshelf.transaction(function (t) {
|
|
|
|
options.transacting = t;
|
|
|
|
|
|
|
|
return doTagUpdates(options);
|
|
|
|
}).then(function () {
|
|
|
|
// Don't do anything, the transaction processed ok
|
2016-10-06 15:27:35 +03:00
|
|
|
}).catch(function failure(err) {
|
|
|
|
return Promise.reject(new errors.GhostError({
|
|
|
|
err: err,
|
|
|
|
context: i18n.t('errors.models.post.tagUpdates.error'),
|
|
|
|
help: i18n.t('errors.models.post.tagUpdates.help')
|
|
|
|
}));
|
2015-09-01 03:51:56 +03:00
|
|
|
});
|
|
|
|
}
|
2013-08-21 16:55:58 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
// Relations
|
2015-06-24 12:00:24 +03:00
|
|
|
author: function author() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.belongsTo('User', 'author_id');
|
2013-08-21 16:55:58 +04:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
created_by: function createdBy() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.belongsTo('User', 'created_by');
|
2014-04-27 20:58:34 +04:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
updated_by: function updatedBy() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.belongsTo('User', 'updated_by');
|
2014-04-27 20:58:34 +04:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
published_by: function publishedBy() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.belongsTo('User', 'published_by');
|
2014-04-27 20:58:34 +04:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
tags: function tags() {
|
2015-09-01 03:51:56 +03:00
|
|
|
return this.belongsToMany('Tag').withPivot('sort_order').query('orderBy', 'sort_order', 'ASC');
|
2014-02-25 00:28:18 +04:00
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
fields: function fields() {
|
2014-07-13 15:17:18 +04:00
|
|
|
return this.morphMany('AppField', 'relatable');
|
2014-04-22 05:08:11 +04:00
|
|
|
},
|
|
|
|
|
2016-07-18 23:21:47 +03:00
|
|
|
defaultColumnsToFetch: function defaultColumnsToFetch() {
|
|
|
|
return ['id', 'published_at', 'slug', 'author_id'];
|
|
|
|
},
|
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
toJSON: function toJSON(options) {
|
2015-07-04 21:27:23 +03:00
|
|
|
options = options || {};
|
|
|
|
|
2014-04-22 05:08:11 +04:00
|
|
|
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
|
|
|
|
2015-07-04 21:27:23 +03:00
|
|
|
if (!options.columns || (options.columns && options.columns.indexOf('author') > -1)) {
|
|
|
|
attrs.author = attrs.author || attrs.author_id;
|
|
|
|
delete attrs.author_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!options.columns || (options.columns && options.columns.indexOf('url') > -1)) {
|
2016-09-09 13:23:47 +03:00
|
|
|
attrs.url = utils.url.urlPathForPost(attrs);
|
2015-07-04 21:27:23 +03:00
|
|
|
}
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
|
2014-04-22 05:08:11 +04:00
|
|
|
return attrs;
|
2015-11-11 20:52:44 +03:00
|
|
|
},
|
|
|
|
enforcedFilters: function enforcedFilters() {
|
|
|
|
return this.isPublicContext() ? 'status:published' : null;
|
|
|
|
},
|
|
|
|
defaultFilters: function defaultFilters() {
|
2016-04-14 18:54:49 +03:00
|
|
|
if (this.isInternalContext()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2015-11-11 20:52:44 +03:00
|
|
|
return this.isPublicContext() ? 'page:false' : 'page:false+status:published';
|
2013-06-25 15:43:15 +04:00
|
|
|
}
|
|
|
|
}, {
|
2015-06-17 16:55:39 +03:00
|
|
|
orderDefaultOptions: function orderDefaultOptions() {
|
|
|
|
return {
|
|
|
|
status: 'ASC',
|
|
|
|
published_at: 'DESC',
|
|
|
|
updated_at: 'DESC',
|
|
|
|
id: 'DESC'
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-07-15 13:04:10 +03:00
|
|
|
orderDefaultRaw: function () {
|
|
|
|
return '' +
|
|
|
|
'CASE WHEN posts.status = \'scheduled\' THEN 1 ' +
|
|
|
|
'WHEN posts.status = \'draft\' THEN 2 ' +
|
|
|
|
'ELSE 3 END ASC,' +
|
|
|
|
'posts.published_at DESC,' +
|
|
|
|
'posts.updated_at DESC,' +
|
|
|
|
'posts.id DESC';
|
|
|
|
},
|
|
|
|
|
2015-11-11 22:31:52 +03:00
|
|
|
/**
|
|
|
|
* @deprecated in favour of filter
|
|
|
|
*/
|
|
|
|
processOptions: function processOptions(options) {
|
|
|
|
if (!options.staticPages && !options.status) {
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is the only place that 'options.where' is set now
|
|
|
|
options.where = {statements: []};
|
|
|
|
|
2015-06-17 16:55:39 +03:00
|
|
|
// Step 4: Setup filters (where clauses)
|
2015-11-11 20:52:44 +03:00
|
|
|
if (options.staticPages && options.staticPages !== 'all') {
|
2015-06-17 16:55:39 +03:00
|
|
|
// convert string true/false to boolean
|
|
|
|
if (!_.isBoolean(options.staticPages)) {
|
2016-06-11 21:23:27 +03:00
|
|
|
options.staticPages = _.includes(['true', '1'], options.staticPages);
|
2015-06-17 16:55:39 +03:00
|
|
|
}
|
2015-11-11 22:31:52 +03:00
|
|
|
options.where.statements.push({prop: 'page', op: '=', value: options.staticPages});
|
|
|
|
delete options.staticPages;
|
|
|
|
} else if (options.staticPages === 'all') {
|
|
|
|
options.where.statements.push({prop: 'page', op: 'IN', value: [true, false]});
|
|
|
|
delete options.staticPages;
|
2015-06-17 16:55:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Unless `all` is passed as an option, filter on
|
|
|
|
// the status provided.
|
2015-11-11 20:52:44 +03:00
|
|
|
if (options.status && options.status !== 'all') {
|
2015-06-17 16:55:39 +03:00
|
|
|
// make sure that status is valid
|
2016-05-19 14:49:22 +03:00
|
|
|
options.status = _.includes(['published', 'draft', 'scheduled'], options.status) ? options.status : 'published';
|
2015-11-11 22:31:52 +03:00
|
|
|
options.where.statements.push({prop: 'status', op: '=', value: options.status});
|
|
|
|
delete options.status;
|
2015-11-11 20:52:44 +03:00
|
|
|
} else if (options.status === 'all') {
|
2016-05-19 14:49:22 +03:00
|
|
|
options.where.statements.push({prop: 'status', op: 'IN', value: ['published', 'draft', 'scheduled']});
|
2015-11-11 22:31:52 +03:00
|
|
|
delete options.status;
|
2015-06-17 16:55:39 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return options;
|
|
|
|
},
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2014-05-06 05:45:08 +04:00
|
|
|
/**
|
|
|
|
* Returns an array of keys permitted in a method's `options` hash, depending on the current method.
|
|
|
|
* @param {String} methodName The name of the method to check valid options for.
|
|
|
|
* @return {Array} Keys allowed in the `options` hash of the model's method.
|
|
|
|
*/
|
2015-06-14 18:58:49 +03:00
|
|
|
permittedOptions: function permittedOptions(methodName) {
|
2014-05-06 05:45:08 +04:00
|
|
|
var options = ghostBookshelf.Model.permittedOptions(),
|
|
|
|
|
|
|
|
// whitelists for the `options` hash argument on methods, by method name.
|
|
|
|
// these are the only options that can be passed to Bookshelf / Knex.
|
|
|
|
validOptions = {
|
2016-03-20 20:26:42 +03:00
|
|
|
findOne: ['columns', 'importing', 'withRelated', 'require'],
|
2015-10-22 15:49:15 +03:00
|
|
|
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status', 'staticPages'],
|
2016-06-03 11:06:18 +03:00
|
|
|
findAll: ['columns', 'filter']
|
2014-05-06 05:45:08 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
if (validOptions[methodName]) {
|
|
|
|
options = options.concat(validOptions[methodName]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return options;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Filters potentially unsafe model attributes, so you can pass them to Bookshelf / Knex.
|
|
|
|
* @param {Object} data Has keys representing the model's attributes/fields in the database.
|
|
|
|
* @return {Object} The filtered results of the passed in data, containing only what's allowed in the schema.
|
|
|
|
*/
|
2015-06-14 18:58:49 +03:00
|
|
|
filterData: function filterData(data) {
|
2014-05-06 05:45:08 +04:00
|
|
|
var permittedAttributes = this.prototype.permittedAttributes(),
|
|
|
|
filteredData;
|
|
|
|
|
|
|
|
// manually add 'tags' attribute since it's not in the schema
|
|
|
|
permittedAttributes.push('tags');
|
|
|
|
|
|
|
|
filteredData = _.pick(data, permittedAttributes);
|
|
|
|
|
|
|
|
return filteredData;
|
|
|
|
},
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
// ## Model Data Functions
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Find One
|
|
|
|
* @extends ghostBookshelf.Model.findOne to handle post status
|
|
|
|
* **See:** [ghostBookshelf.Model.findOne](base.js.html#Find%20One)
|
|
|
|
*/
|
2015-06-14 18:58:49 +03:00
|
|
|
findOne: function findOne(data, options) {
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
options = options || {};
|
2013-06-25 15:43:15 +04:00
|
|
|
|
2016-06-11 21:23:27 +03:00
|
|
|
var withNext = _.includes(options.include, 'next'),
|
|
|
|
withPrev = _.includes(options.include, 'previous'),
|
2015-08-09 22:30:04 +03:00
|
|
|
nextRelations = _.transform(options.include, function (relations, include) {
|
|
|
|
if (include === 'next.tags') {
|
|
|
|
relations.push('tags');
|
|
|
|
} else if (include === 'next.author') {
|
|
|
|
relations.push('author');
|
|
|
|
}
|
|
|
|
}, []),
|
|
|
|
prevRelations = _.transform(options.include, function (relations, include) {
|
|
|
|
if (include === 'previous.tags') {
|
|
|
|
relations.push('tags');
|
|
|
|
} else if (include === 'previous.author') {
|
|
|
|
relations.push('author');
|
|
|
|
}
|
|
|
|
}, []);
|
2015-01-07 20:25:32 +03:00
|
|
|
|
2015-06-25 21:56:27 +03:00
|
|
|
data = _.defaults(data || {}, {
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
status: 'published'
|
2015-06-25 21:56:27 +03:00
|
|
|
});
|
2013-06-09 03:39:24 +04:00
|
|
|
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
if (data.status === 'all') {
|
|
|
|
delete data.status;
|
2013-06-01 18:47:41 +04:00
|
|
|
}
|
|
|
|
|
2015-01-07 20:25:32 +03:00
|
|
|
// Add related objects, excluding next and previous as they are not real db objects
|
2015-08-09 22:30:04 +03:00
|
|
|
options.withRelated = _.union(options.withRelated, _.pull(
|
|
|
|
[].concat(options.include),
|
|
|
|
'next', 'next.author', 'next.tags', 'previous', 'previous.author', 'previous.tags')
|
|
|
|
);
|
2015-01-07 20:25:32 +03:00
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
return ghostBookshelf.Model.findOne.call(this, data, options).then(function then(post) {
|
2015-01-07 20:25:32 +03:00
|
|
|
if ((withNext || withPrev) && post && !post.page) {
|
2016-06-20 16:38:29 +03:00
|
|
|
var publishedAt = moment(post.get('published_at')).format('YYYY-MM-DD HH:mm:ss'),
|
2015-01-07 20:25:32 +03:00
|
|
|
prev,
|
|
|
|
next;
|
|
|
|
|
|
|
|
if (withNext) {
|
2015-06-14 18:58:49 +03:00
|
|
|
next = Post.forge().query(function queryBuilder(qb) {
|
2015-01-07 20:25:32 +03:00
|
|
|
qb.where('status', '=', 'published')
|
|
|
|
.andWhere('page', '=', 0)
|
|
|
|
.andWhere('published_at', '>', publishedAt)
|
|
|
|
.orderBy('published_at', 'asc')
|
|
|
|
.limit(1);
|
2015-08-09 22:30:04 +03:00
|
|
|
}).fetch({withRelated: nextRelations});
|
2015-01-07 20:25:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (withPrev) {
|
2015-06-14 18:58:49 +03:00
|
|
|
prev = Post.forge().query(function queryBuilder(qb) {
|
2015-01-07 20:25:32 +03:00
|
|
|
qb.where('status', '=', 'published')
|
|
|
|
.andWhere('page', '=', 0)
|
|
|
|
.andWhere('published_at', '<', publishedAt)
|
|
|
|
.orderBy('published_at', 'desc')
|
|
|
|
.limit(1);
|
2015-08-09 22:30:04 +03:00
|
|
|
}).fetch({withRelated: prevRelations});
|
2015-01-07 20:25:32 +03:00
|
|
|
}
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
|
2015-01-07 20:25:32 +03:00
|
|
|
return Promise.join(next, prev)
|
2015-06-14 18:58:49 +03:00
|
|
|
.then(function then(nextAndPrev) {
|
2015-01-07 20:25:32 +03:00
|
|
|
if (nextAndPrev[0]) {
|
|
|
|
post.relations.next = nextAndPrev[0];
|
|
|
|
}
|
|
|
|
if (nextAndPrev[1]) {
|
|
|
|
post.relations.previous = nextAndPrev[1];
|
|
|
|
}
|
|
|
|
return post;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return post;
|
|
|
|
});
|
2013-09-13 17:29:59 +04:00
|
|
|
},
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
/**
|
|
|
|
* ### Edit
|
|
|
|
* @extends ghostBookshelf.Model.edit to handle returning the full object and manage _updatedAttributes
|
|
|
|
* **See:** [ghostBookshelf.Model.edit](base.js.html#edit)
|
|
|
|
*/
|
2015-06-14 18:58:49 +03:00
|
|
|
edit: function edit(data, options) {
|
2013-11-03 21:13:19 +04:00
|
|
|
var self = this;
|
2014-04-03 17:03:09 +04:00
|
|
|
options = options || {};
|
2014-02-19 17:57:26 +04:00
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
return ghostBookshelf.Model.edit.call(this, data, options).then(function then(post) {
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
return self.findOne({status: 'all', id: options.id}, options)
|
2015-06-14 18:58:49 +03:00
|
|
|
.then(function then(found) {
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
if (found) {
|
|
|
|
// Pass along the updated attributes for checking status changes
|
|
|
|
found._updatedAttributes = post._updatedAttributes;
|
|
|
|
return found;
|
|
|
|
}
|
|
|
|
});
|
2013-11-03 21:13:19 +04:00
|
|
|
});
|
|
|
|
},
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Add
|
|
|
|
* @extends ghostBookshelf.Model.add to handle returning the full object
|
|
|
|
* **See:** [ghostBookshelf.Model.add](base.js.html#add)
|
|
|
|
*/
|
2015-06-14 18:58:49 +03:00
|
|
|
add: function add(data, options) {
|
2013-11-03 21:13:19 +04:00
|
|
|
var self = this;
|
2014-04-03 17:03:09 +04:00
|
|
|
options = options || {};
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
return ghostBookshelf.Model.add.call(this, data, options).then(function then(post) {
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
return self.findOne({status: 'all', id: post.id}, options);
|
2013-09-13 17:29:59 +04:00
|
|
|
});
|
2013-09-27 14:56:20 +04:00
|
|
|
},
|
2014-04-03 17:03:09 +04:00
|
|
|
|
2014-07-23 12:32:27 +04:00
|
|
|
/**
|
|
|
|
* ### destroyByAuthor
|
|
|
|
* @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy
|
|
|
|
*/
|
2016-03-02 07:42:01 +03:00
|
|
|
destroyByAuthor: Promise.method(function destroyByAuthor(options) {
|
2014-07-23 12:32:27 +04:00
|
|
|
var postCollection = Posts.forge(),
|
|
|
|
authorId = options.id;
|
|
|
|
|
|
|
|
options = this.filterOptions(options, 'destroyByAuthor');
|
2016-03-02 07:42:01 +03:00
|
|
|
|
|
|
|
if (!authorId) {
|
2016-10-06 15:27:35 +03:00
|
|
|
throw new errors.NotFoundError({message: i18n.t('errors.models.post.noUserFound')});
|
2014-07-23 12:32:27 +04:00
|
|
|
}
|
2016-03-02 07:42:01 +03:00
|
|
|
|
2016-09-19 16:45:36 +03:00
|
|
|
return postCollection
|
|
|
|
.query('where', 'author_id', '=', authorId)
|
2016-03-02 07:42:01 +03:00
|
|
|
.fetch(options)
|
|
|
|
.call('invokeThen', 'destroy', options)
|
2016-10-06 15:27:35 +03:00
|
|
|
.catch(function (err) {
|
|
|
|
return Promise.reject(new errors.GhostError({err: err}));
|
2016-03-02 07:42:01 +03:00
|
|
|
});
|
|
|
|
}),
|
2014-07-23 12:32:27 +04:00
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
permissible: function permissible(postModelOrId, action, context, loadedPermissions, hasUserPermission, hasAppPermission) {
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
var self = this,
|
2014-05-14 05:49:07 +04:00
|
|
|
postModel = postModelOrId,
|
|
|
|
origArgs;
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
|
|
|
|
// If we passed in an id instead of a model, get the model
|
|
|
|
// then check the permissions
|
|
|
|
if (_.isNumber(postModelOrId) || _.isString(postModelOrId)) {
|
2014-05-14 05:49:07 +04:00
|
|
|
// Grab the original args without the first one
|
|
|
|
origArgs = _.toArray(arguments).slice(1);
|
2016-04-14 18:54:49 +03:00
|
|
|
|
2014-05-14 05:49:07 +04:00
|
|
|
// Get the actual post model
|
2015-06-14 18:58:49 +03:00
|
|
|
return this.findOne({id: postModelOrId, status: 'all'}).then(function then(foundPostModel) {
|
2014-05-14 05:49:07 +04:00
|
|
|
// Build up the original args but substitute with actual model
|
|
|
|
var newArgs = [foundPostModel].concat(origArgs);
|
|
|
|
|
2014-07-23 22:17:29 +04:00
|
|
|
return self.permissible.apply(self, newArgs);
|
2016-10-04 18:33:43 +03:00
|
|
|
});
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
}
|
|
|
|
|
2014-05-14 05:49:07 +04:00
|
|
|
if (postModel) {
|
|
|
|
// If this is the author of the post, allow it.
|
|
|
|
hasUserPermission = hasUserPermission || context.user === postModel.get('author_id');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasUserPermission && hasAppPermission) {
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.resolve();
|
Consistency in model method naming
- The API has the BREAD naming for methods
- The model now has findAll, findOne, findPage (where needed), edit, add and destroy, meaning it is similar but with a bit more flexibility
- browse, read, update, create, and delete, which were effectively just aliases, have all been removed.
- added jsDoc for the model methods
2014-05-05 19:18:38 +04:00
|
|
|
}
|
|
|
|
|
2016-10-06 15:27:35 +03:00
|
|
|
return Promise.reject(new errors.NoPermissionError({message: i18n.t('errors.models.post.notEnoughPermission')}));
|
2013-06-25 15:43:15 +04:00
|
|
|
}
|
|
|
|
});
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2013-09-23 02:20:08 +04:00
|
|
|
Posts = ghostBookshelf.Collection.extend({
|
2015-12-02 13:06:44 +03:00
|
|
|
model: Post
|
2013-06-25 15:43:15 +04:00
|
|
|
});
|
2013-06-01 18:47:41 +04:00
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
module.exports = {
|
2014-07-13 15:17:18 +04:00
|
|
|
Post: ghostBookshelf.model('Post', Post),
|
|
|
|
Posts: ghostBookshelf.collection('Posts', Posts)
|
2014-02-25 14:18:33 +04:00
|
|
|
};
|