diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index e3d17f652a..f522b2d8c4 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -37,6 +37,8 @@ coreHelpers.tags = require('./tags'); coreHelpers.title = require('./title'); coreHelpers.url = require('./url'); coreHelpers.image = require('./image'); +coreHelpers.prev_post = require('./prev_next'); +coreHelpers.next_post = require('./prev_next'); coreHelpers.helperMissing = function (arg) { if (arguments.length === 2) { @@ -109,6 +111,8 @@ registerHelpers = function (adminHbs) { registerAsyncThemeHelper('meta_description', coreHelpers.meta_description); registerAsyncThemeHelper('meta_title', coreHelpers.meta_title); registerAsyncThemeHelper('post_class', coreHelpers.post_class); + registerAsyncThemeHelper('next_post', coreHelpers.next_post); + registerAsyncThemeHelper('prev_post', coreHelpers.prev_post); // Register admin helpers registerAdminHelper('asset', coreHelpers.asset); diff --git a/core/server/helpers/prev_next.js b/core/server/helpers/prev_next.js new file mode 100644 index 0000000000..8798b82747 --- /dev/null +++ b/core/server/helpers/prev_next.js @@ -0,0 +1,38 @@ +// ### prevNext helper exposes methods for prev_post and next_post - separately defined in helpers index. +// Example usages +// `{{#prev_post}}next post{{/next_post}}' + +var api = require('../api'), + schema = require('../data/schema').checks, + Promise = require('bluebird'), + fetch, prevNext; + +fetch = function (options) { + return api.posts.read(options).then(function (result) { + var related = result.posts[0]; + if (related.previous) { + return options.fn(related.previous); + } else if (related.next) { + return options.fn(related.next); + } else { + return options.inverse(this); + } + }); +}; + +// If prevNext method is called without valid post data then we must return a promise, if there is valid post data +// then the promise is handled in the api call. + +prevNext = function (options) { + options = options || {}; + options.include = options.name === 'prev_post' ? 'previous' : 'next'; + if (schema.isPost(this)) { + options.slug = this.slug; + return fetch(options); + } else { + return Promise.resolve(options.inverse(this)); + } +}; + +module.exports = prevNext; diff --git a/core/test/unit/server_helpers/next_post_spec.js b/core/test/unit/server_helpers/next_post_spec.js new file mode 100644 index 0000000000..9db8e89255 --- /dev/null +++ b/core/test/unit/server_helpers/next_post_spec.js @@ -0,0 +1,126 @@ +/*globals describe, beforeEach, afterEach, it*/ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + Promise = require('bluebird'), + hbs = require('express-hbs'), + utils = require('./utils'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = require('../../../server/helpers'), + api = require('../../../server/api'); + +describe('{{next_post}} helper', function () { + 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') { + return Promise.resolve({ + posts: [{slug: '/current/', title: 'post 2', next: {slug: '/next/', title: 'post 3'}}] + }); + } + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('has loaded next_post helper', function () { + should.exist(handlebars.helpers.prev_post); + }); + + it('shows \'if\' template with next post data', function (done) { + var fn = sinon.spy(), + inverse = sinon.spy(), + optionsData = {name: 'next_post', fn: fn, inverse: inverse}; + + helpers.prev_post.call({html: 'content', + markdown: 'ff', + title: 'post2', + slug: 'current', + created_at: new Date(0), + url: '/current/'}, optionsData).then(function () { + fn.called.should.be.true; + inverse.called.should.be.false; + done(); + }).catch(function (err) { + console.log('err ', err); + done(err); + }); + }); + }); + + 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') { + 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(), + optionsData = {name: 'next_post', fn: fn, inverse: inverse}; + + helpers.prev_post.call({html: 'content', + markdown: 'ff', + title: 'post2', + slug: 'current', + created_at: new Date(0), + url: '/current/'}, optionsData).then(function () { + fn.called.should.be.false; + inverse.called.should.be.true; + done(); + }).catch(function (err) { + done(err); + }); + }); + }); + + 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') { + return Promise.resolve({}); + } + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('shows \'else\' template', function (done) { + var fn = sinon.spy(), + inverse = sinon.spy(), + optionsData = {name: 'next_post', fn: fn, inverse: inverse}; + + helpers.prev_post.call({}, optionsData).then(function () { + fn.called.should.be.false; + inverse.called.should.be.true; + done(); + }).catch(function (err) { + done(err); + }); + }); + }); +}); diff --git a/core/test/unit/server_helpers/prev_post_spec.js b/core/test/unit/server_helpers/prev_post_spec.js new file mode 100644 index 0000000000..79e61c34ea --- /dev/null +++ b/core/test/unit/server_helpers/prev_post_spec.js @@ -0,0 +1,127 @@ +/*globals describe, beforeEach, afterEach, it*/ +/*jshint expr:true*/ +var should = require('should'), + sinon = require('sinon'), + Promise = require('bluebird'), + hbs = require('express-hbs'), + utils = require('./utils'), + +// Stuff we are testing + handlebars = hbs.handlebars, + helpers = require('../../../server/helpers'), + api = require('../../../server/api'); + +describe('{{prev_post}} helper', function () { + 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 === 'previous') { + return Promise.resolve({ + posts: [{slug: '/current/', title: 'post 2', previous: {slug: '/previous/', title: 'post 1'}}] + }); + } + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('has loaded prev_post helper', function () { + should.exist(handlebars.helpers.prev_post); + }); + + it('shows \'if\' template with previous post data', function (done) { + var fn = sinon.spy(), + inverse = sinon.spy(), + optionsData = {name: 'prev_post', fn: fn, inverse: inverse}; + + helpers.prev_post.call({html: 'content', + markdown: 'ff', + title: 'post2', + slug: 'current', + created_at: new Date(0), + url: '/current/'}, optionsData).then(function () { + fn.called.should.be.true; + inverse.called.should.be.false; + done(); + }).catch(function (err) { + console.log('err ', err); + done(err); + }); + }); + }); + + 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') { + 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(), + optionsData = {name: 'prev_post', fn: fn, inverse: inverse}; + + helpers.prev_post.call({html: 'content', + markdown: 'ff', + title: 'post2', + slug: 'current', + created_at: new Date(0), + url: '/current/'}, optionsData).then(function () { + fn.called.should.be.false; + inverse.called.should.be.true; + done(); + }).catch(function (err) { + done(err); + }); + }); + }); + + 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') { + return Promise.resolve({}); + } + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('shows \'else\' template', function (done) { + var fn = sinon.spy(), + inverse = sinon.spy(), + optionsData = {name: 'prev_post', fn: fn, inverse: inverse}; + + helpers.prev_post.call({}, optionsData).then(function () { + fn.called.should.be.false; + inverse.called.should.be.true; + done(); + }).catch(function (err) { + done(err); + }); + }); + }); +});