0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Prev & next post filtering, with primary tag support (#9141)

closes #9140
* Rip out existing prev/next implementation
* New implementation using filter
* Support next/prev in primary_tag
This commit is contained in:
Hannah Wolfe 2017-10-13 15:44:39 +01:00 committed by Kevin Ansfield
parent 8de691d575
commit 1c382792ef
8 changed files with 343 additions and 297 deletions

View file

@ -9,8 +9,7 @@ var Promise = require('bluebird'),
i18n = require('../i18n'),
docName = 'posts',
allowedIncludes = [
'created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields',
'next', 'previous', 'next.author', 'next.tags', 'previous.author', 'previous.tags'
'created_by', 'updated_by', 'published_by', 'author', 'tags', 'fields'
],
unsafeAttrs = ['author_id'],
posts;

View file

@ -4,7 +4,9 @@
// `{{#next_post}}<a href ="{{url absolute="true">next post</a>{{/next_post}}'
var proxy = require('./proxy'),
_ = require('lodash'),
Promise = require('bluebird'),
moment = require('moment'),
logging = proxy.logging,
i18n = proxy.i18n,
@ -13,20 +15,41 @@ var proxy = require('./proxy'),
api = proxy.api,
isPost = proxy.checks.isPost,
fetch;
fetch,
buildApiOptions;
fetch = function fetch(apiOptions, options, data) {
var self = this;
buildApiOptions = function buildApiOptions(options, post) {
var publishedAt = moment(post.published_at).format('YYYY-MM-DD HH:mm:ss'),
slug = post.slug,
op = options.name === 'prev_post' ? '<=' : '>',
order = options.name === 'prev_post' ? 'desc' : 'asc',
apiOptions = {
include: 'author,tags',
order: 'published_at ' + order,
limit: 1,
// This line deliberately uses double quotes because GQL cannot handle either double quotes
// or escaped singles, see TryGhost/GQL#34
filter: "slug:-" + slug + "+published_at:" + op + "'" + publishedAt + "'", // jscs:ignore
};
if (_.get(options, 'hash.in') && options.hash.in === 'primary_tag' && _.get(post, 'primary_tag.slug')) {
apiOptions.filter += '+primary_tag:' + post.primary_tag.slug;
}
return apiOptions;
};
fetch = function fetch(options, data) {
var self = this,
apiOptions = buildApiOptions(options, this);
return api.posts
.read(apiOptions)
.browse(apiOptions)
.then(function handleSuccess(result) {
var related = result.posts[0];
if (related.previous) {
return options.fn(related.previous, {data: data});
} else if (related.next) {
return options.fn(related.next, {data: data});
if (related) {
return options.fn(related, {data: data});
} else {
return options.inverse(self, {data: data});
}
@ -44,21 +67,20 @@ fetch = function fetch(apiOptions, options, data) {
module.exports = function prevNext(options) {
options = options || {};
var data = createFrame(options.data),
apiOptions = {
include: options.name === 'prev_post' ? 'previous,previous.author,previous.tags' : 'next,next.author,next.tags'
};
var data = createFrame(options.data);
if (!options.fn) {
// Guard against incorrect usage of the helpers
if (!options.fn || !options.inverse) {
data.error = i18n.t('warnings.helpers.mustBeCalledAsBlock', {helperName: options.name});
logging.warn(data.error);
return Promise.resolve();
}
if (isPost(this) && this.status === 'published') {
apiOptions.slug = this.slug;
return fetch(apiOptions, options, data);
} else {
// Guard against trying to execute prev/next on previews, pages, or other resources
if (!isPost(this) || this.status !== 'published' || this.page) {
return Promise.resolve(options.inverse(this, {data: data}));
}
// With the guards out of the way, attempt to build the apiOptions, and then fetch the data
return fetch.call(this, options, data);
};

View file

@ -664,23 +664,6 @@ Post = ghostBookshelf.Model.extend({
findOne: function findOne(data, options) {
options = options || {};
var withNext = _.includes(options.include, 'next'),
withPrev = _.includes(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'
});
@ -689,52 +672,9 @@ Post = ghostBookshelf.Model.extend({
delete data.status;
}
// Add related objects, excluding next and previous as they are not real db objects
options.withRelated = _.union(options.withRelated, _.pull(
[].concat(options.include),
'next', 'next.author', 'next.tags', 'previous', 'previous.author', 'previous.tags')
);
options.withRelated = _.union(options.withRelated, options.include);
return ghostBookshelf.Model.findOne.call(this, data, options).then(function then(post) {
if ((withNext || withPrev) && post && !post.page) {
var publishedAt = moment(post.get('published_at')).format('YYYY-MM-DD HH:mm:ss'),
prev,
next;
if (withNext) {
next = Post.forge().query(function queryBuilder(qb) {
qb.where('status', '=', 'published')
.andWhere('page', '=', 0)
.andWhere('published_at', '>', publishedAt)
.orderBy('published_at', 'asc')
.limit(1);
}).fetch({withRelated: nextRelations});
}
if (withPrev) {
prev = Post.forge().query(function queryBuilder(qb) {
qb.where('status', '=', 'published')
.andWhere('page', '=', 0)
.andWhere('published_at', '<', publishedAt)
.orderBy('published_at', 'desc')
.limit(1);
}).fetch({withRelated: prevRelations});
}
return Promise.join(next, prev)
.then(function then(nextAndPrev) {
if (nextAndPrev[0]) {
post.relations.next = nextAndPrev[0];
}
if (nextAndPrev[1]) {
post.relations.previous = nextAndPrev[1];
}
return post;
});
}
return post;
});
return ghostBookshelf.Model.findOne.call(this, data, options);
},
/**

View file

@ -439,32 +439,6 @@ describe('Post API', function () {
});
});
it('can retrieve next and previous posts', function (done) {
request.get(testUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[2].id + '/?include=next,previous'))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
should.not.exist(res.headers['x-cache-invalidate']);
var jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.posts);
testUtils.API.checkResponse(jsonResponse.posts[0], 'post', ['next', 'previous']);
jsonResponse.posts[0].page.should.not.be.ok();
jsonResponse.posts[0].next.should.be.an.Object();
testUtils.API.checkResponse(jsonResponse.posts[0].next, 'post');
jsonResponse.posts[0].previous.should.be.an.Object();
testUtils.API.checkResponse(jsonResponse.posts[0].previous, 'post');
done();
});
});
it('can retrieve a static page', function (done) {
request.get(testUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[5].id + '/'))
.set('Authorization', 'Bearer ' + ownerAccessToken)

View file

@ -1,8 +1,9 @@
var should = require('should'),
supertest = require('supertest'),
_ = require('lodash'),
moment = require('moment'),
testUtils = require('../../../utils'),
configUtils = require('../../../utils/configUtils'),
_ = require('lodash'),
config = require('../../../../../core/server/config'),
ghost = testUtils.startGhost,
request;
@ -504,4 +505,65 @@ describe('Public API', function () {
done();
});
});
it('fetch the most recent post, then the prev, then the next should match the first', function (done) {
function createFilter(publishedAt, op) {
// This line deliberately uses double quotes because GQL cannot handle either double quotes
// or escaped singles, see TryGhost/GQL#34
return encodeURIComponent("published_at:" + op + "'" + publishedAt + "'"); // jscs:ignore
}
request
.get(testUtils.API.getApiQuery(
'posts/?client_id=ghost-admin&client_secret=not_available&limit=1'
))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then(function (res) {
should.exist(res.body.posts[0]);
var post = res.body.posts[0],
publishedAt = moment(post.published_at).format('YYYY-MM-DD HH:mm:ss');
post.title.should.eql('Welcome to Ghost');
return request
.get(testUtils.API.getApiQuery(
'posts/?client_id=ghost-admin&client_secret=not_available&limit=1&filter='
+ createFilter(publishedAt, '<')
))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then(function (res) {
should.exist(res.body.posts[0]);
var post = res.body.posts[0],
publishedAt = moment(post.published_at).format('YYYY-MM-DD HH:mm:ss');
post.title.should.eql('Using the Ghost editor');
return request
.get(testUtils.API.getApiQuery(
'posts/?client_id=ghost-admin&client_secret=not_available&limit=1&filter='
+ createFilter(publishedAt, '>')
))
.set('Origin', testUtils.API.getURL())
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200);
})
.then(function (res) {
should.exist(res.body.posts[0]);
var post = res.body.posts[0];
post.title.should.eql('Welcome to Ghost');
done();
})
.catch(done);
});
});

View file

@ -627,91 +627,6 @@ describe('Post API', function () {
}).catch(done);
});
it('can include next post', function (done) {
PostAPI.read({
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[2].id,
include: 'next'
}).then(function (results) {
should.exist(results.posts[0].next.slug);
results.posts[0].next.slug.should.eql('not-so-short-bit-complex');
done();
}).catch(done);
});
it('can include next post with author and tags', function (done) {
PostAPI.read({
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[2].id,
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: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[1].id,
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: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[2].id,
include: 'previous'
}).then(function (results) {
should.exist(results.posts[0].previous.slug);
results.posts[0].previous.slug.should.eql('ghostly-kitchen-sink');
done();
}).catch(done);
});
it('can include previous post with author and tags', function (done) {
PostAPI.read({
context: {user: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[2].id,
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: testUtils.DataGenerator.Content.users[1].id},
id: testUtils.DataGenerator.Content.posts[2].id,
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);
});
// TODO: this should be a 422?
it('cannot fetch a post with an invalid slug', function (done) {
PostAPI.read({slug: 'invalid!'}).then(function () {

View file

@ -2,6 +2,7 @@ var should = require('should'), // jshint ignore:line
sinon = require('sinon'),
Promise = require('bluebird'),
markdownToMobiledoc = require('../../utils/fixtures/data-generator').markdownToMobiledoc,
// Stuff we are testing
helpers = require('../../../server/helpers'),
api = require('../../../server/api'),
@ -10,7 +11,7 @@ var should = require('should'), // jshint ignore:line
sandbox = sinon.sandbox.create();
describe('{{next_post}} helper', function () {
var readPostStub;
var browsePostStub;
afterEach(function () {
sandbox.restore();
@ -18,10 +19,10 @@ describe('{{next_post}} helper', function () {
describe('with valid post data - ', function () {
beforeEach(function () {
readPostStub = sandbox.stub(api.posts, 'read', function (options) {
if (options.include.indexOf('next') === 0) {
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({
posts: [{slug: '/current/', title: 'post 2', next: {slug: '/next/', title: 'post 3'}}]
posts: [{slug: '/next/', title: 'post 3'}]
});
}
});
@ -32,26 +33,26 @@ describe('{{next_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'next_post', fn: fn, inverse: inverse};
helpers.prev_post
helpers.next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
created_at: new Date(0),
published_at: new Date(0),
url: '/current/'
}, optionsData)
.then(function () {
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
console.log(fn.firstCall.args);
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
readPostStub.calledOnce.should.be.true();
readPostStub.firstCall.args[0].include.should.eql('next,next.author,next.tags');
browsePostStub.calledOnce.should.be.true();
browsePostStub.firstCall.args[0].include.should.eql('author,tags');
done();
})
.catch(done);
@ -60,9 +61,9 @@ describe('{{next_post}} helper', function () {
describe('for valid post with no next post', function () {
beforeEach(function () {
readPostStub = sandbox.stub(api.posts, 'read', function (options) {
if (options.include.indexOf('next') === 0) {
return Promise.resolve({posts: [{slug: '/current/', title: 'post 2'}]});
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({posts: []});
}
});
});
@ -72,20 +73,20 @@ describe('{{next_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'next_post', fn: fn, inverse: inverse};
helpers.prev_post
helpers.next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
created_at: new Date(0),
published_at: new Date(0),
url: '/current/'
}, optionsData)
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.true();
console.log(inverse.firstCall.args);
inverse.firstCall.args.should.have.lengthOf(2);
inverse.firstCall.args[0].should.have.properties('slug', 'title');
inverse.firstCall.args[1].should.be.an.Object().and.have.property('data');
@ -98,8 +99,8 @@ describe('{{next_post}} helper', function () {
describe('for invalid post data', function () {
beforeEach(function () {
readPostStub = sandbox.stub(api.posts, 'read', function (options) {
if (options.include.indexOf('next') === 0) {
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({});
}
});
@ -110,25 +111,24 @@ describe('{{next_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'next_post', fn: fn, inverse: inverse};
helpers.prev_post
helpers.next_post
.call({}, optionsData)
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.true();
readPostStub.called.should.be.false();
browsePostStub.called.should.be.false();
done();
})
.catch(done);
});
});
describe('for unpublished post', function () {
describe('for page', function () {
beforeEach(function () {
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'}}]
});
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({posts: [{slug: '/previous/', title: 'post 1'}]});
}
});
});
@ -138,7 +138,42 @@ describe('{{next_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'next_post', fn: fn, inverse: inverse};
helpers.prev_post
helpers.next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/',
page: true
}, optionsData)
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.true();
done();
})
.catch(done);
});
});
describe('for unpublished post', function () {
beforeEach(function () {
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:>') > -1) {
return Promise.resolve({posts: [{slug: '/next/', title: 'post 3'}]});
}
});
});
it('shows \'else\' template', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy(),
optionsData = {name: 'next_post', fn: fn, inverse: inverse};
helpers.next_post
.call({
html: 'content',
status: 'draft',
@ -151,6 +186,7 @@ describe('{{next_post}} helper', function () {
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.true();
done();
})
.catch(done);
@ -159,7 +195,7 @@ describe('{{next_post}} helper', function () {
describe('general error handling', function () {
beforeEach(function () {
readPostStub = sandbox.stub(api.posts, 'read', function () {
browsePostStub = sandbox.stub(api.posts, 'browse', function () {
return Promise.reject(new errors.NotFoundError({message: 'Something wasn\'t found'}));
});
});
@ -169,14 +205,14 @@ describe('{{next_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'next_post', fn: fn, inverse: inverse};
helpers.prev_post
helpers.next_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
created_at: new Date(0),
published_at: new Date(0),
url: '/current/'
}, optionsData)
.then(function () {
@ -197,7 +233,7 @@ describe('{{next_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'next_post'};
helpers.prev_post
helpers.next_post
.call(
{},
optionsData

View file

@ -6,11 +6,12 @@ var should = require('should'), // jshint ignore:line
// Stuff we are testing
helpers = require('../../../server/helpers'),
api = require('../../../server/api'),
errors = require('../../../server/errors'),
sandbox = sinon.sandbox.create();
describe('{{prev_post}} helper', function () {
var readPostStub;
var browsePostStub;
afterEach(function () {
sandbox.restore();
@ -18,10 +19,10 @@ describe('{{prev_post}} helper', function () {
describe('with valid post data - ', function () {
beforeEach(function () {
readPostStub = sandbox.stub(api.posts, 'read', function (options) {
if (options.include.indexOf('previous') === 0) {
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:<=') > -1) {
return Promise.resolve({
posts: [{slug: '/current/', title: 'post 2', previous: {slug: '/previous/', title: 'post 1'}}]
posts: [{slug: '/previous/', title: 'post 1'}]
});
}
});
@ -32,34 +33,37 @@ describe('{{prev_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'prev_post', fn: fn, inverse: inverse};
helpers.prev_post.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
created_at: new Date(0),
url: '/current/'
}, optionsData).then(function () {
fn.calledOnce.should.be.true();
inverse.calledOnce.should.be.false();
helpers.prev_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData)
.then(function () {
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');
fn.firstCall.args.should.have.lengthOf(2);
fn.firstCall.args[0].should.have.properties('slug', 'title');
fn.firstCall.args[1].should.be.an.Object().and.have.property('data');
browsePostStub.calledOnce.should.be.true();
browsePostStub.firstCall.args[0].include.should.eql('author,tags');
done();
}).catch(function (err) {
console.log('err ', err);
done(err);
});
done();
})
.catch(done);
});
});
describe('for valid post with no previous post', function () {
beforeEach(function () {
readPostStub = sandbox.stub(api.posts, 'read', function (options) {
if (options.include.indexOf('previous') === 0) {
return Promise.resolve({posts: [{slug: '/current/', title: 'post 2'}]});
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:<=') > -1) {
return Promise.resolve({posts: []});
}
});
});
@ -69,28 +73,34 @@ describe('{{prev_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'prev_post', fn: fn, inverse: inverse};
helpers.prev_post.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('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);
});
helpers.prev_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData)
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.true();
inverse.firstCall.args.should.have.lengthOf(2);
inverse.firstCall.args[0].should.have.properties('slug', 'title');
inverse.firstCall.args[1].should.be.an.Object().and.have.property('data');
done();
})
.catch(done);
});
});
describe('for invalid post data', function () {
beforeEach(function () {
readPostStub = sandbox.stub(api.posts, 'read', function (options) {
if (options.include.indexOf('previous') === 0) {
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:<=') > -1) {
return Promise.resolve({});
}
});
@ -101,28 +111,24 @@ describe('{{prev_post}} helper', function () {
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();
readPostStub.called.should.be.false();
done();
}).catch(function (err) {
done(err);
});
helpers.prev_post
.call({}, optionsData)
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.true();
browsePostStub.called.should.be.false();
done();
})
.catch(done);
});
});
describe('for unpublished post', function () {
describe('for page', function () {
beforeEach(function () {
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'}
}]
});
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:<=') > -1) {
return Promise.resolve({posts: [{slug: '/previous/', title: 'post 1'}]});
}
});
});
@ -132,21 +138,113 @@ describe('{{prev_post}} helper', function () {
inverse = sinon.spy(),
optionsData = {name: 'prev_post', fn: fn, inverse: inverse};
helpers.prev_post.call({
html: 'content',
status: 'draft',
mobiledoc: markdownToMobiledoc('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);
helpers.prev_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/',
page: true
}, optionsData)
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.true();
done();
})
.catch(done);
});
});
describe('for unpublished post', function () {
beforeEach(function () {
browsePostStub = sandbox.stub(api.posts, 'browse', function (options) {
if (options.filter.indexOf('published_at:<=') > -1) {
return Promise.resolve({posts: [{slug: '/previous/', title: 'post 1'}]});
}
});
});
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',
status: 'draft',
mobiledoc: markdownToMobiledoc('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(done);
});
});
describe('general error handling', function () {
beforeEach(function () {
browsePostStub = sandbox.stub(api.posts, 'browse', function () {
return Promise.reject(new errors.NotFoundError({message: 'Something wasn\'t found'}));
});
});
it('should handle error from the API', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy(),
optionsData = {name: 'prev_post', fn: fn, inverse: inverse};
helpers.prev_post
.call({
html: 'content',
status: 'published',
mobiledoc: markdownToMobiledoc('ff'),
title: 'post2',
slug: 'current',
published_at: new Date(0),
url: '/current/'
}, optionsData)
.then(function () {
fn.called.should.be.false();
inverse.calledOnce.should.be.true();
inverse.firstCall.args[1].should.be.an.Object().and.have.property('data');
inverse.firstCall.args[1].data.should.be.an.Object().and.have.property('error');
inverse.firstCall.args[1].data.error.should.match(/^Something wasn't found/);
done();
})
.catch(done);
});
it('should show warning for call without any options', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy(),
optionsData = {name: 'prev_post'};
helpers.prev_post
.call(
{},
optionsData
)
.then(function () {
fn.called.should.be.false();
inverse.called.should.be.false();
done();
})
.catch(done);
});
});
});