From 4f2421fac754486f4aad443bfe0c4fe0d3cb8ed6 Mon Sep 17 00:00:00 2001 From: William Dibbern Date: Fri, 16 Aug 2013 09:11:36 -0500 Subject: [PATCH 1/9] MySQL Support Closes #364 - Confirmed integration with local mysql installation works. - Updated fixtures and migration with appropriate schema-conforming values. - Updated schema with appropriate defaults and nullable columns. - Updated fixDates function on model base to appropriately deserialize values coming from SQLite now that dates are stored as actual DateTime objects/ISO strings. - Updated default language to be 'en_US'. --- .gitignore | 1 + config.js | 2 +- core/server/data/fixtures/001.js | 4 +- core/server/data/fixtures/003.js | 13 +++++++ core/server/data/migration/001.js | 12 +++--- core/server/data/migration/003.js | 49 ++++++++++++++++++++++++ core/server/data/migration/index.js | 4 +- core/server/models/base.js | 3 +- core/server/models/post.js | 5 ++- core/shared/lang/{en.json => en_US.json} | 0 core/shared/lang/i18n.js | 8 ++-- package.json | 1 + 12 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 core/server/data/fixtures/003.js create mode 100644 core/server/data/migration/003.js rename core/shared/lang/{en.json => en_US.json} (100%) diff --git a/.gitignore b/.gitignore index 434d54d874..cd668d60b2 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ projectFilesBackup /core/server/data/export/exported* /docs /_site +/core/test/functional/*_test.png # Changelog, which is autogenerated, not committed CHANGELOG.md \ No newline at end of file diff --git a/config.js b/config.js index 7453684cf3..e4a2b67ccb 100644 --- a/config.js +++ b/config.js @@ -6,7 +6,7 @@ var path = require('path'), // ## Admin settings // Default language -config.defaultLang = 'en'; +config.defaultLang = 'en_US'; // Force i18n to be on config.forceI18n = true; diff --git a/core/server/data/fixtures/001.js b/core/server/data/fixtures/001.js index 3452fad213..9c807b511f 100644 --- a/core/server/data/fixtures/001.js +++ b/core/server/data/fixtures/001.js @@ -11,10 +11,10 @@ module.exports = { "meta_title": null, "meta_description": null, "meta_keywords": null, - "featured": null, + "featured": true, "image": null, "status": "published", - "language": null, + "language": "en", "author_id": 1, "created_at": 1373578890610, "created_by": 1, diff --git a/core/server/data/fixtures/003.js b/core/server/data/fixtures/003.js new file mode 100644 index 0000000000..64ee1f721a --- /dev/null +++ b/core/server/data/fixtures/003.js @@ -0,0 +1,13 @@ +var uuid = require('node-uuid'); + +module.exports = { + posts: [], + + settings: [], + + roles: [], + + permissions: [], + + permissions_roles: [] +}; \ No newline at end of file diff --git a/core/server/data/migration/001.js b/core/server/data/migration/001.js index 82f2c7743c..45cda92bb9 100644 --- a/core/server/data/migration/001.js +++ b/core/server/data/migration/001.js @@ -15,13 +15,13 @@ up = function () { t.string('slug'); t.text('content_raw'); t.text('content'); - t.string('meta_title'); - t.string('meta_description'); - t.string('meta_keywords'); - t.bool('featured'); - t.string('image'); + t.string('meta_title').nullable(); + t.string('meta_description').nullable(); + t.string('meta_keywords').nullable(); + t.bool('featured').defaultTo(false); + t.string('image').nullable(); t.string('status'); - t.string('language'); + t.string('language').defaultTo('en'); t.integer('author_id'); t.dateTime('created_at'); t.integer('created_by'); diff --git a/core/server/data/migration/003.js b/core/server/data/migration/003.js new file mode 100644 index 0000000000..a603689cf8 --- /dev/null +++ b/core/server/data/migration/003.js @@ -0,0 +1,49 @@ +var when = require('when'), + _ = require('underscore'), + knex = require('../../models/base').Knex, + migrationVersion = '003', + fixtures = require('../fixtures/' + migrationVersion), + errors = require('../../errorHandling'), + up, + down; + +up = function up() { + + return when.all([ + + knex('posts') + .whereNull('language') + .orWhere('language', 'en') + .update({ + 'language': 'en_US' + }), + + knex('posts') + .whereNull('featured') + .update({ + 'featured': false + }) + + ]).then(function incrementVersion() { + + // Lastly, update the current version settings to reflect this version + return knex('settings') + .where('key', 'currentVersion') + .update({ 'value': migrationVersion }); + + }); +}; + +down = function down() { + + return when.all([ + + // No new tables as of yet, so just return a wrapped value + when(true) + + ]); + +}; + +exports.up = up; +exports.down = down; \ No newline at end of file diff --git a/core/server/data/migration/index.js b/core/server/data/migration/index.js index ee7450a6fa..b77e791929 100644 --- a/core/server/data/migration/index.js +++ b/core/server/data/migration/index.js @@ -4,10 +4,10 @@ var _ = require('underscore'), series = require('when/sequence'), errors = require('../../errorHandling'), knex = require('../../models/base').Knex, - initialVersion = "001", + initialVersion = '001', // This currentVersion string should always be the current version of Ghost, // we could probably load it from the config file. - currentVersion = "002"; + currentVersion = '003'; function getCurrentVersion() { return knex.Schema.hasTable('settings').then(function () { diff --git a/core/server/models/base.js b/core/server/models/base.js index 3a0fb51086..edc52490cb 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -1,5 +1,6 @@ var GhostBookshelf, Bookshelf = require('bookshelf'), + moment = require('moment'), _ = require('underscore'), config = require('../../../config'); @@ -16,7 +17,7 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({ fixDates: function (attrs) { _.each(attrs, function (value, key) { if (key.substr(-3) === '_at' && value !== null) { - attrs[key] = new Date(attrs[key]); + attrs[key] = moment(attrs[key]).toDate(); } }); diff --git a/core/server/models/post.js b/core/server/models/post.js index 4b590e71a3..bcfc4d068b 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -7,6 +7,7 @@ var Post, Showdown = require('showdown'), converter = new Showdown.converter(), User = require('./user').User, + config = require('../../../config'), GhostBookshelf = require('./base'); Post = GhostBookshelf.Model.extend({ @@ -18,8 +19,8 @@ Post = GhostBookshelf.Model.extend({ defaults: function () { return { uuid: uuid.v4(), - status: 'draft' - // TODO: language: ghost.config().defaultLang); + status: 'draft', + language: config.defaultLang }; }, diff --git a/core/shared/lang/en.json b/core/shared/lang/en_US.json similarity index 100% rename from core/shared/lang/en.json rename to core/shared/lang/en_US.json diff --git a/core/shared/lang/i18n.js b/core/shared/lang/i18n.js index b8a29fb21c..86719906a8 100644 --- a/core/shared/lang/i18n.js +++ b/core/shared/lang/i18n.js @@ -14,11 +14,11 @@ I18n = function (ghost) { return function (req, res, next) { - if (lang === 'en') { + if (lang === 'en_US') { // TODO: do stuff here to optimise for en // Make jslint empty block error go away - lang = 'en'; + lang = 'en_US'; } /** TODO: potentially use req.acceptedLanguages rather than the default @@ -26,8 +26,8 @@ I18n = function (ghost) { * TODO: switch this mess to be promise driven */ fs.stat(langFilePath, function (error) { if (error) { - console.log('No language file found for language ' + lang + '. Defaulting to en'); - lang = 'en'; + console.log('No language file found for language ' + lang + '. Defaulting to en_US'); + lang = 'en_US'; } fs.readFile(langFilePath, function (error, data) { diff --git a/package.json b/package.json index 1c62e4aeee..1508594a50 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "underscore": "1.5.1", "showdown": "0.3.1", "sqlite3": "2.1.14", + "mysql": "~2.0.0-alpha8", "bookshelf": "0.2.4", "knex": "0.1.8", "when": "2.2.1", From 72229fa8eaee7aad6942ea4a02756fa586b97fd1 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sat, 14 Sep 2013 19:17:41 +0100 Subject: [PATCH 2/9] Adding the new schema - it doesn't do anything yet issue #632 - shiny new 000 file contains the new much more detailed schema --- core/server/data/migration/000.js | 152 ++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 core/server/data/migration/000.js diff --git a/core/server/data/migration/000.js b/core/server/data/migration/000.js new file mode 100644 index 0000000000..d5b4bf1aab --- /dev/null +++ b/core/server/data/migration/000.js @@ -0,0 +1,152 @@ +var when = require('when'), + knex = require('../../models/base').Knex, + up, + down; + +up = function () { + + return when.all([ + + knex.Schema.createTable('posts', function (t) { + t.increments().primary(); + t.string('uuid', 36).notNull(); + t.string('title', 150).notNull(); + t.string('slug', 150).notNull().unique(); + t.text('markdown', 'medium').nullable(); // max-length 16777215 + t.text('html', 'medium').nullable(); // max-length 16777215 + t.text('image').nullable(); // max-length 2000 + t.bool('featured').notNull().defaultTo(false); + t.bool('page').notNull().defaultTo(false); + t.string('status', 150).notNull().defaultTo('draft'); + t.string('language', 6).notNull().defaultTo('en_US'); + t.string('meta_title', 150).nullable(); + t.string('meta_description', 200).nullable(); + t.integer('author_id').notNull(); + t.dateTime('created_at').notNull(); + t.integer('created_by').notNull(); + t.dateTime('updated_at').nullable(); + t.integer('updated_by').nullable(); + t.dateTime('published_at').nullable(); + t.integer('published_by').nullable(); + }), + + knex.Schema.createTable('users', function (t) { + t.increments().primary(); + t.string('uuid', 36).notNull(); + t.string('name', 150).notNull(); + t.string('slug', 150).notNull().unique(); + t.string('password', 60).notNull(); + t.string('email', 254).notNull().unique(); + t.text('image').nullable(); // max-length 2000 + t.text('cover').nullable(); // max-length 2000 + t.string('bio', 200).nullable(); + t.text('website').nullable(); // max-length 2000 + t.text('location').nullable(); // max-length 65535 + t.text('accessibility').nullable(); // max-length 65535 + t.string('status', 150).notNull().defaultTo('active'); + t.string('language', 6).notNull().defaultTo('en_US'); + t.string('meta_title', 150).nullable(); + t.string('meta_description', 200).nullable(); + t.dateTime('last_login').nullable(); + t.dateTime('created_at').notNull(); + t.integer('created_by').notNull(); + t.dateTime('updated_at').nullable(); + t.integer('updated_by').nullable(); + }), + + knex.Schema.createTable('roles', function (t) { + t.increments().primary(); + t.string('uuid', 36).notNull(); + t.string('name', 150).notNull(); + t.string('description', 200).nullable(); + t.dateTime('created_at').notNull(); + t.integer('created_by').notNull(); + t.dateTime('updated_at').nullable(); + t.integer('updated_by').nullable(); + }), + + knex.Schema.createTable('roles_users', function (t) { + t.increments().primary(); + t.integer('role_id').notNull(); + t.integer('user_id').notNull(); + }), + + knex.Schema.createTable('permissions', function (t) { + t.increments().primary(); + t.string('uuid', 36).notNull(); + t.string('name', 150).notNull(); + t.string('object_type', 150).notNull(); + t.string('action_type', 150).notNull(); + t.integer('object_id').nullable(); + t.dateTime('created_at').notNull(); + t.integer('created_by').notNull(); + t.dateTime('updated_at').nullable(); + t.integer('updated_by').nullable(); + }), + + knex.Schema.createTable('permissions_users', function (t) { + t.increments().primary(); + t.integer('user_id').notNull(); + t.integer('permission_id').notNull(); + }), + + knex.Schema.createTable('permissions_roles', function (t) { + t.increments().primary(); + t.integer('role_id').notNull(); + t.integer('permission_id').notNull(); + }), + + knex.Schema.createTable('settings', function (t) { + t.increments().primary(); + t.string('uuid', 36).notNull(); + t.string('key', 150).notNull().unique(); + t.text('value').nullable(); // max-length 65535 + t.string('type', 150).notNull().defaultTo('core'); + t.dateTime('created_at').notNull(); + t.integer('created_by').notNull(); + t.dateTime('updated_at').nullable(); + t.integer('updated_by').nullable(); + }), + knex.Schema.createTable('tags', function (t) { + t.increments().primary(); + t.string('uuid', 36).notNull(); + t.string('name', 150).notNull(); + t.string('slug', 150).notNull().unique(); + t.string('description', 200).nullable(); + t.integer('parent_id').nullable(); + t.string('meta_title', 150).nullable(); + t.string('meta_description', 200).nullable(); + t.dateTime('created_at').notNull(); + t.integer('created_by').notNull(); + t.dateTime('updated_at').nullable(); + t.integer('updated_by').nullable(); + }), + knex.Schema.createTable('posts_tags', function (t) { + t.increments().primary(); + t.integer('post_id').notNull().unsigned().references('id').inTable('posts'); + t.integer('tag_id').notNull().unsigned().references('id').inTable('tags'); + }) + ]); +}; + +down = function () { + return when.all([ + knex.Schema.dropTableIfExists("posts"), + knex.Schema.dropTableIfExists("users"), + knex.Schema.dropTableIfExists("roles"), + knex.Schema.dropTableIfExists("settings"), + knex.Schema.dropTableIfExists("permissions"), + knex.Schema.dropTableIfExists("tags") + ]).then(function () { + // Drop the relation tables after the model tables + return when.all([ + knex.Schema.dropTableIfExists("roles_users"), + knex.Schema.dropTableIfExists("permissions_users"), + knex.Schema.dropTableIfExists("permissions_roles"), + knex.Schema.dropTableIfExists("posts_tags") + ]); + }); +}; + +exports.up = up; +exports.down = down; \ No newline at end of file From d587a845d45bb05fff34552dfb2181e66df013b8 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Sat, 14 Sep 2013 20:01:46 +0100 Subject: [PATCH 3/9] Set migrations to use new 000 schema issue #632 - removed old schemas - updated base model to reflect all of the consistent behaviours and properties across the models - updated all models to match the new schema TODO - no fixtures are currently loaded except settings - need to rename properties across the codebase --- core/server/data/migration/001.js | 128 ---------------------------- core/server/data/migration/002.js | 83 ------------------ core/server/data/migration/003.js | 49 ----------- core/server/data/migration/index.js | 4 +- core/server/models/base.js | 83 ++++++++++++++++++ core/server/models/permission.js | 15 +--- core/server/models/post.js | 106 +++++------------------ core/server/models/role.js | 15 +--- core/server/models/settings.js | 13 --- core/server/models/tag.js | 28 ++++-- core/server/models/user.js | 74 +++++----------- 11 files changed, 154 insertions(+), 444 deletions(-) delete mode 100644 core/server/data/migration/001.js delete mode 100644 core/server/data/migration/002.js delete mode 100644 core/server/data/migration/003.js diff --git a/core/server/data/migration/001.js b/core/server/data/migration/001.js deleted file mode 100644 index 9823bbebb1..0000000000 --- a/core/server/data/migration/001.js +++ /dev/null @@ -1,128 +0,0 @@ -var when = require('when'), - knex = require('../../models/base').Knex, - fixtures = require('../fixtures/001'), - up, - down; - -up = function () { - - return when.all([ - - knex.Schema.createTable('posts', function (t) { - t.increments().primary(); - t.string('uuid'); - t.string('title'); - t.string('slug'); - t.text('content_raw'); - t.text('content'); - t.string('meta_title').nullable(); - t.string('meta_description').nullable(); - t.string('meta_keywords').nullable(); - t.bool('featured').defaultTo(false); - t.string('image').nullable(); - t.string('status'); - t.string('language').defaultTo('en'); - t.integer('author_id'); - t.dateTime('created_at'); - t.integer('created_by'); - t.dateTime('updated_at').nullable(); - t.integer('updated_by').nullable(); - t.dateTime('published_at').nullable(); - t.integer('published_by').nullable(); - }), - - knex.Schema.createTable('users', function (t) { - t.increments().primary(); - t.string('uuid'); - t.string('full_name'); - t.string('password'); - t.string('email_address'); - t.string('profile_picture'); - t.string('cover_picture'); - t.text('bio'); - t.string('url'); - t.dateTime('created_at'); - t.integer('created_by'); - t.dateTime('updated_at'); - t.integer('updated_by'); - }), - - knex.Schema.createTable('roles', function (t) { - t.increments().primary(); - t.string('name'); - t.string('description'); - }), - - knex.Schema.createTable('roles_users', function (t) { - t.increments().primary(); - t.integer('role_id'); - t.integer('user_id'); - }), - - knex.Schema.createTable('permissions', function (t) { - t.increments().primary(); - t.string('name'); - t.string('object_type'); - t.string('action_type'); - t.integer('object_id'); - }), - - knex.Schema.createTable('permissions_users', function (t) { - t.increments().primary(); - t.integer('user_id'); - t.integer('permission_id'); - }), - - knex.Schema.createTable('permissions_roles', function (t) { - t.increments().primary(); - t.integer('role_id'); - t.integer('permission_id'); - }), - - knex.Schema.createTable('settings', function (t) { - t.increments().primary(); - t.string('uuid'); - t.string('key').unique(); - t.text('value'); - t.string('type'); - t.dateTime('created_at'); - t.integer('created_by'); - t.dateTime('updated_at'); - t.integer('updated_by'); - }) - - // Once we create all of the initial tables, bootstrap any of the data - ]).then(function () { - - return when.all([ - knex('posts').insert(fixtures.posts), - // knex('users').insert(fixtures.users), - knex('roles').insert(fixtures.roles), - // knex('roles_users').insert(fixtures.roles_users), - knex('permissions').insert(fixtures.permissions), - knex('permissions_roles').insert(fixtures.permissions_roles), - knex('settings').insert({ key: 'currentVersion', 'value': '001', type: 'core' }) - ]); - - }); -}; - -down = function () { - return when.all([ - knex.Schema.dropTableIfExists("posts"), - knex.Schema.dropTableIfExists("users"), - knex.Schema.dropTableIfExists("roles"), - knex.Schema.dropTableIfExists("settings"), - knex.Schema.dropTableIfExists("permissions") - ]).then(function () { - // Drop the relation tables after the model tables? - return when.all([ - knex.Schema.dropTableIfExists("roles_users"), - knex.Schema.dropTableIfExists("permissions_users"), - knex.Schema.dropTableIfExists("permissions_roles") - ]); - }); -}; - -exports.up = up; -exports.down = down; \ No newline at end of file diff --git a/core/server/data/migration/002.js b/core/server/data/migration/002.js deleted file mode 100644 index f298cba47f..0000000000 --- a/core/server/data/migration/002.js +++ /dev/null @@ -1,83 +0,0 @@ -var when = require('when'), - knex = require('../../models/base').Knex, - migrationVersion = '002', - fixtures = require('../fixtures/' + migrationVersion), - errors = require('../../errorHandling'), - up, - down; - -up = function () { - - return when.all([ - - knex.Schema.createTable('tags', function (t) { - t.increments().primary(); - t.string('uuid'); - t.string('name'); - t.string('slug'); - t.text('descripton'); - t.integer('parent_id').nullable(); - t.string('meta_title'); - t.text('meta_description'); - t.string('meta_keywords'); - t.dateTime('created_at'); - t.integer('created_by'); - t.dateTime('updated_at').nullable(); - t.integer('updated_by').nullable(); - }), - knex.Schema.createTable('posts_tags', function (t) { - t.increments().primary(); - t.string('uuid'); - t.integer('post_id'); - t.integer('tag_id'); - }), - knex.Schema.createTable('custom_data', function (t) { - t.increments().primary(); - t.string('uuid'); - t.string('name'); - t.string('slug'); - t.text('value'); - t.string('type').defaultTo('html'); - t.string('owner').defaultTo('Ghost'); - t.string('meta_title'); - t.text('meta_description'); - t.string('meta_keywords'); - t.dateTime('created_at'); - t.integer('created_by'); - t.dateTime('updated_at').nullable(); - t.integer('updated_by').nullable(); - }), - knex.Schema.createTable('posts_custom_data', function (t) { - t.increments().primary(); - t.string('uuid'); - t.integer('post_id'); - t.integer('custom_data_id'); - }), - knex.Schema.table('users', function (t) { - t.string('location').after('bio'); - }) - - ]).then(function () { - // Lastly, update the current version settings to reflect this version - return knex('settings') - .where('key', 'currentVersion') - .update({ 'value': migrationVersion }); - }); -}; - -down = function () { - return when.all([ - knex.Schema.dropTableIfExists("tags"), - knex.Schema.dropTableIfExists("custom_data") - ]).then(function () { - // Drop the relation tables after the model tables? - return when.all([ - knex.Schema.dropTableIfExists("posts_tags"), - knex.Schema.dropTableIfExists("posts_custom_data") - ]); - }); - // Should we also drop the currentVersion? -}; - -exports.up = up; -exports.down = down; diff --git a/core/server/data/migration/003.js b/core/server/data/migration/003.js deleted file mode 100644 index a603689cf8..0000000000 --- a/core/server/data/migration/003.js +++ /dev/null @@ -1,49 +0,0 @@ -var when = require('when'), - _ = require('underscore'), - knex = require('../../models/base').Knex, - migrationVersion = '003', - fixtures = require('../fixtures/' + migrationVersion), - errors = require('../../errorHandling'), - up, - down; - -up = function up() { - - return when.all([ - - knex('posts') - .whereNull('language') - .orWhere('language', 'en') - .update({ - 'language': 'en_US' - }), - - knex('posts') - .whereNull('featured') - .update({ - 'featured': false - }) - - ]).then(function incrementVersion() { - - // Lastly, update the current version settings to reflect this version - return knex('settings') - .where('key', 'currentVersion') - .update({ 'value': migrationVersion }); - - }); -}; - -down = function down() { - - return when.all([ - - // No new tables as of yet, so just return a wrapped value - when(true) - - ]); - -}; - -exports.up = up; -exports.down = down; \ No newline at end of file diff --git a/core/server/data/migration/index.js b/core/server/data/migration/index.js index 8db3e8ca98..24b6ceca8c 100644 --- a/core/server/data/migration/index.js +++ b/core/server/data/migration/index.js @@ -4,11 +4,11 @@ var _ = require('underscore'), series = require('when/sequence'), errors = require('../../errorHandling'), knex = require('../../models/base').Knex, - initialVersion = '001', + initialVersion = '000', // This currentVersion string should always be the current version of Ghost, // we could probably load it from the config file. // - Will be possible after default-settings.json restructure - currentVersion = '003'; + currentVersion = '000'; function getCurrentVersion() { return knex.Schema.hasTable('settings').then(function () { diff --git a/core/server/models/base.js b/core/server/models/base.js index 00b99be178..ad073be4f4 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -1,7 +1,9 @@ var GhostBookshelf, Bookshelf = require('bookshelf'), + when = require('when'), moment = require('moment'), _ = require('underscore'), + uuid = require('node-uuid'), config = require('../../../config'), Validator = require('validator').Validator; @@ -15,6 +17,33 @@ GhostBookshelf.validator = new Validator(); // including some convenience functions as static properties on the model. GhostBookshelf.Model = GhostBookshelf.Model.extend({ + hasTimestamps: true, + + defaults: function () { + return { + uuid: uuid.v4() + }; + }, + + initialize: function () { + this.on('creating', this.creating, this); + this.on('saving', this.saving, this); + this.on('saving', this.validate, this); + }, + + creating: function () { + if (!this.get('created_by')) { + this.set('created_by', 1); + } + }, + + saving: function () { + // Remove any properties which don't belong on the post model + this.attributes = this.pick(this.permittedAttributes); + + this.set('updated_by', 1); + }, + // Base prototype properties will go here // Fix problems with dates fixDates: function (attrs) { @@ -46,6 +75,60 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({ }); return attrs; + }, + + // #### generateSlug + // Create a string act as the permalink for an object. + generateSlug: function (Model, base) { + var slug, + slugTryCount = 1, + // Look for a post with a matching slug, append an incrementing number if so + checkIfSlugExists = function (slugToFind) { + return Model.read({slug: slugToFind}).then(function (found) { + var trimSpace; + + if (!found) { + return when.resolve(slugToFind); + } + + slugTryCount += 1; + + // If this is the first time through, add the hyphen + if (slugTryCount === 2) { + slugToFind += '-'; + } else { + // Otherwise, trim the number off the end + trimSpace = -(String(slugTryCount - 1).length); + slugToFind = slugToFind.slice(0, trimSpace); + } + + slugToFind += slugTryCount; + + return checkIfSlugExists(slugToFind); + }); + }; + + // Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"` + slug = base.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '') + // Replace dots and spaces with a dash + .replace(/(\s|\.)/g, '-') + // Convert 2 or more dashes into a single dash + .replace(/-+/g, '-') + // Make the whole thing lowercase + .toLowerCase(); + + // Remove trailing hypen + slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug; + // Check the filtered slug doesn't match any of the reserved keywords + slug = /^(ghost|ghost\-admin|admin|wp\-admin|dashboard|login|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users)$/g + .test(slug) ? slug + '-post' : slug; + + //if slug is empty after trimming use "post" + if (!slug) { + slug = "post"; + } + // Test for duplicate slugs. + return checkIfSlugExists(slug); } }, { diff --git a/core/server/models/permission.js b/core/server/models/permission.js index befe29125f..8fa0688356 100644 --- a/core/server/models/permission.js +++ b/core/server/models/permission.js @@ -5,27 +5,18 @@ var GhostBookshelf = require('./base'), Permissions; Permission = GhostBookshelf.Model.extend({ + tableName: 'permissions', - permittedAttributes: ['id', 'name', 'object_type', 'action_type', 'object_id'], + permittedAttributes: ['id', 'uuid', 'name', 'object_type', 'action_type', 'object_id', 'created_at', 'created_by', + 'updated_at', 'updated_by'], - initialize: function () { - this.on('saving', this.saving, this); - this.on('saving', this.validate, this); - }, validate: function () { // TODO: validate object_type, action_type and object_id GhostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty(); }, - saving: function () { - // Deal with the related data here - - // Remove any properties which don't belong on the post model - this.attributes = this.pick(this.permittedAttributes); - }, - roles: function () { return this.belongsToMany(Role); }, diff --git a/core/server/models/post.js b/core/server/models/post.js index e86e7258c5..73f8212f04 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -18,18 +18,15 @@ Post = GhostBookshelf.Model.extend({ tableName: 'posts', permittedAttributes: [ - 'id', 'uuid', 'title', 'slug', 'content_raw', 'content', 'meta_title', 'meta_description', 'meta_keywords', + 'id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description', 'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at', 'updated_by', 'published_at', 'published_by' ], - hasTimestamps: true, - defaults: function () { return { uuid: uuid.v4(), - status: 'draft', - language: config.defaultLang + status: 'draft' }; }, @@ -47,108 +44,49 @@ Post = GhostBookshelf.Model.extend({ }, saving: function () { - // Deal with the related data here var self = this; // Remove any properties which don't belong on the post model this.attributes = this.pick(this.permittedAttributes); - this.set('content', converter.makeHtml(this.get('content_raw'))); + this.set('html', converter.makeHtml(this.get('markdown'))); this.set('title', this.get('title').trim()); - if (this.hasChanged('slug')) { - // Pass the new slug through the generator to strip illegal characters, detect duplicates - return this.generateSlug(this.get('slug')) - .then(function (slug) { - self.set({slug: slug}); - }); - } - if (this.hasChanged('status') && this.get('status') === 'published') { this.set('published_at', new Date()); // This will need to go elsewhere in the API layer. this.set('published_by', 1); } - this.set('updated_by', 1); + GhostBookshelf.Model.prototype.saving.call(this); - // refactoring of ghost required in order to make these details available here - - }, - - creating: function () { - // set any dynamic default properties - var self = this; - if (!this.get('created_by')) { - this.set('created_by', 1); - } - - if (!this.get('author_id')) { - this.set('author_id', 1); - } - - if (!this.get('slug')) { - // Generating a slug requires a db call to look for conflicting slugs - return this.generateSlug(this.get('title')) + if (this.hasChanged('slug')) { + // Pass the new slug through the generator to strip illegal characters, detect duplicates + return this.generateSlug(Post, this.get('slug')) .then(function (slug) { self.set({slug: slug}); }); } }, - // #### generateSlug - // Create a string act as the permalink for a post. - generateSlug: function (title) { - var slug, - slugTryCount = 1, - // Look for a post with a matching slug, append an incrementing number if so - checkIfSlugExists = function (slugToFind) { - return Post.read({slug: slugToFind}).then(function (found) { - var trimSpace; + creating: function () { + // set any dynamic default properties + var self = this; - if (!found) { - return when.resolve(slugToFind); - } - - slugTryCount += 1; - - // If this is the first time through, add the hyphen - if (slugTryCount === 2) { - slugToFind += '-'; - } else { - // Otherwise, trim the number off the end - trimSpace = -(String(slugTryCount - 1).length); - slugToFind = slugToFind.slice(0, trimSpace); - } - - slugToFind += slugTryCount; - - return checkIfSlugExists(slugToFind); - }); - }; - - // Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"` - slug = title.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '') - // Replace dots and spaces with a dash - .replace(/(\s|\.)/g, '-') - // Convert 2 or more dashes into a single dash - .replace(/-+/g, '-') - // Make the whole thing lowercase - .toLowerCase(); - - // Remove trailing hypen - slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug; - // Check the filtered slug doesn't match any of the reserved keywords - slug = /^(ghost|ghost\-admin|admin|wp\-admin|dashboard|login|archive|archives|category|categories|tag|tags|page|pages|post|posts)$/g - .test(slug) ? slug + '-post' : slug; - - //if slug is empty after trimming use "post" - if (!slug) { - slug = "post"; + if (!this.get('author_id')) { + this.set('author_id', 1); + } + + GhostBookshelf.Model.prototype.creating.call(this); + + if (!this.get('slug')) { + // Generating a slug requires a db call to look for conflicting slugs + return this.generateSlug(Post, this.get('title')) + .then(function (slug) { + self.set({slug: slug}); + }); } - // Test for duplicate slugs. - return checkIfSlugExists(slug); }, updateTags: function (newTags) { diff --git a/core/server/models/role.js b/core/server/models/role.js index 76fa86199c..2e960dc73f 100644 --- a/core/server/models/role.js +++ b/core/server/models/role.js @@ -5,27 +5,16 @@ var User = require('./user').User, Roles; Role = GhostBookshelf.Model.extend({ + tableName: 'roles', - permittedAttributes: ['id', 'name', 'description'], - - initialize: function () { - this.on('saving', this.saving, this); - this.on('saving', this.validate, this); - }, + permittedAttributes: ['id', 'uuid', 'name', 'description', 'created_at', 'created_by', 'updated_at', 'updated_by'], validate: function () { GhostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty(); GhostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty(); }, - saving: function () { - // Deal with the related data here - - // Remove any properties which don't belong on the post model - this.attributes = this.pick(this.permittedAttributes); - }, - users: function () { return this.belongsToMany(User); }, diff --git a/core/server/models/settings.js b/core/server/models/settings.js index 2472def6a7..46fc002ffd 100644 --- a/core/server/models/settings.js +++ b/core/server/models/settings.js @@ -33,8 +33,6 @@ Settings = GhostBookshelf.Model.extend({ tableName: 'settings', - hasTimestamps: true, - permittedAttributes: ['id', 'uuid', 'key', 'value', 'type', 'created_at', 'created_by', 'updated_at', 'update_by'], defaults: function () { @@ -44,10 +42,6 @@ Settings = GhostBookshelf.Model.extend({ }; }, - initialize: function () { - this.on('saving', this.saving, this); - this.on('saving', this.validate, this); - }, // Validate default settings using the validator module. // Each validation's key is a name and its value is an array of options @@ -79,13 +73,6 @@ Settings = GhostBookshelf.Model.extend({ validation[validationName].apply(validation, validationOptions); }, this); } - }, - - saving: function () { - // Deal with the related data here - - // Remove any properties which don't belong on the model - this.attributes = this.pick(this.permittedAttributes); } }, { read: function (_key) { diff --git a/core/server/models/tag.js b/core/server/models/tag.js index 64581ff454..4f20e3af5f 100644 --- a/core/server/models/tag.js +++ b/core/server/models/tag.js @@ -1,18 +1,34 @@ var Tag, Tags, - uuid = require('node-uuid'), Posts = require('./post').Posts, GhostBookshelf = require('./base'); Tag = GhostBookshelf.Model.extend({ + tableName: 'tags', - hasTimestamps: true, + permittedAttributes: [ + 'id', 'uuid', 'name', 'slug', 'description', 'parent_id', 'meta_title', 'meta_description', 'created_at', + 'created_by', 'updated_at', 'updated_by' + ], - defaults: function () { - return { - uuid: uuid.v4() - }; + validate: function () { + + return true; + }, + + creating: function () { + var self = this; + + GhostBookshelf.Model.prototype.creating.call(this); + + if (!this.get('slug')) { + // Generating a slug requires a db call to look for conflicting slugs + return this.generateSlug(Tag, this.get('name')) + .then(function (slug) { + self.set({slug: slug}); + }); + } }, posts: function () { diff --git a/core/server/models/user.js b/core/server/models/user.js index 27cdd1a26e..d30774ee57 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -26,67 +26,33 @@ User = GhostBookshelf.Model.extend({ tableName: 'users', - hasTimestamps: true, - permittedAttributes: [ - 'id', 'uuid', 'full_name', 'password', 'email_address', 'profile_picture', 'cover_picture', 'bio', 'url', 'location', - 'created_at', 'created_by', 'updated_at', 'updated_by' + 'id', 'uuid', 'name', 'slug', 'password', 'email', 'image', 'cover', 'bio', 'website', 'location', + 'accessibility', 'status', 'language', 'meta_title', 'meta_description', 'created_at', 'created_by', + 'updated_at', 'updated_by' ], - defaults: function () { - return { - uuid: uuid.v4() - }; - }, - - parse: function (attrs) { - // temporary alias of name for full_name (will get changed in the schema) - if (attrs.full_name && !attrs.name) { - attrs.name = attrs.full_name; - } - - // temporary alias of website for url (will get changed in the schema) - if (attrs.url && !attrs.website) { - attrs.website = attrs.url; - } - - // temporary alias of email for email_address (will get changed in the schema) - if (attrs.email_address && !attrs.email) { - attrs.email = attrs.email_address; - } - - // temporary alias of image for profile_picture (will get changed in the schema) - if (attrs.profile_picture && !attrs.image) { - attrs.image = attrs.profile_picture; - } - - // temporary alias of cover for cover_picture (will get changed in the schema) - if (attrs.cover_picture && !attrs.cover) { - attrs.cover = attrs.cover_picture; - } - - return attrs; - }, - - initialize: function () { - this.on('saving', this.saving, this); - this.on('saving', this.validate, this); - }, - validate: function () { - GhostBookshelf.validator.check(this.get('email_address'), "Please check your email address. It does not seem to be valid.").isEmail(); + GhostBookshelf.validator.check(this.get('email'), "Please check your email address. It does not seem to be valid.").isEmail(); GhostBookshelf.validator.check(this.get('bio'), "Your bio is too long. Please keep it to 200 chars.").len(0, 200); - if (this.get('url') && this.get('url').length > 0) { - GhostBookshelf.validator.check(this.get('url'), "Your website is not a valid URL.").isUrl(); + if (this.get('website') && this.get('website').length > 0) { + GhostBookshelf.validator.check(this.get('website'), "Your website is not a valid URL.").isUrl(); } return true; }, - saving: function () { - // Deal with the related data here + creating: function () { + var self = this; - // Remove any properties which don't belong on the post model - this.attributes = this.pick(this.permittedAttributes); + GhostBookshelf.Model.prototype.creating.call(this); + + if (!this.get('slug')) { + // Generating a slug requires a db call to look for conflicting slugs + return this.generateSlug(User, this.get('name')) + .then(function (slug) { + self.set({slug: slug}); + }); + } }, posts: function () { @@ -152,7 +118,7 @@ User = GhostBookshelf.Model.extend({ * @author javorszky */ - // return this.forge({email_address: userData.email_address}).fetch().then(function (user) { + // return this.forge({email: userData.email}).fetch().then(function (user) { // if (user !== null) { // return when.reject(new Error('A user with that email address already exists.')); // } @@ -168,7 +134,7 @@ User = GhostBookshelf.Model.extend({ // Finds the user by email, and checks the password check: function (_userdata) { return this.forge({ - email_address: _userdata.email + email: _userdata.email }).fetch({require: true}).then(function (user) { return nodefn.call(bcrypt.compare, _userdata.pw, user.get('password')).then(function (matched) { if (!matched) { @@ -220,7 +186,7 @@ User = GhostBookshelf.Model.extend({ var newPassword = Math.random().toString(36).slice(2, 12), // This is magick user = null; - return this.forge({email_address: email}).fetch({require: true}).then(function (_user) { + return this.forge({email: email}).fetch({require: true}).then(function (_user) { user = _user; return nodefn.call(bcrypt.hash, newPassword, null, null); }).then(function (hash) { From d9684959960b5fbf527a3adbdfbbcdc1e594299e Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 11 Sep 2013 23:04:49 +0100 Subject: [PATCH 4/9] Mass renaming of things Conflicts: core/client/views/settings.js core/server/models/user.js --- content/plugins/FancyFirstChar/index.js | 14 ++--- core/client/tpl/preview.hbs | 4 +- core/client/tpl/settings/user-profile.hbs | 14 ++--- core/client/views/editor.js | 6 +- core/client/views/settings.js | 22 +++---- core/server.js | 4 +- core/server/controllers/admin.js | 4 +- core/server/data/fixtures/001.js | 14 ++--- core/server/data/migration/index.js | 1 + core/server/helpers/index.js | 12 ++-- core/server/models/index.js | 2 +- core/test/functional/admin/03_editor_test.js | 6 +- core/test/functional/base.js | 2 +- core/test/unit/api_posts_spec.js | 30 +++++----- core/test/unit/api_tags_spec.js | 8 +-- core/test/unit/api_users_spec.js | 14 ++--- core/test/unit/permissions_spec.js | 8 +-- core/test/unit/server_helpers_index_spec.js | 60 ++++++++++---------- core/test/unit/testUtils.js | 10 ++-- 19 files changed, 118 insertions(+), 117 deletions(-) diff --git a/content/plugins/FancyFirstChar/index.js b/content/plugins/FancyFirstChar/index.js index ada5004ef2..39b8557bef 100644 --- a/content/plugins/FancyFirstChar/index.js +++ b/content/plugins/FancyFirstChar/index.js @@ -19,11 +19,11 @@ fancifyPlugin = { getIndexOfNextCharacter = function (beginFrom) { var currIndex = beginFrom, nextChar; - + nextChar = originalContent.substr(currIndex, 1); while (_.contains(whiteSpace, nextChar) && currIndex !== originalContent.length) { currIndex += 1; - nextChar = originalContent.substr(currIndex, 1); + nextChar = originalContent.substr(currIndex, 1); } return currIndex; @@ -31,7 +31,7 @@ fancifyPlugin = { getAfterNextClosingTag = function (beginFrom) { return originalContent.indexOf('>', beginFrom) + 1; }; - + // Skip any leading white space until we get a character firstCharIndex = getIndexOfNextCharacter(firstCharIndex); @@ -67,10 +67,10 @@ fancifyPlugin = { if (_.isArray(posts)) { _.each(posts, function (post) { - post.content = self.fancify(post.content); + post.html = self.fancify(post.html); }); - } else if (posts.hasOwnProperty('content')) { - posts.content = this.fancify(posts.content); + } else if (posts.hasOwnProperty('html')) { + posts.html = this.fancify(posts.html); } return posts; @@ -84,7 +84,7 @@ fancifyPlugin = { }, - // Registers the prePostsRender filter to alter the content. + // Registers the prePostsRender filter to alter the html. activate: function (ghost) { ghost.registerFilter('prePostsRender', this.fancifyPosts); }, diff --git a/core/client/tpl/preview.hbs b/core/client/tpl/preview.hbs index 62f449133e..63bb37db1d 100644 --- a/core/client/tpl/preview.hbs +++ b/core/client/tpl/preview.hbs @@ -5,7 +5,7 @@ {{! TODO: JavaScript toggle featured/unfeatured}} {{#if published}}Published{{else}}Written{{/if}} by - {{#if author.name}}{{author.name}}{{else}}{{author.email_address}}{{/if}} + {{#if author.name}}{{author.name}}{{else}}{{author.email}}{{/if}}
@@ -31,5 +31,5 @@
-

