From 2d01e15a18c5011928a763dac8bb675ff939e7d9 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sun, 20 Jul 2014 17:32:14 +0100 Subject: [PATCH 1/2] Author pages refs #3076 - This is a first draft implementation, just to make it work so that we can get casper working --- core/server/controllers/frontend.js | 60 +++++++++++++++++++++++++++++ core/server/models/post.js | 30 +++++++++++++-- core/server/routes/frontend.js | 10 ++++- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js index cc9cf0a92e..906e8fe79d 100644 --- a/core/server/controllers/frontend.js +++ b/core/server/controllers/frontend.js @@ -165,6 +165,66 @@ frontendControllers = { }); }).otherwise(handleError(next)); }, + 'author': function (req, res, next) { + + // Parse the page number + var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1, + options = { + page: pageParam, + author: req.params.slug + }; + + + + // Get url for tag page + function authorUrl(author, page) { + var url = config().paths.subdir + '/author/' + author + '/'; + + if (page && page > 1) { + url += 'page/' + page + '/'; + } + + return url; + } + + // No negative pages, or page 1 + if (isNaN(pageParam) || pageParam < 1 || (req.params.page !== undefined && pageParam === 1)) { + return res.redirect(authorUrl(options.author)); + } + + return getPostPage(options).then(function (page) { + // If page is greater than number of pages we have, redirect to last page + if (pageParam > page.meta.pagination.pages) { + return res.redirect(authorUrl(options.author, page.meta.pagination.pages)); + } + + setReqCtx(req, page.posts); + if (page.meta.filters.author) { + setReqCtx(req, page.meta.filters.author); + } + + // Render the page of posts + filters.doFilter('prePostsRender', page.posts).then(function (posts) { + api.settings.read({key: 'activeTheme', context: {internal: true}}).then(function (response) { + var activeTheme = response.settings[0], + paths = config().paths.availableThemes[activeTheme.value], + view = paths.hasOwnProperty('author.hbs') ? 'author' : 'index', + + // Format data for template + result = _.extend(formatPageResponse(posts, page), { + author: page.meta.filters.author ? page.meta.filters.author : '' + }); + + // If the resulting author is '' then 404. + if (!result.author) { + return next(); + } + res.render(view, result); + }); + }); + }).otherwise(handleError(next)); + }, + 'single': function (req, res, next) { var path = req.path, params, diff --git a/core/server/models/post.js b/core/server/models/post.js index 6020995d7b..6726962c0b 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -6,8 +6,9 @@ var _ = require('lodash'), Showdown = require('showdown'), ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'), converter = new Showdown.converter({extensions: [ghostgfm]}), - Tag = require('./tag').Tag, + Tag = require('./tag').Tag, Tags = require('./tag').Tags, + User = require('./user').User, ghostBookshelf = require('./base'), xmlrpc = require('../xmlrpc'), @@ -278,7 +279,8 @@ Post = ghostBookshelf.Model.extend({ options = options || {}; var postCollection = Posts.forge(), - tagInstance = options.tag !== undefined ? Tag.forge({slug: options.tag}) : false; + tagInstance = options.tag !== undefined ? Tag.forge({slug: options.tag}) : false, + authorInstance = options.author !== undefined ? User.forge({slug: options.author}) : false; if (options.limit) { options.limit = parseInt(options.limit) || 15; @@ -329,7 +331,14 @@ Post = ghostBookshelf.Model.extend({ return false; } - return when(fetchTagQuery()) + function fetchAuthorQuery() { + if (authorInstance) { + return authorInstance.fetch(); + } + return false; + } + + return when.join(fetchTagQuery(), fetchAuthorQuery()) // Set the limit & offset for the query, fetching // with the opts (to specify any eager relations, etc.) @@ -344,6 +353,11 @@ Post = ghostBookshelf.Model.extend({ .query('join', 'posts_tags', 'posts_tags.post_id', '=', 'posts.id') .query('where', 'posts_tags.tag_id', '=', tagInstance.id); } + + if (authorInstance) { + postCollection + .query('where', 'author_id', '=', authorInstance.id); + } return postCollection .query('limit', options.limit) .query('offset', options.limit * (options.page - 1)) @@ -371,6 +385,9 @@ Post = ghostBookshelf.Model.extend({ qb.join('posts_tags', 'posts_tags.post_id', '=', 'posts.id'); qb.where('posts_tags.tag_id', '=', tagInstance.id); } + if (authorInstance) { + qb.where('author_id', '=', authorInstance.id); + } return qb.count(tableName + '.' + idAttribute + ' as aggregate'); }) @@ -419,6 +436,13 @@ Post = ghostBookshelf.Model.extend({ } } + if (authorInstance) { + meta.filters = {}; + if (!authorInstance.isNew()) { + meta.filters.author = authorInstance.toJSON(); + } + } + return data; }) .catch(errors.logAndThrowError); diff --git a/core/server/routes/frontend.js b/core/server/routes/frontend.js index f0aed39df6..fe5612f464 100644 --- a/core/server/routes/frontend.js +++ b/core/server/routes/frontend.js @@ -20,11 +20,19 @@ frontendRoutes = function () { res.redirect(301, subdir + '/rss/'); }); - + // Tags router.get('/tag/:slug/rss/', frontend.rss); router.get('/tag/:slug/rss/:page/', frontend.rss); router.get('/tag/:slug/page/:page/', frontend.tag); router.get('/tag/:slug/', frontend.tag); + + // Authors + router.get('/author/:slug/rss/', frontend.rss); + router.get('/author/:slug/rss/:page/', frontend.rss); + router.get('/author/:slug/page/:page/', frontend.author); + router.get('/author/:slug/', frontend.author); + + // Default router.get('/page/:page/', frontend.homepage); router.get('/', frontend.homepage); router.get('*', frontend.single); From b7aa09f43984d9298a71fd0760bc59e5ac8a1285 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sun, 20 Jul 2014 17:41:59 +0100 Subject: [PATCH 2/2] Author helpers closes #3077 - expend urlFor to handle /author/ urls - update author helper to output a link --- core/server/config/url.js | 5 ++- core/server/data/schema.js | 8 +++- core/server/helpers/index.js | 49 ++++++++++++++++----- core/test/unit/server_helpers_index_spec.js | 17 ++++--- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/core/server/config/url.js b/core/server/config/url.js index 5a79e5c3dc..43a8f7810f 100644 --- a/core/server/config/url.js +++ b/core/server/config/url.js @@ -102,7 +102,7 @@ function urlPathForPost(post, permalinks) { function urlFor(context, data, absolute) { var urlPath = '/', secure, - knownObjects = ['post', 'tag', 'user'], + knownObjects = ['post', 'tag', 'author'], // this will become really big knownPaths = { @@ -130,6 +130,9 @@ function urlFor(context, data, absolute) { } else if (context === 'tag' && data.tag) { urlPath = '/tag/' + data.tag.slug + '/'; secure = data.tag.secure; + } else if (context === 'author' && data.author) { + urlPath = '/author/' + data.author.slug + '/'; + secure = data.author.secure; } // other objects are recognised but not yet supported } else if (_.isString(context) && _.indexOf(_.keys(knownPaths), context) !== -1) { diff --git a/core/server/data/schema.js b/core/server/data/schema.js index 320f79de10..cad79aa250 100644 --- a/core/server/data/schema.js +++ b/core/server/data/schema.js @@ -193,8 +193,14 @@ function isTag(jsonData) { jsonData.hasOwnProperty('description') && jsonData.hasOwnProperty('parent'); } +function isUser(jsonData) { + return jsonData.hasOwnProperty('bio') && jsonData.hasOwnProperty('website') && + jsonData.hasOwnProperty('status') && jsonData.hasOwnProperty('location'); +} + module.exports.tables = db; module.exports.checks = { isPost: isPost, - isTag: isTag + isTag: isTag, + isUser: isUser }; diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index 80a3076e32..0225b2f258 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -154,6 +154,11 @@ coreHelpers.url = function (options) { return when(config.urlFor('tag', {tag: this}, absolute)); } + if (schema.isUser(this)) { + return when(config.urlFor('author', {author: this}, absolute)); + } + + return when(config.urlFor(this, absolute)); }; @@ -200,9 +205,26 @@ coreHelpers.asset = function (context, options) { // Returns the full name of the author of a given post, or a blank string // if the author could not be determined. // -coreHelpers.author = function (context, options) { +coreHelpers.author = function (options) { + options = options || {}; + options.hash = options.hash || {}; + /*jshint unused:false*/ - return this.author ? this.author.name : ''; + var autolink = _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, + output = ''; + + if (this.author && this.author.name) { + if (autolink) { + output = linkTemplate({ + url: config.urlFor('author', {author: this.author}), + text: _.escape(this.author.name) + }); + } else { + output = _.escape(this.author.name); + } + } + + return new hbs.handlebars.SafeString(output); }; // ### Tags Helper @@ -217,10 +239,13 @@ coreHelpers.author = function (context, options) { // Note that the standard {{#each tags}} implementation is unaffected by this helper // and can be used for more complex templates. coreHelpers.tags = function (options) { - var autolink = _.isString(options.hash.autolink) && options.hash.autolink === "false" ? false : true, - separator = _.isString(options.hash.separator) ? options.hash.separator : ', ', - prefix = _.isString(options.hash.prefix) ? options.hash.prefix : '', - suffix = _.isString(options.hash.suffix) ? options.hash.suffix : '', + options = options || {}; + options.hash = options.hash || {}; + + var autolink = options.hash && _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true, + separator = options.hash && _.isString(options.hash.separator) ? options.hash.separator : ', ', + prefix = options.hash && _.isString(options.hash.prefix) ? options.hash.prefix : '', + suffix = options.hash && _.isString(options.hash.suffix) ? options.hash.suffix : '', output = ''; function createTagList(tags) { @@ -481,7 +506,7 @@ coreHelpers.ghost_foot = function (options) { coreHelpers.meta_title = function (options) { /*jshint unused:false*/ - var title = "", + var title = '', blog; if (_.isString(this.relativeUrl)) { @@ -496,7 +521,7 @@ coreHelpers.meta_title = function (options) { } return filters.doFilter('meta_title', title).then(function (title) { - title = title || ""; + title = title || ''; return title.trim(); }); }; @@ -516,7 +541,7 @@ coreHelpers.meta_description = function (options) { } return filters.doFilter('meta_description', description).then(function (description) { - description = description || ""; + description = description || ''; return description.trim(); }); }; @@ -554,7 +579,7 @@ coreHelpers.foreach = function (context, options) { j = 0, columns = options.hash.columns, key, - ret = "", + ret = '', data; if (options.data) { @@ -639,7 +664,7 @@ coreHelpers.has = function (options) { } if (!tagList) { - errors.logWarn("Invalid or no attribute given to has helper"); + errors.logWarn('Invalid or no attribute given to has helper'); return; } @@ -707,7 +732,7 @@ function registerAsyncHelper(hbs, name, fn) { when.resolve(fn.call(this, options)).then(function (result) { cb(result); }).otherwise(function (err) { - errors.logAndThrowError(err, "registerAsyncThemeHelper: " + name); + errors.logAndThrowError(err, 'registerAsyncThemeHelper: ' + name); }); }); } diff --git a/core/test/unit/server_helpers_index_spec.js b/core/test/unit/server_helpers_index_spec.js index 1c49339149..700b6af40e 100644 --- a/core/test/unit/server_helpers_index_spec.js +++ b/core/test/unit/server_helpers_index_spec.js @@ -179,13 +179,21 @@ describe('Core Helpers', function () { should.exist(handlebars.helpers.author); }); - it('Returns the full name of the author from the context', function () { - var data = {'author': {'name': 'abc123'}}, + it('Returns the link to the author from the context', function () { + var data = {'author': {'name': 'abc 123', slug: 'abc123', bio: '', website: '', status: '', location: ''}}, result = helpers.author.call(data); - String(result).should.equal('abc123'); + String(result).should.equal('abc 123'); }); + it('Returns the full name of the author from the context if no autolink', function () { + var data = {'author': {'name': 'abc 123', slug: 'abc123'}}, + result = helpers.author.call(data, {hash: {autolink: 'false'}}); + + String(result).should.equal('abc 123'); + }); + + it('Returns a blank string where author data is missing', function () { var data = {'author': null}, result = helpers.author.call(data); @@ -894,8 +902,7 @@ describe('Core Helpers', function () { it('can autolink tags to tag pages', function () { var tags = [{name: 'foo', slug: 'foo-bar'}, {name: 'bar', slug: 'bar'}], rendered = handlebars.helpers.tags.call( - {tags: tags}, - {'hash': {}} + {tags: tags} ); should.exist(rendered);