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'),
|
2017-01-04 19:10:29 +03:00
|
|
|
uuid = require('uuid'),
|
2016-09-26 16:23:49 +03:00
|
|
|
moment = require('moment'),
|
|
|
|
Promise = require('bluebird'),
|
|
|
|
sequence = require('../utils/sequence'),
|
|
|
|
errors = require('../errors'),
|
2017-04-11 11:55:36 +03:00
|
|
|
htmlToText = require('html-to-text'),
|
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',
|
|
|
|
|
2017-05-22 11:24:59 +03:00
|
|
|
emitChange: function emitChange(event, options) {
|
|
|
|
options = options || {};
|
|
|
|
|
2015-03-24 23:23:23 +03:00
|
|
|
var resourceType = this.get('page') ? 'page' : 'post';
|
2017-05-22 11:24:59 +03:00
|
|
|
|
|
|
|
if (options.usePreviousResourceType) {
|
2015-03-24 23:23:23 +03:00
|
|
|
resourceType = this.updated('page') ? 'page' : 'post';
|
|
|
|
}
|
2017-01-25 16:47:49 +03:00
|
|
|
|
2017-05-22 11:24:59 +03:00
|
|
|
events.emit(resourceType + '.' + event, this, options);
|
2015-03-24 23:23:23 +03:00
|
|
|
},
|
|
|
|
|
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
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2017-04-19 16:53:23 +03:00
|
|
|
/**
|
|
|
|
* We update the tags after the Post was inserted.
|
|
|
|
* We update the tags before the Post was updated, see `onSaving` event.
|
|
|
|
* `onCreated` is called before `onSaved`.
|
|
|
|
*/
|
|
|
|
onCreated: function onCreated(model, response, options) {
|
2016-10-14 15:37:01 +03:00
|
|
|
var status = model.get('status');
|
2017-04-19 16:53:23 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
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) {
|
2017-05-22 11:24:59 +03:00
|
|
|
model.emitChange(status, {importing: options.importing});
|
2016-10-14 15:37:01 +03:00
|
|
|
}
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
|
|
return this.updateTags(model, response, options);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* http://knexjs.org/#Builder-forUpdate
|
|
|
|
* https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
|
|
|
|
*
|
|
|
|
* Lock target collection/model for further update operations.
|
|
|
|
* This avoids collisions and possible content override cases.
|
|
|
|
*
|
|
|
|
* `forUpdate` is only supported for posts right now
|
|
|
|
*/
|
|
|
|
onFetching: function onFetching(model, columns, options) {
|
|
|
|
if (options.forUpdate && options.transacting) {
|
|
|
|
options.query.forUpdate();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onFetchingCollection: function onFetchingCollection(model, columns, options) {
|
|
|
|
if (options.forUpdate && options.transacting) {
|
|
|
|
options.query.forUpdate();
|
|
|
|
}
|
2016-10-14 15:37:01 +03:00
|
|
|
},
|
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) {
|
2017-05-22 11:24:59 +03:00
|
|
|
model.emitChange('unpublished', {usePreviousResourceType: true});
|
2016-10-14 15:37:01 +03:00
|
|
|
}
|
2016-04-14 14:22:38 +03:00
|
|
|
|
2016-10-14 15:37:01 +03:00
|
|
|
if (model.wasScheduled) {
|
2017-05-22 11:24:59 +03:00
|
|
|
model.emitChange('unscheduled', {usePreviousResourceType: true});
|
2016-10-14 15:37:01 +03:00
|
|
|
}
|
|
|
|
|
2017-05-22 11:24:59 +03:00
|
|
|
model.emitChange('deleted', {usePreviousResourceType: 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'),
|
2017-04-06 19:49:59 +03:00
|
|
|
publishedAtHasChanged = this.hasDateChanged('published_at', {beforeWrite: true}),
|
2016-09-26 16:23:49 +03:00
|
|
|
mobiledoc = this.get('mobiledoc'),
|
2017-08-31 13:09:02 +03:00
|
|
|
tags = [], ops = [];
|
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)
|
2017-05-12 15:56:40 +03:00
|
|
|
} else if (
|
|
|
|
publishedAtHasChanged &&
|
|
|
|
moment(publishedAt).isBefore(moment().add(config.get('times').cannotScheduleAPostBeforeInMinutes, 'minutes')) &&
|
|
|
|
!options.importing
|
|
|
|
) {
|
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
|
2017-04-19 16:53:23 +03:00
|
|
|
// get('tags') will be removed after saving, because it's not a direct attribute of posts (it's a relation)
|
2016-06-02 20:11:54 +03:00
|
|
|
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) {
|
2017-08-31 13:09:02 +03:00
|
|
|
this.set('html', utils.mobiledocConverter.render(JSON.parse(mobiledoc)));
|
2016-09-26 16:23:49 +03:00
|
|
|
}
|
2017-04-11 11:55:36 +03:00
|
|
|
|
replace custom showdown fork with markdown-it (#8451)
refs https://github.com/TryGhost/Ghost-Admin/pull/690, closes #1501, closes #2093, closes #4592, closes #4627, closes #4659, closes #5039, closes #5237, closes #5587, closes #5625, closes #5632, closes #5822, closes #5939, closes #6840, closes #7183, closes #7536
- replace custom showdown fork with markdown-it
- swaps showdown for markdown-it when rendering markdown
- match existing header ID behaviour
- allow headers without a space after the #s
- add duplicate header ID handling
- remove legacy markdown spec
- move markdown-it setup into markdown-converter util
- update mobiledoc specs to match markdown-it newline behaviour
- update data-generator HTML to match markdown-it newline behaviour
- fix Post "converts html to plaintext" test
- update rss spec to match markdown-it newline behaviour
- close almost all related showdown bugs
2017-05-15 19:48:14 +03:00
|
|
|
if (this.hasChanged('html') || !this.get('plaintext')) {
|
2017-04-11 11:55:36 +03:00
|
|
|
this.set('plaintext', htmlToText.fromString(this.get('html'), {
|
|
|
|
wordwrap: 80,
|
|
|
|
ignoreImage: true,
|
|
|
|
hideLinkHrefIfSameAsText: true,
|
|
|
|
preserveNewlines: true,
|
|
|
|
returnDomByDefault: true,
|
|
|
|
uppercaseHeadings: false
|
|
|
|
}));
|
|
|
|
}
|
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
|
2017-05-23 19:18:13 +03:00
|
|
|
if (!options.importing) {
|
|
|
|
title = this.get('title') || i18n.t('errors.models.post.untitled');
|
|
|
|
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
|
|
|
|
2017-04-19 16:53:23 +03:00
|
|
|
/**
|
|
|
|
* - `updateTags` happens before the post is saved to the database
|
|
|
|
* - when editing a post, it's running in a transaction, see `Post.edit`
|
|
|
|
* - we are using a update collision detection, we have to know if tags were updated in the client
|
|
|
|
*
|
|
|
|
* NOTE: For adding a post, updateTags happens after the post insert, see `onCreated` event
|
|
|
|
*/
|
|
|
|
if (options.method === 'update' || options.method === 'patch') {
|
|
|
|
ops.push(function updateTags() {
|
|
|
|
return self.updateTags(model, attr, options);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
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) {
|
2017-04-19 16:53:23 +03:00
|
|
|
ops.push(function updateSlug() {
|
|
|
|
// Pass the new slug through the generator to strip illegal characters, detect duplicates
|
|
|
|
return ghostBookshelf.Model.generateSlug(Post, self.get('title'),
|
2015-09-23 13:54:56 +03:00
|
|
|
{status: 'all', transacting: options.transacting, importing: options.importing})
|
2017-04-19 16:53:23 +03:00
|
|
|
.then(function then(slug) {
|
|
|
|
// 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,
|
|
|
|
{status: 'all', transacting: options.transacting, importing: options.importing}
|
|
|
|
).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});
|
|
|
|
}
|
|
|
|
});
|
2016-03-23 16:17:01 +03:00
|
|
|
});
|
2017-04-19 16:53:23 +03:00
|
|
|
});
|
2016-03-23 16:17:01 +03:00
|
|
|
} else {
|
2017-04-19 16:53:23 +03:00
|
|
|
ops.push(function updateSlug() {
|
|
|
|
// If any of the attributes above were false, set initial slug and check to see if slug was changed by the user
|
|
|
|
if (self.hasChanged('slug') || !self.get('slug')) {
|
|
|
|
// Pass the new slug through the generator to strip illegal characters, detect duplicates
|
|
|
|
return ghostBookshelf.Model.generateSlug(Post, self.get('slug') || self.get('title'),
|
2016-03-23 16:17:01 +03:00
|
|
|
{status: 'all', transacting: options.transacting, importing: options.importing})
|
2017-04-19 16:53:23 +03:00
|
|
|
.then(function then(slug) {
|
|
|
|
self.set({slug: slug});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
});
|
2013-09-14 23:01:46 +04:00
|
|
|
}
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
|
|
return sequence(ops);
|
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;
|
|
|
|
|
2017-04-19 16:53:23 +03:00
|
|
|
// CASE: if nothing has changed, unset `tags`.
|
2015-09-01 03:51:56 +03:00
|
|
|
if (baseUtils.tagUpdate.tagSetsAreEqual(newTags, currentTags)) {
|
2017-04-19 16:53:23 +03:00
|
|
|
savedModel.unset('tags');
|
2015-09-01 03:51:56 +03:00
|
|
|
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'];
|
|
|
|
},
|
2017-05-30 13:40:39 +03:00
|
|
|
/**
|
|
|
|
* If the `formats` option is not used, we return `html` be default.
|
|
|
|
* Otherwise we return what is requested e.g. `?formats=mobiledoc,plaintext`
|
|
|
|
*/
|
|
|
|
formatsToJSON: function formatsToJSON(attrs, options) {
|
|
|
|
var defaultFormats = ['html'],
|
|
|
|
formatsToKeep = options.formats || defaultFormats;
|
|
|
|
|
|
|
|
// Iterate over all known formats, and if they are not in the keep list, remove them
|
|
|
|
_.each(Post.allowedFormats, function (format) {
|
|
|
|
if (formatsToKeep.indexOf(format) === -1) {
|
|
|
|
delete attrs[format];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return attrs;
|
|
|
|
},
|
2016-07-18 23:21:47 +03:00
|
|
|
|
2015-06-14 18:58:49 +03:00
|
|
|
toJSON: function toJSON(options) {
|
2015-07-04 21:27:23 +03:00
|
|
|
options = options || {};
|
|
|
|
|
2017-07-27 10:55:23 +03:00
|
|
|
var attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options),
|
|
|
|
oldPostId = attrs.amp,
|
|
|
|
commentId;
|
2014-04-22 05:08:11 +04:00
|
|
|
|
2017-05-30 13:40:39 +03:00
|
|
|
attrs = this.formatsToJSON(attrs, 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;
|
|
|
|
}
|
2017-07-31 12:00:03 +03:00
|
|
|
// If the current column settings allow it...
|
|
|
|
if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
|
2017-08-24 15:07:19 +03:00
|
|
|
// ... attach a computed property of primary_tag which is the first tag if it is public, else null
|
|
|
|
if (attrs.tags && attrs.tags.length > 0 && attrs.tags[0].visibility === 'public') {
|
|
|
|
attrs.primary_tag = attrs.tags[0];
|
|
|
|
} else {
|
|
|
|
attrs.primary_tag = null;
|
|
|
|
}
|
2017-07-31 12:00:03 +03:00
|
|
|
}
|
2015-07-04 21:27:23 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
2017-07-27 10:55:23 +03:00
|
|
|
if (oldPostId) {
|
|
|
|
oldPostId = Number(oldPostId);
|
|
|
|
|
|
|
|
if (isNaN(oldPostId)) {
|
|
|
|
commentId = attrs.id;
|
|
|
|
} else {
|
|
|
|
commentId = oldPostId;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
commentId = attrs.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: we remember the old post id because of disqus
|
|
|
|
attrs.comment_id = commentId;
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}, {
|
2017-05-31 17:46:29 +03:00
|
|
|
allowedFormats: ['mobiledoc', 'html', 'plaintext', 'amp'],
|
2017-05-30 13:40:39 +03: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 = {
|
2017-04-19 16:53:23 +03:00
|
|
|
findOne: ['columns', 'importing', 'withRelated', 'require', 'forUpdate'],
|
2015-10-22 15:49:15 +03:00
|
|
|
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status', 'staticPages'],
|
2017-04-19 16:53:23 +03:00
|
|
|
findAll: ['columns', 'filter', 'forUpdate'],
|
|
|
|
edit: ['forUpdate']
|
2014-05-06 05:45:08 +04:00
|
|
|
};
|
|
|
|
|
2017-05-30 13:40:39 +03:00
|
|
|
// The post model additionally supports having a formats option
|
|
|
|
options.push('formats');
|
|
|
|
|
2014-05-06 05:45:08 +04:00
|
|
|
if (validOptions[methodName]) {
|
|
|
|
options = options.concat(validOptions[methodName]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return options;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2017-04-19 11:59:09 +03:00
|
|
|
* Manually add 'tags' attribute since it's not in the schema and call parent.
|
|
|
|
*
|
2014-05-06 05:45:08 +04:00
|
|
|
* @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) {
|
2017-04-19 11:59:09 +03:00
|
|
|
var filteredData = ghostBookshelf.Model.filterData.apply(this, arguments),
|
|
|
|
extraData = _.pick(data, ['tags']);
|
2014-05-06 05:45:08 +04:00
|
|
|
|
2017-04-19 11:59:09 +03:00
|
|
|
_.merge(filteredData, extraData);
|
2014-05-06 05:45:08 +04:00
|
|
|
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
|
2017-04-19 16:53:23 +03:00
|
|
|
* Fetches and saves to Post. See model.Base.edit
|
|
|
|
*
|
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
|
|
|
* @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) {
|
2017-04-19 16:53:23 +03:00
|
|
|
var self = this,
|
|
|
|
editPost = function editPost(data, options) {
|
|
|
|
options.forUpdate = true;
|
|
|
|
|
|
|
|
return ghostBookshelf.Model.edit.call(self, data, options).then(function then(post) {
|
|
|
|
return self.findOne({status: 'all', id: options.id}, options)
|
|
|
|
.then(function then(found) {
|
|
|
|
if (found) {
|
|
|
|
// Pass along the updated attributes for checking status changes
|
|
|
|
found._updatedAttributes = post._updatedAttributes;
|
|
|
|
return found;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-04-03 17:03:09 +04:00
|
|
|
options = options || {};
|
2014-02-19 17:57:26 +04:00
|
|
|
|
2017-04-19 16:53:23 +03:00
|
|
|
if (options.transacting) {
|
|
|
|
return editPost(data, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ghostBookshelf.transaction(function (transacting) {
|
|
|
|
options.transacting = transacting;
|
|
|
|
return editPost(data, options);
|
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
|
|
|
};
|