diff --git a/app.js b/app.js index bbc0da3830..834624c479 100755 --- a/app.js +++ b/app.js @@ -138,7 +138,7 @@ ghost.app().get('/ghost/editor/:id', auth, admin.editor); ghost.app().get('/ghost/editor', auth, admin.editor); ghost.app().get('/ghost/content', auth, admin.content); - ghost.app().get('/ghost/settings', auth, admin.settings); + ghost.app().get('/ghost/settings*', auth, admin.settings); ghost.app().get('/ghost/debug', auth, admin.debug.index); ghost.app().get('/ghost/debug/db/delete/', auth, admin.debug.dbdelete); ghost.app().get('/ghost/debug/db/populate/', auth, admin.debug.dbpopulate); diff --git a/core/admin/assets/js/init.js b/core/admin/assets/js/init.js index 8f302daa82..79ea27c7fe 100644 --- a/core/admin/assets/js/init.js +++ b/core/admin/assets/js/init.js @@ -21,46 +21,15 @@ router: null }; - Ghost.View = Backbone.View.extend({ - - // Adds a subview to the current view, which will - // ensure its removal when this view is removed, - // or when view.removeSubviews is called - addSubview: function (view) { - if (!(view instanceof Backbone.View)) { - throw new Error("Subview must be a Backbone.View"); - } - this.subviews = this.subviews || []; - this.subviews.push(view); - return view; - }, - - // Removes any subviews associated with this view - // by `addSubview`, which will in-turn remove any - // children of those views, and so on. - removeSubviews: function () { - var i, l, children = this.subviews; - if (!children) { - return this; - } - for (i = 0, l = children.length; i < l; i += 1) { - children[i].remove(); - } - this.subviews = []; - return this; - }, - - // Extends the view's remove, by calling `removeSubviews` - // if any subviews exist. - remove: function () { - if (this.subviews) { - this.removeSubviews(); - } - return Backbone.View.prototype.remove.apply(this, arguments); - } - - }); + Ghost.init = function () { + Ghost.router = new Ghost.Router(); + Backbone.history.start({ + pushState: true, + hashChange: false, + root: '/ghost' + }); + }; window.Ghost = Ghost; -}()); \ No newline at end of file +}()); diff --git a/core/admin/assets/js/models/settings.js b/core/admin/assets/js/models/settings.js new file mode 100644 index 0000000000..a30123b9aa --- /dev/null +++ b/core/admin/assets/js/models/settings.js @@ -0,0 +1,16 @@ +/*global window, document, Ghost, $, Backbone, _ */ +(function () { + "use strict"; + + // Set the url manually and id to '0' to force PUT requests + Ghost.Models.Settings = Backbone.Model.extend({ + url: '/api/v0.1/settings/', + id: "0", + defaults: { + title: 'My Blog', + description: '', + email: 'admin@tryghost.org' + } + }); + +}()); diff --git a/core/admin/assets/js/router.js b/core/admin/assets/js/router.js index dd2f0b958a..4916fb25c2 100644 --- a/core/admin/assets/js/router.js +++ b/core/admin/assets/js/router.js @@ -6,12 +6,41 @@ Ghost.Router = Backbone.Router.extend({ routes: { - '': 'dashboard', - 'content/': 'blog', - 'editor': 'editor', - 'editor/': 'editor', - 'editor/:id': 'editor' + '' : 'dashboard', + 'content/' : 'blog', + 'settings/' : 'settings', + 'settings(/:pane)' : 'settings', + 'editor/' : 'editor', + 'editor(/:id)' : 'editor' }, + + blog: function () { + var posts = new Ghost.Collections.Posts(); + posts.fetch({ data: { status: 'all' } }).then(function () { + Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts }); + }); + }, + + settings: function (pane) { + var settings = new Ghost.Models.Settings(); + settings.fetch().then(function () { + Ghost.currentView = new Ghost.Views.Settings({ el: '#main', model: settings, pane: pane }); + }); + }, + + editor: function (id) { + var post = new Ghost.Models.Post(); + post.urlRoot = Ghost.settings.apiRoot + '/posts'; + if (id) { + post.id = id; + post.fetch().then(function () { + Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); + }); + } else { + Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); + } + }, + dashboard: function () { var widgets = new Ghost.Collections.Widgets(); @@ -418,36 +447,9 @@ } }); - - - - //widgets.fetch().then(function () { Ghost.currentView = new Ghost.Views.Dashboard({ el: '#main', collection: widgets }); //}); - }, - - blog: function () { - var posts = new Ghost.Collections.Posts(); - posts.fetch({data: {status: 'all'}}).then(function () { - Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts }); - }); - - }, - - editor: function (id) { - var post = new Ghost.Models.Post(); - post.urlRoot = Ghost.settings.apiRoot + '/posts'; - if (id) { - post.id = id; - post.fetch().then(function () { - Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); - }); - } else { - Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); - } } - }); - }()); \ No newline at end of file diff --git a/core/admin/assets/js/settings.js b/core/admin/assets/js/settings.js deleted file mode 100644 index d768075332..0000000000 --- a/core/admin/assets/js/settings.js +++ /dev/null @@ -1,60 +0,0 @@ -/*globals document, location, jQuery */ -(function ($) { - "use strict"; - - var changePage = function (e) { - var newPage = $(this).children('a').attr('href'); - - e.preventDefault(); - $('.settings-menu .active').removeClass('active'); - location.hash = $(this).attr('class'); // Placed here so never gets given the active attribute. - $(this).addClass('active'); - - $('.settings-content').fadeOut().delay(250); - $(newPage).fadeIn(); - - }, - - defaultSettings = { - title: 'My Blog', - description: '' - }, - - getSettings = function () { - return $.extend(defaultSettings, { - title : $('#blog-title').val(), - description : $('#blog-description').val() - }); - }; - - $(document).ready(function () { - if (location.hash) { - var page = $(".settings-menu li." + location.hash.replace('#', '')), - newPage = page.children('a').attr('href'); - $('.settings-menu .active').removeClass('active'); - page.addClass('active'); - - $('.settings-content').hide().delay(250); - $(newPage).show(); - } - $('.settings-menu li').on('click', changePage); - - $('input').iCheck({ - checkboxClass: 'icheckbox_square-grey' - }); - - $('.button-save').click(function (e) { - e.preventDefault(); - var data = getSettings(); - $.ajax({ - method: 'PUT', - url: '/api/v0.1/settings', - data: data, - success: function (res, xhr, c) { - console.log(xhr, c); - } - }); - }); - }); - -}(jQuery)); \ No newline at end of file diff --git a/core/admin/assets/js/starter.js b/core/admin/assets/js/starter.js deleted file mode 100644 index adb6c2112d..0000000000 --- a/core/admin/assets/js/starter.js +++ /dev/null @@ -1,15 +0,0 @@ -/*global window, document, Ghost, Backbone, $, _ */ -(function () { - - "use strict"; - - Ghost.router = new Ghost.Router(); - - $(function () { - - Backbone.history.start({pushState: true, hashChange: false, root: '/ghost'}); - - }); - - -}()); \ No newline at end of file diff --git a/core/admin/assets/js/views/base.js b/core/admin/assets/js/views/base.js new file mode 100644 index 0000000000..3c89bf5e35 --- /dev/null +++ b/core/admin/assets/js/views/base.js @@ -0,0 +1,45 @@ +/*global window, document, Ghost, Backbone, $, _ */ +(function () { + "use strict"; + + Ghost.View = Backbone.View.extend({ + + // Adds a subview to the current view, which will + // ensure its removal when this view is removed, + // or when view.removeSubviews is called + addSubview: function (view) { + if (!(view instanceof Backbone.View)) { + throw new Error("Subview must be a Backbone.View"); + } + this.subviews = this.subviews || []; + this.subviews.push(view); + return view; + }, + + // Removes any subviews associated with this view + // by `addSubview`, which will in-turn remove any + // children of those views, and so on. + removeSubviews: function () { + var i, l, children = this.subviews; + if (!children) { + return this; + } + for (i = 0, l = children.length; i < l; i += 1) { + children[i].remove(); + } + this.subviews = []; + return this; + }, + + // Extends the view's remove, by calling `removeSubviews` + // if any subviews exist. + remove: function () { + if (this.subviews) { + this.removeSubviews(); + } + return Backbone.View.prototype.remove.apply(this, arguments); + } + + }); + +}()); diff --git a/core/admin/assets/js/views/settings.js b/core/admin/assets/js/views/settings.js new file mode 100644 index 0000000000..1e7a408ca0 --- /dev/null +++ b/core/admin/assets/js/views/settings.js @@ -0,0 +1,135 @@ +/*global window, document, Ghost, Backbone, $, _, alert */ +(function () { + "use strict"; + + var Settings = {}; + + // Base view + // ---------- + Ghost.Views.Settings = Ghost.View.extend({ + initialize: function (options) { + this.addSubview(new Settings.Sidebar({ + el: '.settings-sidebar', + pane: options.pane, + model: this.model + })); + + this.$('input').iCheck({ + checkboxClass: 'icheckbox_square-grey' + }); + } + }); + + // Sidebar (tabs) + // --------------- + Settings.Sidebar = Ghost.View.extend({ + initialize: function (options) { + this.menu = this.$('.settings-menu'); + this.showContent(options.pane || 'general'); + }, + + events: { + 'click .settings-menu li' : 'switchPane' + }, + + switchPane: function (e) { + e.preventDefault(); + var item = $(e.currentTarget), + id = item.find('a').attr('href').substring(1); + this.showContent(id); + }, + + showContent: function (id) { + Backbone.history.navigate('/settings/' + id); + if (this.pane && '#' + id === this.pane.el) { + return; + } + _.result(this.pane, 'destroy'); + this.setActive(id); + this.pane = new Settings[id]({ model: this.model }); + this.pane.render(); + }, + + setActive: function (id) { + this.menu.find('li').removeClass('active'); + this.menu.find('a[href=#' + id + ']').parent().addClass('active'); + } + }); + + // Content panes + // -------------- + Settings.Pane = Ghost.View.extend({ + destroy: function () { + this.$el.removeClass('active'); + }, + + render: function () { + this.$el.addClass('active'); + } + }); + + // TODO: render templates on the client + // TODO: use some kind of data-binding for forms + + // ### General settings + Settings.general = Settings.Pane.extend({ + el: '#general', + events: { + 'click .button-save': 'saveSettings' + }, + + saveSettings: function () { + this.model.save({ + title: this.$('#blog-title').val(), + email: this.$('#email-address').val() + }, { + success: function () { + alert('Saved'); + } + }); + }, + + render: function () { + var settings = this.model.toJSON(); + this.$('#blog-title').val(settings.title); + this.$('#email-address').val(settings.email); + Settings.Pane.prototype.render.call(this); + } + }); + + // ### Content settings + Settings.content = Settings.Pane.extend({ + el: '#content', + events: { + } + }); + + // ### User settings + Settings.users = Settings.Pane.extend({ + el: '#users', + events: { + } + }); + + // ### Appearance settings + Settings.appearance = Settings.Pane.extend({ + el: '#appearance', + events: { + } + }); + + // ### Services settings + Settings.services = Settings.Pane.extend({ + el: '#services', + events: { + } + }); + + // ### Plugins settings + Settings.plugins = Settings.Pane.extend({ + el: '#plugins', + events: { + } + }); + +}()); \ No newline at end of file diff --git a/core/admin/controllers/index.js b/core/admin/controllers/index.js index bb7dde14f6..a6d0b66183 100755 --- a/core/admin/controllers/index.js +++ b/core/admin/controllers/index.js @@ -144,8 +144,6 @@ 'settings': function (req, res) { api.settings.browse() .then(function (settings) { - settings = settings.toJSON(); - settings = _.object(_.pluck(settings, 'key'), _.pluck(settings, 'value')); res.render('settings', { bodyClass: 'settings', adminNav: setSelected(adminNavbar, 'settings'), diff --git a/core/admin/views/default.hbs b/core/admin/views/default.hbs index b12205f94d..4d2f9cf1fe 100644 --- a/core/admin/views/default.hbs +++ b/core/admin/views/default.hbs @@ -58,12 +58,17 @@ + + + - {{{block "bodyScripts"}}} + \ No newline at end of file diff --git a/core/admin/views/settings.hbs b/core/admin/views/settings.hbs index 7e074b353e..eb0153f569 100644 --- a/core/admin/views/settings.hbs +++ b/core/admin/views/settings.hbs @@ -1,7 +1,3 @@ -{{#contentFor 'bodyScripts'}} - -{{/contentFor}} - {{!< default}}
\ No newline at end of file diff --git a/core/shared/api.js b/core/shared/api.js index 2ddbf0c5c2..305f0cf709 100644 --- a/core/shared/api.js +++ b/core/shared/api.js @@ -17,7 +17,9 @@ posts, users, settings, - requestHandler; + requestHandler, + settingsObject, + settingsCollection; // # Posts posts = { @@ -59,15 +61,38 @@ }; // # Settings + + // Turn a settings collection into a single object/hashmap + settingsObject = function (settings) { + return (settings.toJSON ? settings.toJSON() : settings).reduce(function (res, item) { + if (item.toJSON) { item = item.toJSON(); } + if (item.key) { res[item.key] = item.value; } + return res; + }, {}); + }; + // Turn an object into a collection + settingsCollection = function (settings) { + return _.map(settings, function (value, key) { + return { key: key, value: value }; + }); + }; + settings = { browse: function (options) { - return dataProvider.Setting.browse(options); + return dataProvider.Settings.browse(options).then(settingsObject); }, read: function (options) { - return dataProvider.Setting.read(options.key); + return dataProvider.Settings.read(options.key).then(function (setting) { + return _.pick(setting.toJSON(), 'key', 'value'); + }); }, - edit: function (options) { - return dataProvider.Setting.edit(options); + edit: function (settings) { + settings = settingsCollection(settings); + return dataProvider.Settings.edit(settings).then(settingsObject); + }, + add: function (settings) { + settings = settingsCollection(settings); + return dataProvider.Settings.add(settings).then(settingsObject); } }; diff --git a/core/shared/data/fixtures/001.js b/core/shared/data/fixtures/001.js index cdee1c76fb..81ab53f1b9 100644 --- a/core/shared/data/fixtures/001.js +++ b/core/shared/data/fixtures/001.js @@ -61,6 +61,13 @@ module.exports = { "updated_by": 1, "type": "general" }, + { + "key": "email", + "value": "john@onolan.org", + "created_by": 1, + "updated_by": 1, + "type": "general" + }, { "key": "activePlugins", "value": "", diff --git a/core/shared/data/migration/001.js b/core/shared/data/migration/001.js index d1540e0807..a7aada7d6a 100644 --- a/core/shared/data/migration/001.js +++ b/core/shared/data/migration/001.js @@ -88,7 +88,7 @@ knex.Schema.createTable('settings', function (t) { t.increments().primary(); t.string('uuid'); - t.string('key'); + t.string('key').unique(); t.text('value'); t.string('type'); t.date('created_at'); diff --git a/core/shared/models/index.js b/core/shared/models/index.js index ec9afd8da8..1cfc8fcd64 100644 --- a/core/shared/models/index.js +++ b/core/shared/models/index.js @@ -11,7 +11,7 @@ User: require('./user').User, Role: require('./role').Role, Permission: require('./permission').Permission, - Setting: require('./setting').Setting, + Settings: require('./settings').Settings, init: function () { return knex.Schema.hasTable('posts').then(null, function () { // Simple bootstraping of the data model for now. diff --git a/core/shared/models/setting.js b/core/shared/models/setting.js deleted file mode 100644 index e27f0612b4..0000000000 --- a/core/shared/models/setting.js +++ /dev/null @@ -1,45 +0,0 @@ -(function () { - "use strict"; - - var Setting, - Settings, - GhostBookshelf = require('./base'), - _ = require('underscore'), - when = require('when'); - - Setting = GhostBookshelf.Model.extend({ - - tableName: 'settings', - - hasTimestamps: true - - }, { - - read: function (_key) { - // Allow for just passing the key instead of attributes - if (!_.isObject(_key)) { - _key = { key: _key }; - } - return GhostBookshelf.Model.read.call(this, _key); - }, - - edit: function (_data) { - return when.all(_.map(_data, function (value, key) { - return this.forge({ key: key }).fetch().then(function (setting) { - return setting.set('value', value).save(); - }); - }, this)); - } - - }); - - Settings = GhostBookshelf.Collection.extend({ - model: Setting - }); - - module.exports = { - Setting: Setting, - Settings: Settings - }; - -}()); \ No newline at end of file diff --git a/core/shared/models/settings.js b/core/shared/models/settings.js new file mode 100644 index 0000000000..a3219d7752 --- /dev/null +++ b/core/shared/models/settings.js @@ -0,0 +1,42 @@ +(function () { + "use strict"; + + var Settings, + GhostBookshelf = require('./base'), + _ = require('underscore'), + when = require('when'); + + // Each setting is saved as a separate row in the database, + // but the overlying API treats them as a single key:value mapping + Settings = GhostBookshelf.Model.extend({ + tableName: 'settings', + hasTimestamps: true + }, { + read: function (_key) { + // Allow for just passing the key instead of attributes + if (!_.isObject(_key)) { + _key = { key: _key }; + } + return GhostBookshelf.Model.read.call(this, _key); + }, + + edit: function (_data) { + var settings = this; + if (!Array.isArray(_data)) { + _data = [_data]; + } + return when.map(_data, function (item) { + // Accept an array of models as input + if (item.toJSON) { item = item.toJSON(); } + return settings.forge({ key: item.key }).fetch().then(function (setting) { + return setting.set('value', item.value).save(); + }); + }); + } + }); + + module.exports = { + Settings: Settings + }; + +}()); \ No newline at end of file diff --git a/core/shared/permissions/index.js b/core/shared/permissions/index.js index bdb280e5c6..c31e8e90ad 100644 --- a/core/shared/permissions/index.js +++ b/core/shared/permissions/index.js @@ -81,7 +81,7 @@ } return when.reject(); - }).otherwise(function() { + }).otherwise(function () { // No permissions loaded, or error loading permissions // Still check for permissable without permissions diff --git a/core/shared/permissions/objectTypeModelMap.js b/core/shared/permissions/objectTypeModelMap.js index 2d56251701..9269fa17a3 100644 --- a/core/shared/permissions/objectTypeModelMap.js +++ b/core/shared/permissions/objectTypeModelMap.js @@ -6,6 +6,6 @@ 'role': require('../models/role').Role, 'user': require('../models/user').User, 'permission': require('../models/permission').Permission, - 'setting': require('../models/setting').Setting + 'setting': require('../models/settings').Settings }; }()); \ No newline at end of file diff --git a/core/test/ghost/api_settings_spec.js b/core/test/ghost/api_settings_spec.js index 082aeac61c..670f1a9a23 100644 --- a/core/test/ghost/api_settings_spec.js +++ b/core/test/ghost/api_settings_spec.js @@ -8,9 +8,9 @@ helpers = require('./helpers'), Models = require('../../shared/models'); - describe('Setting Model', function () { + describe('Settings Model', function () { - var SettingModel = Models.Setting; + var SettingsModel = Models.Settings; beforeEach(function (done) { helpers.resetData().then(function () { @@ -19,7 +19,7 @@ }); it('can browse', function (done) { - SettingModel.browse().then(function (results) { + SettingsModel.browse().then(function (results) { should.exist(results); @@ -32,7 +32,7 @@ it('can read', function (done) { var firstSetting; - SettingModel.browse().then(function (results) { + SettingsModel.browse().then(function (results) { should.exist(results); @@ -40,7 +40,7 @@ firstSetting = results.models[0]; - return SettingModel.read(firstSetting.attributes.key); + return SettingsModel.read(firstSetting.attributes.key); }).then(function (found) { @@ -54,22 +54,21 @@ }); it('can edit single', function (done) { - var firstPost, - toEdit = {}; + var firstSetting; - SettingModel.browse().then(function (results) { + SettingsModel.browse().then(function (results) { should.exist(results); results.length.should.be.above(0); - firstPost = results.models[0]; + firstSetting = results.models[0]; // The edit method has been modified to take an object of // key/value pairs - toEdit[firstPost.attributes.key] = "new value"; + firstSetting.set('value', 'new value'); - return SettingModel.edit(toEdit); + return SettingsModel.edit(firstSetting); }).then(function (edited) { @@ -79,7 +78,7 @@ edited = edited[0]; - edited.attributes.key.should.equal(firstPost.attributes.key); + edited.attributes.key.should.equal(firstSetting.attributes.key); edited.attributes.value.should.equal('new value'); done(); @@ -88,26 +87,25 @@ }); it('can edit multiple', function (done) { - var firstPost, - secondPost, - editedPost, - toEdit = {}; + var model1, + model2, + editedModel; - SettingModel.browse().then(function (results) { + SettingsModel.browse().then(function (results) { should.exist(results); results.length.should.be.above(0); - firstPost = results.models[0]; - secondPost = results.models[1]; + model1 = results.models[0]; + model2 = results.models[1]; // The edit method has been modified to take an object of // key/value pairs - toEdit[firstPost.attributes.key] = "new value1"; - toEdit[secondPost.attributes.key] = "new value2"; + model1.set('value', 'new value1'); + model2.set('value', 'new value2'); - return SettingModel.edit(toEdit); + return SettingsModel.edit([model1, model2]); }).then(function (edited) { @@ -115,15 +113,15 @@ edited.length.should.equal(2); - editedPost = edited[0]; + editedModel = edited[0]; - editedPost.attributes.key.should.equal(firstPost.attributes.key); - editedPost.attributes.value.should.equal('new value1'); + editedModel.attributes.key.should.equal(model1.attributes.key); + editedModel.attributes.value.should.equal('new value1'); - editedPost = edited[1]; + editedModel = edited[1]; - editedPost.attributes.key.should.equal(secondPost.attributes.key); - editedPost.attributes.value.should.equal('new value2'); + editedModel.attributes.key.should.equal(model2.attributes.key); + editedModel.attributes.value.should.equal('new value2'); done(); @@ -136,7 +134,7 @@ value: 'Test Content 1' }; - SettingModel.add(newSetting).then(function (createdSetting) { + SettingsModel.add(newSetting).then(function (createdSetting) { should.exist(createdSetting); @@ -150,7 +148,7 @@ it('can delete', function (done) { var firstSettingId; - SettingModel.browse().then(function (results) { + SettingsModel.browse().then(function (results) { should.exist(results); @@ -158,11 +156,11 @@ firstSettingId = results.models[0].id; - return SettingModel.destroy(firstSettingId); + return SettingsModel.destroy(firstSettingId); }).then(function () { - return SettingModel.browse(); + return SettingsModel.browse(); }).then(function (newResults) {