mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
40d0a745df
no issue This PR adds the server side logic for multiple authors. This adds the ability to add multiple authors per post. We keep and support single authors (maybe till the next major - this is still in discussion) ### key notes - `authors` are not fetched by default, only if we need them - the migration script iterates over all posts and figures out if an author_id is valid and exists (in master we can add invalid author_id's) and then adds the relation (falls back to owner if invalid) - ~~i had to push a fork of bookshelf to npm because we currently can't bump bookshelf + the two bugs i discovered are anyway not yet merged (https://github.com/kirrg001/bookshelf/commits/master)~~ replaced by new bookshelf release - the implementation of single & multiple authors lives in a single place (introduction of a new concept: model relation) - if you destroy an author, we keep the behaviour for now -> remove all posts where the primary author id matches. furthermore, remove all relations in posts_authors (e.g. secondary author) - we make re-use of the `excludeAttrs` concept which was invented in the contributors PR (to protect editing authors as author/contributor role) -> i've added a clear todo that we need a logic to make a diff of the target relation -> both for tags and authors - `authors` helper available (same as `tags` helper) - `primary_author` computed field available - `primary_author` functionality available (same as `primary_tag` e.g. permalinks, prev/next helper etc)
263 lines
8.9 KiB
JavaScript
263 lines
8.9 KiB
JavaScript
// # Posts API
|
|
// RESTful API for the Post resource
|
|
var Promise = require('bluebird'),
|
|
_ = require('lodash'),
|
|
pipeline = require('../lib/promise/pipeline'),
|
|
localUtils = require('./utils'),
|
|
models = require('../models'),
|
|
common = require('../lib/common'),
|
|
docName = 'posts',
|
|
/**
|
|
* @deprecated: `author`, will be removed in Ghost 2.0
|
|
*/
|
|
allowedIncludes = [
|
|
'created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields', 'authors', 'authors.roles'
|
|
],
|
|
unsafeAttrs = ['author_id', 'status', 'authors'],
|
|
posts;
|
|
|
|
/**
|
|
* ### Posts API Methods
|
|
*
|
|
* **See:** [API Methods](constants.js.html#api%20methods)
|
|
*/
|
|
|
|
posts = {
|
|
/**
|
|
* ## Browse
|
|
* Find a paginated set of posts
|
|
*
|
|
* Will only return published posts unless we have an authenticated user and an alternative status
|
|
* parameter.
|
|
*
|
|
* Will return without static pages unless told otherwise
|
|
*
|
|
*
|
|
* @public
|
|
* @param {{context, page, limit, status, staticPages, tag, featured}} options (optional)
|
|
* @returns {Promise<Posts>} Posts Collection with Meta
|
|
*/
|
|
browse: function browse(options) {
|
|
var extraOptions = ['status', 'formats'],
|
|
permittedOptions,
|
|
tasks;
|
|
|
|
// Workaround to remove static pages from results
|
|
// TODO: rework after https://github.com/TryGhost/Ghost/issues/5151
|
|
if (options && options.context && (options.context.user || options.context.internal)) {
|
|
extraOptions.push('staticPages');
|
|
}
|
|
permittedOptions = localUtils.browseDefaultOptions.concat(extraOptions);
|
|
|
|
/**
|
|
* ### Model Query
|
|
* Make the call to the Model layer
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
function modelQuery(options) {
|
|
return models.Post.findPage(options);
|
|
}
|
|
|
|
// Push all of our tasks into a `tasks` array in the correct order
|
|
tasks = [
|
|
localUtils.validate(docName, {opts: permittedOptions}),
|
|
localUtils.convertOptions(allowedIncludes, models.Post.allowedFormats),
|
|
localUtils.handlePublicPermissions(docName, 'browse', unsafeAttrs),
|
|
modelQuery
|
|
];
|
|
|
|
// Pipeline calls each task passing the result of one to be the arguments for the next
|
|
return pipeline(tasks, options);
|
|
},
|
|
|
|
/**
|
|
* ## Read
|
|
* Find a post, by ID, UUID, or Slug
|
|
*
|
|
* @public
|
|
* @param {Object} options
|
|
* @return {Promise<Post>} Post
|
|
*/
|
|
read: function read(options) {
|
|
var attrs = ['id', 'slug', 'status', 'uuid'],
|
|
// NOTE: the scheduler API uses the post API and forwards custom options
|
|
extraAllowedOptions = options.opts || ['formats'],
|
|
tasks;
|
|
|
|
/**
|
|
* ### Model Query
|
|
* Make the call to the Model layer
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
function modelQuery(options) {
|
|
return models.Post.findOne(options.data, _.omit(options, ['data']))
|
|
.then(function onModelResponse(model) {
|
|
if (!model) {
|
|
return Promise.reject(new common.errors.NotFoundError({
|
|
message: common.i18n.t('errors.api.posts.postNotFound')
|
|
}));
|
|
}
|
|
|
|
return {
|
|
posts: [model.toJSON(options)]
|
|
};
|
|
});
|
|
}
|
|
|
|
// Push all of our tasks into a `tasks` array in the correct order
|
|
tasks = [
|
|
localUtils.validate(docName, {attrs: attrs, opts: extraAllowedOptions}),
|
|
localUtils.convertOptions(allowedIncludes, models.Post.allowedFormats),
|
|
localUtils.handlePublicPermissions(docName, 'read', unsafeAttrs),
|
|
modelQuery
|
|
];
|
|
|
|
// Pipeline calls each task passing the result of one to be the arguments for the next
|
|
return pipeline(tasks, options);
|
|
},
|
|
|
|
/**
|
|
* ## Edit
|
|
* Update properties of a post
|
|
*
|
|
* @public
|
|
* @param {Post} object Post or specific properties to update
|
|
* @param {{id (required), context, include,...}} options
|
|
* @return {Promise(Post)} Edited Post
|
|
*/
|
|
edit: function edit(object, options) {
|
|
var tasks,
|
|
// NOTE: the scheduler API uses the post API and forwards custom options
|
|
extraAllowedOptions = options.opts || [];
|
|
|
|
/**
|
|
* ### Model Query
|
|
* Make the call to the Model layer
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
function modelQuery(options) {
|
|
return models.Post.edit(options.data.posts[0], _.omit(options, ['data']))
|
|
.then(function onModelResponse(model) {
|
|
if (!model) {
|
|
return Promise.reject(new common.errors.NotFoundError({
|
|
message: common.i18n.t('errors.api.posts.postNotFound')
|
|
}));
|
|
}
|
|
|
|
var post = model.toJSON(options);
|
|
|
|
// If previously was not published and now is (or vice versa), signal the change
|
|
// @TODO: `statusChanged` get's added for the API headers only. Reconsider this.
|
|
post.statusChanged = false;
|
|
if (model.updated('status') !== model.get('status')) {
|
|
post.statusChanged = true;
|
|
}
|
|
|
|
return {
|
|
posts: [post]
|
|
};
|
|
});
|
|
}
|
|
|
|
// Push all of our tasks into a `tasks` array in the correct order
|
|
tasks = [
|
|
localUtils.validate(docName, {opts: localUtils.idDefaultOptions.concat(extraAllowedOptions)}),
|
|
localUtils.convertOptions(allowedIncludes),
|
|
localUtils.handlePermissions(docName, 'edit', unsafeAttrs),
|
|
modelQuery
|
|
];
|
|
|
|
// Pipeline calls each task passing the result of one to be the arguments for the next
|
|
return pipeline(tasks, object, options);
|
|
},
|
|
|
|
/**
|
|
* ## Add
|
|
* Create a new post along with any tags
|
|
*
|
|
* @public
|
|
* @param {Post} object
|
|
* @param {{context, include,...}} options
|
|
* @return {Promise(Post)} Created Post
|
|
*/
|
|
add: function add(object, options) {
|
|
var tasks;
|
|
|
|
/**
|
|
* ### Model Query
|
|
* Make the call to the Model layer
|
|
* @param {Object} options
|
|
* @returns {Object} options
|
|
*/
|
|
function modelQuery(options) {
|
|
return models.Post.add(options.data.posts[0], _.omit(options, ['data']))
|
|
.then(function onModelResponse(model) {
|
|
var post = model.toJSON(options);
|
|
|
|
if (post.status === 'published') {
|
|
// When creating a new post that is published right now, signal the change
|
|
post.statusChanged = true;
|
|
}
|
|
|
|
return {posts: [post]};
|
|
});
|
|
}
|
|
|
|
// Push all of our tasks into a `tasks` array in the correct order
|
|
tasks = [
|
|
localUtils.validate(docName),
|
|
localUtils.convertOptions(allowedIncludes),
|
|
localUtils.handlePermissions(docName, 'add', unsafeAttrs),
|
|
modelQuery
|
|
];
|
|
|
|
// Pipeline calls each task passing the result of one to be the arguments for the next
|
|
return pipeline(tasks, object, options);
|
|
},
|
|
|
|
/**
|
|
* ## Destroy
|
|
* Delete a post, cleans up tag relations, but not unused tags
|
|
*
|
|
* @public
|
|
* @param {{id (required), context,...}} options
|
|
* @return {Promise}
|
|
*/
|
|
destroy: function destroy(options) {
|
|
var tasks;
|
|
|
|
/**
|
|
* @function deletePost
|
|
* @param {Object} options
|
|
*/
|
|
function deletePost(options) {
|
|
var Post = models.Post,
|
|
data = _.defaults({status: 'all'}, options),
|
|
fetchOpts = _.defaults({require: true, columns: 'id'}, options);
|
|
|
|
return Post.findOne(data, fetchOpts).then(function () {
|
|
return Post.destroy(options).return(null);
|
|
}).catch(Post.NotFoundError, function () {
|
|
throw new common.errors.NotFoundError({
|
|
message: common.i18n.t('errors.api.posts.postNotFound')
|
|
});
|
|
});
|
|
}
|
|
|
|
// Push all of our tasks into a `tasks` array in the correct order
|
|
tasks = [
|
|
localUtils.validate(docName, {opts: localUtils.idDefaultOptions}),
|
|
localUtils.convertOptions(allowedIncludes),
|
|
localUtils.handlePermissions(docName, 'destroy', unsafeAttrs),
|
|
deletePost
|
|
];
|
|
|
|
// Pipeline calls each task passing the result of one to be the arguments for the next
|
|
return pipeline(tasks, options);
|
|
}
|
|
};
|
|
|
|
module.exports = posts;
|