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

Merge pull request #5200 from novaugust/preview_step_1

Add post preview via uuid (/p/:uuid)
This commit is contained in:
Paul Davis 2015-04-30 12:48:19 +01:00
commit 3893b18479
14 changed files with 240 additions and 42 deletions

View file

@ -35,6 +35,17 @@ var Post = DS.Model.extend(NProgressSaveMixin, ValidationEngine, {
return this.get('ghostPaths.url').join(blogUrl, postUrl);
}),
previewUrl: Ember.computed('uuid', 'ghostPaths.url', 'config.blogUrl', 'config.routeKeywords.preview', function () {
var blogUrl = this.get('config.blogUrl'),
uuid = this.get('uuid'),
previewKeyword = this.get('config.routeKeywords.preview');
// New posts don't have a preview
if (!uuid) {
return '';
}
return this.get('ghostPaths.url').join(blogUrl, previewKeyword, uuid);
}),
scratch: null,
titleScratch: null,

View file

@ -692,6 +692,13 @@ body.zen {
}
}//.post-settings-menu
.post-preview-link {
position: absolute;
top: 0;
right: 0;
font-size: 1.3rem;
}
//
// Post Settings Menu meta Data

View file

@ -11,6 +11,11 @@
<form>
<div class="form-group">
<label for="url">Post URL</label>
{{#if model.isDraft}}
<a class="post-preview-link" target="_blank" href="{{model.previewUrl}}">
Preview<span class="icon-external"></span>
</a>
{{/if}}
<span class="input-icon icon-link">
{{gh-input class="post-setting-slug" id="url" value=slugValue name="post-setting-slug" focus-out="updateSlug" selectOnClick="true" stopEnterKeyDownPropagation="true"}}
</span>

View file

@ -11,6 +11,8 @@ var isNumeric = function (num) {
return false;
} else if (isNumeric(val)) {
return +val;
} else if (val.indexOf('{') === 0) {
return JSON.parse(val);
} else {
return val;
}

View file

@ -16,7 +16,8 @@ function getValidKeys() {
database: config.database.client,
mail: _.isObject(config.mail) ? config.mail.transport : '',
blogUrl: config.url.replace(/\/$/, ''),
blogTitle: config.theme.title
blogTitle: config.theme.title,
routeKeywords: JSON.stringify(config.routeKeywords)
};
return validKeys;

View file

@ -64,20 +64,20 @@ posts = {
/**
* ### Read
* Find a post, by ID or Slug
* Find a post, by ID, UUID, or Slug
*
* @public
* @param {{id_or_slug (required), context, status, include, ...}} options
* @return {Promise(Post)} Post
*/
read: function read(options) {
var attrs = ['id', 'slug', 'status'],
var attrs = ['id', 'slug', 'status', 'uuid'],
data = _.pick(options, attrs);
options = _.omit(options, attrs);
// only published posts if no user is present
if (!(options.context && options.context.user)) {
if (!data.uuid && !(options.context && options.context.user)) {
data.status = 'published';
}

View file

@ -205,7 +205,8 @@ ConfigManager.prototype.set = function (config) {
routeKeywords: {
tag: 'tag',
author: 'author',
page: 'page'
page: 'page',
preview: 'p'
},
slugs: {
// Used by generateSlug to generate slugs for posts, tags, users, ..

View file

@ -123,6 +123,25 @@ function getActiveThemePaths() {
});
}
/*
* Sets the response context around a post and renders it
* with the current theme's post view. Used by post preview
* and single post methods.
* Returns a function that takes the post to be rendered.
*/
function renderPost(req, res) {
return function (post) {
return getActiveThemePaths().then(function (paths) {
var view = template.getThemeViewForPost(paths, post),
response = formatResponse(post);
setResponseContext(req, res, response);
res.render(view, response);
});
};
}
frontendControllers = {
homepage: function (req, res, next) {
// Parse the page number
@ -271,6 +290,37 @@ frontendControllers = {
}).catch(handleError(next));
},
preview: function (req, res, next) {
var params = {
uuid: req.params.uuid,
status: 'all',
include: 'author,tags,fields'
};
api.posts.read(params).then(function (result) {
var post = result.posts[0];
if (!post) {
return next();
}
if (post.status === 'published') {
return res.redirect(301, config.urlFor('post', {post: post}));
}
setReqCtx(req, post);
filters.doFilter('prePostsRender', post, res.locals)
.then(renderPost(req, res));
}).catch(function (err) {
if (err.errorType === 'NotFoundError') {
return next();
}
return handleError(next)(err);
});
},
single: function (req, res, next) {
var path = req.path,
params,
@ -336,16 +386,8 @@ frontendControllers = {
setReqCtx(req, post);
filters.doFilter('prePostsRender', post, res.locals).then(function (post) {
getActiveThemePaths().then(function (paths) {
var view = template.getThemeViewForPost(paths, post),
response = formatResponse(post);
setResponseContext(req, res, response);
res.render(view, response);
});
});
filters.doFilter('prePostsRender', post, res.locals)
.then(renderPost(req, res));
}
// If we've checked the path with the static permalink structure

View file

@ -32,7 +32,7 @@ prevNext = function (options) {
include: options.name === 'prev_post' ? 'previous' : 'next'
};
if (schema.isPost(this)) {
if (schema.isPost(this) && this.status === 'published') {
apiOptions.slug = this.slug;
return fetch(apiOptions, options);
} else {

View file

@ -7,7 +7,8 @@ var frontend = require('../controllers/frontend'),
frontendRoutes = function () {
var router = express.Router(),
subdir = config.paths.subdir;
subdir = config.paths.subdir,
routeKeywords = config.routeKeywords;
// ### Admin routes
router.get(/^\/(logout|signout)\/$/, function redirect(req, res) {
@ -37,19 +38,22 @@ frontendRoutes = function () {
});
// Tags
router.get('/' + config.routeKeywords.tag + '/:slug/rss/', frontend.rss);
router.get('/' + config.routeKeywords.tag + '/:slug/rss/:page/', frontend.rss);
router.get('/' + config.routeKeywords.tag + '/:slug/' + config.routeKeywords.page + '/:page/', frontend.tag);
router.get('/' + config.routeKeywords.tag + '/:slug/', frontend.tag);
router.get('/' + routeKeywords.tag + '/:slug/rss/', frontend.rss);
router.get('/' + routeKeywords.tag + '/:slug/rss/:page/', frontend.rss);
router.get('/' + routeKeywords.tag + '/:slug/' + routeKeywords.page + '/:page/', frontend.tag);
router.get('/' + routeKeywords.tag + '/:slug/', frontend.tag);
// Authors
router.get('/' + config.routeKeywords.author + '/:slug/rss/', frontend.rss);
router.get('/' + config.routeKeywords.author + '/:slug/rss/:page/', frontend.rss);
router.get('/' + config.routeKeywords.author + '/:slug/' + config.routeKeywords.page + '/:page/', frontend.author);
router.get('/' + config.routeKeywords.author + '/:slug/', frontend.author);
router.get('/' + routeKeywords.author + '/:slug/rss/', frontend.rss);
router.get('/' + routeKeywords.author + '/:slug/rss/:page/', frontend.rss);
router.get('/' + routeKeywords.author + '/:slug/' + routeKeywords.page + '/:page/', frontend.author);
router.get('/' + routeKeywords.author + '/:slug/', frontend.author);
// Post Live Preview
router.get('/' + routeKeywords.preview + '/:uuid', frontend.preview);
// Default
router.get('/' + config.routeKeywords.page + '/:page/', frontend.homepage);
router.get('/' + routeKeywords.page + '/:page/', frontend.homepage);
router.get('/', frontend.homepage);
router.get('*', frontend.single);

View file

@ -29,6 +29,14 @@ describe('Frontend Routing', function () {
};
}
function addPosts(done) {
testUtils.initData().then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
done();
});
}
before(function (done) {
ghost().then(function (ghostServer) {
// Setup the request object with the ghost express app
@ -249,13 +257,7 @@ describe('Frontend Routing', function () {
});
describe('Static page', function () {
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
done();
});
});
before(addPosts);
after(testUtils.teardown);
@ -276,15 +278,54 @@ describe('Frontend Routing', function () {
});
});
describe('Post with Ghost in the url', function () {
before(function (done) {
testUtils.initData().then(function () {
return testUtils.fixtures.insertPosts();
}).then(function () {
done();
});
describe('Post preview', function () {
before(addPosts);
after(testUtils.teardown);
it('should display draft posts accessed via uuid', function (done) {
request.get('/p/d52c42ae-2755-455c-80ec-70b2ec55c903/')
.expect('Content-Type', /html/)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
var $ = cheerio.load(res.text);
should.not.exist(res.headers['x-cache-invalidate']);
should.not.exist(res.headers['X-CSRF-Token']);
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
$('title').text().should.equal('Not finished yet');
$('.content .post').length.should.equal(1);
$('.poweredby').text().should.equal('Proudly published with Ghost');
$('body.post-template').length.should.equal(1);
$('article.post').length.should.equal(1);
done();
});
});
it('should redirect published posts to their live url', function (done) {
request.get('/p/2ac6b4f6-e1f3-406c-9247-c94a0496d39d/')
.expect(301)
.expect('Location', '/short-and-sweet/')
.end(doEnd(done));
});
it('404s unknown uuids', function (done) {
request.get('/p/aac6b4f6-e1f3-406c-9247-c94a0496d39f/')
.expect(404)
.end(doEnd(done));
});
});
describe('Post with Ghost in the url', function () {
before(addPosts);
after(testUtils.teardown);
// All of Ghost's admin depends on the /ghost/ in the url to work properly

View file

@ -40,6 +40,7 @@ describe('{{next_post}} helper', function () {
optionsData = {name: 'next_post', fn: fn, inverse: inverse};
helpers.prev_post.call({html: 'content',
status: 'published',
markdown: 'ff',
title: 'post2',
slug: 'current',
@ -123,4 +124,45 @@ 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') {
return Promise.resolve({
posts: [{slug: '/current/', title: 'post 2', next: {slug: '/next/', title: 'post 3'}}]
});
}
});
});
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',
status: 'published',
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) {
done(err);
});
});
});
});

View file

@ -41,6 +41,7 @@ describe('{{prev_post}} helper', function () {
optionsData = {name: 'prev_post', fn: fn, inverse: inverse};
helpers.prev_post.call({html: 'content',
status: 'published',
markdown: 'ff',
title: 'post2',
slug: 'current',
@ -79,6 +80,7 @@ describe('{{prev_post}} helper', function () {
optionsData = {name: 'prev_post', fn: fn, inverse: inverse};
helpers.prev_post.call({html: 'content',
status: 'published',
markdown: 'ff',
title: 'post2',
slug: 'current',
@ -124,4 +126,42 @@ 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') {
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(),
optionsData = {name: 'prev_post', fn: fn, inverse: inverse};
helpers.prev_post.call({html: 'content',
status: 'draft',
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);
});
});
});
});

View file

@ -25,13 +25,15 @@ DataGenerator.Content = {
html: "<h2 id=\"testing\">testing</h2>\n\n<p>mctesters</p>\n\n<ul>\n<li>test</li>\n<li>line</li>\n<li>items</li>\n</ul>",
image: "http://placekitten.com/500/200",
meta_description: "test stuff",
published_at: new Date("2015-01-03")
published_at: new Date("2015-01-03"),
uuid: "2ac6b4f6-e1f3-406c-9247-c94a0496d39d"
},
{
title: "Not finished yet",
slug: "unfinished",
markdown: "<h1>HTML Ipsum Presents</h1><p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href=\"#\">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p><h2>Header Level 2</h2><ol><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ol><blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote><h3>Header Level 3</h3><ul><li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li><li>Aliquam tincidunt mauris eu risus.</li></ul><pre><code>#header h1 a{display: block;width: 300px;height: 80px;}</code></pre>",
status: "draft"
status: "draft",
uuid: "d52c42ae-2755-455c-80ec-70b2ec55c903"
},
{
title: "Not so short, bit complex",