mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
add events for post scheduling
refs #6413 - accept scheduled status - add a lot of tests for all kinds of edge cases - compare dates without ms because mysql does not store ms
This commit is contained in:
parent
817a302885
commit
d24466a284
6 changed files with 356 additions and 17 deletions
|
@ -1,6 +1,7 @@
|
|||
// # Post Model
|
||||
var _ = require('lodash'),
|
||||
uuid = require('node-uuid'),
|
||||
moment = require('moment'),
|
||||
Promise = require('bluebird'),
|
||||
sequence = require('../utils/sequence'),
|
||||
errors = require('../errors'),
|
||||
|
@ -43,38 +44,73 @@ Post = ghostBookshelf.Model.extend({
|
|||
});
|
||||
|
||||
this.on('created', function onCreated(model) {
|
||||
var status = model.get('status');
|
||||
|
||||
model.emitChange('added');
|
||||
|
||||
if (model.get('status') === 'published') {
|
||||
model.emitChange('published');
|
||||
if (['published', 'scheduled'].indexOf(status) !== -1) {
|
||||
model.emitChange(status);
|
||||
}
|
||||
});
|
||||
|
||||
this.on('updated', 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.needsReschedule = model.get('published_at') !== model.updated('published_at');
|
||||
|
||||
// Handle added and deleted for changing resource
|
||||
// Handle added and deleted for post -> page or page -> post
|
||||
if (model.resourceTypeChanging) {
|
||||
if (model.wasPublished) {
|
||||
model.emitChange('unpublished', true);
|
||||
}
|
||||
|
||||
if (model.wasScheduled) {
|
||||
model.emitChange('unscheduled', true);
|
||||
}
|
||||
|
||||
model.emitChange('deleted', true);
|
||||
model.emitChange('added');
|
||||
|
||||
if (model.isPublished) {
|
||||
model.emitChange('published');
|
||||
}
|
||||
|
||||
if (model.isScheduled) {
|
||||
model.emitChange('scheduled');
|
||||
}
|
||||
} else {
|
||||
if (model.statusChanging) {
|
||||
model.emitChange(model.isPublished ? 'published' : 'unpublished');
|
||||
// CASE: was published before and is now e.q. draft or scheduled
|
||||
if (model.wasPublished) {
|
||||
model.emitChange('unpublished');
|
||||
}
|
||||
|
||||
// CASE: was draft or scheduled before and is now e.q. published
|
||||
if (model.isPublished) {
|
||||
model.emitChange('published');
|
||||
}
|
||||
|
||||
// CASE: was draft or published before and is now e.q. scheduled
|
||||
if (model.isScheduled) {
|
||||
model.emitChange('scheduled');
|
||||
}
|
||||
|
||||
// CASE: from scheduled to something
|
||||
if (model.wasScheduled && !model.isScheduled) {
|
||||
model.emitChange('unscheduled');
|
||||
}
|
||||
} else {
|
||||
if (model.isPublished) {
|
||||
model.emitChange('published.edited');
|
||||
}
|
||||
|
||||
if (model.needsReschedule) {
|
||||
model.emitChange('rescheduled');
|
||||
}
|
||||
}
|
||||
|
||||
// Fire edited if this wasn't a change between resourceType
|
||||
|
@ -93,20 +129,41 @@ Post = ghostBookshelf.Model.extend({
|
|||
},
|
||||
|
||||
saving: function saving(model, attr, options) {
|
||||
options = options || {};
|
||||
|
||||
var self = this,
|
||||
tagsToCheck,
|
||||
title,
|
||||
i,
|
||||
// Variables to make the slug checking more readable
|
||||
newTitle = this.get('title'),
|
||||
newStatus = this.get('status'),
|
||||
prevTitle = this._previousAttributes.title,
|
||||
prevSlug = this._previousAttributes.slug,
|
||||
postStatus = this.get('status'),
|
||||
tagsToCheck = this.get('tags'),
|
||||
publishedAt = this.get('published_at');
|
||||
|
||||
options = options || {};
|
||||
// both page and post can get scheduled
|
||||
if (newStatus === 'scheduled') {
|
||||
if (!publishedAt) {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'})
|
||||
));
|
||||
} else if (!moment(publishedAt).isValid()) {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'})
|
||||
));
|
||||
} else if (moment(publishedAt).isBefore(moment())) {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.expectedPublishedAtInFuture')
|
||||
));
|
||||
} else if (moment(publishedAt).isBefore(moment().add(5, 'minutes'))) {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.expectedPublishedAtInFuture')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// keep tags for 'saved' event and deduplicate upper/lowercase tags
|
||||
tagsToCheck = this.get('tags');
|
||||
this.myTags = [];
|
||||
|
||||
_.each(tagsToCheck, function each(item) {
|
||||
|
@ -129,12 +186,12 @@ Post = ghostBookshelf.Model.extend({
|
|||
|
||||
// ### Business logic for published_at and published_by
|
||||
// If the current status is 'published' and published_at is not set, set it to now
|
||||
if (this.get('status') === 'published' && !this.get('published_at')) {
|
||||
if (newStatus === 'published' && !publishedAt) {
|
||||
this.set('published_at', new Date());
|
||||
}
|
||||
|
||||
// If the current status is 'published' and the status has just changed ensure published_by is set correctly
|
||||
if (this.get('status') === 'published' && this.hasChanged('status')) {
|
||||
if (newStatus === 'published' && this.hasChanged('status')) {
|
||||
// unless published_by is set and we're importing, set published_by to contextUser
|
||||
if (!(this.get('published_by') && options.importing)) {
|
||||
this.set('published_by', this.contextUser(options));
|
||||
|
@ -147,7 +204,7 @@ Post = ghostBookshelf.Model.extend({
|
|||
}
|
||||
|
||||
// If a title is set, not the same as the old title, a draft post, and has never been published
|
||||
if (prevTitle !== undefined && newTitle !== prevTitle && postStatus === 'draft' && !publishedAt) {
|
||||
if (prevTitle !== undefined && newTitle !== prevTitle && newStatus === 'draft' && !publishedAt) {
|
||||
// Pass the new slug through the generator to strip illegal characters, detect duplicates
|
||||
return ghostBookshelf.Model.generateSlug(Post, this.get('title'),
|
||||
{status: 'all', transacting: options.transacting, importing: options.importing})
|
||||
|
|
|
@ -199,6 +199,8 @@
|
|||
"models": {
|
||||
"post": {
|
||||
"untitled": "(Untitled)",
|
||||
"valueCannotBeBlank": "Value in {key} cannot be blank.",
|
||||
"expectedPublishedAtInFuture": "Expected published_at to be in the future.",
|
||||
"noUserFound": "No user found",
|
||||
"notEnoughPermission": "You do not have permission to perform this action",
|
||||
"tagUpdates": {
|
||||
|
|
|
@ -342,7 +342,7 @@ describe('Post API', function () {
|
|||
it('can order posts using asc', function (done) {
|
||||
var posts, expectedTitles;
|
||||
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').reject({status: 'scheduled'}).value();
|
||||
expectedTitles = _(posts).pluck('title').sortBy().value();
|
||||
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', order: 'title asc', fields: 'title'}).then(function (results) {
|
||||
|
@ -358,7 +358,7 @@ describe('Post API', function () {
|
|||
it('can order posts using desc', function (done) {
|
||||
var posts, expectedTitles;
|
||||
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').reject({status: 'scheduled'}).value();
|
||||
expectedTitles = _(posts).pluck('title').sortBy().reverse().value();
|
||||
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', order: 'title DESC', fields: 'title'}).then(function (results) {
|
||||
|
@ -374,7 +374,7 @@ describe('Post API', function () {
|
|||
it('can order posts and filter disallowed attributes', function (done) {
|
||||
var posts, expectedTitles;
|
||||
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').reject({status: 'scheduled'}).value();
|
||||
expectedTitles = _(posts).pluck('title').sortBy().value();
|
||||
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', order: 'bunny DESC, title ASC', fields: 'title'}).then(function (results) {
|
||||
|
|
|
@ -205,7 +205,7 @@ describe('Users API', function () {
|
|||
response.users[0].count.posts.should.eql(0);
|
||||
response.users[1].count.posts.should.eql(0);
|
||||
response.users[2].count.posts.should.eql(0);
|
||||
response.users[3].count.posts.should.eql(7);
|
||||
response.users[3].count.posts.should.eql(8);
|
||||
response.users[4].count.posts.should.eql(0);
|
||||
response.users[5].count.posts.should.eql(0);
|
||||
response.users[6].count.posts.should.eql(0);
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
sequence = require('../../../server/utils/sequence'),
|
||||
moment = require('moment'),
|
||||
_ = require('lodash'),
|
||||
sinon = require('sinon'),
|
||||
|
||||
// Stuff we are testing
|
||||
sequence = require('../../../server/utils/sequence'),
|
||||
ghostBookshelf = require('../../../server/models/base'),
|
||||
PostModel = require('../../../server/models/post').Post,
|
||||
events = require('../../../server/events'),
|
||||
errors = require('../../../server/errors'),
|
||||
DataGenerator = testUtils.DataGenerator,
|
||||
context = testUtils.context.owner,
|
||||
sandbox = sinon.sandbox.create(),
|
||||
|
@ -425,6 +427,142 @@ describe('Post Model', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('draft -> scheduled without published_at update', function (done) {
|
||||
PostModel.findOne({status: 'draft'}).then(function (results) {
|
||||
var post;
|
||||
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.status.should.equal('draft');
|
||||
|
||||
return PostModel.edit({
|
||||
status: 'scheduled'
|
||||
}, _.extend({}, context, {id: post.id}));
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('draft -> scheduled: invalid published_at update', function (done) {
|
||||
PostModel.findOne({status: 'draft'}).then(function (results) {
|
||||
var post;
|
||||
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.status.should.equal('draft');
|
||||
|
||||
return PostModel.edit({
|
||||
status: 'scheduled',
|
||||
published_at: '328432423'
|
||||
}, _.extend({}, context, {id: post.id}));
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('draft -> scheduled: expect update of published_at', function (done) {
|
||||
var newPublishedAt = moment().add(1, 'day').toDate();
|
||||
|
||||
PostModel.findOne({status: 'draft'}).then(function (results) {
|
||||
var post;
|
||||
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.status.should.equal('draft');
|
||||
|
||||
return PostModel.edit({
|
||||
status: 'scheduled',
|
||||
published_at: newPublishedAt
|
||||
}, _.extend({}, context, {id: post.id}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('scheduled');
|
||||
|
||||
// mysql does not store ms
|
||||
moment(edited.attributes.published_at).startOf('seconds').diff(moment(newPublishedAt).startOf('seconds')).should.eql(0);
|
||||
eventSpy.calledTwice.should.be.true();
|
||||
eventSpy.firstCall.calledWith('post.scheduled').should.be.true();
|
||||
eventSpy.secondCall.calledWith('post.edited').should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('scheduled -> draft: expect unschedule', function (done) {
|
||||
PostModel.findOne({status: 'scheduled'}).then(function (results) {
|
||||
var post;
|
||||
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.status.should.equal('scheduled');
|
||||
|
||||
return PostModel.edit({
|
||||
status: 'draft'
|
||||
}, _.extend({}, context, {id: post.id}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('draft');
|
||||
eventSpy.callCount.should.eql(2);
|
||||
eventSpy.firstCall.calledWith('post.unscheduled').should.be.true();
|
||||
eventSpy.secondCall.calledWith('post.edited').should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('scheduled -> scheduled with updated published_at', function (done) {
|
||||
PostModel.findOne({status: 'scheduled'}).then(function (results) {
|
||||
var post;
|
||||
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.status.should.equal('scheduled');
|
||||
|
||||
return PostModel.edit({
|
||||
status: 'scheduled',
|
||||
published_at: moment().add(20, 'days')
|
||||
}, _.extend({}, context, {id: post.id}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('scheduled');
|
||||
eventSpy.callCount.should.eql(2);
|
||||
eventSpy.firstCall.calledWith('post.rescheduled').should.be.true();
|
||||
eventSpy.secondCall.calledWith('post.edited').should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('published -> scheduled and expect update of published_at', function (done) {
|
||||
var postId = 1;
|
||||
|
||||
PostModel.findOne({id: postId}).then(function (results) {
|
||||
var post;
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.id.should.equal(postId);
|
||||
post.status.should.equal('published');
|
||||
|
||||
return PostModel.edit({
|
||||
status: 'scheduled',
|
||||
published_at: moment().add(1, 'day').toDate()
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('scheduled');
|
||||
eventSpy.callCount.should.eql(3);
|
||||
eventSpy.firstCall.calledWith('post.unpublished').should.be.true();
|
||||
eventSpy.secondCall.calledWith('post.scheduled').should.be.true();
|
||||
eventSpy.thirdCall.calledWith('post.edited').should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can convert draft post to page and back', function (done) {
|
||||
var postId = 4;
|
||||
|
||||
|
@ -456,6 +594,41 @@ describe('Post Model', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can convert draft to schedule AND post to page and back', function (done) {
|
||||
PostModel.findOne({status: 'draft'}).then(function (results) {
|
||||
var post;
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.status.should.equal('draft');
|
||||
|
||||
return PostModel.edit({
|
||||
page: 1,
|
||||
status: 'scheduled',
|
||||
published_at: moment().add(10, 'days')
|
||||
}, _.extend({}, context, {id: post.id}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('scheduled');
|
||||
edited.attributes.page.should.equal(true);
|
||||
eventSpy.callCount.should.be.eql(3);
|
||||
eventSpy.firstCall.calledWith('post.deleted').should.be.true();
|
||||
eventSpy.secondCall.calledWith('page.added').should.be.true();
|
||||
eventSpy.thirdCall.calledWith('page.scheduled').should.be.true();
|
||||
|
||||
return PostModel.edit({page: 0}, _.extend({}, context, {id: edited.id}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('scheduled');
|
||||
edited.attributes.page.should.equal(false);
|
||||
eventSpy.callCount.should.equal(7);
|
||||
eventSpy.getCall(3).calledWith('page.unscheduled').should.be.true();
|
||||
eventSpy.getCall(4).calledWith('page.deleted').should.be.true();
|
||||
eventSpy.getCall(5).calledWith('post.added').should.be.true();
|
||||
eventSpy.getCall(6).calledWith('post.scheduled').should.be.true();
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can convert published post to page and back', function (done) {
|
||||
var postId = 1;
|
||||
|
||||
|
@ -666,6 +839,106 @@ describe('Post Model', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('add draft post without published_at -> we expect no auto insert of published_at', function (done) {
|
||||
PostModel.add({
|
||||
status: 'draft',
|
||||
title: 'draft 1',
|
||||
markdown: 'This is some content'
|
||||
}, context).then(function (newPost) {
|
||||
should.exist(newPost);
|
||||
should.not.exist(newPost.get('published_at'));
|
||||
eventSpy.calledOnce.should.be.true();
|
||||
eventSpy.firstCall.calledWith('post.added').should.be.true();
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('add draft post with published_at -> we expect published_at to exist', function (done) {
|
||||
PostModel.add({
|
||||
status: 'draft',
|
||||
published_at: moment().toDate(),
|
||||
title: 'draft 1',
|
||||
markdown: 'This is some content'
|
||||
}, context).then(function (newPost) {
|
||||
should.exist(newPost);
|
||||
should.exist(newPost.get('published_at'));
|
||||
eventSpy.calledOnce.should.be.true();
|
||||
eventSpy.firstCall.calledWith('post.added').should.be.true();
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('add scheduled post without published_at -> we expect an error', function (done) {
|
||||
PostModel.add({
|
||||
status: 'scheduled',
|
||||
title: 'scheduled 1',
|
||||
markdown: 'This is some content'
|
||||
}, context).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
eventSpy.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add scheduled post with published_at not in future-> we expect an error', function (done) {
|
||||
PostModel.add({
|
||||
status: 'scheduled',
|
||||
published_at: moment().subtract(1, 'minute'),
|
||||
title: 'scheduled 1',
|
||||
markdown: 'This is some content'
|
||||
}, context).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
eventSpy.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add scheduled post with published_at 1 minutes in future -> we expect an error', function (done) {
|
||||
PostModel.add({
|
||||
status: 'scheduled',
|
||||
published_at: moment().add(1, 'minute'),
|
||||
title: 'scheduled 1',
|
||||
markdown: 'This is some content'
|
||||
}, context).catch(function (err) {
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
eventSpy.called.should.be.false();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('add scheduled post with published_at 10 minutes in future -> we expect success', function (done) {
|
||||
PostModel.add({
|
||||
status: 'scheduled',
|
||||
published_at: moment().add(10, 'minute'),
|
||||
title: 'scheduled 1',
|
||||
markdown: 'This is some content'
|
||||
}, context).then(function (post) {
|
||||
should.exist(post);
|
||||
eventSpy.calledTwice.should.be.true();
|
||||
eventSpy.firstCall.calledWith('post.added').should.be.true();
|
||||
eventSpy.secondCall.calledWith('post.scheduled').should.be.true();
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('add scheduled page with published_at 10 minutes in future -> we expect success', function (done) {
|
||||
PostModel.add({
|
||||
status: 'scheduled',
|
||||
page: 1,
|
||||
published_at: moment().add(10, 'minute'),
|
||||
title: 'scheduled 1',
|
||||
markdown: 'This is some content'
|
||||
}, context).then(function (post) {
|
||||
should.exist(post);
|
||||
eventSpy.calledTwice.should.be.true();
|
||||
eventSpy.firstCall.calledWith('page.added').should.be.true();
|
||||
eventSpy.secondCall.calledWith('page.scheduled').should.be.true();
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can add default title, if it\'s missing', function (done) {
|
||||
PostModel.add({
|
||||
markdown: 'Content'
|
||||
|
|
|
@ -53,6 +53,12 @@ DataGenerator.Content = {
|
|||
markdown: "<h1>Static page test is what this is for.</h1><p>Hopefully you don't find it a bore.</p>",
|
||||
page: 1,
|
||||
status: "draft"
|
||||
},
|
||||
{
|
||||
title: "This is a scheduled post!!",
|
||||
slug: "scheduled-post",
|
||||
markdown: "<h1>Welcome to my invisible post!</h1>",
|
||||
status: "scheduled"
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -363,7 +369,8 @@ DataGenerator.forKnex = (function () {
|
|||
createPost(DataGenerator.Content.posts[3]),
|
||||
createPost(DataGenerator.Content.posts[4]),
|
||||
createPost(DataGenerator.Content.posts[5]),
|
||||
createPost(DataGenerator.Content.posts[6])
|
||||
createPost(DataGenerator.Content.posts[6]),
|
||||
createPost(DataGenerator.Content.posts[7])
|
||||
];
|
||||
|
||||
tags = [
|
||||
|
|
Loading…
Add table
Reference in a new issue