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:
commit
3893b18479
14 changed files with 240 additions and 42 deletions
|
@ -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,
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
@ -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, ..
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue