diff --git a/core/server/api/posts.js b/core/server/api/posts.js index e7c5a76ccd..9d267b770c 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -9,7 +9,10 @@ var Promise = require('bluebird'), pipeline = require('../utils/pipeline'), docName = 'posts', - allowedIncludes = ['created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields', 'next', 'previous'], + allowedIncludes = [ + 'created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields', + 'next', 'previous', 'next.author', 'next.tags', 'previous.author', 'previous.tags' + ], posts; /** diff --git a/core/server/helpers/prev_next.js b/core/server/helpers/prev_next.js index 292b0c3b63..554b01ce5f 100644 --- a/core/server/helpers/prev_next.js +++ b/core/server/helpers/prev_next.js @@ -29,7 +29,7 @@ prevNext = function (options) { options = options || {}; var apiOptions = { - include: options.name === 'prev_post' ? 'previous' : 'next' + include: options.name === 'prev_post' ? 'previous,previous.author,previous.tags' : 'next,next.author,next.tags' }; if (schema.isPost(this) && this.status === 'published') { diff --git a/core/server/models/post.js b/core/server/models/post.js index 53a3e871af..b276b7a93f 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -399,7 +399,21 @@ Post = ghostBookshelf.Model.extend({ options = options || {}; var withNext = _.contains(options.include, 'next'), - withPrev = _.contains(options.include, 'previous'); + withPrev = _.contains(options.include, 'previous'), + 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'); + } + }, []); data = _.defaults(data || {}, { status: 'published' @@ -410,7 +424,10 @@ Post = ghostBookshelf.Model.extend({ } // Add related objects, excluding next and previous as they are not real db objects - options.withRelated = _.union(options.withRelated, _.pull([].concat(options.include), 'next', 'previous')); + options.withRelated = _.union(options.withRelated, _.pull( + [].concat(options.include), + 'next', 'next.author', 'next.tags', 'previous', 'previous.author', 'previous.tags') + ); return ghostBookshelf.Model.findOne.call(this, data, options).then(function then(post) { if ((withNext || withPrev) && post && !post.page) { @@ -425,7 +442,7 @@ Post = ghostBookshelf.Model.extend({ .andWhere('published_at', '>', publishedAt) .orderBy('published_at', 'asc') .limit(1); - }).fetch(); + }).fetch({withRelated: nextRelations}); } if (withPrev) { @@ -435,7 +452,7 @@ Post = ghostBookshelf.Model.extend({ .andWhere('published_at', '<', publishedAt) .orderBy('published_at', 'desc') .limit(1); - }).fetch(); + }).fetch({withRelated: prevRelations}); } return Promise.join(next, prev) diff --git a/core/test/integration/api/api_posts_spec.js b/core/test/integration/api/api_posts_spec.js index 255e2fd486..373b6d969c 100644 --- a/core/test/integration/api/api_posts_spec.js +++ b/core/test/integration/api/api_posts_spec.js @@ -320,6 +320,27 @@ describe('Post API', function () { }).catch(done); }); + it('can include next post with author and tags', function (done) { + PostAPI.read({context: {user: 1}, id: 3, include: 'next,next.tags,next.author'}).then(function (results) { + should.exist(results.posts[0].next.slug); + results.posts[0].next.slug.should.eql('not-so-short-bit-complex'); + results.posts[0].next.author.should.be.an.Object; + results.posts[0].next.tags.should.be.an.Array; + done(); + }).catch(done); + }); + + it('can include next post with just tags', function (done) { + PostAPI.read({context: {user: 1}, id: 2, include: 'next,next.tags'}).then(function (results) { + should.exist(results.posts[0].next.slug); + results.posts[0].next.slug.should.eql('short-and-sweet'); + results.posts[0].next.author.should.eql(1); + results.posts[0].next.tags.should.be.an.Array; + results.posts[0].next.tags[0].name.should.eql('chorizo'); + done(); + }).catch(done); + }); + it('can include previous post', function (done) { PostAPI.read({context: {user: 1}, id: 3, include: 'previous'}).then(function (results) { should.exist(results.posts[0].previous.slug); @@ -327,5 +348,29 @@ describe('Post API', function () { done(); }).catch(done); }); + + it('can include previous post with author and tags', function (done) { + PostAPI.read({context: {user: 1}, id: 3, include: 'previous,previous.author,previous.tags'}).then(function (results) { + should.exist(results.posts[0].previous.slug); + results.posts[0].previous.slug.should.eql('ghostly-kitchen-sink'); + results.posts[0].previous.author.should.be.an.Object; + results.posts[0].previous.author.name.should.eql('Joe Bloggs'); + results.posts[0].previous.tags.should.be.an.Array; + results.posts[0].previous.tags.should.have.lengthOf(2); + results.posts[0].previous.tags[0].slug.should.eql('kitchen-sink'); + done(); + }).catch(done); + }); + + it('can include previous post with just author', function (done) { + PostAPI.read({context: {user: 1}, id: 3, include: 'previous,previous.author'}).then(function (results) { + should.exist(results.posts[0].previous.slug); + should.not.exist(results.posts[0].previous.tags); + results.posts[0].previous.slug.should.eql('ghostly-kitchen-sink'); + results.posts[0].previous.author.should.be.an.Object; + results.posts[0].previous.author.name.should.eql('Joe Bloggs'); + done(); + }).catch(done); + }); }); }); diff --git a/core/test/unit/server_helpers/next_post_spec.js b/core/test/unit/server_helpers/next_post_spec.js index 4351483506..d0558961bf 100644 --- a/core/test/unit/server_helpers/next_post_spec.js +++ b/core/test/unit/server_helpers/next_post_spec.js @@ -9,16 +9,22 @@ var should = require('should'), // Stuff we are testing handlebars = hbs.handlebars, helpers = require('../../../server/helpers'), - api = require('../../../server/api'); + api = require('../../../server/api'), + + sandbox = sinon.sandbox.create(); describe('{{next_post}} helper', function () { + var readPostStub; + + afterEach(function () { + sandbox.restore(); + }); + describe('with valid post data - ', function () { - var sandbox; beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'next') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('next') === 0) { return Promise.resolve({ posts: [{slug: '/current/', title: 'post 2', next: {slug: '/next/', title: 'post 3'}}] }); @@ -26,10 +32,6 @@ describe('{{next_post}} helper', function () { }); }); - afterEach(function () { - sandbox.restore(); - }); - it('has loaded next_post helper', function () { should.exist(handlebars.helpers.prev_post); }); @@ -46,8 +48,11 @@ describe('{{next_post}} helper', function () { slug: 'current', created_at: new Date(0), url: '/current/'}, optionsData).then(function () { - fn.called.should.be.true; - inverse.called.should.be.false; + fn.calledOnce.should.be.true; + inverse.calledOnce.should.be.false; + + readPostStub.calledOnce.should.be.true; + readPostStub.firstCall.args[0].include.should.eql('next,next.author,next.tags'); done(); }).catch(function (err) { console.log('err ', err); @@ -57,22 +62,15 @@ describe('{{next_post}} helper', function () { }); describe('for valid post with no next post', function () { - var sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'next') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('next') === 0) { return Promise.resolve({posts: [{slug: '/current/', title: 'post 2'}]}); } }); }); - afterEach(function () { - sandbox.restore(); - }); - it('shows \'else\' template', function (done) { var fn = sinon.spy(), inverse = sinon.spy(), @@ -94,22 +92,15 @@ describe('{{next_post}} helper', function () { }); describe('for invalid post data', function () { - var sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'previous') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('next') === 0) { return Promise.resolve({}); } }); }); - afterEach(function () { - sandbox.restore(); - }); - it('shows \'else\' template', function (done) { var fn = sinon.spy(), inverse = sinon.spy(), @@ -118,6 +109,7 @@ describe('{{next_post}} helper', function () { helpers.prev_post.call({}, optionsData).then(function () { fn.called.should.be.false; inverse.called.should.be.true; + readPostStub.called.should.be.false; done(); }).catch(function (err) { done(err); @@ -126,13 +118,10 @@ describe('{{next_post}} helper', function () { }); describe('for unpublished post', function () { - var sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'next') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('next') === 0) { return Promise.resolve({ posts: [{slug: '/current/', title: 'post 2', next: {slug: '/next/', title: 'post 3'}}] }); @@ -140,10 +129,6 @@ describe('{{next_post}} helper', function () { }); }); - afterEach(function () { - sandbox.restore(); - }); - it('shows \'else\' template', function (done) { var fn = sinon.spy(), inverse = sinon.spy(), diff --git a/core/test/unit/server_helpers/prev_post_spec.js b/core/test/unit/server_helpers/prev_post_spec.js index 8cebdeb29a..e37d954e91 100644 --- a/core/test/unit/server_helpers/prev_post_spec.js +++ b/core/test/unit/server_helpers/prev_post_spec.js @@ -9,17 +9,22 @@ var should = require('should'), // Stuff we are testing handlebars = hbs.handlebars, helpers = require('../../../server/helpers'), - api = require('../../../server/api'); + api = require('../../../server/api'), + + sandbox = sinon.sandbox.create(); describe('{{prev_post}} helper', function () { - describe('with valid post data - ', function () { - var sandbox; + var readPostStub; + afterEach(function () { + sandbox.restore(); + }); + + describe('with valid post data - ', function () { beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'previous') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('previous') === 0) { return Promise.resolve({ posts: [{slug: '/current/', title: 'post 2', previous: {slug: '/previous/', title: 'post 1'}}] }); @@ -27,10 +32,6 @@ describe('{{prev_post}} helper', function () { }); }); - afterEach(function () { - sandbox.restore(); - }); - it('has loaded prev_post helper', function () { should.exist(handlebars.helpers.prev_post); }); @@ -47,8 +48,12 @@ describe('{{prev_post}} helper', function () { slug: 'current', created_at: new Date(0), url: '/current/'}, optionsData).then(function () { - fn.called.should.be.true; - inverse.called.should.be.false; + fn.calledOnce.should.be.true; + inverse.calledOnce.should.be.false; + + readPostStub.calledOnce.should.be.true; + readPostStub.firstCall.args[0].include.should.eql('previous,previous.author,previous.tags'); + done(); }).catch(function (err) { console.log('err ', err); @@ -58,22 +63,15 @@ describe('{{prev_post}} helper', function () { }); describe('for valid post with no previous post', function () { - var sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'previous') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('previous') === 0) { return Promise.resolve({posts: [{slug: '/current/', title: 'post 2'}]}); } }); }); - afterEach(function () { - sandbox.restore(); - }); - it('shows \'else\' template', function (done) { var fn = sinon.spy(), inverse = sinon.spy(), @@ -96,22 +94,15 @@ describe('{{prev_post}} helper', function () { }); describe('for invalid post data', function () { - var sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'previous') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('previous') === 0) { return Promise.resolve({}); } }); }); - afterEach(function () { - sandbox.restore(); - }); - it('shows \'else\' template', function (done) { var fn = sinon.spy(), inverse = sinon.spy(), @@ -120,6 +111,7 @@ describe('{{prev_post}} helper', function () { helpers.prev_post.call({}, optionsData).then(function () { fn.called.should.be.false; inverse.called.should.be.true; + readPostStub.called.should.be.false; done(); }).catch(function (err) { done(err); @@ -128,22 +120,15 @@ describe('{{prev_post}} helper', function () { }); describe('for unpublished post', function () { - var sandbox; - beforeEach(function () { - sandbox = sinon.sandbox.create(); utils.loadHelpers(); - sandbox.stub(api.posts, 'read', function (options) { - if (options.include === 'previous') { + readPostStub = sandbox.stub(api.posts, 'read', function (options) { + if (options.include.indexOf('previous') === 0) { return Promise.resolve({posts: [{slug: '/current/', title: 'post 2', previous: {slug: '/previous/', title: 'post 1'}}]}); } }); }); - afterEach(function () { - sandbox.restore(); - }); - it('shows \'else\' template', function (done) { var fn = sinon.spy(), inverse = sinon.spy(),