🔥 No more availableThemes (#8085)

no issue

🎨 Switch themes API to use config.availableThemes
- this gets rid of the only places where settings.availableThemes are used

🔥 Get rid of settings.availableThemes
- this is no longer used anywhere
- also get rid of every related call to updateSettingsCache

🔥 Replace config.availableThemes with theme cache
- Creates a tailor-made in-memory cache for themes inside the theme module
- Add methods for getting & setting items on the cache
- Move all references to config.availableThemes to use the new cache
- This can be abstracted later to support other kinds of caches?

🎨 Start improving theme lib's API
Still TODO: simplifying/clarifying:
- what is the structure of the internal list
- what is the difference between a package list, and a theme list?
- what is the difference between reading a theme and loading it?
- how do we update the theme list (add/remove)
- how do we refresh the theme list? (hot reload?!)
- how do we get from an internal list, to one that is sent as part of the API?
- how are we going to handle theme storage: read/write, such that the path is configurable

🎨 Use themeList consistently
🎨 Update list after storage
This commit is contained in:
Hannah Wolfe 2017-03-02 16:53:48 +00:00 committed by Katharina Irrgang
parent 0b68458eb7
commit f8b498d6e7
19 changed files with 277 additions and 154 deletions

View File

@ -3,12 +3,10 @@
var _ = require('lodash'),
dataProvider = require('../models'),
Promise = require('bluebird'),
config = require('../config'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
utils = require('./utils'),
i18n = require('../i18n'),
filterPackages = require('../utils/packages').filterPackages,
docName = 'settings',
settings,
@ -68,6 +66,17 @@ settingsFilter = function (settings, filter) {
/**
* ### Read Settings Result
*
* Converts the models to keyed JSON
* E.g.
* dbHash: {
* id: '123abc',
* key: 'dbash',
* value: 'xxxx',
* type: 'core',
* timestamps
* }
*
* @private
* @param {Array} settingsModels
* @returns {Settings}
@ -79,20 +88,7 @@ readSettingsResult = function (settingsModels) {
}
return memo;
}, {}),
themes = config.get('paths').availableThemes,
res;
// @TODO: remove availableThemes from settings cache and create an endpoint to fetch themes
if (settings.activeTheme && themes) {
res = filterPackages(themes, settings.activeTheme.value);
settings.availableThemes = {
key: 'availableThemes',
value: res,
type: 'theme'
};
}
}, {});
return settings;
};
@ -255,7 +251,7 @@ settings = {
}
object.settings = _.reject(object.settings, function (setting) {
return setting.key === 'type' || setting.key === 'availableThemes';
return setting.key === 'type';
});
return canEditAllSettings(object.settings, options).then(function () {

View File

@ -1,6 +1,7 @@
// # Themes API
// RESTful API for Themes
var Promise = require('bluebird'),
var debug = require('debug')('ghost:api:themes'),
Promise = require('bluebird'),
_ = require('lodash'),
gscan = require('gscan'),
fs = require('fs-extra'),
@ -9,12 +10,12 @@ var Promise = require('bluebird'),
events = require('../events'),
logging = require('../logging'),
storage = require('../storage'),
settings = require('./settings'),
settingsCache = require('../settings/cache'),
apiUtils = require('./utils'),
utils = require('./../utils'),
i18n = require('../i18n'),
themeUtils = require('../themes'),
themeList = themeUtils.list,
packageUtils = require('../utils/packages'),
themes;
/**
@ -24,7 +25,10 @@ var Promise = require('bluebird'),
*/
themes = {
browse: function browse() {
return Promise.resolve({themes: settingsCache.get('availableThemes')});
debug('browsing');
var result = packageUtils.filterPackages(themeList.getAll());
debug('got result');
return Promise.resolve({themes: result});
},
upload: function upload(options) {
@ -80,23 +84,18 @@ themes = {
}, config.getContentPath('themes'));
})
.then(function () {
// force reload of availableThemes
// right now the logic is in the ConfigManager
// if we create a theme collection, we don't have to read them from disk
return themeUtils.load();
return themeUtils.loadOne(zip.shortName);
})
.then(function () {
// the settings endpoint is used to fetch the availableThemes
// so we have to force updating the in process cache
return settings.updateSettingsCache();
})
.then(function (settings) {
.then(function (themeObject) {
// @TODO fix this craziness
var toFilter = {};
toFilter[zip.shortName] = themeObject;
themeObject = packageUtils.filterPackages(toFilter);
// gscan theme structure !== ghost theme structure
var themeObject = _.find(settings.availableThemes.value, {name: zip.shortName}) || {};
if (theme.results.warning.length > 0) {
themeObject.warnings = _.cloneDeep(theme.results.warning);
}
return {themes: [themeObject]};
return {themes: themeObject};
})
.finally(function () {
// remove zip upload from multer
@ -119,7 +118,7 @@ themes = {
download: function download(options) {
var themeName = options.name,
theme = config.get('paths').availableThemes[themeName],
theme = themeList.get(themeName),
storageAdapter = storage.getStorage('themes');
if (!theme) {
@ -148,20 +147,17 @@ themes = {
throw new errors.ValidationError({message: i18n.t('errors.api.themes.destroyCasper')});
}
theme = config.get('paths').availableThemes[name];
theme = themeList.get(name);
if (!theme) {
throw new errors.NotFoundError({message: i18n.t('errors.api.themes.themeDoesNotExist')});
}
events.emit('theme.deleted', name);
return storageAdapter.delete(name, config.getContentPath('themes'));
})
.then(function () {
return themeUtils.load();
})
.then(function () {
return settings.updateSettingsCache();
themeList.del(name);
events.emit('theme.deleted', name);
});
}
};

View File

@ -7,6 +7,7 @@ var rewire = require('rewire'),
errors = require('../../../errors'),
should = require('should'),
configUtils = require('../../../../test/utils/configUtils'),
themeList = require('../../../themes').list,
sandbox = sinon.sandbox.create();
// Helper function to prevent unit tests
@ -53,10 +54,11 @@ describe('AMP Controller', function () {
afterEach(function () {
sandbox.restore();
configUtils.restore();
themeList.init();
});
it('should render default amp page when theme has no amp template', function (done) {
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeList.init({casper: {}});
setResponseContextStub = sandbox.stub();
ampController.__set__('setResponseContext', setResponseContextStub);
@ -70,9 +72,9 @@ describe('AMP Controller', function () {
});
it('should render theme amp page when theme has amp template', function (done) {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
'amp.hbs': '/content/themes/casper/amp.hbs'
}}}});
}});
setResponseContextStub = sandbox.stub();
ampController.__set__('setResponseContext', setResponseContextStub);
@ -86,7 +88,7 @@ describe('AMP Controller', function () {
});
it('should render with error when error is passed in', function (done) {
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeList.init({casper: {}});
res.error = 'Test Error';
setResponseContextStub = sandbox.stub();
@ -103,7 +105,7 @@ describe('AMP Controller', function () {
it('does not render amp page when amp context is missing', function (done) {
var renderSpy;
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeList.init({casper: {}});
setResponseContextStub = sandbox.stub();
ampController.__set__('setResponseContext', setResponseContextStub);
@ -121,7 +123,7 @@ describe('AMP Controller', function () {
it('does not render amp page when context is other than amp and post', function (done) {
var renderSpy;
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeList.init({casper: {}});
setResponseContextStub = sandbox.stub();
ampController.__set__('setResponseContext', setResponseContextStub);

View File

@ -3,6 +3,7 @@ var privateController = require('../lib/router').controller,
path = require('path'),
sinon = require('sinon'),
configUtils = require('../../../../test/utils/configUtils'),
themeList = require('../../../themes').list,
sandbox = sinon.sandbox.create();
describe('Private Controller', function () {
@ -42,10 +43,11 @@ describe('Private Controller', function () {
afterEach(function () {
sandbox.restore();
configUtils.restore();
themeList.init();
});
it('Should render default password page when theme has no password template', function (done) {
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeList.init({casper: {}});
res.render = function (view) {
view.should.eql(defaultPath);
@ -56,9 +58,9 @@ describe('Private Controller', function () {
});
it('Should render theme password page when it exists', function (done) {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
'private.hbs': '/content/themes/casper/private.hbs'
}}}});
}});
res.render = function (view) {
view.should.eql('private');
@ -69,7 +71,7 @@ describe('Private Controller', function () {
});
it('Should render with error when error is passed in', function (done) {
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeList.init({casper: {}});
res.error = 'Test Error';
res.render = function (view, context) {

View File

@ -2,11 +2,11 @@
//
// Figure out which template should be used to render a request
// based on the templates which are allowed, and what is available in the theme
var _ = require('lodash'),
config = require('../../config');
var _ = require('lodash'),
themeList = require('../../themes').list;
function getActiveThemePaths(activeTheme) {
return config.get('paths').availableThemes[activeTheme];
return themeList.get(activeTheme);
}
/**

View File

@ -9,6 +9,7 @@ var _ = require('lodash'),
logging = require('../logging'),
errors = require('../errors'),
i18n = require('../i18n'),
themeList = require('../themes').list,
themeHandler;
themeHandler = {
@ -95,7 +96,7 @@ themeHandler = {
// Check if the theme changed
if (activeTheme.value !== blogApp.get('activeTheme')) {
// Change theme
if (!config.get('paths').availableThemes.hasOwnProperty(activeTheme.value)) {
if (!themeList.get(activeTheme.value)) {
if (!res.isAdmin) {
return next(new errors.NotFoundError({
message: i18n.t('errors.middleware.themehandler.missingTheme', {theme: activeTheme.value})

View File

@ -1,7 +1,11 @@
var themeLoader = require('./loader');
// @TODO: reduce the amount of things we expose to the outside world
// Make this a nice clean sensible API we can all understand!
module.exports = {
init: themeLoader.init,
load: themeLoader.load,
loadAll: themeLoader.loadAllThemes,
loadOne: themeLoader.loadOneTheme,
list: require('./list'),
validate: require('./validate')
};

View File

@ -0,0 +1,36 @@
/**
* Store themes after loading them from the file system
*/
var _ = require('lodash'),
themeListCache = {};
module.exports = {
get: function get(key) {
return themeListCache[key];
},
getAll: function getAll() {
return themeListCache;
},
set: function set(key, theme) {
themeListCache[key] = _.cloneDeep(theme);
return themeListCache[key];
},
del: function del(key) {
delete themeListCache[key];
},
init: function init(themes) {
var self = this;
// First, reset the cache
themeListCache = {};
// For each theme, call set. Allows us to do processing on set later.
_.each(themes, function (theme, key) {
self.set(key, theme);
});
return themeListCache;
}
};

View File

@ -1,23 +1,32 @@
var debug = require('debug')('ghost:themes:loader'),
config = require('../config'),
events = require('../events'),
themeList = require('./list'),
read = require('./read'),
settingsApi = require('../api/settings'),
settingsCache = require('../settings/cache'),
updateConfigAndCache,
loadThemes,
updateThemeList,
loadAllThemes,
loadOneTheme,
initThemes;
updateConfigAndCache = function updateConfigAndCache(themes) {
updateThemeList = function updateThemeList(themes) {
debug('loading themes', Object.keys(themes));
config.set('paths:availableThemes', themes);
settingsApi.updateSettingsCache();
themeList.init(themes);
};
loadThemes = function loadThemes() {
loadAllThemes = function loadAllThemes() {
return read
.all(config.getContentPath('themes'))
.then(updateConfigAndCache);
.then(updateThemeList);
};
loadOneTheme = function loadOneTheme(themeName) {
return read
.one(config.getContentPath('themes'), themeName)
.then(function (readThemes) {
// @TODO change read one to not return a keyed object
return themeList.set(themeName, readThemes[themeName]);
});
};
initThemes = function initThemes() {
@ -25,16 +34,17 @@ initThemes = function initThemes() {
// Register a listener for server-start to load all themes
events.on('server:start', function readAllThemesOnServerStart() {
loadThemes();
loadAllThemes();
});
// Just read the active theme for now
return read
.one(config.getContentPath('themes'), settingsCache.get('activeTheme'))
.then(updateConfigAndCache);
.then(updateThemeList);
};
module.exports = {
init: initThemes,
load: loadThemes
loadAllThemes: loadAllThemes,
loadOneTheme: loadOneTheme
};

View File

@ -1,19 +1,19 @@
var Promise = require('bluebird'),
config = require('../config'),
errors = require('../errors'),
i18n = require('../i18n'),
themeList = require('./list'),
validateActiveTheme;
// @TODO replace this with something PROPER - we should probably attempt to read the theme from the
// File system at this point and validate the theme using gscan rather than just checking if it's in a cache object
validateActiveTheme = function validateActiveTheme(themeName) {
if (!config.get('paths').availableThemes || Object.keys(config.get('paths').availableThemes).length === 0) {
if (!themeList.getAll() || Object.keys(themeList.getAll()).length === 0) {
// We haven't yet loaded all themes, this is probably being called early?
return Promise.resolve();
}
// Else, if we have a list, check if the theme is in it
if (!config.get('paths').availableThemes.hasOwnProperty(themeName)) {
if (!themeList.get(themeName)) {
return Promise.reject(new errors.ValidationError({message: i18n.t('notices.data.validation.index.themeCannotBeActivated', {themeName: themeName}), context: 'activeTheme'}));
}
};

View File

@ -65,17 +65,18 @@ describe('Themes API', function () {
});
describe('success cases', function () {
it('get all available themes', function (done) {
request.get(testUtils.API.getApiQuery('settings/'))
it('get all themes', function (done) {
request.get(testUtils.API.getApiQuery('themes/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.end(function (err, res) {
if (err) {
return done(err);
}
var availableThemes = _.find(res.body.settings, {key: 'availableThemes'});
should.exist(availableThemes);
availableThemes.value.length.should.be.above(0);
var jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.be.above(0);
done();
});
});
@ -108,19 +109,21 @@ describe('Themes API', function () {
});
});
it('get all available themes + new theme', function (done) {
request.get(testUtils.API.getApiQuery('settings/'))
it('get all themes + new theme', function (done) {
request.get(testUtils.API.getApiQuery('themes/'))
.set('Authorization', 'Bearer ' + scope.ownerAccessToken)
.end(function (err, res) {
if (err) {
return done(err);
}
var availableThemes = _.find(res.body.settings, {key: 'availableThemes'});
should.exist(availableThemes);
var jsonResponse = res.body;
should.exist(jsonResponse.themes);
testUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.be.above(0);
// ensure the new 'valid' theme is available
should.exist(_.find(availableThemes.value, {name: 'valid'}));
should.exist(_.find(jsonResponse.themes, {name: 'valid'}));
done();
});
});

View File

@ -6,7 +6,7 @@ var should = require('should'),
// Stuff we are testing
channels = require('../../../../server/controllers/frontend/channels'),
api = require('../../../../server/api'),
configUtils = require('../../../utils/configUtils'),
themeList = require('../../../../server/themes').list,
sandbox = sinon.sandbox.create();
describe('Channels', function () {
@ -104,7 +104,7 @@ describe('Channels', function () {
});
afterEach(function () {
configUtils.restore();
themeList.init();
sandbox.restore();
});
@ -120,9 +120,9 @@ describe('Channels', function () {
// Return basic paths for the activeTheme
function setupActiveTheme() {
configUtils.set('paths', {availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs'
}}});
}});
}
beforeEach(function () {
@ -141,10 +141,10 @@ describe('Channels', function () {
});
it('should render the first page of the index channel using home.hbs if available', function (done) {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'home.hbs': '/content/themes/casper/home.hbs'
}}}});
}});
testChannelRender({url: '/'}, function (view) {
should.exist(view);
@ -163,10 +163,10 @@ describe('Channels', function () {
});
it('should use index.hbs for second page even if home.hbs is available', function (done) {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'home.hbs': '/content/themes/casper/home.hbs'
}}}});
}});
testChannelRender({url: '/page/2/'}, function (view) {
should.exist(view);
@ -255,9 +255,9 @@ describe('Channels', function () {
// Return basic paths for the activeTheme
function setupActiveTheme() {
configUtils.set('paths', {availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs'
}}});
}});
}
beforeEach(function () {
@ -277,10 +277,10 @@ describe('Channels', function () {
});
it('should render the first page of the tag channel using tag.hbs by default', function (done) {
configUtils.set('paths',{availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'tag.hbs': '/content/themes/casper/tag.hbs'
}}});
}});
testChannelRender({url: '/tag/my-tag/'}, function (view) {
should.exist(view);
@ -291,11 +291,11 @@ describe('Channels', function () {
});
it('should render the first page of the tag channel using tag-:slug.hbs if available', function (done) {
configUtils.set('paths', {availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'tag.hbs': '/content/themes/casper/tag.hbs',
'tag-my-tag.hbs': '/content/themes/casper/tag-my-tag.hbs'
}}});
}});
testChannelRender({url: '/tag/my-tag/'}, function (view) {
should.exist(view);
@ -315,10 +315,10 @@ describe('Channels', function () {
});
it('should use tag.hbs to render the tag channel if available', function (done) {
configUtils.set('paths', {availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'tag.hbs': '/content/themes/casper/tag.hbs'
}}});
}});
testChannelRender({url: '/tag/my-tag/page/2/'}, function (view) {
should.exist(view);
@ -328,11 +328,11 @@ describe('Channels', function () {
});
it('should use tag-:slug.hbs to render the tag channel if available', function (done) {
configUtils.set('paths', {availableThemes: {casper: {
themeList.init({casper: {
'index.hbs': '/content/themes/casper/index.hbs',
'tag.hbs': '/content/themes/casper/tag.hbs',
'tag-my-tag.hbs': '/content/themes/casper/tag-my-tag.hbs'
}}});
}});
testChannelRender({url: '/tag/my-tag/page/2/'}, function (view) {
should.exist(view);

View File

@ -6,6 +6,7 @@ var moment = require('moment'),
api = require('../../../../server/api'),
frontend = require('../../../../server/controllers/frontend'),
configUtils = require('../../../utils/configUtils'),
themeList = require('../../../../server/themes').list,
settingsCache = require('../../../../server/settings/cache'),
sandbox = sinon.sandbox.create();
@ -121,7 +122,7 @@ describe('Frontend Controller', function () {
describe('static pages', function () {
describe('custom page templates', function () {
it('it will render a custom page-slug template if it exists', function (done) {
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
req.path = '/' + mockPosts[2].posts[0].slug + '/';
req.route = {path: '*'};
res.render = function (view, context) {
@ -136,7 +137,7 @@ describe('Frontend Controller', function () {
it('it will use page.hbs if it exists and no page-slug template is present', function (done) {
delete casper['page-about.hbs'];
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
req.path = '/' + mockPosts[2].posts[0].slug + '/';
req.route = {path: '*'};
res.render = function (view, context) {
@ -152,7 +153,7 @@ describe('Frontend Controller', function () {
it('defaults to post.hbs without a page.hbs or page-slug template', function (done) {
delete casper['page-about.hbs'];
delete casper['page.hbs'];
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
req.path = '/' + mockPosts[2].posts[0].slug + '/';
req.route = {path: '*'};
res.render = function (view, context) {
@ -168,7 +169,7 @@ describe('Frontend Controller', function () {
describe('permalink set to slug', function () {
it('will render static page via /:slug/', function (done) {
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
req.path = '/' + mockPosts[0].posts[0].slug + '/';
req.route = {path: '*'};
@ -237,7 +238,7 @@ describe('Frontend Controller', function () {
});
it('will render static page via /:slug', function (done) {
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
req.path = '/' + mockPosts[0].posts[0].slug + '/';
req.route = {path: '*'};
@ -293,7 +294,7 @@ describe('Frontend Controller', function () {
});
it('will render post via /:slug/', function (done) {
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
req.path = '/' + mockPosts[1].posts[0].slug + '/';
req.route = {path: '*'};
@ -382,7 +383,7 @@ describe('Frontend Controller', function () {
});
it('will render post via /YYYY/MM/DD/:slug/', function (done) {
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
var date = moment(mockPosts[1].posts[0].published_at).format('YYYY/MM/DD');
req.path = '/' + [date, mockPosts[1].posts[0].slug].join('/') + '/';
req.route = {path: '*'};
@ -459,7 +460,7 @@ describe('Frontend Controller', function () {
});
it('will render post via /:author/:slug/', function (done) {
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
req.path = '/' + ['test', mockPosts[1].posts[0].slug].join('/') + '/';
req.route = {path: '*'};
@ -540,7 +541,7 @@ describe('Frontend Controller', function () {
beforeEach(function () {
localSettingsCache.permalinks = '/:year/:slug/';
configUtils.set({paths: {availableThemes: {casper: casper}}});
themeList.init({casper: casper});
var date = moment(mockPosts[1].posts[0].published_at).format('YYYY');
mockPosts[1].posts[0].url = '/' + date + '/' + mockPosts[1].posts[0].slug + '/';
@ -752,7 +753,7 @@ describe('Frontend Controller', function () {
redirect: sinon.spy()
};
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeList.init({casper: {}});
});
it('should render draft post', function (done) {
@ -768,7 +769,7 @@ describe('Frontend Controller', function () {
});
it('should render draft page', function (done) {
configUtils.set({paths: {availableThemes: {casper: {'page.hbs': '/content/themes/casper/page.hbs'}}}});
themeList.init({casper: {'page.hbs': '/content/themes/casper/page.hbs'}});
req.params = {uuid: 'abc-1234-01'};
res.render = function (view, context) {
view.should.equal('page');

View File

@ -4,11 +4,11 @@ var should = require('should'),
// Stuff we are testing
templates = rewire('../../../../server/controllers/frontend/templates'),
configUtils = require('../../../utils/configUtils');
themeList = require('../../../../server/themes').list;
describe('templates', function () {
afterEach(function () {
configUtils.restore();
themeList.init();
});
describe('utils', function () {
@ -48,21 +48,16 @@ describe('templates', function () {
describe('single', function () {
describe('with many templates', function () {
beforeEach(function () {
configUtils.set({
paths: {
availableThemes: {
casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs',
'index.hbs': '/content/themes/casper/index.hbs',
'page.hbs': '/content/themes/casper/page.hbs',
'page-about.hbs': '/content/themes/casper/page-about.hbs',
'post.hbs': '/content/themes/casper/post.hbs',
'post-welcome-to-ghost.hbs': '/content/themes/casper/post-welcome-to-ghost.hbs'
}
}
}
});
themeList.init({casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs',
'index.hbs': '/content/themes/casper/index.hbs',
'page.hbs': '/content/themes/casper/page.hbs',
'page-about.hbs': '/content/themes/casper/page-about.hbs',
'post.hbs': '/content/themes/casper/post.hbs',
'post-welcome-to-ghost.hbs': '/content/themes/casper/post-welcome-to-ghost.hbs'
}});
});
it('will return correct template for a post WITHOUT custom template', function () {
@ -103,10 +98,10 @@ describe('templates', function () {
});
it('will fall back to post even if no index.hbs', function () {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs'
}}}});
}});
var view = templates.single('casper', {page: 1});
should.exist(view);
@ -117,11 +112,11 @@ describe('templates', function () {
describe('channel', function () {
describe('without tag templates', function () {
beforeEach(function () {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs',
'index.hbs': '/content/themes/casper/index.hbs'
}}}});
}});
});
it('will return correct view for a tag', function () {
@ -133,13 +128,13 @@ describe('templates', function () {
describe('with tag templates', function () {
beforeEach(function () {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs',
'index.hbs': '/content/themes/casper/index.hbs',
'tag.hbs': '/content/themes/casper/tag.hbs',
'tag-design.hbs': '/content/themes/casper/tag-about.hbs'
}}}});
}});
});
it('will return correct view for a tag', function () {
@ -156,10 +151,10 @@ describe('templates', function () {
});
it('will fall back to index even if no index.hbs', function () {
configUtils.set({paths: {availableThemes: {casper: {
themeList.init({casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs'
}}}});
}});
var view = templates.channel('casper', {name: 'tag', slugParam: 'development', slugTemplate: true});
should.exist(view);

View File

@ -4,10 +4,10 @@ var sinon = require('sinon'),
Promise = require('bluebird'),
fs = require('fs'),
hbs = require('express-hbs'),
themeList = require('../../../server/themes').list,
themeHandler = require('../../../server/middleware/theme-handler'),
logging = require('../../../server/logging'),
api = require('../../../server/api'),
configUtils = require('../../utils/configUtils'),
sandbox = sinon.sandbox.create();
describe('Theme Handler', function () {
@ -23,7 +23,7 @@ describe('Theme Handler', function () {
afterEach(function () {
sandbox.restore();
configUtils.restore();
themeList.init();
});
describe('activateTheme', function () {
@ -89,6 +89,10 @@ describe('Theme Handler', function () {
});
describe('updateActiveTheme', function () {
beforeEach(function () {
themeList.init({casper: {}});
});
it('updates the active theme if changed', function (done) {
var activateThemeSpy = sandbox.spy(themeHandler, 'activateTheme');
@ -99,7 +103,6 @@ describe('Theme Handler', function () {
}]
}));
blogApp.set('activeTheme', 'not-casper');
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeHandler.updateActiveTheme(req, res, function () {
activateThemeSpy.called.should.be.true();
@ -116,7 +119,6 @@ describe('Theme Handler', function () {
}]
}));
blogApp.set('activeTheme', 'casper');
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeHandler.updateActiveTheme(req, res, function () {
activateThemeSpy.called.should.be.false();
@ -135,7 +137,6 @@ describe('Theme Handler', function () {
}));
blogApp.set('activeTheme', 'not-casper');
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeHandler.updateActiveTheme(req, res, function (err) {
should.exist(err);
@ -158,7 +159,6 @@ describe('Theme Handler', function () {
res.isAdmin = true;
blogApp.set('activeTheme', 'not-casper');
configUtils.set({paths: {availableThemes: {casper: {}}}});
themeHandler.updateActiveTheme(req, res, function () {
activateThemeSpy.called.should.be.false();

View File

@ -1,7 +1,7 @@
var should = require('should'),
hbs = require('express-hbs'),
utils = require('./utils'),
configUtils = require('../../utils/configUtils'),
themeList = require('../../../server/themes').list,
// Stuff we are testing
handlebars = hbs.handlebars,
@ -11,18 +11,16 @@ describe('{{body_class}} helper', function () {
var options = {};
before(function () {
utils.loadHelpers();
configUtils.set({paths: {
availableThemes: {
casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs',
'index.hbs': '/content/themes/casper/index.hbs',
'page.hbs': '/content/themes/casper/page.hbs',
'page-about.hbs': '/content/themes/casper/page-about.hbs',
'post.hbs': '/content/themes/casper/post.hbs'
}
themeList.init({
casper: {
assets: null,
'default.hbs': '/content/themes/casper/default.hbs',
'index.hbs': '/content/themes/casper/index.hbs',
'page.hbs': '/content/themes/casper/page.hbs',
'page-about.hbs': '/content/themes/casper/page-about.hbs',
'post.hbs': '/content/themes/casper/post.hbs'
}
}});
});
});
beforeEach(function () {
@ -37,7 +35,7 @@ describe('{{body_class}} helper', function () {
});
after(function () {
configUtils.restore();
themeList.init();
});
it('has loaded body_class helper', function () {

View File

@ -1,13 +1,22 @@
var should = require('should'),
sinon = require('sinon'),
_ = require('lodash'),
fs = require('fs'),
tmp = require('tmp'),
join = require('path').join,
readThemes = require('../../server/themes/read');
themeList = require('../../server/themes').list,
readThemes = require('../../server/themes/read'),
sandbox = sinon.sandbox.create();
// To stop jshint complaining
should.equal(true, true);
describe('Themes', function () {
afterEach(function () {
sandbox.restore();
});
describe('Read All', function () {
it('should read directory and include only folders', function (done) {
var themePath = tmp.dirSync({unsafeCleanup: true});
@ -126,4 +135,73 @@ describe('Themes', function () {
.finally(themePath.removeCallback);
});
});
describe('List', function () {
beforeEach(function () {
themeList.init({
casper: {foo: 'bar'},
'not-casper': {bar: 'baz'}
});
});
it('get() allows getting a single theme', function () {
themeList.get('casper').should.eql({foo: 'bar'});
});
it('get() with no args should do nothing', function () {
should.not.exist(themeList.get());
});
it('getAll() returns all themes', function () {
themeList.getAll().should.be.an.Object().with.properties('casper', 'not-casper');
Object.keys(themeList.getAll()).should.have.length(2);
});
it('set() updates an existing theme', function () {
var origCasper = _.cloneDeep(themeList.get('casper'));
themeList.set('casper', {magic: 'update'});
themeList.get('casper').should.not.eql(origCasper);
themeList.get('casper').should.eql({magic: 'update'});
});
it('set() can add a new theme', function () {
themeList.set('rasper', {color: 'red'});
themeList.get('rasper').should.eql({color: 'red'});
});
it('del() removes a key from the list', function () {
should.exist(themeList.get('casper'));
should.exist(themeList.get('not-casper'));
themeList.del('casper');
should.not.exist(themeList.get('casper'));
should.exist(themeList.get('not-casper'));
});
it('del() with no argument does nothing', function () {
should.exist(themeList.get('casper'));
should.exist(themeList.get('not-casper'));
themeList.del();
should.exist(themeList.get('casper'));
should.exist(themeList.get('not-casper'));
});
it('init() calls set for each theme', function () {
var setSpy = sandbox.spy(themeList, 'set');
themeList.init({test: {a: 'b'}, casper: {c: 'd'}});
setSpy.calledTwice.should.be.true();
setSpy.firstCall.calledWith('test', {a: 'b'}).should.be.true();
setSpy.secondCall.calledWith('casper', {c: 'd'}).should.be.true();
});
it('init() with empty object resets the list', function () {
themeList.init();
var result = themeList.getAll();
should.exist(result);
result.should.be.an.Object();
result.should.eql({});
Object.keys(result).should.have.length(0);
});
});
});

View File

@ -32,6 +32,7 @@ var _ = require('lodash'),
permission: _.keys(schema.permissions),
notification: ['type', 'message', 'status', 'id', 'dismissible', 'location'],
theme: ['uuid', 'name', 'version', 'active'],
themes: ['themes'],
invites: _(schema.invites).keys().without('token').value()
};

View File

@ -449,7 +449,7 @@ toDoList = {
clients: function insertClients() { return fixtures.insertClients(); },
filter: function createFilterParamFixtures() { return filterData(DataGenerator); },
invites: function insertInvites() { return fixtures.insertInvites(); },
themes: function loadThemes() { return themes.load(); }
themes: function loadThemes() { return themes.loadAll(); }
};
/**