{{{title}}}

{{{content}}}
+

{{{title}}}

{{{html}}}
\ No newline at end of file diff --git a/core/client/tpl/settings/user-profile.hbs b/core/client/tpl/settings/user-profile.hbs index 2aa4019cb4..38d67abad4 100644 --- a/core/client/tpl/settings/user-profile.hbs +++ b/core/client/tpl/settings/user-profile.hbs @@ -7,25 +7,25 @@
- - + +
- +

Email will not be publicly displayed. Learn more.

@@ -37,7 +37,7 @@
- +

Have a website or blog other than this one? Link it.

diff --git a/core/client/views/editor.js b/core/client/views/editor.js index eb25bdbe20..f1c27b3b03 100644 --- a/core/client/views/editor.js +++ b/core/client/views/editor.js @@ -199,14 +199,14 @@ }, savePost: function (data) { - // TODO: The content_raw getter here isn't great, shouldn't rely on currentView. + // TODO: The markdown getter here isn't great, shouldn't rely on currentView. _.each(this.model.blacklist, function (item) { this.model.unset(item); }, this); var saved = this.model.save(_.extend({ title: $('#entry-title').val(), - content_raw: Ghost.currentView.editor.getValue() + markdown: Ghost.currentView.editor.getValue() }, data)); // TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone @@ -255,7 +255,7 @@ this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render(); this.$('#entry-title').val(this.model.get('title')); - this.$('#entry-markdown').html(this.model.get('content_raw')); + this.$('#entry-markdown').html(this.model.get('markdown')); this.initMarkdown(); this.renderPreview(); diff --git a/core/client/views/settings.js b/core/client/views/settings.js index b642ca1a69..cbe0269cf4 100644 --- a/core/client/views/settings.js +++ b/core/client/views/settings.js @@ -248,17 +248,17 @@ events: { 'click .button-save': 'saveUser', 'click .button-change-password': 'changePassword', - 'click .js-modal-cover-picture': 'showCoverPicture', - 'click .js-modal-profile-picture': 'showProfilePicture' + 'click .js-modal-cover': 'showCover', + 'click .js-modal-image': 'showImage' }, - showCoverPicture: function () { + showCover: function () { var user = this.model.toJSON(); - this.showUpload('#user-cover-picture', 'cover_picture', user.cover_picture); + this.showUpload('#user-cover', 'cover', user.cover); }, - showProfilePicture: function (e) { + showImage: function (e) { e.preventDefault(); var user = this.model.toJSON(); - this.showUpload('#user-profile-picture', 'profile_picture', user.profile_picture); + this.showUpload('#user-image', 'image', user.image); }, showUpload: function (id, key, src) { var self = this, upload = new Ghost.Models.uploadModal({'id': id, 'key': key, 'src': src, 'accept': { @@ -314,13 +314,13 @@ } else { this.model.save({ - 'full_name': userName, - 'email_address': userEmail, + 'name': userName, + 'email': userEmail, 'location': userLocation, - 'url': userWebsite, + 'website': userWebsite, 'bio': userBio, - 'profile_picture': this.$('#user-profile-picture').attr('src'), - 'cover_picture': this.$('#user-cover-picture').attr('src') + 'image': this.$('#user-image').attr('src'), + 'cover': this.$('#user-cover').attr('src') }, { success: this.saveSuccess, error: this.saveError diff --git a/core/server.js b/core/server.js index 6795d5a9eb..7e60e64f18 100644 --- a/core/server.js +++ b/core/server.js @@ -118,8 +118,8 @@ function ghostLocals(req, res, next) { availableThemes: ghost.paths().availableThemes, availablePlugins: ghost.paths().availablePlugins, currentUser: { - name: currentUser.attributes.full_name, - profile: currentUser.attributes.profile_picture + name: currentUser.attributes.name, + profile: currentUser.attributes.image } }); next(); diff --git a/core/server/controllers/admin.js b/core/server/controllers/admin.js index 8eabae3a46..ed5eec4109 100644 --- a/core/server/controllers/admin.js +++ b/core/server/controllers/admin.js @@ -167,8 +167,8 @@ adminControllers = { password = req.body.password; api.users.add({ - full_name: name, - email_address: email, + name: name, + email: email, password: password }).then(function (user) { diff --git a/core/server/data/fixtures/001.js b/core/server/data/fixtures/001.js index a2d810e9cc..64a29fe43f 100644 --- a/core/server/data/fixtures/001.js +++ b/core/server/data/fixtures/001.js @@ -6,15 +6,15 @@ module.exports = { "uuid": uuid.v4(), "title": "Welcome to Ghost", "slug": "welcome-to-ghost", - "content_raw": "This short guide will teach you how to get Ghost up and running on your computer. It doesn't cover deploying it to a live server, just getting it running on your machine so that you can use it, and develop on top of it.\n\n## Setup Instructions\n\n### Compatibility Notes\n\nGhost uses SQLite which must be built natively on the operating system you intend to run Ghost on. We are working to improve this process, but in the meantime the following OS compatibility notes apply:\n\n* **Linux** - Ghost should install and run with no problems\n* **Mac** - you may require Xcode (free) and the CLI Tools which can be installed from Xcode to get Ghost installed\n* **Windows** - Ghost will and does install and run (really well actually) on Windows, but there are a set of pre-requisites which are tricky to install. Detailed instructions for this are coming very soon.\n\n### Pre-requisites\n\nGhost requires [node][1] 0.10.* or 0.11.* and npm. Download and install from [nodejs.org][1]\n\n### Installing\n\n1. Once you've downloaded one of the release packages, unzip it, and place the directory wherever you would like to run the code\n2. Fire up a terminal (or node command prompt in Windows) and change directory to the root of the Ghost application (where config.js and index.js are)\n3. run `npm install` to install the node dependencies (if you get errors to do with SQLite, please see the compatibility notes)\n4. To start ghost, run `npm start`\n5. Visit [http://localhost:2368/](http://localhost:2368/) in your web browser\n\n## Logging in For The First Time\n\nOnce you have the Ghost server up and running, you should be able to navigate to [http://localhost:2368/ghost](http://localhost:2368/ghost) from a web browser, where you will be prompted for a login.\n\n1. Click on the \"register new user\" link\n2. Enter your user details (careful here: There is no password reset yet!)\n3. Return to the login screen and use those details to log in.\n\n## Finding Your Way Around Ghost\n\nYou should now be logged in and up and running with the very first, very earliest, most historically significant, most prototypal version of the Ghost blogging platform. Click around the dashboard. You will find that most things work, but many things do not. We're still working on those. Keep downloading the new packages as we release them, and you should hopefully see big changes between each version as we go!\n\n [1]: http://nodejs.org/", - "content": "

This short guide will teach you how to get Ghost up and running on your computer. It doesn't cover deploying it to a live server, just getting it running on your machine so that you can use it, and develop on top of it.

\n\n

Setup Instructions

\n\n

Compatibility Notes

\n\n

Ghost uses SQLite which must be built natively on the operating system you intend to run Ghost on. We are working to improve this process, but in the meantime the following OS compatibility notes apply:

\n\n
    \n
  • Linux - Ghost should install and run with no problems
  • \n
  • Mac - you may require Xcode (free) and the CLI Tools which can be installed from Xcode to get Ghost installed
  • \n
  • Windows - Ghost will and does install and run (really well actually) on Windows, but there are a set of pre-requisites which are tricky to install. Detailed instructions for this are coming very soon.
  • \n
\n\n

Pre-requisites

\n\n

Ghost requires node 0.10.* or 0.11.* and npm. Download and install from nodejs.org

\n\n

Installing

\n\n
    \n
  1. Once you've downloaded one of the release packages, unzip it, and place the directory wherever you would like to run the code
  2. \n
  3. Fire up a terminal (or node command prompt in Windows) and change directory to the root of the Ghost application (where config.js and index.js are)
  4. \n
  5. run npm install to install the node dependencies (if you get errors to do with SQLite, please see the compatibility notes)
  6. \n
  7. To start ghost, run npm start
  8. \n
  9. Visit http://localhost:2368/ in your web browser
  10. \n
\n\n

Logging in For The First Time

\n\n

Once you have the Ghost server up and running, you should be able to navigate to http://localhost:2368/ghost from a web browser, where you will be prompted for a login.

\n\n
    \n
  1. Click on the \"register new user\" link
  2. \n
  3. Enter your user details (careful here: There is no password reset yet!)
  4. \n
  5. Return to the login screen and use those details to log in.
  6. \n
\n\n

Finding Your Way Around Ghost

\n\n

You should now be logged in and up and running with the very first, very earliest, most historically significant, most prototypal version of the Ghost blogging platform. Click around the dashboard. You will find that most things work, but many things do not. We're still working on those. Keep downloading the new packages as we release them, and you should hopefully see big changes between each version as we go!

", + "markdown": "This short guide will teach you how to get Ghost up and running on your computer. It doesn't cover deploying it to a live server, just getting it running on your machine so that you can use it, and develop on top of it.\n\n## Setup Instructions\n\n### Compatibility Notes\n\nGhost uses SQLite which must be built natively on the operating system you intend to run Ghost on. We are working to improve this process, but in the meantime the following OS compatibility notes apply:\n\n* **Linux** - Ghost should install and run with no problems\n* **Mac** - you may require Xcode (free) and the CLI Tools which can be installed from Xcode to get Ghost installed\n* **Windows** - Ghost will and does install and run (really well actually) on Windows, but there are a set of pre-requisites which are tricky to install. Detailed instructions for this are coming very soon.\n\n### Pre-requisites\n\nGhost requires [node][1] 0.10.* or 0.11.* and npm. Download and install from [nodejs.org][1]\n\n### Installing\n\n1. Once you've downloaded one of the release packages, unzip it, and place the directory wherever you would like to run the code\n2. Fire up a terminal (or node command prompt in Windows) and change directory to the root of the Ghost application (where config.js and index.js are)\n3. run `npm install` to install the node dependencies (if you get errors to do with SQLite, please see the compatibility notes)\n4. To start ghost, run `npm start`\n5. Visit [http://localhost:2368/](http://localhost:2368/) in your web browser\n\n## Logging in For The First Time\n\nOnce you have the Ghost server up and running, you should be able to navigate to [http://localhost:2368/ghost](http://localhost:2368/ghost) from a web browser, where you will be prompted for a login.\n\n1. Click on the \"register new user\" link\n2. Enter your user details (careful here: There is no password reset yet!)\n3. Return to the login screen and use those details to log in.\n\n## Finding Your Way Around Ghost\n\nYou should now be logged in and up and running with the very first, very earliest, most historically significant, most prototypal version of the Ghost blogging platform. Click around the dashboard. You will find that most things work, but many things do not. We're still working on those. Keep downloading the new packages as we release them, and you should hopefully see big changes between each version as we go!\n\n [1]: http://nodejs.org/", + "html": "

This short guide will teach you how to get Ghost up and running on your computer. It doesn't cover deploying it to a live server, just getting it running on your machine so that you can use it, and develop on top of it.

\n\n

Setup Instructions

\n\n

Compatibility Notes

\n\n

Ghost uses SQLite which must be built natively on the operating system you intend to run Ghost on. We are working to improve this process, but in the meantime the following OS compatibility notes apply:

\n\n
    \n
  • Linux - Ghost should install and run with no problems
  • \n
  • Mac - you may require Xcode (free) and the CLI Tools which can be installed from Xcode to get Ghost installed
  • \n
  • Windows - Ghost will and does install and run (really well actually) on Windows, but there are a set of pre-requisites which are tricky to install. Detailed instructions for this are coming very soon.
  • \n
\n\n

Pre-requisites

\n\n

Ghost requires node 0.10.* or 0.11.* and npm. Download and install from nodejs.org

\n\n

Installing

\n\n
    \n
  1. Once you've downloaded one of the release packages, unzip it, and place the directory wherever you would like to run the code
  2. \n
  3. Fire up a terminal (or node command prompt in Windows) and change directory to the root of the Ghost application (where config.js and index.js are)
  4. \n
  5. run npm install to install the node dependencies (if you get errors to do with SQLite, please see the compatibility notes)
  6. \n
  7. To start ghost, run npm start
  8. \n
  9. Visit http://localhost:2368/ in your web browser
  10. \n
\n\n

Logging in For The First Time

\n\n

Once you have the Ghost server up and running, you should be able to navigate to http://localhost:2368/ghost from a web browser, where you will be prompted for a login.

\n\n
    \n
  1. Click on the \"register new user\" link
  2. \n
  3. Enter your user details (careful here: There is no password reset yet!)
  4. \n
  5. Return to the login screen and use those details to log in.
  6. \n
\n\n

Finding Your Way Around Ghost

\n\n

You should now be logged in and up and running with the very first, very earliest, most historically significant, most prototypal version of the Ghost blogging platform. Click around the dashboard. You will find that most things work, but many things do not. We're still working on those. Keep downloading the new packages as we release them, and you should hopefully see big changes between each version as we go!

", + "image": null, + "featured": false, + "page": false, + "status": "published", + "language": "en_US", "meta_title": null, "meta_description": null, - "meta_keywords": null, - "featured": true, - "image": null, - "status": "published", - "language": "en", "author_id": 1, "created_at": 1373578890610, "created_by": 1, diff --git a/core/server/data/migration/index.js b/core/server/data/migration/index.js index 24b6ceca8c..a220059cb7 100644 --- a/core/server/data/migration/index.js +++ b/core/server/data/migration/index.js @@ -95,6 +95,7 @@ module.exports = { }, migrateDownFromVersion: function (version) { + console.log('version', version); var versions = [], minVersion = this.getVersionBefore(initialVersion), currVersion = version, diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js index a611c79944..26d3c9c6c8 100644 --- a/core/server/helpers/index.js +++ b/core/server/helpers/index.js @@ -44,10 +44,10 @@ coreHelpers = function (ghost) { }); // ### Page URL Helper - // + // // *Usage example:* // `{{pageUrl 2}}` - // + // // Returns the URL for the page specified in the current object // context. // @@ -87,7 +87,7 @@ coreHelpers = function (ghost) { // if the author could not be determined. // ghost.registerThemeHelper('author', function (context, options) { - return this.author ? this.author.full_name : ""; + return this.author ? this.author.name : ""; }); // ### Tags Helper @@ -133,11 +133,11 @@ coreHelpers = function (ghost) { if (truncateOptions.words || truncateOptions.characters) { return new hbs.handlebars.SafeString( - downsize(this.content, truncateOptions) + downsize(this.html, truncateOptions) ); } - return new hbs.handlebars.SafeString(this.content); + return new hbs.handlebars.SafeString(this.html); }); @@ -161,7 +161,7 @@ coreHelpers = function (ghost) { truncateOptions = _.pick(truncateOptions, ["words", "characters"]); /*jslint regexp:true */ - excerpt = String(this.content).replace(/<\/?[^>]+>/gi, ""); + excerpt = String(this.html).replace(/<\/?[^>]+>/gi, ""); /*jslint regexp:false */ if (!truncateOptions.words && !truncateOptions.characters) { diff --git a/core/server/models/index.js b/core/server/models/index.js index 9f6f25b74e..991fe3741f 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -16,7 +16,7 @@ module.exports = { }); }, isPost: function (jsonData) { - return jsonData.hasOwnProperty("content") && jsonData.hasOwnProperty("content_raw") + return jsonData.hasOwnProperty("html") && jsonData.hasOwnProperty("markdown") && jsonData.hasOwnProperty("title") && jsonData.hasOwnProperty("slug"); } }; diff --git a/core/test/functional/admin/03_editor_test.js b/core/test/functional/admin/03_editor_test.js index fa8b0451f2..8e0594d868 100644 --- a/core/test/functional/admin/03_editor_test.js +++ b/core/test/functional/admin/03_editor_test.js @@ -29,7 +29,7 @@ casper.test.begin("Ghost editor is correct", 10, function suite(test) { casper.then(function createTestPost() { casper.sendKeys('#entry-title', testPost.title); - casper.writeContentToCodeMirror(testPost.content); + casper.writeContentToCodeMirror(testPost.html); }); // We must wait after sending keys to CodeMirror @@ -50,7 +50,7 @@ casper.test.begin("Ghost editor is correct", 10, function suite(test) { }, testPost.title, 'Title is correct'); // TODO: make this work - spaces & newlines are problematic - // test.assertTextExists(testPost.content, 'Post content exists'); + // test.assertTextExists(testPost.html, 'Post html exists'); }); casper.run(function () { @@ -149,7 +149,7 @@ casper.test.begin('Title Trimming', function suite(test) { test.assertEvalEquals(function () { return $('#entry-title').val(); - + }, trimmedTitle, 'Entry title should match expected value.'); }); diff --git a/core/test/functional/base.js b/core/test/functional/base.js index e7a0dec100..6a193ee362 100644 --- a/core/test/functional/base.js +++ b/core/test/functional/base.js @@ -42,7 +42,7 @@ var host = casper.cli.options.host || 'localhost', }, testPost = { title: "Bacon ipsum dolor sit amet", - content: "I am a test post.\n#I have some small content" + html: "I am a test post.\n#I have some small content" }; casper.writeContentToCodeMirror = function (content) { diff --git a/core/test/unit/api_posts_spec.js b/core/test/unit/api_posts_spec.js index 165516b789..d1dac21be7 100644 --- a/core/test/unit/api_posts_spec.js +++ b/core/test/unit/api_posts_spec.js @@ -14,8 +14,8 @@ describe('Post Model', function () { UserModel = Models.User, userData = { password: 'testpass1', - email_address: "test@test1.com", - full_name: "Mr Biscuits" + email: "test@test1.com", + name: "Mr Biscuits" }; before(function (done) { @@ -81,8 +81,8 @@ describe('Post Model', function () { firstPost.author.should.be.a("object"); firstPost.user.should.be.a("object"); - firstPost.author.full_name.should.equal("Mr Biscuits"); - firstPost.user.full_name.should.equal("Mr Biscuits"); + firstPost.author.name.should.equal("Mr Biscuits"); + firstPost.user.name.should.equal("Mr Biscuits"); done(); }, done); @@ -97,8 +97,8 @@ describe('Post Model', function () { firstPost.author.should.be.a("object"); firstPost.user.should.be.a("object"); - firstPost.author.full_name.should.equal("Mr Biscuits"); - firstPost.user.full_name.should.equal("Mr Biscuits"); + firstPost.author.name.should.equal("Mr Biscuits"); + firstPost.user.name.should.equal("Mr Biscuits"); done(); }, done); @@ -125,7 +125,7 @@ describe('Post Model', function () { var createdPostUpdatedDate, newPost = { title: 'Test Title 1', - content_raw: 'Test Content 1' + markdown: 'Test Content 1' }; PostModel.add(newPost).then(function (createdPost) { @@ -135,9 +135,9 @@ describe('Post Model', function () { createdPost.has('uuid').should.equal(true); createdPost.get('status').should.equal('draft'); createdPost.get('title').should.equal(newPost.title, "title is correct"); - createdPost.get('content_raw').should.equal(newPost.content_raw, "content_raw is correct"); - createdPost.has('content').should.equal(true); - createdPost.get('content').should.equal('

' + newPost.content_raw + '

'); + createdPost.get('markdown').should.equal(newPost.markdown, "markdown is correct"); + createdPost.has('html').should.equal(true); + createdPost.get('html').should.equal('

' + newPost.markdown + '

'); createdPost.get('slug').should.equal('test-title-1'); createdPost.get('created_at').should.be.below(new Date().getTime()).and.be.above(new Date(0).getTime()); createdPost.get('created_by').should.equal(1); @@ -169,7 +169,7 @@ describe('Post Model', function () { untrimmedUpdateTitle = ' test trimmed update title ', newPost = { title: untrimmedCreateTitle, - content_raw: 'Test Content' + markdown: 'Test Content' }; PostModel.add(newPost).then(function (createdPost) { @@ -189,7 +189,7 @@ describe('Post Model', function () { it('can generate a non conflicting slug', function (done) { var newPost = { title: 'Test Title', - content_raw: 'Test Content 1' + markdown: 'Test Content 1' }; this.timeout(5000); // this is a patch to ensure it doesn't timeout. @@ -199,7 +199,7 @@ describe('Post Model', function () { return function () { return PostModel.add({ title: "Test Title", - content_raw: "Test Content " + (i+1) + markdown: "Test Content " + (i+1) }); }; })).then(function (createdPosts) { @@ -217,7 +217,7 @@ describe('Post Model', function () { } post.get('slug').should.equal('test-title-' + num); - post.get('content_raw').should.equal('Test Content ' + num); + post.get('markdown').should.equal('Test Content ' + num); }); done(); @@ -228,7 +228,7 @@ describe('Post Model', function () { it('can generate slugs without duplicate hyphens', function (done) { var newPost = { title: 'apprehensive titles have too many spaces ', - content_raw: 'Test Content 1' + markdown: 'Test Content 1' }; PostModel.add(newPost).then(function (createdPost) { diff --git a/core/test/unit/api_tags_spec.js b/core/test/unit/api_tags_spec.js index 557433fd3d..a05f74f44e 100644 --- a/core/test/unit/api_tags_spec.js +++ b/core/test/unit/api_tags_spec.js @@ -37,7 +37,7 @@ describe('Tag Model', function () { var PostModel = Models.Post; it('can add a tag', function (done) { - var newPost = {title: 'Test Title 1', content_raw: 'Test Content 1'}, + var newPost = {title: 'Test Title 1', markdown: 'Test Content 1'}, newTag = {name: 'tag1'}, createdPostID; @@ -63,7 +63,7 @@ describe('Tag Model', function () { // The majority of this test is ripped from above, which is obviously a Bad Thing. // Would be nice to find a way to seed data with relations for cases like this, // because there are more DB hits than needed - var newPost = {title: 'Test Title 1', content_raw: 'Test Content 1'}, + var newPost = {title: 'Test Title 1', markdown: 'Test Content 1'}, newTag = {name: 'tag1'}, createdTagID, createdPostID; @@ -97,7 +97,7 @@ describe('Tag Model', function () { function seedTags(tagNames) { var createOperations = [ - PostModel.add({title: 'title', content_raw: 'content'}) + PostModel.add({title: 'title', markdown: 'content'}) ]; var tagModels = tagNames.map(function (tagName) { return TagModel.add({name: tagName}); }); @@ -210,7 +210,7 @@ describe('Tag Model', function () { it('can add a tag to a post on creation', function (done) { - var newPost = {title: 'Test Title 1', content_raw: 'Test Content 1', tags: ['test_tag_1']}; + var newPost = {title: 'Test Title 1', markdown: 'Test Content 1', tags: ['test_tag_1']}; PostModel.add(newPost).then(function (createdPost) { return PostModel.read({id: createdPost.id}, { withRelated: ['tags']}); diff --git a/core/test/unit/api_users_spec.js b/core/test/unit/api_users_spec.js index c909a3100c..13155ae44a 100644 --- a/core/test/unit/api_users_spec.js +++ b/core/test/unit/api_users_spec.js @@ -35,14 +35,14 @@ describe('User Model', function run() { it('can add first', function (done) { var userData = { password: 'testpass1', - email_address: "test@test1.com" + email: "test@test1.com" }; UserModel.add(userData).then(function (createdUser) { should.exist(createdUser); createdUser.has('uuid').should.equal(true); createdUser.attributes.password.should.not.equal(userData.password, "password was hashed"); - createdUser.attributes.email_address.should.eql(userData.email_address, "email address correct"); + createdUser.attributes.email.should.eql(userData.email, "email address correct"); done(); }).then(null, done); @@ -65,7 +65,7 @@ describe('User Model', function run() { it('can\'t add second', function (done) { var userData = { password: 'testpass3', - email_address: "test3@test1.com" + email: "test3@test1.com" }; return testUtils.insertDefaultUser().then(function () { @@ -99,13 +99,13 @@ describe('User Model', function run() { firstUser = results.models[0]; - return UserModel.read({email_address: firstUser.attributes.email_address}); + return UserModel.read({email: firstUser.attributes.email}); }).then(function (found) { should.exist(found); - found.attributes.full_name.should.equal(firstUser.attributes.full_name); + found.attributes.name.should.equal(firstUser.attributes.name); done(); @@ -124,13 +124,13 @@ describe('User Model', function run() { firstUser = results.models[0]; - return UserModel.edit({id: firstUser.id, url: "some.newurl.com"}); + return UserModel.edit({id: firstUser.id, website: "some.newurl.com"}); }).then(function (edited) { should.exist(edited); - edited.attributes.url.should.equal('some.newurl.com'); + edited.attributes.website.should.equal('some.newurl.com'); done(); diff --git a/core/test/unit/permissions_spec.js b/core/test/unit/permissions_spec.js index 33daa60b7b..112d20f4ec 100644 --- a/core/test/unit/permissions_spec.js +++ b/core/test/unit/permissions_spec.js @@ -51,15 +51,15 @@ describe('permissions', function () { ], currTestPermId = 1, // currTestUserId = 1, - // createTestUser = function (email_address) { - // if (!email_address) { + // createTestUser = function (email) { + // if (!email) { // currTestUserId += 1; - // email_address = "test" + currTestPermId + "@test.com"; + // email = "test" + currTestPermId + "@test.com"; // } // var newUser = { // id: currTestUserId, - // email_address: email_address, + // email: email, // password: "testing123" // }; diff --git a/core/test/unit/server_helpers_index_spec.js b/core/test/unit/server_helpers_index_spec.js index 632c77e7e0..aed22c6b44 100644 --- a/core/test/unit/server_helpers_index_spec.js +++ b/core/test/unit/server_helpers_index_spec.js @@ -28,19 +28,19 @@ describe('Core Helpers', function () { }); it('can render content', function () { - var content = "Hello World", - rendered = handlebars.helpers.content.call({content: content}); + var html = "Hello World", + rendered = handlebars.helpers.content.call({html: html}); should.exist(rendered); - rendered.string.should.equal(content); + rendered.string.should.equal(html); }); - it('can truncate content by word', function () { - var content = "

Hello World! It's me!

", + it('can truncate html by word', function () { + var html = "

Hello World! It's me!

", rendered = ( handlebars.helpers.content .call( - {content: content}, + {html: html}, {"hash":{"words": 2}} ) ); @@ -49,12 +49,12 @@ describe('Core Helpers', function () { rendered.string.should.equal("

Hello World

"); }); - it('can truncate content by character', function () { - var content = "

Hello World! It's me!

", + it('can truncate html by character', function () { + var html = "

Hello World! It's me!

", rendered = ( handlebars.helpers.content .call( - {content: content}, + {html: html}, {"hash":{"characters": 8}} ) ); @@ -71,15 +71,15 @@ describe('Core Helpers', function () { }); it("Returns the full name of the author from the context",function() { - var content = {"author":{"full_name":"abc123"}}, - result = handlebars.helpers.author.call(content); + var data = {"author":{"name":"abc123"}}, + result = handlebars.helpers.author.call(data); String(result).should.equal("abc123"); }); it("Returns a blank string where author data is missing",function() { - var content = {"author":null}, - result = handlebars.helpers.author.call(content); + var data = {"author": null}, + result = handlebars.helpers.author.call(data); String(result).should.equal(""); }); @@ -93,33 +93,33 @@ describe('Core Helpers', function () { }); it('can render excerpt', function () { - var content = "Hello World", - rendered = handlebars.helpers.excerpt.call({content: content}); + var html = "Hello World", + rendered = handlebars.helpers.excerpt.call({html: html}); should.exist(rendered); - rendered.string.should.equal(content); + rendered.string.should.equal(html); }); it('does not output HTML', function () { - var content = '

There are
10
types
of people in the world:' + var html = '

There are
10
types
of people in the world:' + '\"c\" those who ' + "understand trinary

, those who don't
and" + "< test > those<<< test >>> who mistake it <for> binary.", expected = "There are 10 types of people in the world: those who understand trinary, those who don't " + "and those>> who mistake it <for> binary.", - rendered = handlebars.helpers.excerpt.call({content: content}); + rendered = handlebars.helpers.excerpt.call({html: html}); should.exist(rendered); rendered.string.should.equal(expected); }); - it('can truncate content by word', function () { - var content = "

Hello World! It's me!

", + it('can truncate html by word', function () { + var html = "

Hello World! It's me!

", expected = "Hello World", rendered = ( handlebars.helpers.excerpt.call( - {content: content}, + {html: html}, {"hash": {"words": 2}} ) ); @@ -128,12 +128,12 @@ describe('Core Helpers', function () { rendered.string.should.equal(expected); }); - it('can truncate content by character', function () { - var content = "

Hello World! It's me!

", + it('can truncate html by character', function () { + var html = "

Hello World! It's me!

", expected = "Hello Wo", rendered = ( handlebars.helpers.excerpt.call( - {content: content}, + {html: html}, {"hash": {"characters": 8}} ) ); @@ -213,7 +213,7 @@ describe('Core Helpers', function () { }); it('should return a the slug with a prefix slash if the context is a post', function () { - var rendered = handlebars.helpers.url.call({content: 'content', content_raw: "ff", title: "title", slug: "slug"}); + var rendered = handlebars.helpers.url.call({html: 'content', markdown: "ff", title: "title", slug: "slug"}); should.exist(rendered); rendered.should.equal('/slug'); }); @@ -224,7 +224,7 @@ describe('Core Helpers', function () { }), rendered = handlebars.helpers.url.call( - {content: 'content', content_raw: "ff", title: "title", slug: "slug"}, + {html: 'content', markdown: "ff", title: "title", slug: "slug"}, {hash: { absolute: 'true'}} ); @@ -235,10 +235,10 @@ describe('Core Helpers', function () { }); it('should return empty string if not a post', function () { - handlebars.helpers.url.call({content_raw: "ff", title: "title", slug: "slug"}).should.equal(''); - handlebars.helpers.url.call({content: 'content', title: "title", slug: "slug"}).should.equal(''); - handlebars.helpers.url.call({content: 'content', content_raw: "ff", slug: "slug"}).should.equal(''); - handlebars.helpers.url.call({content: 'content', content_raw: "ff", title: "title"}).should.equal(''); + handlebars.helpers.url.call({markdown: "ff", title: "title", slug: "slug"}).should.equal(''); + handlebars.helpers.url.call({html: 'content', title: "title", slug: "slug"}).should.equal(''); + handlebars.helpers.url.call({html: 'content', markdown: "ff", slug: "slug"}).should.equal(''); + handlebars.helpers.url.call({html: 'content', markdown: "ff", title: "title"}).should.equal(''); }); }); diff --git a/core/test/unit/testUtils.js b/core/test/unit/testUtils.js index 5c04acc983..68132c8298 100644 --- a/core/test/unit/testUtils.js +++ b/core/test/unit/testUtils.js @@ -11,8 +11,8 @@ samplePost = function (i, status, lang) { return { title: "Test Post " + i, slug: "ghost-from-fiction-to-function-" + i, - content_raw: "Three days ago I released a concept page<\/a> for a lite version of WordPress that I've been thinking about for a long time, called Ghost. I think it's fair to say that I didn't quite anticipate how strong the reaction would be - and I've hardly had time to catch my breath in the last 72 hours.\n\nThe response was overwhelming, and overwhelmingly positive. In the first 6 hours my site got 35,000 page views after hitting the number 1 slot on Hacker News<\/a>. As of right now, the traffic count is just over 91,000 page views<\/a> - and Ghost has been featured all over the place. Notable mentions so far include Christina Warren from Mashable, who wrote about it<\/a>. Michael Carney from PandoDaily interviewed me about it<\/a>. Someone even wrote about it in Chinese<\/a>. That's pretty cool.\n\n\nThe feedback has been amazing, and while it's impossible to reply to all of the messages individually, I'm getting to as many of them as I can and I want to thank each and every one of you who took the time to send me a message or share the concept because you liked it. Now that the initial storm has died down a bit, I wanted to take some time to answer some of the more common questions and talk about what's next.\n

FAQ - Continued...<\/h2>\n\nThe most common question, bizarrely:\n

Oh my god, why is that whole page made of images? What's wrong with you? \/\/ I can't take you seriously \/\/ Don't you know anything about the web? \/\/ You are literally Satan re-incarnate.<\/strong><\/em><\/h5>\n\nThis was really the only negativity I got in response to the post, and it surprised me. I put together the concept page as... just that... a concept. It was a way for me to get the ideas out of my head and \"down on paper\" - or so to speak. I used photoshop as a tool<\/em> to write down my idea with text and images. If I used a sketchbook as a tool <\/em>to create images and handwritten notes, then uploaded scans of it, I doubt anyone would complain. The concept page was never supposed to be a finished product because I had no idea if there would be any interest in it. I had no motivation to waste hours coding a custom layout for something might only ever be read by a few people and then forgotten.\n\nHardware manufacturers make hundreds of foam cutout prototypes of products before they build one with working buttons and screens. I'm aware of all the usability problems with a web page made of images, and equally, foam cutouts without buttons or screens aren't particularly user friendly either. They're not supposed to be.\n\nLet's move on.\n
What? Why no comments? I need comments.<\/strong><\/em><\/h5>\n\nBecause comments add a layer of complexity that is beyond the core focus of this platform, which is publishing. Again, that's not to say you couldn't have any comments. This could easily be added with a dedicated plugin where you own the data or (as mentioned) there are third party providers such as Disqus, IntenseDebate, Livefyre and Facebook who all have great platforms. The point of this isn't to say \"you can't have comments\" - it's to say \"comments aren't on by default\". It's about simplicity, more than anything else.\n
Yeah, but WordPress are already going to revise their dashboard, WordPress.com is experimenting with a potential simplified version... so why bother with this?<\/strong><\/em><\/h5>\n\n\"\"<\/a>\n\nSorry, but Tumblr already did this - it's not the future of blogging, it's the past.\n\nGhost isn't about sharing \"Fuck Yeah [Dogs<\/a>\/Sharks<\/a>\/Girls with Tattoos<\/a>]\" - it's about publishing - which means writing - rather than mashing a few buttons to make sure that everyone can see and appreciate your latest funny picture\/status, which is surely the most funny picture\/status you've ever posted.\n\nTumblr, Pinterest and Facebook already have this locked down. It's not the future.\n
So... are you actually going to build this thing?<\/strong><\/em><\/h5>\n\nThe concept page was a way for me to test demand and interest. To see if anyone actually agreed with my frustrations and, more importantly, my solutions. I plucked a random figure of \"10,000 pageviews\" out of the air before I hit the publish button. If it got less than 10,000 pageviews, I would surrender to the fact that it would only ever be an idea. I've now exceeded that goal 9 times over, so yes, I'm looking at how Ghost can now be made into a reality.\n
How can I find out when it's done? \/\/ SHUT UP AND TAKE MY MONEY<\/strong><\/em><\/h5>\n\nOk, ok - there's a holding page up on http:\/\/TryGhost.org<\/a> - put your email address in.\n
\n

How are you going to do this?<\/h3>\n\nThere's three main ways of going about this, each has merits as well as drawbacks.\n\n1.) Build it from scratch<\/strong><\/em> - Many people (particularly the Hacker News crowd) expressed the sentiment that there was little point in forking WordPress. When you're going to strip out so much, you get to a point where you might as well start from scratch anyway. Take away the crutches of being stuck with older technologies and put together something which is as sophisticated in code as it is in UI\/UX.\n