From 98f5ae00fc1cff1f97adbd2483f278c11d9489e4 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 10 Nov 2017 12:44:29 +0000 Subject: [PATCH] Introduced renderer to DRY up controllers (#9235) refs #5091, #9192 - Renderer figures out templates, contexts, and does a render call - Templating is now handled with a single function - Context call is made in the renderer Note: to make this work, all controllers now define a little bit of config, currently stored in res._route. (That's a totally temporary location, as is res._template... when a sensible naming convention reveals itself I'll get rid of the weird _). This exposes a type and for custom routes a template name & default. --- core/server/apps/amp/lib/router.js | 25 +- .../apps/private-blogging/lib/router.js | 23 +- core/server/apps/subscribers/lib/router.js | 26 +- core/server/controllers/entry.js | 6 + .../controllers/frontend/render-channel.js | 14 +- .../controllers/frontend/render-entry.js | 14 +- core/server/controllers/frontend/renderer.js | 16 + core/server/controllers/frontend/templates.js | 87 ++++-- core/server/controllers/preview.js | 6 + core/server/middleware/error-handler.js | 11 +- core/server/services/channels/router.js | 2 + .../controllers/frontend/templates_spec.js | 283 ++++++++++++++---- 12 files changed, 359 insertions(+), 154 deletions(-) create mode 100644 core/server/controllers/frontend/renderer.js diff --git a/core/server/apps/amp/lib/router.js b/core/server/apps/amp/lib/router.js index 300f2aad63..29e1d55de4 100644 --- a/core/server/apps/amp/lib/router.js +++ b/core/server/apps/amp/lib/router.js @@ -5,14 +5,20 @@ var path = require('path'), // Dirty requires errors = require('../../../errors'), - templates = require('../../../controllers/frontend/templates'), postLookup = require('../../../controllers/frontend/post-lookup'), - setResponseContext = require('../../../controllers/frontend/context'), + renderer = require('../../../controllers/frontend/renderer'), - templateName = 'amp', - defaultTemplate = path.resolve(__dirname, 'views', templateName + '.hbs'); + templateName = 'amp'; function _renderer(req, res, next) { + // Note: this is super similar to the config middleware used in channels + // @TODO refactor into to something explicit & DRY this up + res._route = { + type: 'custom', + templateName: templateName, + defaultTemplate: path.resolve(__dirname, 'views', templateName + '.hbs') + }; + // Renderer begin // Format data var data = req.body || {}; @@ -22,14 +28,8 @@ function _renderer(req, res, next) { return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')})); } - // Context - setResponseContext(req, res, data); - - // Template - res.template = templates.pickTemplate(templateName, defaultTemplate); - // Render Call - return res.render(res.template, data); + return renderer(req, res, data); } // This here is a controller. @@ -49,7 +49,8 @@ function getPostData(req, res, next) { } // AMP frontend route -ampRouter.route('/') +ampRouter + .route('/') .get( getPostData, _renderer diff --git a/core/server/apps/private-blogging/lib/router.js b/core/server/apps/private-blogging/lib/router.js index 77b5ca68ed..a5fb97500d 100644 --- a/core/server/apps/private-blogging/lib/router.js +++ b/core/server/apps/private-blogging/lib/router.js @@ -2,16 +2,22 @@ var path = require('path'), express = require('express'), middleware = require('./middleware'), bodyParser = require('body-parser'), - templates = require('../../../controllers/frontend/templates'), - setResponseContext = require('../../../controllers/frontend/context'), + renderer = require('../../../controllers/frontend/renderer'), brute = require('../../../middleware/brute'), templateName = 'private', - defaultTemplate = path.resolve(__dirname, 'views', templateName + '.hbs'), privateRouter = express.Router(); function _renderer(req, res) { + // Note: this is super similar to the config middleware used in channels + // @TODO refactor into to something explicit & DRY this up + res._route = { + type: 'custom', + templateName: templateName, + defaultTemplate: path.resolve(__dirname, 'views', templateName + '.hbs') + }; + // Renderer begin // Format data var data = {}; @@ -20,18 +26,13 @@ function _renderer(req, res) { data.error = res.error; } - // Context - setResponseContext(req, res); - - // Template - res.template = templates.pickTemplate(templateName, defaultTemplate); - // Render Call - return res.render(res.template, data); + return renderer(req, res, data); } // password-protected frontend route -privateRouter.route('/') +privateRouter + .route('/') .get( middleware.isPrivateSessionAuth, _renderer diff --git a/core/server/apps/subscribers/lib/router.js b/core/server/apps/subscribers/lib/router.js index 1e711657f3..1cfc552285 100644 --- a/core/server/apps/subscribers/lib/router.js +++ b/core/server/apps/subscribers/lib/router.js @@ -8,27 +8,26 @@ var path = require('path'), api = require('../../../api'), errors = require('../../../errors'), validator = require('../../../data/validation').validator, - templates = require('../../../controllers/frontend/templates'), postLookup = require('../../../controllers/frontend/post-lookup'), - setResponseContext = require('../../../controllers/frontend/context'), + renderer = require('../../../controllers/frontend/renderer'), - templateName = 'subscribe', - defaultTemplate = path.resolve(__dirname, 'views', templateName + '.hbs'); + templateName = 'subscribe'; -// In future we'd have a more complex controller here - showing if someone already subscribed?! function _renderer(req, res) { + // Note: this is super similar to the config middleware used in channels + // @TODO refactor into to something explicit & DRY this up + res._route = { + type: 'custom', + templateName: templateName, + defaultTemplate: path.resolve(__dirname, 'views', templateName + '.hbs') + }; + // Renderer begin // Format data var data = req.body; - // Context - setResponseContext(req, res); - - // Template - res.template = templates.pickTemplate(templateName, defaultTemplate); - // Render Call - return res.render(res.template, data); + return renderer(req, res, data); } /** @@ -106,7 +105,8 @@ function storeSubscriber(req, res, next) { } // subscribe frontend route -subscribeRouter.route('/') +subscribeRouter + .route('/') .get( _renderer ) diff --git a/core/server/controllers/entry.js b/core/server/controllers/entry.js index e5b6513ccd..28b55ae65e 100644 --- a/core/server/controllers/entry.js +++ b/core/server/controllers/entry.js @@ -9,6 +9,12 @@ var utils = require('../utils'), // It renders entries = individual posts or pages // The "route" is handled in site/routes.js module.exports = function entryController(req, res, next) { + // Note: this is super similar to the config middleware used in channels + // @TODO refactor into to something explicit + res._route = { + type: 'entry' + }; + // Query database to find post return postLookup(req.path).then(function then(lookup) { // Format data 1 diff --git a/core/server/controllers/frontend/render-channel.js b/core/server/controllers/frontend/render-channel.js index 18c4365f07..f367d94031 100644 --- a/core/server/controllers/frontend/render-channel.js +++ b/core/server/controllers/frontend/render-channel.js @@ -1,7 +1,6 @@ var debug = require('ghost-ignition').debug('channels:render'), formatResponse = require('./format-response'), - setResponseContext = require('./context'), - templates = require('./templates'); + renderer = require('./renderer'); module.exports = function renderChannel(req, res) { debug('renderChannel called'); @@ -9,16 +8,9 @@ module.exports = function renderChannel(req, res) { // Renderer begin // Format data 2 // Do final data formatting and then render - result = formatResponse.channel(result); - - // Context - setResponseContext(req, res); - - // Template - res.template = templates.channel(res.locals.channel); + var data = formatResponse.channel(result); // Render Call - debug('Rendering view: ' + res.template); - res.render(res.template, result); + return renderer(req, res, data); }; }; diff --git a/core/server/controllers/frontend/render-entry.js b/core/server/controllers/frontend/render-entry.js index 7ed04024cc..d25041afaa 100644 --- a/core/server/controllers/frontend/render-entry.js +++ b/core/server/controllers/frontend/render-entry.js @@ -1,7 +1,6 @@ var debug = require('ghost-ignition').debug('channels:render-post'), - templates = require('./templates'), formatResponse = require('./format-response'), - setResponseContext = require('./context'); + renderer = require('./renderer'); /* * Sets the response context around an entry (post or page) * and renders it with the correct template. @@ -13,16 +12,9 @@ module.exports = function renderEntry(req, res) { return function renderEntry(entry) { // Renderer begin // Format data 2 - 1 is in preview/entry - var response = formatResponse.entry(entry); - - // Context - setResponseContext(req, res, response); - - // Template - res.template = templates.entry(entry); + var data = formatResponse.entry(entry); // Render Call - debug('Rendering view: ' + res.template); - res.render(res.template, response); + return renderer(req, res, data); }; }; diff --git a/core/server/controllers/frontend/renderer.js b/core/server/controllers/frontend/renderer.js new file mode 100644 index 0000000000..4204f9be0d --- /dev/null +++ b/core/server/controllers/frontend/renderer.js @@ -0,0 +1,16 @@ +var debug = require('ghost-ignition').debug('renderer'), + setContext = require('./context'), + templates = require('./templates'); + +module.exports = function renderer(req, res, data) { + // Context + setContext(req, res, data); + + // Template + templates.setTemplate(req, res, data); + + // Render Call + debug('Rendering template: ' + res._template + ' for: ' + req.originalUrl); + debug('res.locals', res.locals); + res.render(res._template, data); +}; diff --git a/core/server/controllers/frontend/templates.js b/core/server/controllers/frontend/templates.js index 96d33f7df9..4c6407057d 100644 --- a/core/server/controllers/frontend/templates.js +++ b/core/server/controllers/frontend/templates.js @@ -6,7 +6,8 @@ var _ = require('lodash'), path = require('path'), config = require('../../config'), - themes = require('../../themes'); + themes = require('../../themes'), + _private = {}; /** * ## Get Error Template Hierarchy @@ -18,7 +19,7 @@ var _ = require('lodash'), * @param {integer} statusCode * @returns {String[]} */ -function getErrorTemplateHierarchy(statusCode) { +_private.getErrorTemplateHierarchy = function getErrorTemplateHierarchy(statusCode) { var errorCode = _.toString(statusCode), templateList = ['error']; @@ -29,7 +30,7 @@ function getErrorTemplateHierarchy(statusCode) { templateList.unshift('error-' + errorCode); return templateList; -} +}; /** * ## Get Channel Template Hierarchy @@ -43,7 +44,7 @@ function getErrorTemplateHierarchy(statusCode) { * @param {Object} channelOpts * @returns {String[]} */ -function getChannelTemplateHierarchy(channelOpts) { +_private.getChannelTemplateHierarchy = function getChannelTemplateHierarchy(channelOpts) { var templateList = ['index']; if (channelOpts.name && channelOpts.name !== 'index') { @@ -59,7 +60,7 @@ function getChannelTemplateHierarchy(channelOpts) { } return templateList; -} +}; /** * ## Get Entry Template Hierarchy @@ -72,7 +73,7 @@ function getChannelTemplateHierarchy(channelOpts) { * @param {Object} postObject * @returns {String[]} */ -function getEntryTemplateHierarchy(postObject) { +_private.getEntryTemplateHierarchy = function getEntryTemplateHierarchy(postObject) { var templateList = ['post'], slugTemplate = 'post-' + postObject.slug; @@ -88,7 +89,7 @@ function getEntryTemplateHierarchy(postObject) { templateList.unshift(slugTemplate); return templateList; -} +}; /** * ## Pick Template @@ -99,7 +100,7 @@ function getEntryTemplateHierarchy(postObject) { * @param {Array|String} templateList * @param {String} fallback - a fallback template */ -function pickTemplate(templateList, fallback) { +_private.pickTemplate = function pickTemplate(templateList, fallback) { var template; if (!_.isArray(templateList)) { @@ -119,29 +120,49 @@ function pickTemplate(templateList, fallback) { } return template; -} - -function getTemplateForEntry(postObject) { - var templateList = getEntryTemplateHierarchy(postObject), - fallback = templateList[templateList.length - 1]; - return pickTemplate(templateList, fallback); -} - -function getTemplateForChannel(channelOpts) { - var templateList = getChannelTemplateHierarchy(channelOpts), - fallback = templateList[templateList.length - 1]; - return pickTemplate(templateList, fallback); -} - -function getTemplateForError(statusCode) { - var templateList = getErrorTemplateHierarchy(statusCode), - fallback = path.resolve(config.get('paths').defaultViews, 'error.hbs'); - return pickTemplate(templateList, fallback); -} - -module.exports = { - channel: getTemplateForChannel, - entry: getTemplateForEntry, - error: getTemplateForError, - pickTemplate: pickTemplate +}; + +_private.getTemplateForEntry = function getTemplateForEntry(postObject) { + var templateList = _private.getEntryTemplateHierarchy(postObject), + fallback = templateList[templateList.length - 1]; + return _private.pickTemplate(templateList, fallback); +}; + +_private.getTemplateForChannel = function getTemplateForChannel(channelOpts) { + var templateList = _private.getChannelTemplateHierarchy(channelOpts), + fallback = templateList[templateList.length - 1]; + return _private.pickTemplate(templateList, fallback); +}; + +_private.getTemplateForError = function getTemplateForError(statusCode) { + var templateList = _private.getErrorTemplateHierarchy(statusCode), + fallback = path.resolve(config.get('paths').defaultViews, 'error.hbs'); + return _private.pickTemplate(templateList, fallback); +}; + +module.exports.setTemplate = function setTemplate(req, res, data) { + var routeConfig = res._route || {}; + + if (res._template) { + return; + } + + if (req.err) { + res._template = _private.getTemplateForError(res.statusCode); + return; + } + + switch (routeConfig.type) { + case 'custom': + res._template = _private.pickTemplate(routeConfig.templateName, routeConfig.defaultTemplate); + break; + case 'channel': + res._template = _private.getTemplateForChannel(res.locals.channel); + break; + case 'entry': + res._template = _private.getTemplateForEntry(data.post); + break; + default: + res._template = 'index'; + } }; diff --git a/core/server/controllers/preview.js b/core/server/controllers/preview.js index a872cdad4b..4ccfea505f 100644 --- a/core/server/controllers/preview.js +++ b/core/server/controllers/preview.js @@ -14,6 +14,12 @@ module.exports = function previewController(req, res, next) { include: 'author,tags' }; + // Note: this is super similar to the config middleware used in channels + // @TODO refactor into to something explicit + res._route = { + type: 'entry' + }; + api.posts.read(params).then(function then(result) { // Format data 1 var post = result.posts[0]; diff --git a/core/server/middleware/error-handler.js b/core/server/middleware/error-handler.js index 6513710351..8447ed47ec 100644 --- a/core/server/middleware/error-handler.js +++ b/core/server/middleware/error-handler.js @@ -71,7 +71,7 @@ _private.JSONErrorRenderer = function JSONErrorRenderer(err, req, res, next) { / }); }; -// @TODO: differenciate properly between rendering errors for theme templates, and other situations +// @TODO: differentiate properly between rendering errors for theme templates, and other situations _private.HTMLErrorRenderer = function HTMLErrorRender(err, req, res, next) { // If the error code is explicitly set to STATIC_FILE_NOT_FOUND, // Skip trying to render an HTML error, and move on to the basic error renderer @@ -83,7 +83,7 @@ _private.HTMLErrorRenderer = function HTMLErrorRender(err, req, res, next) { // Renderer begin // Format Data - var templateData = { + var data = { message: err.message, // @deprecated code: err.statusCode, @@ -95,20 +95,21 @@ _private.HTMLErrorRenderer = function HTMLErrorRender(err, req, res, next) { // We don't do context for errors?! // Template - res.template = templates.error(err.statusCode); + templates.setTemplate(req, res); // It can be that something went wrong with the theme or otherwise loading handlebars // This ensures that no matter what res.render will work here // @TODO: split the error handler for assets, admin & theme to refactor this away if (_.isEmpty(req.app.engines)) { - res.template = 'error'; + res._template = 'error'; req.app.engine('hbs', _private.createHbsEngine()); req.app.set('view engine', 'hbs'); req.app.set('views', config.get('paths').defaultViews); } + // @TODO use renderer here?! // Render Call - featuring an error handler for what happens if rendering fails - res.render(res.template, templateData, function renderResponse(err, html) { + res.render(res._template, data, function renderResponse(err, html) { if (!err) { return res.send(html); } diff --git a/core/server/services/channels/router.js b/core/server/services/channels/router.js index 273a64b9a4..fa4582d554 100644 --- a/core/server/services/channels/router.js +++ b/core/server/services/channels/router.js @@ -40,6 +40,8 @@ function rssConfigMiddleware(req, res, next) { function channelConfigMiddleware(channel) { return function doChannelConfig(req, res, next) { res.locals.channel = _.cloneDeep(channel); + // @TODO refactor into to something explicit + res._route = {type: 'channel'}; next(); }; } diff --git a/core/test/unit/controllers/frontend/templates_spec.js b/core/test/unit/controllers/frontend/templates_spec.js index 3a7fe729c2..41d942d172 100644 --- a/core/test/unit/controllers/frontend/templates_spec.js +++ b/core/test/unit/controllers/frontend/templates_spec.js @@ -9,14 +9,15 @@ var should = require('should'), sandbox = sinon.sandbox.create(); describe('templates', function () { - var getActiveThemeStub, hasTemplateStub; + var getActiveThemeStub, hasTemplateStub, + _private = templates.__get__('_private'); afterEach(function () { sandbox.restore(); }); describe('[private] getChannelTemplateHierarchy', function () { - var channelTemplateList = templates.__get__('getChannelTemplateHierarchy'); + var channelTemplateList = _private.getChannelTemplateHierarchy; it('should return just index for empty channelOpts', function () { channelTemplateList({}).should.eql(['index']); @@ -61,7 +62,7 @@ describe('templates', function () { }); }); - describe('pickTemplate', function () { + describe('[private] pickTemplate', function () { beforeEach(function () { hasTemplateStub = sandbox.stub().returns(false); @@ -73,13 +74,13 @@ describe('templates', function () { it('returns fallback if there is no active_theme', function () { getActiveThemeStub.returns(undefined); - templates.pickTemplate(['tag-test', 'tag', 'index'], 'fallback').should.eql('fallback'); - templates.pickTemplate(['page-my-post', 'page', 'post'], 'fallback').should.eql('fallback'); + _private.pickTemplate(['tag-test', 'tag', 'index'], 'fallback').should.eql('fallback'); + _private.pickTemplate(['page-my-post', 'page', 'post'], 'fallback').should.eql('fallback'); }); it('returns fallback if active_theme has no templates', function () { - templates.pickTemplate(['tag-test', 'tag', 'index'], 'fallback').should.eql('fallback'); - templates.pickTemplate(['page-about', 'page', 'post'], 'fallback').should.eql('fallback'); + _private.pickTemplate(['tag-test', 'tag', 'index'], 'fallback').should.eql('fallback'); + _private.pickTemplate(['page-about', 'page', 'post'], 'fallback').should.eql('fallback'); }); describe('with many templates', function () { @@ -94,20 +95,20 @@ describe('templates', function () { }); it('returns first matching template', function () { - templates.pickTemplate(['page-about', 'page', 'post'], 'fallback').should.eql('page-about'); - templates.pickTemplate(['page-magic', 'page', 'post'], 'fallback').should.eql('page'); - templates.pickTemplate(['page', 'post'], 'fallback').should.eql('page'); + _private.pickTemplate(['page-about', 'page', 'post'], 'fallback').should.eql('page-about'); + _private.pickTemplate(['page-magic', 'page', 'post'], 'fallback').should.eql('page'); + _private.pickTemplate(['page', 'post'], 'fallback').should.eql('page'); }); it('returns correctly if template list is a string', function () { - templates.pickTemplate('amp', 'fallback').should.eql('amp'); - templates.pickTemplate('subscribe', 'fallback').should.eql('fallback'); - templates.pickTemplate('post', 'fallback').should.eql('post'); + _private.pickTemplate('amp', 'fallback').should.eql('amp'); + _private.pickTemplate('subscribe', 'fallback').should.eql('fallback'); + _private.pickTemplate('post', 'fallback').should.eql('post'); }); }); }); - describe('entry', function () { + describe('[private] getTemplateForEntry', function () { beforeEach(function () { hasTemplateStub = sandbox.stub().returns(false); @@ -127,7 +128,7 @@ describe('templates', function () { }); it('post without custom slug template', function () { - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 0, slug: 'test-post' }); @@ -137,7 +138,7 @@ describe('templates', function () { it('post with custom slug template', function () { hasTemplateStub.withArgs('post-welcome-to-ghost').returns(true); - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 0, slug: 'welcome-to-ghost' }); @@ -146,7 +147,7 @@ describe('templates', function () { }); it('page without custom slug template', function () { - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 1, slug: 'contact' }); @@ -155,7 +156,7 @@ describe('templates', function () { }); it('page with custom slug template', function () { - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 1, slug: 'about' }); @@ -166,7 +167,7 @@ describe('templates', function () { it('post with custom template', function () { hasTemplateStub.withArgs('custom-about').returns(true); - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 0, custom_template: 'custom-about' }); @@ -177,7 +178,7 @@ describe('templates', function () { it('page with custom template', function () { hasTemplateStub.withArgs('custom-about').returns(true); - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 1, custom_template: 'custom-about' }); @@ -188,7 +189,7 @@ describe('templates', function () { it('post with custom template configured, but the template is missing', function () { hasTemplateStub.withArgs('custom-about').returns(false); - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 0, custom_template: 'custom-about' }); @@ -199,7 +200,7 @@ describe('templates', function () { it('page with custom template configured, but the template is missing', function () { hasTemplateStub.withArgs('custom-about').returns(false); - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 1, custom_template: 'custom-about' }); @@ -211,7 +212,7 @@ describe('templates', function () { hasTemplateStub.withArgs('custom-about').returns(true); hasTemplateStub.withArgs('post-about').returns(true); - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 0, slug: 'about', custom_template: 'custom-about' @@ -224,7 +225,7 @@ describe('templates', function () { hasTemplateStub.withArgs('custom-about').returns(false); hasTemplateStub.withArgs('post-about').returns(false); - var view = templates.entry({ + var view = _private.getTemplateForEntry({ page: 0, slug: 'about', custom_template: 'custom-about' @@ -237,13 +238,13 @@ describe('templates', function () { it('will fall back to post even if no index.hbs', function () { hasTemplateStub.returns(false); - var view = templates.entry({page: 1}); + var view = _private.getTemplateForEntry({page: 1}); should.exist(view); view.should.eql('post'); }); }); - describe('channel', function () { + describe('[private] getTemplateForChannel', function () { beforeEach(function () { hasTemplateStub = sandbox.stub().returns(false); @@ -259,7 +260,7 @@ describe('templates', function () { }); it('will return correct view for a tag', function () { - var view = templates.channel({name: 'tag', slugParam: 'development', slugTemplate: true}); + var view = _private.getTemplateForChannel({name: 'tag', slugParam: 'development', slugTemplate: true}); should.exist(view); view.should.eql('index'); }); @@ -274,26 +275,26 @@ describe('templates', function () { }); it('will return correct view for a tag', function () { - var view = templates.channel({name: 'tag', slugParam: 'design', slugTemplate: true}); + var view = _private.getTemplateForChannel({name: 'tag', slugParam: 'design', slugTemplate: true}); should.exist(view); view.should.eql('tag-design'); }); it('will return correct view for a tag', function () { - var view = templates.channel({name: 'tag', slugParam: 'development', slugTemplate: true}); + var view = _private.getTemplateForChannel({name: 'tag', slugParam: 'development', slugTemplate: true}); should.exist(view); view.should.eql('tag'); }); }); it('will fall back to index even if no index.hbs', function () { - var view = templates.channel({name: 'tag', slugParam: 'development', slugTemplate: true}); + var view = _private.getTemplateForChannel({name: 'tag', slugParam: 'development', slugTemplate: true}); should.exist(view); view.should.eql('index'); }); }); - describe('error', function () { + describe('[private] getTemplateForError', function () { beforeEach(function () { hasTemplateStub = sandbox.stub().returns(false); @@ -305,33 +306,33 @@ describe('templates', function () { it('will fall back to default if there is no active_theme', function () { getActiveThemeStub.returns(undefined); - templates.error(500).should.match(/core\/server\/views\/error.hbs$/); + _private.getTemplateForError(500).should.match(/core\/server\/views\/error.hbs$/); }); it('will fall back to default for all statusCodes with no custom error templates', function () { - templates.error(500).should.match(/core\/server\/views\/error.hbs$/); - templates.error(503).should.match(/core\/server\/views\/error.hbs$/); - templates.error(422).should.match(/core\/server\/views\/error.hbs$/); - templates.error(404).should.match(/core\/server\/views\/error.hbs$/); + _private.getTemplateForError(500).should.match(/core\/server\/views\/error.hbs$/); + _private.getTemplateForError(503).should.match(/core\/server\/views\/error.hbs$/); + _private.getTemplateForError(422).should.match(/core\/server\/views\/error.hbs$/); + _private.getTemplateForError(404).should.match(/core\/server\/views\/error.hbs$/); }); it('will use custom error.hbs for all statusCodes if there are no other templates', function () { hasTemplateStub.withArgs('error').returns(true); - templates.error(500).should.eql('error'); - templates.error(503).should.eql('error'); - templates.error(422).should.eql('error'); - templates.error(404).should.eql('error'); + _private.getTemplateForError(500).should.eql('error'); + _private.getTemplateForError(503).should.eql('error'); + _private.getTemplateForError(422).should.eql('error'); + _private.getTemplateForError(404).should.eql('error'); }); it('will use more specific error-4xx.hbs for all 4xx statusCodes if available', function () { hasTemplateStub.withArgs('error').returns(true); hasTemplateStub.withArgs('error-4xx').returns(true); - templates.error(500).should.eql('error'); - templates.error(503).should.eql('error'); - templates.error(422).should.eql('error-4xx'); - templates.error(404).should.eql('error-4xx'); + _private.getTemplateForError(500).should.eql('error'); + _private.getTemplateForError(503).should.eql('error'); + _private.getTemplateForError(422).should.eql('error-4xx'); + _private.getTemplateForError(404).should.eql('error-4xx'); }); it('will use explicit error-404.hbs for 404 statusCode if available', function () { @@ -339,10 +340,10 @@ describe('templates', function () { hasTemplateStub.withArgs('error-4xx').returns(true); hasTemplateStub.withArgs('error-404').returns(true); - templates.error(500).should.eql('error'); - templates.error(503).should.eql('error'); - templates.error(422).should.eql('error-4xx'); - templates.error(404).should.eql('error-404'); + _private.getTemplateForError(500).should.eql('error'); + _private.getTemplateForError(503).should.eql('error'); + _private.getTemplateForError(422).should.eql('error-4xx'); + _private.getTemplateForError(404).should.eql('error-404'); }); it('cascade works the same for 500 errors', function () { @@ -350,10 +351,10 @@ describe('templates', function () { hasTemplateStub.withArgs('error-5xx').returns(true); hasTemplateStub.withArgs('error-503').returns(true); - templates.error(500).should.eql('error-5xx'); - templates.error(503).should.eql('error-503'); - templates.error(422).should.eql('error'); - templates.error(404).should.eql('error'); + _private.getTemplateForError(500).should.eql('error-5xx'); + _private.getTemplateForError(503).should.eql('error-503'); + _private.getTemplateForError(422).should.eql('error'); + _private.getTemplateForError(404).should.eql('error'); }); it('cascade works with many specific templates', function () { @@ -363,12 +364,178 @@ describe('templates', function () { hasTemplateStub.withArgs('error-4xx').returns(true); hasTemplateStub.withArgs('error-404').returns(true); - templates.error(500).should.eql('error-5xx'); - templates.error(503).should.eql('error-503'); - templates.error(422).should.eql('error-4xx'); - templates.error(404).should.eql('error-404'); - templates.error(401).should.eql('error-4xx'); - templates.error(501).should.eql('error-5xx'); + _private.getTemplateForError(500).should.eql('error-5xx'); + _private.getTemplateForError(503).should.eql('error-503'); + _private.getTemplateForError(422).should.eql('error-4xx'); + _private.getTemplateForError(404).should.eql('error-404'); + _private.getTemplateForError(401).should.eql('error-4xx'); + _private.getTemplateForError(501).should.eql('error-5xx'); + }); + }); + + describe('setTemplate', function () { + var stubs = {}, req, res, data; + + beforeEach(function () { + req = {}; + res = { + locals: {} + }; + data = {}; + + stubs.pickTemplate = sandbox.stub(_private, 'pickTemplate').returns('testFromPickTemplate'); + stubs.getTemplateForEntry = sandbox.stub(_private, 'getTemplateForEntry').returns('testFromEntry'); + stubs.getTemplateForChannel = sandbox.stub(_private, 'getTemplateForChannel').returns('testFromChannel'); + stubs.getTemplateForError = sandbox.stub(_private, 'getTemplateForError').returns('testFromError'); + }); + + it('does nothing if template is already set', function () { + // Pre-set template + res._template = 'thing'; + + // Call setTemplate + templates.setTemplate(req, res, data); + + // It hasn't changed + res._template.should.eql('thing'); + + // And nothing got called + stubs.pickTemplate.called.should.be.false(); + stubs.getTemplateForEntry.called.should.be.false(); + stubs.getTemplateForChannel.called.should.be.false(); + stubs.getTemplateForError.called.should.be.false(); + }); + + it('defaults to index', function () { + // No route or template config here!!! + + // Call setTemplate + templates.setTemplate(req, res, data); + + // It should be index + res._template.should.eql('index'); + + // And nothing got called + stubs.pickTemplate.called.should.be.false(); + stubs.getTemplateForEntry.called.should.be.false(); + stubs.getTemplateForChannel.called.should.be.false(); + stubs.getTemplateForError.called.should.be.false(); + }); + + it('calls pickTemplate for custom routes', function () { + res._route = { + type: 'custom', + templateName: 'test', + defaultTemplate: 'path/to/local/test.hbs' + }; + + // Call setTemplate + templates.setTemplate(req, res, data); + + // should be testFromPickTemplate + res._template.should.eql('testFromPickTemplate'); + + // Only pickTemplate got called + stubs.pickTemplate.called.should.be.true(); + stubs.getTemplateForEntry.called.should.be.false(); + stubs.getTemplateForChannel.called.should.be.false(); + stubs.getTemplateForError.called.should.be.false(); + + stubs.pickTemplate.calledWith('test', 'path/to/local/test.hbs').should.be.true(); + }); + + it('calls pickTemplate for custom routes', function () { + res._route = { + type: 'custom', + templateName: 'test', + defaultTemplate: 'path/to/local/test.hbs' + }; + + // Call setTemplate + templates.setTemplate(req, res, data); + + // should be testFromPickTemplate + res._template.should.eql('testFromPickTemplate'); + + // Only pickTemplate got called + stubs.pickTemplate.called.should.be.true(); + stubs.getTemplateForEntry.called.should.be.false(); + stubs.getTemplateForChannel.called.should.be.false(); + stubs.getTemplateForError.called.should.be.false(); + + stubs.pickTemplate.calledWith('test', 'path/to/local/test.hbs').should.be.true(); + }); + + it('calls getTemplateForEntry for entry routes', function () { + res._route = { + type: 'entry' + }; + + // Requires a post to be set + data = {post: {slug: 'test'}}; + + // Call setTemplate + templates.setTemplate(req, res, data); + + // should be getTemplateForEntry + res._template.should.eql('testFromEntry'); + + // Only pickTemplate got called + stubs.pickTemplate.called.should.be.false(); + stubs.getTemplateForEntry.called.should.be.true(); + stubs.getTemplateForChannel.called.should.be.false(); + stubs.getTemplateForError.called.should.be.false(); + + stubs.getTemplateForEntry.calledWith({slug: 'test'}).should.be.true(); + }); + + it('calls getTemplateForChannel for channel routes', function () { + res._route = { + type: 'channel' + }; + + res.locals.channel = {testChannel: 'test'}; + + // Call setTemplate + templates.setTemplate(req, res, data); + + // should be testFromChannel + res._template.should.eql('testFromChannel'); + + // Only pickTemplate got called + stubs.pickTemplate.called.should.be.false(); + stubs.getTemplateForEntry.called.should.be.false(); + stubs.getTemplateForChannel.called.should.be.true(); + stubs.getTemplateForError.called.should.be.false(); + + stubs.getTemplateForChannel.calledWith({testChannel: 'test'}).should.be.true(); + }); + + it('calls getTemplateForError if there is an error', function () { + // Make the config look like a custom route + res._route = { + type: 'custom', + templateName: 'test', + defaultTemplate: 'path/to/local/test.hbs' + }; + + // Setup an error + res.statusCode = 404; + req.err = new Error(); + + // Call setTemplate + templates.setTemplate(req, res, data); + + // should be testFromError + res._template.should.eql('testFromError'); + + // Only pickTemplate got called + stubs.pickTemplate.called.should.be.false(); + stubs.getTemplateForEntry.called.should.be.false(); + stubs.getTemplateForChannel.called.should.be.false(); + stubs.getTemplateForError.called.should.be.true(); + + stubs.getTemplateForError.calledWith(404).should.be.true(); }); }); });