Merge branch '0.3.1-wip'

Conflicts:
	core/server/controllers/admin.js
This commit is contained in:
Hannah Wolfe 2013-09-27 17:22:55 +01:00
commit 6bd62538af
62 changed files with 946 additions and 844 deletions

View File

@ -1,13 +1,13 @@
var path = require('path'), var path = require('path'),
when = require('when'), when = require('when'),
semver = require("semver"), semver = require('semver'),
fs = require("fs"), fs = require('fs'),
path = require("path"), path = require('path'),
_ = require('underscore'), _ = require('underscore'),
spawn = require("child_process").spawn, spawn = require("child_process").spawn,
buildDirectory = path.resolve(process.cwd(), '.build'), buildDirectory = path.resolve(process.cwd(), '.build'),
distDirectory = path.resolve(process.cwd(), '.dist'), distDirectory = path.resolve(process.cwd(), '.dist'),
configLoader = require('./core/config-loader.js'), configLoader = require('./core/config-loader.js'),
buildGlob = [ buildGlob = [
'**', '**',
@ -94,7 +94,7 @@ var path = require('path'),
// Start our server in development // Start our server in development
express: { express: {
options: { options: {
script: "index.js" script: 'index.js'
}, },
dev: { dev: {
@ -136,9 +136,9 @@ var path = require('path'),
}, },
files: { files: {
src: [ src: [
"*.js", '*.js',
"core/*.js", 'core/*.js',
"core/server/**/*.js" 'core/server/**/*.js'
] ]
} }
}, },
@ -156,11 +156,11 @@ var path = require('path'),
unparam: true unparam: true
}, },
files: { files: {
src: "core/client/**/*.js" src: 'core/client/**/*.js'
}, },
exclude: [ exclude: [
"core/client/assets/**/*.js", 'core/client/assets/**/*.js',
"core/client/tpl/**/*.js" 'core/client/tpl/**/*.js'
] ]
}, },
shared: { shared: {
@ -180,19 +180,19 @@ var path = require('path'),
}, },
files: { files: {
src: [ src: [
"core/shared/**/*.js" 'core/shared/**/*.js'
] ]
}, },
exclude: [ exclude: [
"core/shared/vendor/**/*.js" 'core/shared/vendor/**/*.js'
] ]
} }
}, },
mochacli: { mochacli: {
options: { options: {
ui: "bdd", ui: 'bdd',
reporter: "spec" reporter: 'spec'
}, },
all: { all: {
@ -245,14 +245,14 @@ var path = require('path'),
handlebars: { handlebars: {
core: { core: {
options: { options: {
namespace: "JST", namespace: 'JST',
processName: function (filename) { processName: function (filename) {
filename = filename.replace('core/client/tpl/', ''); filename = filename.replace('core/client/tpl/', '');
return filename.replace('.hbs', ''); return filename.replace('.hbs', '');
} }
}, },
files: { files: {
"core/client/tpl/hbs-tpl.js": "core/client/tpl/**/*.hbs" 'core/client/tpl/hbs-tpl.js': 'core/client/tpl/**/*.hbs'
} }
} }
}, },
@ -260,19 +260,19 @@ var path = require('path'),
groc: { groc: {
docs: { docs: {
options: { options: {
"out": "./docs/", 'out': './docs/',
"glob": [ 'glob': [
"README.md", 'README.md',
"config.example.js", 'config.example.js',
"index.js", 'index.js',
"core/*.js", 'core/*.js',
"core/server/**/*.js", 'core/server/**/*.js',
"core/shared/**/*.js", 'core/shared/**/*.js',
"core/client/**/*.js" 'core/client/**/*.js'
], ],
"except": [ 'except': [
"!core/**/vendor/**/*.js", '!core/**/vendor/**/*.js',
"!core/client/tpl/**/*.js" '!core/client/tpl/**/*.js'
] ]
} }
} }
@ -280,7 +280,7 @@ var path = require('path'),
clean: { clean: {
build: { build: {
src: ["<%= paths.buildBuild %>/**"] src: ['<%= paths.buildBuild %>/**']
} }
}, },
@ -289,21 +289,21 @@ var path = require('path'),
files: [{ files: [{
expand: true, expand: true,
src: buildGlob, src: buildGlob,
dest: "<%= paths.nightlyBuild %>/<%= pkg.version %>/" dest: '<%= paths.nightlyBuild %>/<%= pkg.version %>/'
}] }]
}, },
weekly: { weekly: {
files: [{ files: [{
expand: true, expand: true,
src: buildGlob, src: buildGlob,
dest: "<%= paths.weeklyBuild %>/<%= pkg.version %>/" dest: '<%= paths.weeklyBuild %>/<%= pkg.version %>/'
}] }]
}, },
build: { build: {
files: [{ files: [{
expand: true, expand: true,
src: buildGlob, src: buildGlob,
dest: "<%= paths.buildBuild %>/" dest: '<%= paths.buildBuild %>/'
}] }]
} }
}, },
@ -311,27 +311,27 @@ var path = require('path'),
compress: { compress: {
nightly: { nightly: {
options: { options: {
archive: "<%= paths.nightlyDist %>/Ghost-Nightly-<%= pkg.version %>.zip" archive: '<%= paths.nightlyDist %>/Ghost-Nightly-<%= pkg.version %>.zip'
}, },
expand: true, expand: true,
cwd: "<%= paths.nightlyBuild %>/<%= pkg.version %>/", cwd: '<%= paths.nightlyBuild %>/<%= pkg.version %>/',
src: ["**"] src: ['**']
}, },
weekly: { weekly: {
options: { options: {
archive: "<%= paths.weeklyDist %>/Ghost-Weekly-<%= pkg.version %>.zip" archive: '<%= paths.weeklyDist %>/Ghost-Weekly-<%= pkg.version %>.zip'
}, },
expand: true, expand: true,
cwd: "<%= paths.weeklyBuild %>/<%= pkg.version %>/", cwd: '<%= paths.weeklyBuild %>/<%= pkg.version %>/',
src: ["**"] src: ['**']
}, },
build: { build: {
options: { options: {
archive: "<%= paths.buildDist %>/Ghost-Build.zip" archive: '<%= paths.buildDist %>/Ghost-Build.zip'
}, },
expand: true, expand: true,
cwd: "<%= paths.buildBuild %>/", cwd: '<%= paths.buildBuild %>/',
src: ["**"] src: ['**']
} }
}, },
@ -340,113 +340,113 @@ var path = require('path'),
tagName: '%VERSION%', tagName: '%VERSION%',
commitMessage: '<%= buildType %> Release %VERSION%', commitMessage: '<%= buildType %> Release %VERSION%',
tagMessage: '<%= buildType %> Release %VERSION%', tagMessage: '<%= buildType %> Release %VERSION%',
pushTo: "origin build" pushTo: 'origin build'
} }
}, },
concat: { concat: {
dev: { dev: {
files: { files: {
"core/built/scripts/vendor.js": [ 'core/built/scripts/vendor.js': [
"core/shared/vendor/jquery/jquery.js", 'core/shared/vendor/jquery/jquery.js',
"core/shared/vendor/jquery/jquery-ui-1.10.3.custom.min.js", 'core/shared/vendor/jquery/jquery-ui-1.10.3.custom.min.js',
"core/client/assets/lib/jquery-utils.js", 'core/client/assets/lib/jquery-utils.js',
"core/client/assets/lib/uploader.js", 'core/client/assets/lib/uploader.js',
"core/shared/vendor/underscore.js", 'core/shared/vendor/underscore.js',
"core/shared/vendor/backbone/backbone.js", 'core/shared/vendor/backbone/backbone.js',
"core/shared/vendor/handlebars/handlebars-runtime.js", 'core/shared/vendor/handlebars/handlebars-runtime.js',
"core/shared/vendor/moment.js", 'core/shared/vendor/moment.js',
"core/client/assets/vendor/icheck/jquery.icheck.min.js", 'core/client/assets/vendor/icheck/jquery.icheck.min.js',
"core/shared/vendor/jquery/jquery.ui.widget.js", 'core/shared/vendor/jquery/jquery.ui.widget.js',
"core/shared/vendor/jquery/jquery.iframe-transport.js", 'core/shared/vendor/jquery/jquery.iframe-transport.js',
"core/shared/vendor/jquery/jquery.fileupload.js", 'core/shared/vendor/jquery/jquery.fileupload.js',
"core/client/assets/vendor/codemirror/codemirror.js", 'core/client/assets/vendor/codemirror/codemirror.js',
"core/client/assets/vendor/codemirror/addon/mode/overlay.js", 'core/client/assets/vendor/codemirror/addon/mode/overlay.js',
"core/client/assets/vendor/codemirror/mode/markdown/markdown.js", 'core/client/assets/vendor/codemirror/mode/markdown/markdown.js',
"core/client/assets/vendor/codemirror/mode/gfm/gfm.js", 'core/client/assets/vendor/codemirror/mode/gfm/gfm.js',
"core/client/assets/vendor/showdown/showdown.js", 'core/client/assets/vendor/showdown/showdown.js',
"core/client/assets/vendor/showdown/extensions/ghostdown.js", 'core/client/assets/vendor/showdown/extensions/ghostdown.js',
"core/shared/vendor/showdown/extensions/github.js", 'core/shared/vendor/showdown/extensions/github.js',
"core/client/assets/vendor/shortcuts.js", 'core/client/assets/vendor/shortcuts.js',
"core/client/assets/vendor/validator-client.js", 'core/client/assets/vendor/validator-client.js',
"core/client/assets/vendor/countable.js", 'core/client/assets/vendor/countable.js',
"core/client/assets/vendor/to-title-case.js", 'core/client/assets/vendor/to-title-case.js',
"core/client/assets/vendor/packery.pkgd.min.js", 'core/client/assets/vendor/packery.pkgd.min.js',
"core/client/assets/vendor/fastclick.js" 'core/client/assets/vendor/fastclick.js'
], ],
"core/built/scripts/helpers.js": [ 'core/built/scripts/helpers.js': [
"core/client/init.js", 'core/client/init.js',
"core/client/mobile-interactions.js", 'core/client/mobile-interactions.js',
"core/client/toggle.js", 'core/client/toggle.js',
"core/client/markdown-actions.js", 'core/client/markdown-actions.js',
"core/client/helpers/index.js" 'core/client/helpers/index.js'
], ],
"core/built/scripts/templates.js": [ 'core/built/scripts/templates.js': [
"core/client/tpl/hbs-tpl.js" 'core/client/tpl/hbs-tpl.js'
], ],
"core/built/scripts/models.js": [ 'core/built/scripts/models.js': [
"core/client/models/**/*.js" 'core/client/models/**/*.js'
], ],
"core/built/scripts/views.js": [ 'core/built/scripts/views.js': [
"core/client/views/**/*.js", 'core/client/views/**/*.js',
"core/client/router.js" 'core/client/router.js'
] ]
} }
}, },
prod: { prod: {
files: { files: {
"core/built/scripts/ghost.js": [ 'core/built/scripts/ghost.js': [
"core/shared/vendor/jquery/jquery.js", 'core/shared/vendor/jquery/jquery.js',
"core/shared/vendor/jquery/jquery-ui-1.10.3.custom.min.js", 'core/shared/vendor/jquery/jquery-ui-1.10.3.custom.min.js',
"core/client/assets/lib/jquery-utils.js", 'core/client/assets/lib/jquery-utils.js',
"core/client/assets/lib/uploader.js", 'core/client/assets/lib/uploader.js',
"core/shared/vendor/underscore.js", 'core/shared/vendor/underscore.js',
"core/shared/vendor/backbone/backbone.js", 'core/shared/vendor/backbone/backbone.js',
"core/shared/vendor/handlebars/handlebars-runtime.js", 'core/shared/vendor/handlebars/handlebars-runtime.js',
"core/shared/vendor/moment.js", 'core/shared/vendor/moment.js',
"core/client/assets/vendor/icheck/jquery.icheck.min.js", 'core/client/assets/vendor/icheck/jquery.icheck.min.js',
"core/shared/vendor/jquery/jquery.ui.widget.js", 'core/shared/vendor/jquery/jquery.ui.widget.js',
"core/shared/vendor/jquery/jquery.iframe-transport.js", 'core/shared/vendor/jquery/jquery.iframe-transport.js',
"core/shared/vendor/jquery/jquery.fileupload.js", 'core/shared/vendor/jquery/jquery.fileupload.js',
"core/client/assets/vendor/codemirror/codemirror.js", 'core/client/assets/vendor/codemirror/codemirror.js',
"core/client/assets/vendor/codemirror/addon/mode/overlay.js", 'core/client/assets/vendor/codemirror/addon/mode/overlay.js',
"core/client/assets/vendor/codemirror/mode/markdown/markdown.js", 'core/client/assets/vendor/codemirror/mode/markdown/markdown.js',
"core/client/assets/vendor/codemirror/mode/gfm/gfm.js", 'core/client/assets/vendor/codemirror/mode/gfm/gfm.js',
"core/client/assets/vendor/showdown/showdown.js", 'core/client/assets/vendor/showdown/showdown.js',
"core/client/assets/vendor/showdown/extensions/ghostdown.js", 'core/client/assets/vendor/showdown/extensions/ghostdown.js',
"core/shared/vendor/showdown/extensions/github.js", 'core/shared/vendor/showdown/extensions/github.js',
"core/client/assets/vendor/shortcuts.js", 'core/client/assets/vendor/shortcuts.js',
"core/client/assets/vendor/validator-client.js", 'core/client/assets/vendor/validator-client.js',
"core/client/assets/vendor/countable.js", 'core/client/assets/vendor/countable.js',
"core/client/assets/vendor/to-title-case.js", 'core/client/assets/vendor/to-title-case.js',
"core/client/assets/vendor/packery.pkgd.min.js", 'core/client/assets/vendor/packery.pkgd.min.js',
"core/client/assets/vendor/fastclick.js", 'core/client/assets/vendor/fastclick.js',
"core/client/init.js", 'core/client/init.js',
"core/client/mobile-interactions.js", 'core/client/mobile-interactions.js',
"core/client/toggle.js", 'core/client/toggle.js',
"core/client/markdown-actions.js", 'core/client/markdown-actions.js',
"core/client/helpers/index.js", 'core/client/helpers/index.js',
"core/client/tpl/hbs-tpl.js", 'core/client/tpl/hbs-tpl.js',
"core/client/models/**/*.js", 'core/client/models/**/*.js',
"core/client/views/**/*.js", 'core/client/views/**/*.js',
"core/client/router.js" 'core/client/router.js'
] ]
} }
} }
@ -455,7 +455,7 @@ var path = require('path'),
uglify: { uglify: {
prod: { prod: {
files: { files: {
"core/built/scripts/ghost.min.js": "core/built/scripts/ghost.js" 'core/built/scripts/ghost.min.js': 'core/built/scripts/ghost.js'
} }
} }
} }
@ -526,17 +526,17 @@ var path = require('path'),
depth = depth || 0; depth = depth || 0;
if (!depth) { if (!depth) {
grunt.log.writeln("git " + args.join(" ")); grunt.log.writeln('git ' + args.join(' '));
} }
var buffer = []; var buffer = [];
spawn("git", args, { spawn('git', args, {
// We can reasonably assume the gruntfile will be in the root of the repo. // We can reasonably assume the gruntfile will be in the root of the repo.
cwd : __dirname, cwd : __dirname,
stdio : ["ignore", "pipe", process.stderr] stdio : ['ignore', 'pipe', process.stderr]
}).on("exit", function (code) { }).on('exit', function (code) {
// Process exited correctly but we got no output. // Process exited correctly but we got no output.
// Spawn again, but make sure we don't spiral out of control. // Spawn again, but make sure we don't spiral out of control.
@ -552,7 +552,7 @@ var path = require('path'),
} }
if (code === 0) { if (code === 0) {
return callback(buffer.join("")); return callback(buffer.join(''));
} }
// We failed. Git returned a non-standard exit code. // We failed. Git returned a non-standard exit code.
@ -560,7 +560,7 @@ var path = require('path'),
done(false); done(false);
// Push returned data into the buffer // Push returned data into the buffer
}).stdout.on("data", buffer.push.bind(buffer)); }).stdout.on('data', buffer.push.bind(buffer));
} }
// Crazy function for getting around inconsistencies in tagging // Crazy function for getting around inconsistencies in tagging
@ -575,14 +575,14 @@ var path = require('path'),
// into sort directly. Could be something to think about // into sort directly. Could be something to think about
// in future. // in future.
if (semver.rcompare(a, "0.2.0") < 0 || if (semver.rcompare(a, '0.2.0') < 0 ||
semver.rcompare(b, "0.2.0") < 0) { semver.rcompare(b, '0.2.0') < 0) {
return semver.rcompare(a, b); return semver.rcompare(a, b);
} }
a = a.split("-"); a = a.split('-');
b = b.split("-"); b = b.split('-');
if (semver.rcompare(a[0], b[0]) !== 0) { if (semver.rcompare(a[0], b[0]) !== 0) {
return semver.rcompare(a[0], b[0]); return semver.rcompare(a[0], b[0]);
@ -597,7 +597,7 @@ var path = require('path'),
// Gets tags in master branch, sorts them with semver, // Gets tags in master branch, sorts them with semver,
function getTags(callback) { function getTags(callback) {
git(["show-ref", "--tags"], function (results) { git(['show-ref', '--tags'], function (results) {
results = results results = results
.split(/\n+/) .split(/\n+/)
.filter(function (tag) { .filter(function (tag) {
@ -605,8 +605,8 @@ var path = require('path'),
}) })
.map(function (tag) { .map(function (tag) {
return { return {
"tag": tag.split(/tags\//).pop().trim(), 'tag': tag.split(/tags\//).pop().trim(),
"ref": tag.split(/\s+/).shift().trim() 'ref': tag.split(/\s+/).shift().trim()
}; };
}) })
.sort(sortTags); .sort(sortTags);
@ -620,11 +620,11 @@ var path = require('path'),
var commits = [], var commits = [],
commitRegex = commitRegex =
new RegExp( new RegExp(
"\\n*[|\\*\\s]*commit\\s+([a-f0-9]+)" + '\\n*[|\\*\\s]*commit\\s+([a-f0-9]+)' +
"\\n[|\\*\\s]*Author:\\s+([^<\\n]+)<([^>\\n]+)>" + '\\n[|\\*\\s]*Author:\\s+([^<\\n]+)<([^>\\n]+)>' +
"\\n[|\\*\\s]*Date:\\s+([^\\n]+)" + '\\n[|\\*\\s]*Date:\\s+([^\\n]+)' +
"\\n+[|\\*\\s]*[ ]{4}([^\\n]+)", '\\n+[|\\*\\s]*[ ]{4}([^\\n]+)',
"ig" 'ig'
); );
// Using String.prototype.replace as a kind of poor-man's substitute // Using String.prototype.replace as a kind of poor-man's substitute
@ -641,16 +641,16 @@ var path = require('path'),
date = date =
date.replace( date.replace(
/^(\w+)\s(\w+)\s(\d+)\s([\d\:]+)\s(\d+)\s([\+\-\d]+)$/, /^(\w+)\s(\w+)\s(\d+)\s([\d\:]+)\s(\d+)\s([\+\-\d]+)$/,
"$1, $2 $3 $5 $4 $6" '$1, $2 $3 $5 $4 $6'
); );
commits.push({ commits.push({
"hash": hash, 'hash': hash,
"author": author, 'author': author,
"email": email, 'email': email,
"date": date, 'date': date,
"parsedDate": new Date(Date.parse(date)), 'parsedDate': new Date(Date.parse(date)),
"message": message 'message': message
}); });
return null; return null;
@ -662,8 +662,8 @@ var path = require('path'),
// Gets git log for specified range. // Gets git log for specified range.
function getLog(to, from, callback) { function getLog(to, from, callback) {
var range = from && to ? from + ".." + to : "", var range = from && to ? from + '..' + to : '',
args = [ "log", "master", "--no-color", "--no-merges", "--graph" ]; args = [ 'log', 'master', '--no-color', '--no-merges', '--graph' ];
if (range) { if (range) {
args.push(range); args.push(range);
@ -676,12 +676,12 @@ var path = require('path'),
// Run the job // Run the job
getTags(function (tags) { getTags(function (tags) {
var logPath = path.join(__dirname, "CHANGELOG.md"), var logPath = path.join(__dirname, 'CHANGELOG.md'),
log = fs.createWriteStream(logPath), log = fs.createWriteStream(logPath),
commitCache = {}; commitCache = {};
function processTag(tag, callback) { function processTag(tag, callback) {
var buffer = "", var buffer = '',
peek = tag[1]; peek = tag[1];
tag = tag[0]; tag = tag[0];
@ -699,7 +699,7 @@ var path = require('path'),
return callback(""); return callback("");
} }
buffer += "## Release " + tag.tag + "\n"; buffer += '## Release ' + tag.tag + '\n';
commits = commits commits = commits
.filter(function (commit) { .filter(function (commit) {
@ -707,12 +707,12 @@ var path = require('path'),
// Get rid of jenkins' release tagging commits // Get rid of jenkins' release tagging commits
// Remove commits we've already spat out // Remove commits we've already spat out
return ( return (
commit.author !== "TryGhost-Jenkins" && commit.author !== 'TryGhost-Jenkins' &&
!commitCache[commit.hash] !commitCache[commit.hash]
); );
}) })
.map(function (commit) { .map(function (commit) {
buffer += "\n* " + commit.message + " (_" + commit.author + "_)"; buffer += '\n* ' + commit.message + ' (_' + commit.author + '_)';
commitCache[commit.hash] = true; commitCache[commit.hash] = true;
}); });
@ -720,12 +720,12 @@ var path = require('path'),
buffer += "\nNo changes were made in this build.\n"; buffer += "\nNo changes were made in this build.\n";
} }
callback(buffer + "\n"); callback(buffer + '\n');
}); });
} }
// Get two weeks' worth of tags // Get two weeks' worth of tags
tags.unshift({"tag": "HEAD"}); tags.unshift({'tag': 'HEAD'});
tags = tags =
tags tags
@ -737,17 +737,17 @@ var path = require('path'),
]; ];
}); });
log.write("# Ghost Changelog\n\n"); log.write('# Ghost Changelog\n\n');
log.write("_Showing " + tags.length + " releases._\n"); log.write('_Showing ' + tags.length + ' releases._\n');
when.reduce(tags, when.reduce(tags,
function (prev, tag, idx) { function (prev, tag, idx) {
return when.promise(function (resolve) { return when.promise(function (resolve) {
processTag(tag, function (releaseData) { processTag(tag, function (releaseData) {
resolve(prev + "\n" + releaseData); resolve(prev + '\n' + releaseData);
}); });
}); });
}, "") }, '')
.then(function (reducedChangelog) { .then(function (reducedChangelog) {
log.write(reducedChangelog); log.write(reducedChangelog);
log.close(); log.close();
@ -765,75 +765,75 @@ var path = require('path'),
* - Zip files in build folder to dist-folder/#{version} directory * - Zip files in build folder to dist-folder/#{version} directory
*/ */
grunt.registerTask("nightly", [ grunt.registerTask("nightly", [
"setCurrentBuildType:Nightly", 'setCurrentBuildType:Nightly',
"shell:bourbon", 'shell:bourbon',
"sass:admin", 'sass:admin',
"handlebars", 'handlebars',
"concat", 'concat',
"uglify", 'uglify',
"bump:build", 'bump:build',
"updateCurrentPackageInfo", 'updateCurrentPackageInfo',
"changelog", 'changelog',
"copy:nightly", 'copy:nightly',
"compress:nightly" 'compress:nightly'
]); ]);
grunt.registerTask("weekly", [ grunt.registerTask("weekly", [
"setCurrentBuildType:Weekly", 'setCurrentBuildType:Weekly',
"shell:bourbon", 'shell:bourbon',
"sass:admin", 'sass:admin',
"handlebars", 'handlebars',
"concat", 'concat',
"uglify", 'uglify',
"bump:build", 'bump:build',
"updateCurrentPackageInfo", 'updateCurrentPackageInfo',
"changelog", 'changelog',
"copy:weekly", 'copy:weekly',
"compress:weekly" 'compress:weekly'
]); ]);
grunt.registerTask("build", [ grunt.registerTask('build', [
"shell:bourbon", 'shell:bourbon',
"sass:admin", 'sass:admin',
"handlebars", 'handlebars',
"concat", 'concat',
"uglify", 'uglify',
"changelog", 'changelog',
"clean:build", 'clean:build',
"copy:build", 'copy:build',
"compress:build" 'compress:build'
]); ]);
// Dev Mode; watch files and restart server on changes // Dev Mode; watch files and restart server on changes
grunt.registerTask("dev", [ grunt.registerTask('dev', [
"default", 'default',
"express:dev", 'express:dev',
"open", 'open',
"watch" 'watch'
]); ]);
// Prepare the project for development // Prepare the project for development
// TODO: Git submodule init/update (https://github.com/jaubourg/grunt-update-submodules)? // TODO: Git submodule init/update (https://github.com/jaubourg/grunt-update-submodules)?
grunt.registerTask("init", ["shell:bourbon", "default"]); grunt.registerTask('init', ['shell:bourbon', 'default']);
// Run unit tests // Run unit tests
grunt.registerTask("test-unit", ['setTestEnv', 'loadConfig', "mochacli:all"]); grunt.registerTask('test-unit', ['setTestEnv', 'loadConfig', 'mochacli:all']);
// Run casperjs tests only // Run casperjs tests only
grunt.registerTask('test-functional', ['setTestEnv', 'express:test', 'spawn-casperjs']); grunt.registerTask('test-functional', ['setTestEnv', 'express:test', 'spawn-casperjs']);
// Run tests and lint code // Run tests and lint code
grunt.registerTask("validate", ["jslint", "test-unit", "test-functional"]); grunt.registerTask('validate', ['jslint', 'test-unit', 'test-functional']);
// Generate Docs // Generate Docs
grunt.registerTask("docs", ["groc"]); grunt.registerTask('docs', ['groc']);
// TODO: Production build task that minifies with uglify:prod // TODO: Production build task that minifies with uglify:prod
grunt.registerTask("prod", ['sass:admin', 'handlebars', 'concat', "uglify"]); grunt.registerTask('prod', ['sass:admin', 'handlebars', 'concat', 'uglify']);
// When you just say "grunt" // When you just say 'grunt'
grunt.registerTask("default", ['sass:admin', 'handlebars', 'concat']); grunt.registerTask('default', ['sass:admin', 'handlebars', 'concat']);
}; };
module.exports = configureGrunt; module.exports = configureGrunt;

View File

@ -34,7 +34,9 @@ config = {
debug: false debug: false
}, },
server: { server: {
// Host to be passed to node's `net.Server#listen()`
host: '127.0.0.1', host: '127.0.0.1',
// Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
port: '2368' port: '2368'
} }
}, },
@ -53,7 +55,9 @@ config = {
debug: false debug: false
}, },
server: { server: {
// Host to be passed to node's `net.Server#listen()`
host: '127.0.0.1', host: '127.0.0.1',
// Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
port: '2368' port: '2368'
} }
}, },

@ -1 +1 @@
Subproject commit 2bd73d696a53671afe1de0fd5a59a4c22e86b740 Subproject commit c4c276653dc751e50fd927bd8c88a72930f4beff

View File

@ -23,8 +23,6 @@
complete: function (result) { complete: function (result) {
var self = this; var self = this;
$dropzone.trigger("uploadsuccess", [result, $dropzone.attr('id')]);
function showImage(width, height) { function showImage(width, height) {
$dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"}); $dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"});
$dropzone.find('.fileupload-loading').remove(); $dropzone.find('.fileupload-loading').remove();
@ -137,7 +135,7 @@
var self = this; var self = this;
//This is the start point if no image exists //This is the start point if no image exists
$dropzone.find('img.js-upload-target').css({"display": "none"}); $dropzone.find('img.js-upload-target').css({"display": "none"});
$dropzone.removeClass('pre-image-uploader').addClass('image-uploader'); $dropzone.removeClass('pre-image-uploader image-uploader-url').addClass('image-uploader');
this.removeExtras(); this.removeExtras();
this.buildExtras(); this.buildExtras();
this.bindFileUpload(); this.bindFileUpload();
@ -148,6 +146,7 @@
initUrl: function () { initUrl: function () {
var self = this, val; var self = this, val;
this.removeExtras(); this.removeExtras();
$dropzone.addClass('image-uploader-url').removeClass('pre-image-uploader');
$dropzone.find('.js-fileupload').addClass('right'); $dropzone.find('.js-fileupload').addClass('right');
$dropzone.append($cancel); $dropzone.append($cancel);
$dropzone.find('.js-cancel').on('click', function () { $dropzone.find('.js-cancel').on('click', function () {
@ -163,6 +162,7 @@
$dropzone.find('div.description').before($url); $dropzone.find('div.description').before($url);
$dropzone.find('.js-button-accept').on('click', function () { $dropzone.find('.js-button-accept').on('click', function () {
$dropzone.trigger('uploadstart', [$dropzone.attr('id')]);
$dropzone.find('div.description').hide(); $dropzone.find('div.description').hide();
val = $('#uploadurl').val(); val = $('#uploadurl').val();
$dropzone.find('.js-fileupload').removeClass('right'); $dropzone.find('.js-fileupload').removeClass('right');
@ -175,7 +175,7 @@
var self = this; var self = this;
// This is the start point if an image already exists // This is the start point if an image already exists
source = $dropzone.find('img.js-upload-target').attr('src'); source = $dropzone.find('img.js-upload-target').attr('src');
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader'); $dropzone.removeClass('image-uploader image-uploader-url').addClass('pre-image-uploader');
$dropzone.find('div.description').hide(); $dropzone.find('div.description').hide();
$dropzone.append($cancel); $dropzone.append($cancel);
$dropzone.find('.js-cancel').on('click', function () { $dropzone.find('.js-cancel').on('click', function () {

View File

@ -1277,7 +1277,8 @@ main {
.centre{ .centre{
position: relative; position: relative;
top: 50px; top: 50px;
margin-bottom: -6px; display: block;
margin: 0 auto -6px auto;
} }
.media { .media {
@include icon($i-image, 60px, darken($lightbrown, 3%)) { @include icon($i-image, 60px, darken($lightbrown, 3%)) {
@ -1408,6 +1409,7 @@ main {
color: $brown; color: $brown;
background: rgba(0,0,0,0.1); background: rgba(0,0,0,0.1);
border-radius: 2px; border-radius: 2px;
min-height: 46px;
input { input {
position: absolute; position: absolute;

View File

@ -1,9 +1,9 @@
/*globals Handlebars, moment /*globals Handlebars, moment
*/ */
(function () { (function () {
"use strict"; 'use strict';
Handlebars.registerHelper('date', function (context, block) { Handlebars.registerHelper('date', function (context, block) {
var f = block.hash.format || "MMM Do, YYYY", var f = block.hash.format || 'MMM Do, YYYY',
timeago = block.hash.timeago, timeago = block.hash.timeago,
date; date;
if (timeago) { if (timeago) {

View File

@ -1,6 +1,6 @@
/*globals window, $, _, Backbone, Validator */ /*globals window, $, _, Backbone, Validator */
(function () { (function () {
"use strict"; 'use strict';
var Ghost = { var Ghost = {
Layout : {}, Layout : {},

View File

@ -2,7 +2,7 @@
/*global $, window, CodeMirror, Showdown, moment */ /*global $, window, CodeMirror, Showdown, moment */
(function () { (function () {
"use strict"; 'use strict';
var Markdown = { var Markdown = {
init : function (options, elem) { init : function (options, elem) {
var self = this; var self = this;
@ -17,59 +17,59 @@
replace: function () { replace: function () {
var text = this.elem.getSelection(), pass = true, md, cursor, line, word, letterCount, converter; var text = this.elem.getSelection(), pass = true, md, cursor, line, word, letterCount, converter;
switch (this.style) { switch (this.style) {
case "h1": case 'h1':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line); line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "# " + line); this.elem.setLine(cursor.line, '# ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 2); this.elem.setCursor(cursor.line, cursor.ch + 2);
pass = false; pass = false;
break; break;
case "h2": case 'h2':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line); line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "## " + line); this.elem.setLine(cursor.line, '## ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 3); this.elem.setCursor(cursor.line, cursor.ch + 3);
pass = false; pass = false;
break; break;
case "h3": case 'h3':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line); line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "### " + line); this.elem.setLine(cursor.line, '### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 4); this.elem.setCursor(cursor.line, cursor.ch + 4);
pass = false; pass = false;
break; break;
case "h4": case 'h4':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line); line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "#### " + line); this.elem.setLine(cursor.line, '#### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 5); this.elem.setCursor(cursor.line, cursor.ch + 5);
pass = false; pass = false;
break; break;
case "h5": case 'h5':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line); line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "##### " + line); this.elem.setLine(cursor.line, '##### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 6); this.elem.setCursor(cursor.line, cursor.ch + 6);
pass = false; pass = false;
break; break;
case "h6": case 'h6':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
line = this.elem.getLine(cursor.line); line = this.elem.getLine(cursor.line);
this.elem.setLine(cursor.line, "###### " + line); this.elem.setLine(cursor.line, '###### ' + line);
this.elem.setCursor(cursor.line, cursor.ch + 7); this.elem.setCursor(cursor.line, cursor.ch + 7);
pass = false; pass = false;
break; break;
case "link": case 'link':
md = this.options.syntax.link.replace('$1', text); md = this.options.syntax.link.replace('$1', text);
this.elem.replaceSelection(md, "end"); this.elem.replaceSelection(md, 'end');
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
this.elem.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1}); this.elem.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1});
pass = false; pass = false;
break; break;
case "image": case 'image':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
md = this.options.syntax.image.replace('$1', text); md = this.options.syntax.image.replace('$1', text);
if (this.elem.getLine(cursor.line) !== "") { if (this.elem.getLine(cursor.line) !== '') {
md = "\n\n" + md; md = "\n\n" + md;
} }
this.elem.replaceSelection(md, "end"); this.elem.replaceSelection(md, "end");
@ -77,16 +77,16 @@
this.elem.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1}); this.elem.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1});
pass = false; pass = false;
break; break;
case "uppercase": case 'uppercase':
md = text.toLocaleUpperCase(); md = text.toLocaleUpperCase();
break; break;
case "lowercase": case 'lowercase':
md = text.toLocaleLowerCase(); md = text.toLocaleLowerCase();
break; break;
case "titlecase": case 'titlecase':
md = text.toTitleCase(); md = text.toTitleCase();
break; break;
case "selectword": case 'selectword':
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();
word = this.elem.getTokenAt(cursor); word = this.elem.getTokenAt(cursor);
if (!/\w$/g.test(word.string)) { if (!/\w$/g.test(word.string)) {
@ -95,7 +95,7 @@
this.elem.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end}); this.elem.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end});
} }
break; break;
case "copyHTML": case 'copyHTML':
converter = new Showdown.converter(); converter = new Showdown.converter();
if (text) { if (text) {
md = converter.makeHtml(text); md = converter.makeHtml(text);
@ -106,13 +106,15 @@
$(".modal-copyToHTML-content").text(md).selectText(); $(".modal-copyToHTML-content").text(md).selectText();
pass = false; pass = false;
break; break;
case "list": case 'list':
md = text.replace(/^(\s*)(\w\W*)/gm, "$1* $2"); md = text.replace(/^(\s*)(\w\W*)/gm, '$1* $2');
this.elem.replaceSelection(md, "end"); this.elem.replaceSelection(md, 'end');
pass = false; pass = false;
break; break;
case "currentDate": case 'currentDate':
md = moment(new Date()).format("D MMMM YYYY"); md = moment(new Date()).format('D MMMM YYYY');
this.elem.replaceSelection(md, 'end');
pass = false;
break; break;
default: default:
if (this.options.syntax[this.style]) { if (this.options.syntax[this.style]) {
@ -120,7 +122,7 @@
} }
} }
if (pass && md) { if (pass && md) {
this.elem.replaceSelection(md, "end"); this.elem.replaceSelection(md, 'end');
if (!text) { if (!text) {
letterCount = md.length; letterCount = md.length;
cursor = this.elem.getCursor(); cursor = this.elem.getCursor();

View File

@ -3,49 +3,49 @@
/*global window, document, $, FastClick */ /*global window, document, $, FastClick */
(function () { (function () {
"use strict"; 'use strict';
FastClick.attach(document.body); FastClick.attach(document.body);
// ### Show content preview when swiping left on content list // ### Show content preview when swiping left on content list
$(".manage").on("click", ".content-list ol li", function (event) { $('.manage').on('click', '.content-list ol li', function (event) {
if (window.matchMedia('(max-width: 800px)').matches) { if (window.matchMedia('(max-width: 800px)').matches) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$(".content-list").animate({right: "100%", left: "-100%", 'margin-right': "15px"}, 300); $('.content-list').animate({right: '100%', left: '-100%', 'margin-right': '15px'}, 300);
$(".content-preview").animate({right: "0", left: "0", 'margin-left': "0"}, 300); $('.content-preview').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
} }
}); });
// ### Hide content preview // ### Hide content preview
$(".manage").on("click", ".content-preview .button-back", function (event) { $('.manage').on('click', '.content-preview .button-back', function (event) {
if (window.matchMedia('(max-width: 800px)').matches) { if (window.matchMedia('(max-width: 800px)').matches) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$(".content-list").animate({right: "0", left: "0", 'margin-right': "0"}, 300); $('.content-list').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$(".content-preview").animate({right: "-100%", left: "100%", 'margin-left': "15px"}, 300); $('.content-preview').animate({right: '-100%', left: '100%', 'margin-left': '15px'}, 300);
} }
}); });
// ### Show settings options page when swiping left on settings menu link // ### Show settings options page when swiping left on settings menu link
$(".settings").on("click", ".settings-menu li", function (event) { $('.settings').on('click', '.settings-menu li', function (event) {
if (window.matchMedia('(max-width: 800px)').matches) { if (window.matchMedia('(max-width: 800px)').matches) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$(".settings-sidebar").animate({right: "100%", left: "-102%", 'margin-right': "15px"}, 300); $('.settings-sidebar').animate({right: '100%', left: '-102%', 'margin-right': '15px'}, 300);
$(".settings-content").animate({right: "0", left: "0", 'margin-left': "0"}, 300); $('.settings-content').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
$(".settings-content .button-back, .settings-content .button-save").css("display", "inline-block"); $('.settings-content .button-back, .settings-content .button-save').css('display', 'inline-block');
} }
}); });
// ### Hide settings options page // ### Hide settings options page
$(".settings").on("click", ".settings-content .button-back", function (event) { $('.settings').on('click', '.settings-content .button-back', function (event) {
if (window.matchMedia('(max-width: 800px)').matches) { if (window.matchMedia('(max-width: 800px)').matches) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$(".settings-sidebar").animate({right: "0", left: "0", 'margin-right': "0"}, 300); $('.settings-sidebar').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$(".settings-content").animate({right: "-100%", left: "100%", 'margin-left': "15"}, 300); $('.settings-content').animate({right: '-100%', left: '100%', 'margin-left': '15'}, 300);
$(".settings-content .button-back, .settings-content .button-save").css("display", "none"); $('.settings-content .button-back, .settings-content .button-save').css('display', 'none');
} }
}); });

View File

@ -1,6 +1,6 @@
/*global window, document, Ghost, $, _, Backbone */ /*global window, document, Ghost, $, _, Backbone */
(function () { (function () {
"use strict"; 'use strict';
Ghost.Models.Post = Backbone.Model.extend({ Ghost.Models.Post = Backbone.Model.extend({
@ -12,8 +12,8 @@
parse: function (resp) { parse: function (resp) {
if (resp.status) { if (resp.status) {
resp.published = !!(resp.status === "published"); resp.published = !!(resp.status === 'published');
resp.draft = !!(resp.status === "draft"); resp.draft = !!(resp.status === 'draft');
} }
if (resp.tags) { if (resp.tags) {
// TODO: parse tags into it's own collection on the model (this.tags) // TODO: parse tags into it's own collection on the model (this.tags)

View File

@ -1,10 +1,10 @@
/*global window, document, Ghost, $, _, Backbone */ /*global window, document, Ghost, $, _, Backbone */
(function () { (function () {
"use strict"; 'use strict';
//id:0 is used to issue PUT requests //id:0 is used to issue PUT requests
Ghost.Models.Settings = Backbone.Model.extend({ Ghost.Models.Settings = Backbone.Model.extend({
url: Ghost.settings.apiRoot + '/settings/?type=blog,theme', url: Ghost.settings.apiRoot + '/settings/?type=blog,theme',
id: "0" id: '0'
}); });
}()); }());

View File

@ -1,6 +1,6 @@
/*global window, document, Ghost, $, _, Backbone */ /*global window, document, Ghost, $, _, Backbone */
(function () { (function () {
"use strict"; 'use strict';
Ghost.Collections.Tags = Backbone.Collection.extend({ Ghost.Collections.Tags = Backbone.Collection.extend({
url: Ghost.settings.apiRoot + '/tags/' url: Ghost.settings.apiRoot + '/tags/'

View File

@ -1,6 +1,6 @@
/*global window, document, Ghost, $, _, Backbone */ /*global window, document, Ghost, $, _, Backbone */
(function () { (function () {
"use strict"; 'use strict';
Ghost.Models.Themes = Backbone.Model.extend({ Ghost.Models.Themes = Backbone.Model.extend({
url: Ghost.settings.apiRoot + '/themes' url: Ghost.settings.apiRoot + '/themes'

View File

@ -1,11 +1,11 @@
/*global Ghost, Backbone */ /*global Ghost, Backbone */
(function () { (function () {
"use strict"; 'use strict';
Ghost.Models.uploadModal = Backbone.Model.extend({ Ghost.Models.uploadModal = Backbone.Model.extend({
options: { options: {
close: true, close: true,
type: "action", type: 'action',
style: ["wide"], style: ["wide"],
animation: 'fade', animation: 'fade',
afterRender: function () { afterRender: function () {

View File

@ -1,6 +1,6 @@
/*global window, document, Ghost, $, _, Backbone */ /*global window, document, Ghost, $, _, Backbone */
(function () { (function () {
"use strict"; 'use strict';
Ghost.Models.User = Backbone.Model.extend({ Ghost.Models.User = Backbone.Model.extend({
url: Ghost.settings.apiRoot + '/users/me/' url: Ghost.settings.apiRoot + '/users/me/'

View File

@ -1,15 +1,15 @@
/*global window, document, Ghost, $, _, Backbone */ /*global window, document, Ghost, $, _, Backbone */
(function () { (function () {
"use strict"; 'use strict';
Ghost.Models.Widget = Backbone.Model.extend({ Ghost.Models.Widget = Backbone.Model.extend({
defaults: { defaults: {
title: "", title: '',
name: "", name: '',
author: "", author: '',
applicationID: "", applicationID: '',
size: "", size: '',
content: { content: {
template: '', template: '',
data: { data: {
@ -17,9 +17,9 @@
count: 0, count: 0,
sub: { sub: {
value: 0, value: 0,
dir: "", // "up" or "down" dir: '', // "up" or "down"
item: "", item: '',
period: "" period: ''
} }
} }
} }
@ -28,8 +28,8 @@
settingsPane: false, settingsPane: false,
enabled: false, enabled: false,
options: [{ options: [{
title: "ERROR", title: 'ERROR',
value: "Widget options not set" value: 'Widget options not set'
}] }]
} }
} }

View File

@ -2,7 +2,7 @@
/*global document, $, Ghost */ /*global document, $, Ghost */
(function () { (function () {
"use strict"; 'use strict';
Ghost.temporary.hideToggles = function () { Ghost.temporary.hideToggles = function () {
$('[data-toggle]').each(function () { $('[data-toggle]').each(function () {
@ -11,7 +11,7 @@
}); });
// Toggle active classes on menu headers // Toggle active classes on menu headers
$("[data-toggle].active").removeClass("active"); $('[data-toggle].active').removeClass('active');
}; };
Ghost.temporary.initToggles = function ($el) { Ghost.temporary.initToggles = function ($el) {

View File

@ -281,7 +281,7 @@
this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render(); this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();
this.$('#entry-title').val(this.model.get('title')).focus(); this.$('#entry-title').val(this.model.get('title')).focus();
this.$('#entry-markdown').html(this.model.get('markdown')); this.$('#entry-markdown').text(this.model.get('markdown'));
this.initMarkdown(); this.initMarkdown();
this.renderPreview(); this.renderPreview();

View File

@ -1,5 +1,7 @@
var fs = require('fs'), var fs = require('fs'),
when = require('when'); url = require('url'),
when = require('when'),
errors = require('./server/errorHandling');
function writeConfigFile() { function writeConfigFile() {
var written = when.defer(); var written = when.defer();
@ -11,19 +13,19 @@ function writeConfigFile() {
write; write;
if (!templateExists) { if (!templateExists) {
throw new Error('Could not locate a configuration file. Please check your deployment for config.js or config.example.js.'); return errors.logError(new Error('Could not locate a configuration file.'), process.cwd(), 'Please check your deployment for config.js or config.example.js.');
} }
// Copy config.example.js => config.js // Copy config.example.js => config.js
read = fs.createReadStream('config.example.js'); read = fs.createReadStream('config.example.js');
read.on('error', function (err) { read.on('error', function (err) {
throw new Error('Could not open config.example.js for read.'); return errors.logError(new Error('Could not open config.example.js for read.'), process.cwd(), 'Please check your deployment for config.js or config.example.js.');
}); });
read.on('end', written.resolve); read.on('end', written.resolve);
write = fs.createWriteStream('config.js'); write = fs.createWriteStream('config.js');
write.on('error', function (err) { write.on('error', function (err) {
throw new Error('Could not open config.js for write.'); return errors.logError(new Error('Could not open config.js for write.'), process.cwd(), 'Please check your deployment for config.js or config.example.js.');
}); });
read.pipe(write); read.pipe(write);
@ -32,15 +34,54 @@ function writeConfigFile() {
return written.promise; return written.promise;
} }
function validateConfigEnvironment() {
var envVal = process.env.NODE_ENV || 'undefined',
config,
parsedUrl;
try {
config = require('../config')[envVal];
} catch (ignore) {
}
// Check if we don't even have a config
if (!config) {
errors.logError(new Error('Cannot find the configuration for the current NODE_ENV'), "NODE_ENV=" + envVal, 'Ensure your config.js has a section for the current NODE_ENV value');
return when.reject();
}
// Check that our url is valid
parsedUrl = url.parse(config.url || 'invalid');
if (!parsedUrl.protocol || !parsedUrl.host) {
errors.logError(new Error('Your site url in config.js is invalid.'), config.url, 'Please make sure this is a valid url before restarting');
return when.reject();
}
// Check that we have database values
if (!config.database) {
errors.logError(new Error('Your database configuration in config.js is invalid.'), JSON.stringify(config.database), 'Please make sure this is a valid Bookshelf database configuration');
return when.reject();
}
// Check for valid server host and port values
if (!config.server || !config.server.host || !config.server.port) {
errors.logError(new Error('Your server values (host and port) in config.js are invalid.'), JSON.stringify(config.server), 'Please provide them before restarting.');
return when.reject();
}
return when.resolve();
}
exports.loadConfig = function () { exports.loadConfig = function () {
var loaded = when.defer(); var loaded = when.defer();
/* Check for config file and copy from config.example.js /* Check for config file and copy from config.example.js
if one doesn't exist. After that, start the server. */ if one doesn't exist. After that, start the server. */
fs.exists('config.js', function checkConfig(configExists) { fs.exists('config.js', function checkConfig(configExists) {
if (configExists) { if (configExists) {
loaded.resolve(); validateConfigEnvironment().then(loaded.resolve).otherwise(loaded.reject);
} else { } else {
writeConfigFile().then(loaded.resolve).otherwise(loaded.reject); writeConfigFile().then(validateConfigEnvironment).then(loaded.resolve).otherwise(loaded.reject);
} }
}); });
return loaded.promise; return loaded.promise;

View File

@ -2,28 +2,28 @@
// Defines core methods required to build the application // Defines core methods required to build the application
// Module dependencies // Module dependencies
var config = require('../config'), var config = require('../config'),
when = require('when'), when = require('when'),
express = require('express'), express = require('express'),
errors = require('./server/errorHandling'), errors = require('./server/errorHandling'),
fs = require('fs'), fs = require('fs'),
path = require('path'), path = require('path'),
hbs = require('express-hbs'), hbs = require('express-hbs'),
nodefn = require('when/node/function'), nodefn = require('when/node/function'),
_ = require('underscore'), _ = require('underscore'),
Polyglot = require('node-polyglot'), Polyglot = require('node-polyglot'),
Mailer = require('./server/mail'), Mailer = require('./server/mail'),
models = require('./server/models'), models = require('./server/models'),
plugins = require('./server/plugins'), plugins = require('./server/plugins'),
requireTree = require('./server/require-tree'), requireTree = require('./server/require-tree'),
permissions = require('./server/permissions'), permissions = require('./server/permissions'),
uuid = require('node-uuid'), uuid = require('node-uuid'),
// Variables // Variables
appRoot = path.resolve(__dirname, '../'), appRoot = path.resolve(__dirname, '../'),
themePath = path.resolve(appRoot + '/content/themes'), themePath = path.resolve(appRoot + '/content/themes'),
pluginPath = path.resolve(appRoot + '/content/plugins'), pluginPath = path.resolve(appRoot + '/content/plugins'),
themeDirectories = requireTree(themePath), themeDirectories = requireTree(themePath),
pluginDirectories = requireTree(pluginPath), pluginDirectories = requireTree(pluginPath),
Ghost, Ghost,
@ -111,7 +111,7 @@ Ghost = function () {
'appRoot': appRoot, 'appRoot': appRoot,
'themePath': themePath, 'themePath': themePath,
'pluginPath': pluginPath, 'pluginPath': pluginPath,
'activeTheme': path.join(themePath, !instance.settingsCache ? "" : instance.settingsCache.activeTheme.value), 'activeTheme': path.join(themePath, !instance.settingsCache ? '' : instance.settingsCache.activeTheme.value),
'adminViews': path.join(appRoot, '/core/server/views/'), 'adminViews': path.join(appRoot, '/core/server/views/'),
'helperTemplates': path.join(appRoot, '/core/server/helpers/tpl/'), 'helperTemplates': path.join(appRoot, '/core/server/helpers/tpl/'),
'lang': path.join(appRoot, '/core/shared/lang/'), 'lang': path.join(appRoot, '/core/shared/lang/'),
@ -130,14 +130,14 @@ Ghost.prototype.init = function () {
function doFirstRun() { function doFirstRun() {
var firstRunMessage = [ var firstRunMessage = [
"Welcome to Ghost.", 'Welcome to Ghost.',
"You're running under the <strong>", 'You\'re running under the <strong>',
process.env.NODE_ENV, process.env.NODE_ENV,
"</strong>environment.", '</strong>environment.',
"Your URL is set to", 'Your URL is set to',
"<strong>" + self.config().url + "</strong>.", '<strong>' + self.config().url + '</strong>.',
"See <a href=\"http://docs.ghost.org/\">http://docs.ghost.org</a> for instructions." 'See <a href="http://docs.ghost.org/">http://docs.ghost.org</a> for instructions.'
]; ];
self.notifications.push({ self.notifications.push({
@ -165,14 +165,13 @@ Ghost.prototype.init = function () {
} }
// ### Initialisation // ### Initialisation
// make sure things are done in order
return when.join( return when.join(
// Initialise the models // Initialise the models
instance.dataProvider.init(), self.dataProvider.init(),
// Calculate paths // Calculate paths
instance.getPaths(), self.getPaths(),
// Initialise mail after first run // Initialise mail after first run
instance.mail.init(self) self.mail.init(self)
).then(function () { ).then(function () {
// Populate any missing default settings // Populate any missing default settings
return models.Settings.populateDefaults(); return models.Settings.populateDefaults();
@ -183,7 +182,7 @@ Ghost.prototype.init = function () {
return when.join( return when.join(
// Check for or initialise a dbHash. // Check for or initialise a dbHash.
initDbHashAndFirstRun(), initDbHashAndFirstRun(),
// Initialize plugins // Initialize plugins
self.initPlugins(), self.initPlugins(),
// Initialize the permissions actions and objects // Initialize the permissions actions and objects
permissions.init() permissions.init()
@ -261,7 +260,7 @@ Ghost.prototype.loadTemplate = function (name) {
var self = this, var self = this,
templateFileName = name + '.hbs', templateFileName = name + '.hbs',
// Check for theme specific version first // Check for theme specific version first
templatePath = path.join(this.paths().activeTheme, "partials", templateFileName), templatePath = path.join(this.paths().activeTheme, 'partials', templateFileName),
deferred = when.defer(); deferred = when.defer();
// Can't use nodefn here because exists just returns one parameter, true or false // Can't use nodefn here because exists just returns one parameter, true or false

View File

@ -1,18 +1,18 @@
// Module dependencies // Module dependencies
var express = require('express'), var express = require('express'),
when = require('when'), when = require('when'),
_ = require('underscore'), _ = require('underscore'),
colors = require("colors"), colors = require('colors'),
semver = require("semver"), semver = require('semver'),
slashes = require("connect-slashes"), slashes = require('connect-slashes'),
errors = require('./server/errorHandling'), errors = require('./server/errorHandling'),
admin = require('./server/controllers/admin'), admin = require('./server/controllers/admin'),
frontend = require('./server/controllers/frontend'), frontend = require('./server/controllers/frontend'),
api = require('./server/api'), api = require('./server/api'),
path = require('path'), path = require('path'),
hbs = require('express-hbs'), hbs = require('express-hbs'),
Ghost = require('./ghost'), Ghost = require('./ghost'),
helpers = require('./server/helpers'), helpers = require('./server/helpers'),
packageInfo = require('../package.json'), packageInfo = require('../package.json'),
// Variables // Variables
@ -112,7 +112,8 @@ function ghostLocals(req, res, next) {
_.extend(res.locals, { _.extend(res.locals, {
currentUser: { currentUser: {
name: currentUser.attributes.name, name: currentUser.attributes.name,
profile: currentUser.attributes.image email: currentUser.attributes.email,
image: currentUser.attributes.image
} }
}); });
next(); next();
@ -128,8 +129,8 @@ function ghostLocals(req, res, next) {
// Disable any caching until it can be done properly // Disable any caching until it can be done properly
function disableCachedResult(req, res, next) { function disableCachedResult(req, res, next) {
res.set({ res.set({
"Cache-Control": "no-cache, must-revalidate", 'Cache-Control': 'no-cache, must-revalidate',
"Expires": "Sat, 26 Jul 1997 05:00:00 GMT" 'Expires': 'Sat, 26 Jul 1997 05:00:00 GMT'
}); });
next(); next();
@ -222,13 +223,15 @@ function manageAdminAndTheme(req, res, next) {
// Expose the promise we will resolve after our pre-loading // Expose the promise we will resolve after our pre-loading
ghost.loaded = loading.promise; ghost.loaded = loading.promise;
when.all([ghost.init(), helpers.loadCoreHelpers(ghost)]).then(function () { when(ghost.init()).then(function () {
return helpers.loadCoreHelpers(ghost);
}).then(function () {
// ##Configuration // ##Configuration
var oneYear = 31536000000; var oneYear = 31536000000;
// Logging configuration // Logging configuration
if (server.get('env') !== "development") { if (server.get('env') !== 'development') {
server.use(express.logger()); server.use(express.logger());
} else { } else {
server.use(express.logger('dev')); server.use(express.logger('dev'));
@ -243,7 +246,7 @@ when.all([ghost.init(), helpers.loadCoreHelpers(ghost)]).then(function () {
server.use('/shared', express['static'](path.join(__dirname, '/shared'))); server.use('/shared', express['static'](path.join(__dirname, '/shared')));
server.use('/content/images', express['static'](path.join(__dirname, '/../content/images'))); server.use('/content/images', express['static'](path.join(__dirname, '/../content/images')));
// Serve our built scripts; can't use /scripts here because themes already are // Serve our built scripts; can't use /scripts here because themes already are
server.use("/built/scripts", express['static'](path.join(__dirname, '/built/scripts'), { server.use('/built/scripts', express['static'](path.join(__dirname, '/built/scripts'), {
// Put a maxAge of one year on built scripts // Put a maxAge of one year on built scripts
maxAge: oneYear maxAge: oneYear
})); }));
@ -264,6 +267,7 @@ when.all([ghost.init(), helpers.loadCoreHelpers(ghost)]).then(function () {
server.use(express.urlencoded()); server.use(express.urlencoded());
server.use('/ghost/upload/', express.multipart()); server.use('/ghost/upload/', express.multipart());
server.use('/ghost/upload/', express.multipart({uploadDir: __dirname + '/content/images'})); server.use('/ghost/upload/', express.multipart({uploadDir: __dirname + '/content/images'}));
server.use('/ghost/debug/db/import/', express.multipart());
server.use(express.cookieParser(ghost.dbHash)); server.use(express.cookieParser(ghost.dbHash));
server.use(express.cookieSession({ cookie: { maxAge: 60000000 }})); server.use(express.cookieSession({ cookie: { maxAge: 60000000 }}));
@ -339,7 +343,11 @@ when.all([ghost.init(), helpers.loadCoreHelpers(ghost)]).then(function () {
server.get('/ghost/debug/db/reset/', auth, admin.debug.reset); server.get('/ghost/debug/db/reset/', auth, admin.debug.reset);
// We don't want to register bodyParser globally b/c of security concerns, so use multipart only here // We don't want to register bodyParser globally b/c of security concerns, so use multipart only here
server.post('/ghost/upload/', admin.uploader); server.post('/ghost/upload/', admin.uploader);
server.get(/^\/(ghost$|(ghost-admin|admin|wp-admin|dashboard|signin)\/?)/, auth, function (req, res) { // redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
server.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)/, function (req, res) {
res.redirect('/ghost/');
});
server.get(/^\/(ghost$\/?)/, auth, function (req, res) {
res.redirect('/ghost/'); res.redirect('/ghost/');
}); });
server.get('/ghost/', redirectToSignup, auth, admin.index); server.get('/ghost/', redirectToSignup, auth, admin.index);

View File

@ -1,14 +1,14 @@
// # Ghost Data API // # Ghost Data API
// Provides access to the data model // Provides access to the data model
var Ghost = require('../ghost'), var Ghost = require('../ghost'),
_ = require('underscore'), _ = require('underscore'),
when = require('when'), when = require('when'),
errors = require('./errorHandling'), errors = require('./errorHandling'),
permissions = require('./permissions'), permissions = require('./permissions'),
canThis = permissions.canThis, canThis = permissions.canThis,
ghost = new Ghost(), ghost = new Ghost(),
dataProvider = ghost.dataProvider, dataProvider = ghost.dataProvider,
posts, posts,
users, users,
@ -228,7 +228,7 @@ settingsCollection = function (settings) {
settingsFilter = function (settings, filter) { settingsFilter = function (settings, filter) {
return _.object(_.filter(_.pairs(settings), function (setting) { return _.object(_.filter(_.pairs(settings), function (setting) {
if (filter) { if (filter) {
return _.some(filter.split(","), function (f) { return _.some(filter.split(','), function (f) {
return setting[1].type === f; return setting[1].type === f;
}); });
} }

View File

@ -1,17 +1,17 @@
var Ghost = require('../../ghost'), var Ghost = require('../../ghost'),
dataExport = require('../data/export'), dataExport = require('../data/export'),
dataImport = require('../data/import'), dataImport = require('../data/import'),
_ = require('underscore'), _ = require('underscore'),
fs = require('fs-extra'), fs = require('fs-extra'),
path = require('path'), path = require('path'),
when = require('when'), when = require('when'),
nodefn = require('when/node/function'), nodefn = require('when/node/function'),
api = require('../api'), api = require('../api'),
moment = require('moment'), moment = require('moment'),
errors = require('../errorHandling'), errors = require('../errorHandling'),
ghost = new Ghost(), ghost = new Ghost(),
dataProvider = ghost.dataProvider, dataProvider = ghost.dataProvider,
adminNavbar, adminNavbar,
adminControllers, adminControllers,
loginSecurity = []; loginSecurity = [];
@ -269,8 +269,8 @@ adminControllers = {
// TODO: Centralise list/enumeration of settings panes, so we don't // TODO: Centralise list/enumeration of settings panes, so we don't
// run into trouble in future. // run into trouble in future.
var allowedSections = ["", "general", "user"], var allowedSections = ['', 'general', 'user'],
section = req.url.replace(/(^\/ghost\/settings[\/]*|\/$)/ig, ""); section = req.url.replace(/(^\/ghost\/settings[\/]*|\/$)/ig, '');
if (allowedSections.indexOf(section) < 0) { if (allowedSections.indexOf(section) < 0) {
return next(); return next();
@ -312,7 +312,7 @@ adminControllers = {
}; };
return api.notifications.add(notification).then(function () { return api.notifications.add(notification).then(function () {
res.redirect("/ghost/debug/"); res.redirect('/ghost/debug/');
}); });
}); });
}, },
@ -327,16 +327,16 @@ adminControllers = {
}; };
return api.notifications.add(notification).then(function () { return api.notifications.add(notification).then(function () {
res.redirect("/ghost/debug/"); res.redirect('/ghost/debug/');
}); });
} }
// Get the current version for importing // Get the current version for importing
api.settings.read({ key: "databaseVersion" }) api.settings.read({ key: 'databaseVersion' })
.then(function (setting) { .then(function (setting) {
return when(setting.value); return when(setting.value);
}, function () { }, function () {
return when("001"); return when('001');
}) })
.then(function (databaseVersion) { .then(function (databaseVersion) {
// Read the file contents // Read the file contents

View File

@ -4,14 +4,16 @@
/*global require, module */ /*global require, module */
var Ghost = require('../../ghost'), var Ghost = require('../../ghost'),
api = require('../api'), api = require('../api'),
RSS = require('rss'), RSS = require('rss'),
_ = require('underscore'), _ = require('underscore'),
errors = require('../errorHandling'), errors = require('../errorHandling'),
when = require('when'), when = require('when'),
url = require('url'),
ghost = new Ghost(),
ghost = new Ghost(),
frontendControllers; frontendControllers;
frontendControllers = { frontendControllers = {
@ -24,7 +26,7 @@ frontendControllers = {
// No negative pages // No negative pages
if (isNaN(pageParam) || pageParam < 1) { if (isNaN(pageParam) || pageParam < 1) {
//redirect to 404 page? //redirect to 404 page?
return res.redirect("/"); return res.redirect('/');
} }
options.page = pageParam; options.page = pageParam;
@ -87,14 +89,14 @@ frontendControllers = {
description: ghost.settings('description'), description: ghost.settings('description'),
generator: 'Ghost v' + res.locals.version, generator: 'Ghost v' + res.locals.version,
author: user ? user.attributes.name : null, author: user ? user.attributes.name : null,
feed_url: siteUrl + '/rss/', feed_url: url.resolve(siteUrl, '/rss/'),
site_url: siteUrl, site_url: siteUrl,
ttl: '60' ttl: '60'
}); });
// No negative pages // No negative pages
if (isNaN(pageParam) || pageParam < 1) { if (isNaN(pageParam) || pageParam < 1) {
return res.redirect("/rss/"); return res.redirect('/rss/');
} }
if (pageParam === 1 && req.route.path === '/rss/:page/') { if (pageParam === 1 && req.route.path === '/rss/:page/') {
@ -112,22 +114,30 @@ frontendControllers = {
// If page is greater than number of pages we have, redirect to last page // If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) { if (pageParam > maxPage) {
return res.redirect("/rss/" + maxPage + "/"); return res.redirect('/rss/' + maxPage + '/');
} }
ghost.doFilter('prePostsRender', page.posts, function (posts) { ghost.doFilter('prePostsRender', page.posts, function (posts) {
posts.forEach(function (post) { posts.forEach(function (post) {
var item = { var item = {
title: _.escape(post.title), title: _.escape(post.title),
guid: post.uuid, guid: post.uuid,
url: siteUrl + '/' + post.slug + '/', url: siteUrl + '/' + post.slug + '/',
date: post.published_at date: post.published_at,
}; },
content = post.html;
if (post.meta_description !== null) {
item.push({ description: post.meta_description });
}
//set img src to absolute url
content = content.replace(/src=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
p1 = url.resolve(siteUrl, p1);
return "src='" + p1 + "' ";
});
//set a href to absolute url
content = content.replace(/href=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
p1 = url.resolve(siteUrl, p1);
return "href='" + p1 + "' ";
});
item.description = content;
feed.item(item); feed.item(item);
}); });
res.set('Content-Type', 'text/xml'); res.set('Content-Type', 'text/xml');
@ -138,6 +148,7 @@ frontendControllers = {
return next(new Error(err)); return next(new Error(err));
}); });
} }
}; };
module.exports = frontendControllers; module.exports = frontendControllers;

View File

@ -1,8 +1,8 @@
var when = require('when'), var when = require('when'),
_ = require('underscore'), _ = require('underscore'),
migration = require('../migration'), migration = require('../migration'),
client = require('../../models/base').client, client = require('../../models/base').client,
knex = require('../../models/base').Knex, knex = require('../../models/base').Knex,
exporter; exporter;
@ -15,7 +15,7 @@ function getTablesFromSqlite3() {
} }
function getTablesFromMySQL() { function getTablesFromMySQL() {
knex.Raw("show tables").then(function (response) { return knex.Raw('show tables').then(function (response) {
return _.flatten(_.map(response, function (entry) { return _.flatten(_.map(response, function (entry) {
return _.values(entry); return _.values(entry);
})); }));

View File

@ -1,10 +1,10 @@
var sequence = require('when/sequence'), var sequence = require('when/sequence'),
_ = require('underscore'), _ = require('underscore'),
Post = require('../../models/post').Post, Post = require('../../models/post').Post,
Tag = require('../../models/tag').Tag, Tag = require('../../models/tag').Tag,
Role = require('../../models/role').Role, Role = require('../../models/role').Role,
Permission = require('../../models/permission').Permission, Permission = require('../../models/permission').Permission,
uuid = require('node-uuid'); uuid = require('node-uuid');
var fixtures = { var fixtures = {
posts: [ posts: [
@ -12,7 +12,7 @@ var fixtures = {
"title": "Welcome to Ghost", "title": "Welcome to Ghost",
"slug": "welcome-to-ghost", "slug": "welcome-to-ghost",
"markdown": "You're in! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. Go ahead and edit this post to get going and learn how it all works!\n\n## Getting Started\n\nWriting in markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use *formatting* shortcuts to style your content. For example, a list:\n\n* Item number one\n* Item number two\n * A nested item\n* A final item\n\nor with numbers!\n\n1. Remember to buy some milk\n2. Drink the milk\n3. Tweet that I remembered to buy the milk, and drank it\n\n### Links\n\nWant to link to a source? No problem. If you paste in url, like http://ghost.org - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to [the Ghost website](http://ghost.org). Neat.\n\n### What about Images?\n\nImages work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:\n\n![The Ghost Logo](http://tryghost.org/ghost.png)\n\nNot sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:\n\n![A bowl of bananas]\n\n\n### Quoting\n\nSometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.\n\n> Wisdomous - it's definitely a word.\n\n### Working with Code\n\nGot a streak of geek? We've got you covered there, too. You can write inline `<code>` blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.\n\n .awesome-thing {\n display: block;\n width: 100%;\n }\n\n### Ready for a Break? \n\nThrow 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.\n\n---\n\n### Advanced Usage\n\nThere's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.\n\n<input type=\"text\" placeholder=\"I'm an input field!\" />\n\nThat should be enough to get you started. Have fun - and let us know what you think :)", "markdown": "You're in! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. Go ahead and edit this post to get going and learn how it all works!\n\n## Getting Started\n\nWriting in markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use *formatting* shortcuts to style your content. For example, a list:\n\n* Item number one\n* Item number two\n * A nested item\n* A final item\n\nor with numbers!\n\n1. Remember to buy some milk\n2. Drink the milk\n3. Tweet that I remembered to buy the milk, and drank it\n\n### Links\n\nWant to link to a source? No problem. If you paste in url, like http://ghost.org - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to [the Ghost website](http://ghost.org). Neat.\n\n### What about Images?\n\nImages work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:\n\n![The Ghost Logo](http://tryghost.org/ghost.png)\n\nNot sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:\n\n![A bowl of bananas]\n\n\n### Quoting\n\nSometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.\n\n> Wisdomous - it's definitely a word.\n\n### Working with Code\n\nGot a streak of geek? We've got you covered there, too. You can write inline `<code>` blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.\n\n .awesome-thing {\n display: block;\n width: 100%;\n }\n\n### Ready for a Break? \n\nThrow 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.\n\n---\n\n### Advanced Usage\n\nThere's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.\n\n<input type=\"text\" placeholder=\"I'm an input field!\" />\n\nThat should be enough to get you started. Have fun - and let us know what you think :)",
"html": "<p>You're in! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. Go ahead and edit this post to get going and learn how it all works!</p>\n\n<h2 id=\"gettingstarted\">Getting Started</h2>\n\n<p>Writing in markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use <em>formatting</em> shortcuts to style your content. For example, a list:</p>\n\n<ul>\n<li>Item number one</li>\n<li>Item number two\n<ul><li>A nested item</li></ul></li>\n<li>A final item</li>\n</ul>\n\n<p>or with numbers!</p>\n\n<ol>\n<li>Remember to buy some milk </li>\n<li>Drink the milk </li>\n<li>Tweet that I remembered to buy the milk, and drank it</li>\n</ol>\n\n<h3 id=\"links\">Links</h3>\n\n<p>Want to link to a source? No problem. If you paste in url, like <a href='http://ghost.org'>http://ghost.org</a> - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to <a href=\"http://ghost.org\">the Ghost website</a>. Neat.</p>\n\n<h3 id=\"whataboutimages\">What about Images?</h3>\n\n<p>Images work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:</p>\n\n<p><img src=\"http://tryghost.org/ghost.png\" alt=\"The Ghost Logo\" /></p>\n\n<p>Not sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:</p>\n\n<p><img src=\"undefined\" alt=\"A bowl of bananas\" /></p>\n\n<h3 id=\"quoting\">Quoting</h3>\n\n<p>Sometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.</p>\n\n<blockquote>\n <p>Wisdomous - it's definitely a word.</p>\n</blockquote>\n\n<h3 id=\"workingwithcode\">Working with Code</h3>\n\n<p>Got a streak of geek? We've got you covered there, too. You can write inline <code>&lt;code&gt;</code> blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.</p>\n\n<pre><code>.awesome-thing {\n display: block;\n width: 100%;\n}\n</code></pre>\n\n<h3 id=\"readyforabreak\">Ready for a Break?</h3>\n\n<p>Throw 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.</p>\n\n<hr />\n\n<h3 id=\"advancedusage\">Advanced Usage</h3>\n\n<p>There's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.</p>\n\n<p><input type=\"text\" placeholder=\"I'm an input field!\" /></p>\n\n<p>That should be enough to get you started. Have fun - and let us know what you think :)</p>", "html": "<p>You're in! Nice. We've put together a little post to introduce you to the Ghost editor and get you started. Go ahead and edit this post to get going and learn how it all works!</p>\n\n<h2 id=\"gettingstarted\">Getting Started</h2>\n\n<p>Writing in markdown is really easy. In the left hand panel of Ghost, you simply write as you normally would. Where appropriate, you can use <em>formatting</em> shortcuts to style your content. For example, a list:</p>\n\n<ul>\n<li>Item number one</li>\n<li>Item number two\n<ul><li>A nested item</li></ul></li>\n<li>A final item</li>\n</ul>\n\n<p>or with numbers!</p>\n\n<ol>\n<li>Remember to buy some milk </li>\n<li>Drink the milk </li>\n<li>Tweet that I remembered to buy the milk, and drank it</li>\n</ol>\n\n<h3 id=\"links\">Links</h3>\n\n<p>Want to link to a source? No problem. If you paste in url, like <a href='http://ghost.org'>http://ghost.org</a> - it'll automatically be linked up. But if you want to customise your anchor text, you can do that too! Here's a link to <a href=\"http://ghost.org\">the Ghost website</a>. Neat.</p>\n\n<h3 id=\"whataboutimages\">What about Images?</h3>\n\n<p>Images work too! Already know the URL of the image you want to include in your article? Simply paste it in like this to make it show up:</p>\n\n<p><img src=\"http://tryghost.org/ghost.png\" alt=\"The Ghost Logo\" /></p>\n\n<p>Not sure which image you want to use yet? That's ok too. Leave yourself a descriptive placeholder and keep writing. Come back later and drag and drop the image in to upload:</p>\n\n<h3 id=\"quoting\">Quoting</h3>\n\n<p>Sometimes a link isn't enough, you want to quote someone on what they've said. It was probably very wisdomous. Is wisdomous a word? Find out in a future release when we introduce spellcheck! For now - it's definitely a word.</p>\n\n<blockquote>\n <p>Wisdomous - it's definitely a word.</p>\n</blockquote>\n\n<h3 id=\"workingwithcode\">Working with Code</h3>\n\n<p>Got a streak of geek? We've got you covered there, too. You can write inline <code>&lt;code&gt;</code> blocks really easily with back ticks. Want to show off something more comprehensive? 4 spaces of indentation gets you there.</p>\n\n<pre><code>.awesome-thing {\n display: block;\n width: 100%;\n}\n</code></pre>\n\n<h3 id=\"readyforabreak\">Ready for a Break?</h3>\n\n<p>Throw 3 or more dashes down on any new line and you've got yourself a fancy new divider. Aw yeah.</p>\n\n<hr />\n\n<h3 id=\"advancedusage\">Advanced Usage</h3>\n\n<p>There's one fantastic secret about Markdown. If you want, you can write plain old HTML and it'll still work! Very flexible.</p>\n\n<p><input type=\"text\" placeholder=\"I'm an input field!\" /></p>\n\n<p>That should be enough to get you started. Have fun - and let us know what you think :)</p>",
"image": null, "image": null,
"featured": false, "featured": false,
"page": false, "page": false,

View File

@ -1,19 +1,19 @@
var when = require("when"), var when = require('when'),
_ = require("underscore"), _ = require('underscore'),
models = require('../../models'), models = require('../../models'),
errors = require('../../errorHandling'), errors = require('../../errorHandling'),
Importer000; Importer000;
Importer000 = function () { Importer000 = function () {
_.bindAll(this, "basicImport"); _.bindAll(this, 'basicImport');
this.version = "000"; this.version = '000';
this.importFrom = { this.importFrom = {
"000": this.basicImport, '000': this.basicImport,
"001": this.tempImport, '001': this.tempImport,
"002": this.tempImport '002': this.tempImport
}; };
}; };
@ -81,7 +81,12 @@ function preProcessPostTags(tableData) {
function importTags(ops, tableData) { function importTags(ops, tableData) {
tableData = stripProperties(['id'], tableData); tableData = stripProperties(['id'], tableData);
_.each(tableData, function (tag) { _.each(tableData, function (tag) {
ops.push(models.Tag.add(tag)); ops.push(models.Tag.read({name: tag.name}).then(function (_tag) {
if (!_tag) {
return models.Tag.add(tag);
}
return when.resolve(_tag);
}));
}); });
} }

View File

@ -4,7 +4,7 @@ module.exports = function (version, data) {
var importer; var importer;
try { try {
importer = require("./" + version); importer = require('./' + version);
} catch (ignore) { } catch (ignore) {
// Zero effs given // Zero effs given
} }

View File

@ -131,22 +131,22 @@ up = function () {
down = function () { down = function () {
return when.all([ return when.all([
knex.Schema.dropTableIfExists("posts"), knex.Schema.dropTableIfExists('posts_tags'),
knex.Schema.dropTableIfExists("users"), knex.Schema.dropTableIfExists('roles_users'),
knex.Schema.dropTableIfExists("roles"), knex.Schema.dropTableIfExists('permissions_users'),
knex.Schema.dropTableIfExists("settings"), knex.Schema.dropTableIfExists('permissions_roles'),
knex.Schema.dropTableIfExists("permissions"), knex.Schema.dropTableIfExists('users')
knex.Schema.dropTableIfExists("tags")
]).then(function () { ]).then(function () {
// Drop the relation tables after the model tables
return when.all([ return when.all([
knex.Schema.dropTableIfExists("roles_users"), knex.Schema.dropTableIfExists('roles'),
knex.Schema.dropTableIfExists("permissions_users"), knex.Schema.dropTableIfExists('settings'),
knex.Schema.dropTableIfExists("permissions_roles"), knex.Schema.dropTableIfExists('permissions'),
knex.Schema.dropTableIfExists("posts_tags") knex.Schema.dropTableIfExists('tags'),
knex.Schema.dropTableIfExists('posts')
]); ]);
}); });
}; };
exports.up = up; exports.up = up;
exports.down = down; exports.down = down;

View File

@ -1,15 +1,15 @@
var _ = require('underscore'), var _ = require('underscore'),
when = require('when'), when = require('when'),
series = require('when/sequence'), series = require('when/sequence'),
errors = require('../../errorHandling'), errors = require('../../errorHandling'),
knex = require('../../models/base').Knex, knex = require('../../models/base').Knex,
defaultSettings = require('../default-settings'), defaultSettings = require('../default-settings'),
Settings = require('../../models/settings').Settings, Settings = require('../../models/settings').Settings,
fixtures = require('../fixtures'), fixtures = require('../fixtures'),
initialVersion = '000', initialVersion = '000',
defaultDatabaseVersion; defaultDatabaseVersion;
// Default Database Version // Default Database Version

View File

@ -1,16 +1,16 @@
/*jslint regexp: true */ /*jslint regexp: true */
var _ = require('underscore'), var _ = require('underscore'),
colors = require("colors"), colors = require('colors'),
fs = require('fs'), fs = require('fs'),
path = require('path'), path = require('path'),
errors, errors,
// Paths for views // Paths for views
appRoot = path.resolve(__dirname, '../'), appRoot = path.resolve(__dirname, '../'),
themePath = path.resolve(appRoot + '/content/themes'), themePath = path.resolve(appRoot + '/content/themes'),
adminTemplatePath = path.resolve(appRoot + '/server/views/'), adminTemplatePath = path.resolve(appRoot + '/server/views/'),
defaultErrorTemplatePath = path.resolve(adminTemplatePath + '/user-error.hbs'), defaultErrorTemplatePath = path.resolve(adminTemplatePath + '/user-error.hbs'),
userErrorTemplatePath = path.resolve(themePath + '/error.hbs'), userErrorTemplatePath = path.resolve(themePath + '/error.hbs'),
userErrorTemplateExists; userErrorTemplateExists;
/** /**
@ -31,14 +31,14 @@ errors = {
logError: function (err, context, help) { logError: function (err, context, help) {
var stack = err ? err.stack : null; var stack = err ? err.stack : null;
err = err.message || err || "Unknown"; err = err.message || err || 'Unknown';
// TODO: Logging framework hookup // TODO: Logging framework hookup
// Eventually we'll have better logging which will know about envs // Eventually we'll have better logging which will know about envs
if ((process.env.NODE_ENV === 'development' || if ((process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'staging' || process.env.NODE_ENV === 'staging' ||
process.env.NODE_ENV === 'production')) { process.env.NODE_ENV === 'production')) {
console.error("\nERROR:".red, err.red); console.error('\nERROR:'.red, err.red);
if (context) { if (context) {
console.error(context); console.error(context);
@ -49,10 +49,10 @@ errors = {
} }
// add a new line // add a new line
console.error(""); console.error('');
if (stack) { if (stack) {
console.error(stack, "\n"); console.error(stack, '\n');
} }
} }
}, },
@ -84,7 +84,7 @@ errors = {
renderErrorPage: function (code, err, req, res, next) { renderErrorPage: function (code, err, req, res, next) {
function parseStack(stack) { function parseStack(stack) {
if (typeof stack !== "string") { if (typeof stack !== 'string') {
return stack; return stack;
} }
@ -102,8 +102,8 @@ errors = {
} }
return { return {
"function": parts[1], 'function': parts[1],
"at": parts[2] 'at': parts[2]
}; };
}) })
.filter(function (line) { .filter(function (line) {
@ -116,12 +116,12 @@ errors = {
function renderErrorInt(errorView) { function renderErrorInt(errorView) {
var stack = null; var stack = null;
if (process.env.NODE_ENV !== "production" && err.stack) { if (process.env.NODE_ENV !== 'production' && err.stack) {
stack = parseStack(err.stack); stack = parseStack(err.stack);
} }
// TODO: Attach node-polyglot // TODO: Attach node-polyglot
res.render((errorView || "error"), { res.render((errorView || 'error'), {
message: err.message || err, message: err.message || err,
code: code, code: code,
stack: stack stack: stack
@ -177,13 +177,13 @@ errors = {
// Ensure our 'this' context in the functions // Ensure our 'this' context in the functions
_.bindAll( _.bindAll(
errors, errors,
"throwError", 'throwError',
"logError", 'logError',
"logAndThrowError", 'logAndThrowError',
"logErrorWithRedirect", 'logErrorWithRedirect',
"renderErrorPage", 'renderErrorPage',
"render404Page", 'render404Page',
"render500Page" 'render500Page'
); );
module.exports = errors; module.exports = errors;

View File

@ -1,18 +1,18 @@
var _ = require('underscore'), var _ = require('underscore'),
moment = require('moment'), moment = require('moment'),
downsize = require('downsize'), downsize = require('downsize'),
when = require('when'), when = require('when'),
hbs = require('express-hbs'), hbs = require('express-hbs'),
packageInfo = require('../../../package.json'), packageInfo = require('../../../package.json'),
errors = require('../errorHandling'), errors = require('../errorHandling'),
models = require('../models'), models = require('../models'),
coreHelpers; coreHelpers;
coreHelpers = function (ghost) { coreHelpers = function (ghost) {
var paginationHelper, var paginationHelper,
scriptTemplate = _.template("<script src='/built/scripts/<%= name %>?v=<%= version %>'></script>"), scriptTemplate = _.template("<script src='/built/scripts/<%= name %>?v=<%= version %>'></script>"),
isProduction = process.env.NODE_ENV === "production", isProduction = process.env.NODE_ENV === 'production',
version = encodeURIComponent(packageInfo.version); version = encodeURIComponent(packageInfo.version);
/** /**
@ -34,7 +34,7 @@ coreHelpers = function (ghost) {
} }
} }
var f = options.hash.format || "MMM Do, YYYY", var f = options.hash.format || 'MMM Do, YYYY',
timeago = options.hash.timeago, timeago = options.hash.timeago,
date; date;
@ -76,7 +76,7 @@ coreHelpers = function (ghost) {
} }
if (models.isPost(this)) { if (models.isPost(this)) {
output += "/" + this.slug + '/'; output += '/' + this.slug + '/';
} }
return output; return output;
@ -91,7 +91,7 @@ coreHelpers = function (ghost) {
// if the author could not be determined. // if the author could not be determined.
// //
ghost.registerThemeHelper('author', function (context, options) { ghost.registerThemeHelper('author', function (context, options) {
return this.author ? this.author.name : ""; return this.author ? this.author.name : '';
}); });
// ### Tags Helper // ### Tags Helper
@ -106,10 +106,10 @@ coreHelpers = function (ghost) {
// Note that the standard {{#each tags}} implementation is unaffected by this helper // Note that the standard {{#each tags}} implementation is unaffected by this helper
// and can be used for more complex templates. // and can be used for more complex templates.
ghost.registerThemeHelper('tags', function (options) { ghost.registerThemeHelper('tags', function (options) {
var separator = ", ", var separator = ', ',
tagNames; tagNames;
if (typeof options.hash.separator === "string") { if (typeof options.hash.separator === 'string') {
separator = options.hash.separator; separator = options.hash.separator;
} }
@ -133,7 +133,7 @@ coreHelpers = function (ghost) {
// //
ghost.registerThemeHelper('content', function (options) { ghost.registerThemeHelper('content', function (options) {
var truncateOptions = (options || {}).hash || {}; var truncateOptions = (options || {}).hash || {};
truncateOptions = _.pick(truncateOptions, ["words", "characters"]); truncateOptions = _.pick(truncateOptions, ['words', 'characters']);
if (truncateOptions.words || truncateOptions.characters) { if (truncateOptions.words || truncateOptions.characters) {
return new hbs.handlebars.SafeString( return new hbs.handlebars.SafeString(
@ -162,10 +162,10 @@ coreHelpers = function (ghost) {
var truncateOptions = (options || {}).hash || {}, var truncateOptions = (options || {}).hash || {},
excerpt; excerpt;
truncateOptions = _.pick(truncateOptions, ["words", "characters"]); truncateOptions = _.pick(truncateOptions, ['words', 'characters']);
/*jslint regexp:true */ /*jslint regexp:true */
excerpt = String(this.html).replace(/<\/?[^>]+>/gi, ""); excerpt = String(this.html).replace(/<\/?[^>]+>/gi, '');
/*jslint regexp:false */ /*jslint regexp:false */
if (!truncateOptions.words && !truncateOptions.characters) { if (!truncateOptions.words && !truncateOptions.characters) {
@ -198,7 +198,7 @@ coreHelpers = function (ghost) {
var classes = ['post']; var classes = ['post'];
if (this.tags) { if (this.tags) {
classes = classes.concat(this.tags.map(function (tag) { return "tag-" + tag.name; })); classes = classes.concat(this.tags.map(function (tag) { return 'tag-' + tag.name; }));
} }
return ghost.doFilter('post_class', classes, function (classes) { return ghost.doFilter('post_class', classes, function (classes) {
@ -208,12 +208,15 @@ coreHelpers = function (ghost) {
}); });
ghost.registerThemeHelper('ghost_head', function (options) { ghost.registerThemeHelper('ghost_head', function (options) {
var head = []; var head = [],
head.push('<meta name="generator" content="Ghost ' + this.version + '" />'); majorMinor = /^(\d+\.)?(\d+)/,
trimmedVersion = this.version.match(majorMinor)[0];
head.push('<meta name="generator" content="Ghost ' + trimmedVersion + '" />');
head.push('<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss/">'); head.push('<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss/">');
return ghost.doFilter('ghost_head', head, function (head) { return ghost.doFilter('ghost_head', head, function (head) {
var headString = _.reduce(head, function (memo, item) { return memo + "\n" + item; }, ''); var headString = _.reduce(head, function (memo, item) { return memo + '\n' + item; }, '');
return new hbs.handlebars.SafeString(headString.trim()); return new hbs.handlebars.SafeString(headString.trim());
}); });
}); });
@ -292,7 +295,7 @@ coreHelpers = function (ghost) {
j = 0, j = 0,
columns = options.hash.columns, columns = options.hash.columns,
key, key,
ret = "", ret = '',
data; data;
if (options.data) { if (options.data) {
@ -356,18 +359,18 @@ coreHelpers = function (ghost) {
}); });
// A helper for inserting the javascript tags with version hashes // A helper for inserting the javascript tags with version hashes
ghost.registerThemeHelper("ghostScriptTags", function () { ghost.registerThemeHelper('ghostScriptTags', function () {
var scriptFiles = []; var scriptFiles = [];
if (isProduction) { if (isProduction) {
scriptFiles.push("ghost.min.js"); scriptFiles.push("ghost.min.js");
} else { } else {
scriptFiles = [ scriptFiles = [
"vendor.js", 'vendor.js',
"helpers.js", 'helpers.js',
"templates.js", 'templates.js',
"models.js", 'models.js',
"views.js" 'views.js'
]; ];
} }
@ -378,7 +381,7 @@ coreHelpers = function (ghost) {
}); });
}); });
return scriptFiles.join(""); return scriptFiles.join('');
}); });
// ## Template driven helpers // ## Template driven helpers

View File

@ -1,8 +1,8 @@
var cp = require('child_process'), var cp = require('child_process'),
url = require('url'), url = require('url'),
_ = require('underscore'), _ = require('underscore'),
when = require('when'), when = require('when'),
nodefn = require('when/node/function'), nodefn = require('when/node/function'),
nodemailer = require('nodemailer'); nodemailer = require('nodemailer');
function GhostMailer(opts) { function GhostMailer(opts) {
@ -51,7 +51,7 @@ GhostMailer.prototype.detectSendmail = function () {
if (err && !/bin\/sendmail/.test(stdout)) { if (err && !/bin\/sendmail/.test(stdout)) {
return reject(); return reject();
} }
resolve(stdout.toString()); resolve(stdout.toString().replace(/(\n|\r|\r\n)$/, ''));
}); });
}); });
}; };

View File

@ -1,10 +1,10 @@
var GhostBookshelf, var GhostBookshelf,
Bookshelf = require('bookshelf'), Bookshelf = require('bookshelf'),
when = require('when'), when = require('when'),
moment = require('moment'), moment = require('moment'),
_ = require('underscore'), _ = require('underscore'),
uuid = require('node-uuid'), uuid = require('node-uuid'),
config = require('../../../config'), config = require('../../../config'),
Validator = require('validator').Validator; Validator = require('validator').Validator;
// Initializes Bookshelf as its own instance, so we can modify the Models and not mess up // Initializes Bookshelf as its own instance, so we can modify the Models and not mess up
@ -70,7 +70,7 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
} }
_.each(relations, function (relation, key) { _.each(relations, function (relation, key) {
if (key.substring(0, 7) !== "_pivot_") { if (key.substring(0, 7) !== '_pivot_') {
attrs[key] = relation.toJSON ? relation.toJSON() : relation; attrs[key] = relation.toJSON ? relation.toJSON() : relation;
} }
}); });
@ -126,7 +126,7 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
//if slug is empty after trimming use "post" //if slug is empty after trimming use "post"
if (!slug) { if (!slug) {
slug = "post"; slug = 'post';
} }
// Test for duplicate slugs. // Test for duplicate slugs.
return checkIfSlugExists(slug); return checkIfSlugExists(slug);

View File

@ -16,7 +16,7 @@ module.exports = {
}); });
}, },
isPost: function (jsonData) { isPost: function (jsonData) {
return jsonData.hasOwnProperty("html") && jsonData.hasOwnProperty("markdown") return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown')
&& jsonData.hasOwnProperty("title") && jsonData.hasOwnProperty("slug"); && jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug');
} }
}; };

View File

@ -1,6 +1,6 @@
var GhostBookshelf = require('./base'), var GhostBookshelf = require('./base'),
User = require('./user').User, User = require('./user').User,
Role = require('./role').Role, Role = require('./role').Role,
Permission, Permission,
Permissions; Permissions;

View File

@ -128,7 +128,7 @@ Post = GhostBookshelf.Model.extend({
} }
}); });
if (tagsToAttach) { if (!_.isEmpty(tagsToAttach)) {
return Tags.forge().query('whereIn', 'name', _.pluck(tagsToAttach, 'name')).fetch().then(function (matchingTags) { return Tags.forge().query('whereIn', 'name', _.pluck(tagsToAttach, 'name')).fetch().then(function (matchingTags) {
_.each(matchingTags.toJSON(), function (matchingTag) { _.each(matchingTags.toJSON(), function (matchingTag) {
tagOperations.push(self.tags().attach(matchingTag.id)); tagOperations.push(self.tags().attach(matchingTag.id));
@ -173,7 +173,7 @@ Post = GhostBookshelf.Model.extend({
// Extends base model findAll to eager-fetch author and user relationships. // Extends base model findAll to eager-fetch author and user relationships.
findAll: function (options) { findAll: function (options) {
options = options || {}; options = options || {};
options.withRelated = [ "author", "user", "tags" ]; options.withRelated = [ 'author', 'user', 'tags' ];
return GhostBookshelf.Model.findAll.call(this, options); return GhostBookshelf.Model.findAll.call(this, options);
}, },
@ -181,7 +181,7 @@ Post = GhostBookshelf.Model.extend({
// Extends base model findOne to eager-fetch author and user relationships. // Extends base model findOne to eager-fetch author and user relationships.
findOne: function (args, options) { findOne: function (args, options) {
options = options || {}; options = options || {};
options.withRelated = [ "author", "user", "tags" ]; options.withRelated = [ 'author', 'user', 'tags' ];
return GhostBookshelf.Model.findOne.call(this, args, options); return GhostBookshelf.Model.findOne.call(this, args, options);
}, },
@ -235,7 +235,7 @@ Post = GhostBookshelf.Model.extend({
postCollection.query('where', opts.where); postCollection.query('where', opts.where);
} }
opts.withRelated = [ "author", "user", "tags" ]; opts.withRelated = [ 'author', 'user', 'tags' ];
// Set the limit & offset for the query, fetching // Set the limit & offset for the query, fetching
// with the opts (to specify any eager relations, etc.) // with the opts (to specify any eager relations, etc.)
@ -332,6 +332,19 @@ Post = GhostBookshelf.Model.extend({
// associated models can't be created until the post has an ID, so run this after // associated models can't be created until the post has an ID, so run this after
return post.updateTags(newPostData.tags); return post.updateTags(newPostData.tags);
}); });
},
destroy: function (_identifier, options) {
options = options || {};
return this.forge({id: _identifier}).fetch({withRelated: ['tags']}).then(function destroyTags(post) {
var tagIds = _.pluck(post.related('tags').toJSON(), 'id');
if (tagIds) {
return post.tags().detach(tagIds).then(function destroyPost() {
return post.destroy(options);
});
}
return post.destroy(options);
});
} }
}); });

View File

@ -1,5 +1,5 @@
var User = require('./user').User, var User = require('./user').User,
Permission = require('./permission').Permission, Permission = require('./permission').Permission,
GhostBookshelf = require('./base'), GhostBookshelf = require('./base'),
Role, Role,
Roles; Roles;

View File

@ -1,10 +1,10 @@
var Settings, var Settings,
GhostBookshelf = require('./base'), GhostBookshelf = require('./base'),
validator = GhostBookshelf.validator, validator = GhostBookshelf.validator,
uuid = require('node-uuid'), uuid = require('node-uuid'),
_ = require('underscore'), _ = require('underscore'),
errors = require('../errorHandling'), errors = require('../errorHandling'),
when = require('when'), when = require('when'),
defaultSettings; defaultSettings;
// For neatness, the defaults file is split into categories. // For neatness, the defaults file is split into categories.

View File

@ -1,6 +1,6 @@
var Tag, var Tag,
Tags, Tags,
Posts = require('./post').Posts, Posts = require('./post').Posts,
GhostBookshelf = require('./base'); GhostBookshelf = require('./base');
Tag = GhostBookshelf.Model.extend({ Tag = GhostBookshelf.Model.extend({

View File

@ -1,20 +1,20 @@
var User, var User,
Users, Users,
_ = require('underscore'), _ = require('underscore'),
uuid = require('node-uuid'), uuid = require('node-uuid'),
when = require('when'), when = require('when'),
errors = require('../errorHandling'), errors = require('../errorHandling'),
nodefn = require('when/node/function'), nodefn = require('when/node/function'),
bcrypt = require('bcrypt-nodejs'), bcrypt = require('bcrypt-nodejs'),
Posts = require('./post').Posts, Posts = require('./post').Posts,
GhostBookshelf = require('./base'), GhostBookshelf = require('./base'),
Role = require('./role').Role, Role = require('./role').Role,
Permission = require('./permission').Permission; Permission = require('./permission').Permission;
function validatePasswordLength(password) { function validatePasswordLength(password) {
try { try {
GhostBookshelf.validator.check(password, "Your password is not long enough. It must be at least 8 characters long.").len(8); GhostBookshelf.validator.check(password, "Your must be at least 8 characters long.").len(8);
} catch (error) { } catch (error) {
return when.reject(error); return when.reject(error);
} }
@ -33,10 +33,10 @@ User = GhostBookshelf.Model.extend({
], ],
validate: function () { validate: function () {
GhostBookshelf.validator.check(this.get('email'), "Please check your email address. It does not seem to be valid.").isEmail(); GhostBookshelf.validator.check(this.get('email'), "Please enter a valid email address. That one looks a bit dodgy.").isEmail();
GhostBookshelf.validator.check(this.get('bio'), "Your bio is too long. Please keep it to 200 chars.").len(0, 200); GhostBookshelf.validator.check(this.get('bio'), "We're not writing a novel here! I'm afraid your bio has to stay under 200 characters.").len(0, 200);
if (this.get('website') && this.get('website').length > 0) { if (this.get('website') && this.get('website').length > 0) {
GhostBookshelf.validator.check(this.get('website'), "Your website is not a valid URL.").isUrl(); GhostBookshelf.validator.check(this.get('website'), "Looks like your website is not actually a website. Try again?").isUrl();
} }
return true; return true;
}, },

View File

@ -1,11 +1,11 @@
// canThis(someUser).edit.posts([id]|[[ids]]) // canThis(someUser).edit.posts([id]|[[ids]])
// canThis(someUser).edit.post(somePost|somePostId) // canThis(someUser).edit.post(somePost|somePostId)
var _ = require('underscore'), var _ = require('underscore'),
when = require('when'), when = require('when'),
Models = require('../models'), Models = require('../models'),
objectTypeModelMap = require('./objectTypeModelMap'), objectTypeModelMap = require('./objectTypeModelMap'),
UserProvider = Models.User, UserProvider = Models.User,
PermissionsProvider = Models.Permission, PermissionsProvider = Models.Permission,
init, init,
refresh, refresh,

View File

@ -1,7 +1,7 @@
module.exports = { module.exports = {
'post': require('../models/post').Post, 'post': require('../models/post').Post,
'role': require('../models/role').Role, 'role': require('../models/role').Role,
'user': require('../models/user').User, 'user': require('../models/user').User,
'permission': require('../models/permission').Permission, 'permission': require('../models/permission').Permission,
'setting': require('../models/settings').Settings 'setting': require('../models/settings').Settings
}; };

View File

@ -1,17 +1,17 @@
var _ = require("underscore"), var _ = require('underscore'),
when = require('when'), when = require('when'),
ghostApi, ghostApi,
loader = require("./loader"), loader = require('./loader'),
GhostPlugin = require("./GhostPlugin"); GhostPlugin = require('./GhostPlugin');
function getInstalledPlugins() { function getInstalledPlugins() {
if (!ghostApi) { if (!ghostApi) {
ghostApi = require('../api'); ghostApi = require('../api');
} }
return ghostApi.settings.read("installedPlugins").then(function (installed) { return ghostApi.settings.read('installedPlugins').then(function (installed) {
installed.value = installed.value || "[]"; installed.value = installed.value || '[]';
try { try {
installed = JSON.parse(installed.value); installed = JSON.parse(installed.value);
@ -27,7 +27,7 @@ function saveInstalledPlugins(installedPlugins) {
return getInstalledPlugins().then(function (currentInstalledPlugins) { return getInstalledPlugins().then(function (currentInstalledPlugins) {
var updatedPluginsInstalled = _.uniq(installedPlugins.concat(currentInstalledPlugins)); var updatedPluginsInstalled = _.uniq(installedPlugins.concat(currentInstalledPlugins));
return ghostApi.settings.edit("installedPlugins", updatedPluginsInstalled); return ghostApi.settings.edit('installedPlugins', updatedPluginsInstalled);
}); });
} }

View File

@ -1,7 +1,7 @@
var path = require("path"), var path = require('path'),
_ = require("underscore"), _ = require('underscore'),
when = require("when"), when = require('when'),
ghostInstance, ghostInstance,
loader; loader;
@ -10,7 +10,7 @@ function getGhostInstance() {
return ghostInstance; return ghostInstance;
} }
var Ghost = require("../../ghost"); var Ghost = require('../../ghost');
ghostInstance = new Ghost(); ghostInstance = new Ghost();

View File

@ -1,7 +1,7 @@
var when = require('when'), var when = require('when'),
keys = require('when/keys'), keys = require('when/keys'),
fs = require('fs'), fs = require('fs'),
path = require('path'), path = require('path'),
extend = function (obj, source) { extend = function (obj, source) {
var key; var key;
for (key in source) { for (key in source) {

View File

@ -34,15 +34,6 @@
</div> </div>
</fieldset> </fieldset>
</form> </form>
<form id="settings-resetdb">
<fieldset>
<div class="form-group">
<label>Reset Database</label>
<a href="/ghost/debug/db/reset/" class="button-delete">Reset</a>
<p>Delete the entire database so you can start again with empty tables</p>
</div>
</fieldset>
</form>
</section> </section>
</section> </section>
</div> </div>

View File

@ -9,13 +9,13 @@
<li id="usermenu" class="subnav"> <li id="usermenu" class="subnav">
<a href="#" data-toggle="ul" class="dropdown"> <a href="#" data-toggle="ul" class="dropdown">
<img class="avatar" src="{{#if currentUser.profile}}{{currentUser.profile}}{{else}}/shared/img/user-image.png{{/if}}" alt="Avatar" /> <img class="avatar" src="{{#if currentUser.image}}{{currentUser.image}}{{else}}/shared/img/user-image.png{{/if}}" alt="Avatar" />
<span class="name">{{#if currentUser.name}}{{currentUser.name}}{{else}}Ghost{{/if}}</span> <span class="name">{{#if currentUser.name}}{{currentUser.name}}{{else}}{{currentUser.email}}{{/if}}</span>
</a> </a>
<ul class="overlay"> <ul class="overlay">
<li class="usermenu-profile"><a href="/ghost/settings/user/">Your Profile</a></li> <li class="usermenu-profile"><a href="/ghost/settings/user/">Your Profile</a></li>
<li class="divider"></li> <li class="divider"></li>
<li class="usermenu-help"><a href="http://forum.tryghost.org">Help / Support</a></li> <li class="usermenu-help"><a href="http://ghost.org/forum/">Help / Support</a></li>
<li class="divider"></li> <li class="divider"></li>
<li class="usermenu-signout"><a href="/signout/">Sign Out</a></li> <li class="usermenu-signout"><a href="/signout/">Sign Out</a></li>
</ul> </ul>

View File

@ -16,10 +16,10 @@
} }
}, },
{ {
// GFM newline and underscore modifications // GFM newline and underscore modifications, happen BEFORE showdown
type : 'lang', type : 'lang',
filter : function (text) { filter : function (text) {
var extractions = {}, var preExtractions = {},
imageMarkdownRegex = /^(?:\{(.*?)\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim, imageMarkdownRegex = /^(?:\{(.*?)\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
hashID = 0; hashID = 0;
@ -30,15 +30,10 @@
// Extract pre blocks // Extract pre blocks
text = text.replace(/<pre>[\s\S]*?<\/pre>/gim, function (x) { text = text.replace(/<pre>[\s\S]*?<\/pre>/gim, function (x) {
var hash = hashId(); var hash = hashId();
extractions[hash] = x; preExtractions[hash] = x;
return "{gfm-js-extract-pre-" + hash + "}"; return "{gfm-js-extract-pre-" + hash + "}";
}, 'm'); }, 'm');
// better URL support, but no title support
text = text.replace(imageMarkdownRegex, function (match, key, alt, src) {
return '<img src="' + src + '" alt="' + alt + '" />';
});
//prevent foo_bar and foo_bar_baz from ending up with an italic word in the middle //prevent foo_bar and foo_bar_baz from ending up with an italic word in the middle
text = text.replace(/(^(?! {4}|\t)\w+_\w+_\w[\w_]*)/gm, function (x) { text = text.replace(/(^(?! {4}|\t)\w+_\w+_\w[\w_]*)/gm, function (x) {
return x.replace(/_/gm, '\\_'); return x.replace(/_/gm, '\\_');
@ -49,8 +44,17 @@
return x.match(/\n{2}/) ? x : x.trim() + " \n"; return x.match(/\n{2}/) ? x : x.trim() + " \n";
}); });
// better URL support, but no title support
text = text.replace(imageMarkdownRegex, function (match, key, alt, src) {
if (src) {
return '<img src="' + src + '" alt="' + alt + '" />';
}
return '';
});
text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) { text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) {
return "\n\n" + extractions[y]; return "\n\n" + preExtractions[y];
}); });
@ -58,22 +62,30 @@
} }
}, },
{ {
// Auto-link URLs and emails // GFM autolinking & custom image handling, happens AFTER showdown
type : 'lang', type : 'html',
filter : function (text) { filter : function (text) {
var extractions = {}, var refExtractions = {},
preExtractions = {},
hashID = 0; hashID = 0;
function hashId() { function hashId() {
return hashID++; return hashID++;
} }
// Extract pre blocks
text = text.replace(/<(pre|code)>[\s\S]*?<\/(\1)>/gim, function (x) {
var hash = hashId();
preExtractions[hash] = x;
return "{gfm-js-extract-pre-" + hash + "}";
}, 'm');
// filter out def urls // filter out def urls
// from Marked https://github.com/chjj/marked/blob/master/lib/marked.js#L24 // from Marked https://github.com/chjj/marked/blob/master/lib/marked.js#L24
text = text.replace(/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gmi, text = text.replace(/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gmi,
function (x) { function (x) {
var hash = hashId(); var hash = hashId();
extractions[hash] = x; refExtractions[hash] = x;
return "{gfm-js-extract-ref-url-" + hash + "}"; return "{gfm-js-extract-ref-url-" + hash + "}";
}); });
@ -91,13 +103,18 @@
return lookBehind ? wholeMatch : "<a href='" + wholeMatch + "'>" + wholeMatch + "</a>"; return lookBehind ? wholeMatch : "<a href='" + wholeMatch + "'>" + wholeMatch + "</a>";
}); });
// match emil // match email
text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/gmi, function (wholeMatch) { text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/gmi, function (wholeMatch) {
return "<a href='mailto:" + wholeMatch + "'>" + wholeMatch + "</a>"; return "<a href='mailto:" + wholeMatch + "'>" + wholeMatch + "</a>";
}); });
// replace extractions
text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) {
return preExtractions[y];
});
text = text.replace(/\{gfm-js-extract-ref-url-([0-9]+)\}/gi, function (x, y) { text = text.replace(/\{gfm-js-extract-ref-url-([0-9]+)\}/gi, function (x, y) {
return "\n\n" + extractions[y]; return "\n\n" + refExtractions[y];
}); });
return text; return text;

View File

@ -146,6 +146,11 @@ describe('Admin Controller', function() {
fs.exists.withArgs('content/images/2013/Sep/IMAGE.jpg').yields(true); fs.exists.withArgs('content/images/2013/Sep/IMAGE.jpg').yields(true);
fs.exists.withArgs('content/images/2013/Sep/IMAGE-1.jpg').yields(false); fs.exists.withArgs('content/images/2013/Sep/IMAGE-1.jpg').yields(false);
// if on windows need to setup with back slashes
// doesn't hurt for the test to cope with both
fs.exists.withArgs('content\\images\\2013\\Sep\\IMAGE.jpg').yields(true);
fs.exists.withArgs('content\\images\\2013\\Sep\\IMAGE-1.jpg').yields(false);
sinon.stub(res, 'send', function(data) { sinon.stub(res, 'send', function(data) {
data.should.equal('/content/images/2013/Sep/IMAGE-1.jpg'); data.should.equal('/content/images/2013/Sep/IMAGE-1.jpg');
return done(); return done();
@ -163,6 +168,13 @@ describe('Admin Controller', function() {
fs.exists.withArgs('content/images/2013/Sep/IMAGE-3.jpg').yields(true); fs.exists.withArgs('content/images/2013/Sep/IMAGE-3.jpg').yields(true);
fs.exists.withArgs('content/images/2013/Sep/IMAGE-4.jpg').yields(false); fs.exists.withArgs('content/images/2013/Sep/IMAGE-4.jpg').yields(false);
// windows setup
fs.exists.withArgs('content\\images\\2013\\Sep\\IMAGE.jpg').yields(true);
fs.exists.withArgs('content\\images\\2013\\Sep\\IMAGE-1.jpg').yields(true);
fs.exists.withArgs('content\\images\\2013\\Sep\\IMAGE-2.jpg').yields(true);
fs.exists.withArgs('content\\images\\2013\\Sep\\IMAGE-3.jpg').yields(true);
fs.exists.withArgs('content\\images\\2013\\Sep\\IMAGE-4.jpg').yields(false);
sinon.stub(res, 'send', function(data) { sinon.stub(res, 'send', function(data) {
data.should.equal('/content/images/2013/Sep/IMAGE-4.jpg'); data.should.equal('/content/images/2013/Sep/IMAGE-4.jpg');
return done(); return done();

View File

@ -1,7 +1,7 @@
/*globals describe, before, beforeEach, afterEach, it */ /*globals describe, before, beforeEach, afterEach, it */
var testUtils = require('./testUtils'), var testUtils = require('./testUtils'),
should = require('should'), should = require('should'),
_ = require("underscore"), _ = require('underscore'),
when = require('when'), when = require('when'),
sequence = require('when/sequence'), sequence = require('when/sequence'),
@ -14,8 +14,8 @@ describe('Post Model', function () {
UserModel = Models.User, UserModel = Models.User,
userData = { userData = {
password: 'testpass1', password: 'testpass1',
email: "test@test1.com", email: 'test@test1.com',
name: "Mr Biscuits" name: 'Mr Biscuits'
}; };
before(function (done) { before(function (done) {
@ -79,10 +79,10 @@ describe('Post Model', function () {
results.length.should.be.above(0); results.length.should.be.above(0);
firstPost = results.models[0].toJSON(); firstPost = results.models[0].toJSON();
firstPost.author.should.be.a("object"); firstPost.author.should.be.a('object');
firstPost.user.should.be.a("object"); firstPost.user.should.be.a('object');
firstPost.author.name.should.equal("Mr Biscuits"); firstPost.author.name.should.equal('Mr Biscuits');
firstPost.user.name.should.equal("Mr Biscuits"); firstPost.user.name.should.equal('Mr Biscuits');
done(); done();
}, done); }, done);
@ -95,10 +95,10 @@ describe('Post Model', function () {
should.exist(result); should.exist(result);
firstPost = result.toJSON(); firstPost = result.toJSON();
firstPost.author.should.be.a("object"); firstPost.author.should.be.a('object');
firstPost.user.should.be.a("object"); firstPost.user.should.be.a('object');
firstPost.author.name.should.equal("Mr Biscuits"); firstPost.author.name.should.equal('Mr Biscuits');
firstPost.user.name.should.equal("Mr Biscuits"); firstPost.user.name.should.equal('Mr Biscuits');
done(); done();
}, done); }, done);
@ -112,7 +112,7 @@ describe('Post Model', function () {
results.length.should.be.above(0); results.length.should.be.above(0);
firstPost = results.models[0]; firstPost = results.models[0];
return PostModel.edit({id: firstPost.id, title: "new title"}); return PostModel.edit({id: firstPost.id, title: 'new title'});
}).then(function (edited) { }).then(function (edited) {
should.exist(edited); should.exist(edited);
edited.attributes.title.should.equal('new title'); edited.attributes.title.should.equal('new title');
@ -134,16 +134,16 @@ describe('Post Model', function () {
should.exist(createdPost); should.exist(createdPost);
createdPost.has('uuid').should.equal(true); createdPost.has('uuid').should.equal(true);
createdPost.get('status').should.equal('draft'); createdPost.get('status').should.equal('draft');
createdPost.get('title').should.equal(newPost.title, "title is correct"); createdPost.get('title').should.equal(newPost.title, 'title is correct');
createdPost.get('markdown').should.equal(newPost.markdown, "markdown is correct"); createdPost.get('markdown').should.equal(newPost.markdown, 'markdown is correct');
createdPost.has('html').should.equal(true); createdPost.has('html').should.equal(true);
createdPost.get('html').should.equal('<p>' + newPost.markdown + '</p>'); createdPost.get('html').should.equal('<p>' + newPost.markdown + '</p>');
createdPost.get('slug').should.equal('test-title-1'); 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_at').should.be.above(new Date(0).getTime());
createdPost.get('created_by').should.equal(1); createdPost.get('created_by').should.equal(1);
createdPost.get('author_id').should.equal(1); createdPost.get('author_id').should.equal(1);
createdPost.get('created_by').should.equal(createdPost.get('author_id')); createdPost.get('created_by').should.equal(createdPost.get('author_id'));
createdPost.get('updated_at').should.be.below(new Date().getTime()).and.be.above(new Date(0).getTime()); createdPost.get('updated_at').should.be.above(new Date(0).getTime());
createdPost.get('updated_by').should.equal(1); createdPost.get('updated_by').should.equal(1);
should.equal(createdPost.get('published_at'), null); should.equal(createdPost.get('published_at'), null);
should.equal(createdPost.get('published_by'), null); should.equal(createdPost.get('published_by'), null);
@ -198,8 +198,8 @@ describe('Post Model', function () {
sequence(_.times(12, function (i) { sequence(_.times(12, function (i) {
return function () { return function () {
return PostModel.add({ return PostModel.add({
title: "Test Title", title: 'Test Title',
markdown: "Test Content " + (i+1) markdown: 'Test Content ' + (i+1)
}); });
}; };
})).then(function (createdPosts) { })).then(function (createdPosts) {
@ -224,7 +224,6 @@ describe('Post Model', function () {
}).otherwise(done); }).otherwise(done);
}); });
it('can generate slugs without duplicate hyphens', function (done) { it('can generate slugs without duplicate hyphens', function (done) {
var newPost = { var newPost = {
title: 'apprehensive titles have too many spaces ', title: 'apprehensive titles have too many spaces ',
@ -239,6 +238,53 @@ describe('Post Model', function () {
}).then(null, done); }).then(null, done);
}); });
it('detects duplicate slugs before saving', function (done) {
var firstPost = {
title: 'First post',
markdown: 'First content 1'
},
secondPost = {
title: 'Second post',
markdown: 'Second content 1'
};
// Create the first post
PostModel.add(firstPost)
.then(function (createdFirstPost) {
// Store the slug for later
firstPost.slug = createdFirstPost.get('slug');
// Create the second post
return PostModel.add(secondPost);
}).then(function (createdSecondPost) {
// Store the slug for comparison later
secondPost.slug = createdSecondPost.get('slug');
// Update with a conflicting slug from the first post
return createdSecondPost.save({
slug: firstPost.slug
});
}).then(function (updatedSecondPost) {
// Should have updated from original
updatedSecondPost.get('slug').should.not.equal(secondPost.slug);
// Should not have a conflicted slug from the first
updatedSecondPost.get('slug').should.not.equal(firstPost.slug);
return PostModel.read({
id: updatedSecondPost.id
});
}).then(function (foundPost) {
// Should have updated from original
foundPost.get('slug').should.not.equal(secondPost.slug);
// Should not have a conflicted slug from the first
foundPost.get('slug').should.not.equal(firstPost.slug);
done();
}).otherwise(done);
});
it('can delete', function (done) { it('can delete', function (done) {
var firstPostId; var firstPostId;
PostModel.browse().then(function (results) { PostModel.browse().then(function (results) {
@ -252,7 +298,7 @@ describe('Post Model', function () {
}).then(function (newResults) { }).then(function (newResults) {
var ids, hasDeletedId; var ids, hasDeletedId;
ids = _.pluck(newResults.models, "id"); ids = _.pluck(newResults.models, 'id');
hasDeletedId = _.any(ids, function (id) { hasDeletedId = _.any(ids, function (id) {
return id === firstPostId; return id === firstPostId;
}); });
@ -262,6 +308,25 @@ describe('Post Model', function () {
}).then(null, done); }).then(null, done);
}); });
it('can create a new Post with a previous published_at date', function (done) {
var previousPublishedAtDate = new Date(2013, 8, 21, 12);
PostModel.add({
status: 'published',
published_at: previousPublishedAtDate,
title: 'published_at test',
markdown: 'This is some content'
}).then(function (newPost) {
should.exist(newPost);
newPost.get('published_at').should.equal(previousPublishedAtDate);
done();
}).otherwise(done);
});
it('can fetch a paginated set, with various options', function (done) { it('can fetch a paginated set, with various options', function (done) {
this.timeout(10000); // this is a patch to ensure it doesn't timeout. this.timeout(10000); // this is a patch to ensure it doesn't timeout.

View File

@ -76,13 +76,7 @@ describe('Settings Model', function () {
results.length.should.be.above(0); results.length.should.be.above(0);
firstSetting = results.models[1]; return SettingsModel.edit({key: "description", value: "new value"});
// The edit method has been modified to take an object of
// key/value pairs
firstSetting.set('value', 'new value');
return SettingsModel.edit(firstSetting);
}).then(function (edited) { }).then(function (edited) {
@ -92,7 +86,7 @@ describe('Settings Model', function () {
edited = edited[0]; edited = edited[0];
edited.attributes.key.should.equal(firstSetting.attributes.key); edited.attributes.key.should.equal('description');
edited.attributes.value.should.equal('new value'); edited.attributes.value.should.equal('new value');
done(); done();
@ -111,13 +105,8 @@ describe('Settings Model', function () {
results.length.should.be.above(0); results.length.should.be.above(0);
model1 = results.models[1]; model1 = {key: "description", value: "another new value"};
model2 = results.models[2]; model2 = {key: "title", value: "new title"};
// The edit method has been modified to take an object of
// key/value pairs
model1.set('value', 'new value1');
model2.set('value', 'new value2');
return SettingsModel.edit([model1, model2]); return SettingsModel.edit([model1, model2]);
@ -129,13 +118,13 @@ describe('Settings Model', function () {
editedModel = edited[0]; editedModel = edited[0];
editedModel.attributes.key.should.equal(model1.attributes.key); editedModel.attributes.key.should.equal(model1.key);
editedModel.attributes.value.should.equal('new value1'); editedModel.attributes.value.should.equal(model1.value);
editedModel = edited[1]; editedModel = edited[1];
editedModel.attributes.key.should.equal(model2.attributes.key); editedModel.attributes.key.should.equal(model2.key);
editedModel.attributes.value.should.equal('new value2'); editedModel.attributes.value.should.equal(model2.value);
done(); done();
@ -205,7 +194,6 @@ describe('Settings Model', function () {
it('populates any unset settings from the JSON defaults', function (done) { it('populates any unset settings from the JSON defaults', function (done) {
SettingsModel.findAll().then(function (allSettings) { SettingsModel.findAll().then(function (allSettings) {
console.log(allSettings.models)
allSettings.length.should.equal(0); allSettings.length.should.equal(0);
return SettingsModel.populateDefaults(); return SettingsModel.populateDefaults();
}).then(function () { }).then(function () {

View File

@ -130,7 +130,7 @@ describe('Tag Model', function () {
return postModel.save(); return postModel.save();
}).then(function (postModel) { }).then(function (postModel) {
var tagNames = postModel.related('tags').models.map(function (t) { return t.attributes.name; }); var tagNames = postModel.related('tags').models.map(function (t) { return t.attributes.name; });
tagNames.should.eql(seededTagNames); tagNames.sort().should.eql(seededTagNames);
return TagModel.findAll(); return TagModel.findAll();
}).then(function (tagsFromDB) { }).then(function (tagsFromDB) {
@ -155,7 +155,7 @@ describe('Tag Model', function () {
return PostModel.read({id: postModel.id}, { withRelated: ['tags']}); return PostModel.read({id: postModel.id}, { withRelated: ['tags']});
}).then(function (reloadedPost) { }).then(function (reloadedPost) {
var tagNames = reloadedPost.related('tags').models.map(function (t) { return t.attributes.name; }); var tagNames = reloadedPost.related('tags').models.map(function (t) { return t.attributes.name; });
tagNames.should.eql(['tag1', 'tag3']); tagNames.sort().should.eql(['tag1', 'tag3']);
done(); done();
}).then(null, done); }).then(null, done);
@ -180,7 +180,7 @@ describe('Tag Model', function () {
}).then(function (reloadedPost) { }).then(function (reloadedPost) {
var tagModels = reloadedPost.related('tags').models, var tagModels = reloadedPost.related('tags').models,
tagNames = tagModels.map(function (t) { return t.attributes.name; }); tagNames = tagModels.map(function (t) { return t.attributes.name; });
tagNames.should.eql(['tag1', 'tag2', 'tag3']); tagNames.sort().should.eql(['tag1', 'tag2', 'tag3']);
tagModels[2].id.should.eql(4); // make sure it hasn't just added a new tag with the same name tagModels[2].id.should.eql(4); // make sure it hasn't just added a new tag with the same name
done(); done();
@ -201,7 +201,7 @@ describe('Tag Model', function () {
return PostModel.read({id: postModel.id}, { withRelated: ['tags']}); return PostModel.read({id: postModel.id}, { withRelated: ['tags']});
}).then(function (reloadedPost) { }).then(function (reloadedPost) {
var tagNames = reloadedPost.related('tags').models.map(function (t) { return t.attributes.name; }); var tagNames = reloadedPost.related('tags').models.map(function (t) { return t.attributes.name; });
tagNames.should.eql(['tag1', 'tag2', 'tag3']); tagNames.sort().should.eql(['tag1', 'tag2', 'tag3']);
done(); done();
}).then(null, done); }).then(null, done);

View File

@ -178,7 +178,7 @@ describe("Showdown client side converter", function () {
}, },
{ {
input: "# http://google.co.uk", input: "# http://google.co.uk",
output: /^<h1 id="ahrefhttpgooglecoukhttpgooglecouka"><a href=\'http:\/\/google.co.uk\'>http:\/\/google.co.uk<\/a><\/h1>$/ output: /^<h1 id="httpgooglecouk"><a href=\'http:\/\/google.co.uk\'>http:\/\/google.co.uk<\/a><\/h1>$/
}, },
{ {
input: "* http://google.co.uk", input: "* http://google.co.uk",
@ -279,6 +279,30 @@ describe("Showdown client side converter", function () {
}); });
}); });
it("should NOT auto-link URLS inside of code/pre blocks", function () {
var testPhrases = [
{
input: "```\nurl: http://google.co.uk\n```",
output: /^<pre><code>url: http:\/\/google.co.uk \n<\/code><\/pre>$/
},
{
input: "`url: http://google.co.uk`",
output: /^<p><code>url: http:\/\/google.co.uk<\/code><\/p>$/
},
{
input: "Hello type some `url: http://google.co.uk` stuff",
output: /^<p>Hello type some <code>url: http:\/\/google.co.uk<\/code> stuff<\/p>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it("should not display anything for reference URL", function () { it("should not display anything for reference URL", function () {
var testPhrases = [ var testPhrases = [
{ {

View File

@ -12,6 +12,7 @@ var testUtils = require('./testUtils'),
describe("Ghost API", function () { describe("Ghost API", function () {
var testTemplatePath = 'core/test/unit/fixtures/', var testTemplatePath = 'core/test/unit/fixtures/',
themeTemplatePath = 'core/test/unit/fixtures/theme', themeTemplatePath = 'core/test/unit/fixtures/theme',
sandbox,
ghost; ghost;
before(function (done) { before(function (done) {
@ -21,23 +22,26 @@ describe("Ghost API", function () {
}); });
beforeEach(function (done) { beforeEach(function (done) {
sandbox = sinon.sandbox.create();
testUtils.initData().then(function () { testUtils.initData().then(function () {
ghost = new Ghost(); ghost = new Ghost();
done(); done();
}, done); }, done);
}); });
it("is a singleton", function () { afterEach(function () {
var logStub = sinon.stub(console, "log"), sandbox.restore();
ghost1 = new Ghost(), });
ghost2 = new Ghost();
should.strictEqual(ghost1, ghost2); it("is a singleton", function () {
logStub.restore(); var ghost2 = new Ghost();
should.strictEqual(ghost, ghost2);
}); });
it("uses init() to initialize", function (done) { it("uses init() to initialize", function (done) {
var dataProviderInitMock = sinon.stub(ghost.dataProvider, "init", function () { var dataProviderInitMock = sandbox.stub(ghost.dataProvider, "init", function () {
return when.resolve(); return when.resolve();
}); });
@ -49,8 +53,6 @@ describe("Ghost API", function () {
dataProviderInitMock.called.should.equal(true); dataProviderInitMock.called.should.equal(true);
dataProviderInitMock.restore();
done(); done();
}, done); }, done);
@ -59,7 +61,7 @@ describe("Ghost API", function () {
it("can register filters with specific priority", function () { it("can register filters with specific priority", function () {
var filterName = 'test', var filterName = 'test',
filterPriority = 9, filterPriority = 9,
testFilterHandler = sinon.spy(); testFilterHandler = sandbox.spy();
ghost.registerFilter(filterName, filterPriority, testFilterHandler); ghost.registerFilter(filterName, filterPriority, testFilterHandler);
@ -72,7 +74,7 @@ describe("Ghost API", function () {
it("can register filters with default priority", function () { it("can register filters with default priority", function () {
var filterName = 'test', var filterName = 'test',
defaultPriority = 5, defaultPriority = 5,
testFilterHandler = sinon.spy(); testFilterHandler = sandbox.spy();
ghost.registerFilter(filterName, testFilterHandler); ghost.registerFilter(filterName, testFilterHandler);
@ -84,9 +86,9 @@ describe("Ghost API", function () {
it("executes filters in priority order", function (done) { it("executes filters in priority order", function (done) {
var filterName = 'testpriority', var filterName = 'testpriority',
testFilterHandler1 = sinon.spy(), testFilterHandler1 = sandbox.spy(),
testFilterHandler2 = sinon.spy(), testFilterHandler2 = sandbox.spy(),
testFilterHandler3 = sinon.spy(); testFilterHandler3 = sandbox.spy();
ghost.registerFilter(filterName, 0, testFilterHandler1); ghost.registerFilter(filterName, 0, testFilterHandler1);
ghost.registerFilter(filterName, 2, testFilterHandler2); ghost.registerFilter(filterName, 2, testFilterHandler2);
@ -118,13 +120,13 @@ describe("Ghost API", function () {
}); });
it("loads templates for helpers", function (done) { it("loads templates for helpers", function (done) {
var compileSpy = sinon.spy(ghost, 'compileTemplate'), var compileSpy = sandbox.spy(ghost, 'compileTemplate'),
pathsStub; pathsStub;
should.exist(ghost.loadTemplate, 'load template function exists'); should.exist(ghost.loadTemplate, 'load template function exists');
// In order for the test to work, need to replace the path to the template // In order for the test to work, need to replace the path to the template
pathsStub = sinon.stub(ghost, "paths", function () { pathsStub = sandbox.stub(ghost, "paths", function () {
return { return {
// Forcing the theme path to be the same // Forcing the theme path to be the same
activeTheme: path.join(process.cwd(), testTemplatePath), activeTheme: path.join(process.cwd(), testTemplatePath),
@ -145,20 +147,18 @@ describe("Ghost API", function () {
templateFn().should.equal('<h1>HelloWorld</h1>'); templateFn().should.equal('<h1>HelloWorld</h1>');
done(); done();
}).then(null, done); }).then(null, done);
}); });
it("loads templates from themes first", function (done) { it("loads templates from themes first", function (done) {
var compileSpy = sinon.spy(ghost, 'compileTemplate'), var compileSpy = sandbox.spy(ghost, 'compileTemplate'),
pathsStub; pathsStub;
should.exist(ghost.loadTemplate, 'load template function exists'); should.exist(ghost.loadTemplate, 'load template function exists');
// In order for the test to work, need to replace the path to the template // In order for the test to work, need to replace the path to the template
pathsStub = sinon.stub(ghost, "paths", function () { pathsStub = sandbox.stub(ghost, "paths", function () {
return { return {
activeTheme: path.join(process.cwd(), themeTemplatePath), activeTheme: path.join(process.cwd(), themeTemplatePath),
helperTemplates: path.join(process.cwd(), testTemplatePath) helperTemplates: path.join(process.cwd(), testTemplatePath)

View File

@ -1,202 +1,98 @@
///*globals describe, beforeEach, it*/ /*globals describe, beforeEach, it*/
//var testUtils = require('./testUtils'), var testUtils = require('./testUtils'),
// should = require('should'), should = require('should'),
// sinon = require('sinon'), sinon = require('sinon'),
// when = require('when'), when = require('when'),
// _ = require("underscore"), _ = require("underscore"),
// errors = require('../../server/errorHandling'), errors = require('../../server/errorHandling'),
//
// // Stuff we are testing // Stuff we are testing
// knex = require("../../server/models/base").Knex, knex = require("../../server/models/base").Knex,
// migration = require('../../server/data/migration'), migration = require('../../server/data/migration'),
// exporter = require('../../server/data/export'), exporter = require('../../server/data/export'),
// importer = require('../../server/data/import'), importer = require('../../server/data/import'),
// Importer001 = require('../../server/data/import/001'), Importer000 = require('../../server/data/import/000'),
// Importer002 = require('../../server/data/import/002'), fixtures = require('../../server/data/fixtures'),
// Settings = require('../../server/models/settings').Settings; Settings = require('../../server/models/settings').Settings;
//
//describe("Import", function () { describe("Import", function () {
//
// should.exist(exporter); should.exist(exporter);
// should.exist(importer); should.exist(importer);
//
// beforeEach(function (done) { beforeEach(function (done) {
// // clear database... we need to initialise it manually for each test // clear database... we need to initialise it manually for each test
// testUtils.clearData().then(function () { testUtils.clearData().then(function () {
// done(); done();
// }, done); }, done);
// }); });
//
// it("resolves 001", function (done) { it("resolves 000", function (done) {
// var importStub = sinon.stub(Importer001, "importData", function () { var importStub = sinon.stub(Importer000, "importData", function () {
// return when.resolve(); return when.resolve();
// }), }),
// fakeData = { test: true }; fakeData = { test: true };
//
// importer("001", fakeData).then(function () { importer("000", fakeData).then(function () {
// importStub.calledWith(fakeData).should.equal(true); importStub.calledWith(fakeData).should.equal(true);
//
// importStub.restore(); importStub.restore();
//
// done(); done();
// }).then(null, done); }).then(null, done);
// }); });
//
// describe("001", function () { describe("000", function () {
// this.timeout(4000); this.timeout(4000);
//
// should.exist(Importer001); should.exist(Importer000);
//
// it("imports data from 001", function (done) { it("imports data from 000", function (done) {
// var exportData; var exportData;
//
// // initialise database to version 001 - confusingly we have to set the max version to be one higher // initialise database to version 000 - confusingly we have to set the max version to be one higher
// // than the migration version we want // than the migration version we want. Could just use migrate from fresh here... but this is more explicit
// migration.migrateUpFromVersion('001', '002').then(function () { migration.migrateUpFromVersion('000', '001').then(function () {
// return Settings.populateDefaults(); // Load the fixtures
// }).then(function () { return fixtures.populateFixtures();
// // export the version 001 data ready to import }).then(function () {
// // TODO: Should have static test data here? // Initialise the default settings
// return exporter("001"); return Settings.populateDefaults();
// }).then(function (exported) { }).then(function () {
// exportData = exported; // export the version 000 data ready to import
// // TODO: Should have static test data here?
// // Version 001 exporter required the database be empty... return exporter();
// var tables = [ }).then(function (exported) {
// 'posts', 'users', 'roles', 'roles_users', 'permissions', 'permissions_roles', exportData = exported;
// 'settings'
// ], return importer("000", exportData);
// truncateOps = _.map(tables, function (name) { }).then(function () {
// return knex(name).truncate(); // Grab the data from tables
// }); return when.all([
// knex("users").select(),
// return when.all(truncateOps); knex("posts").select(),
// }).then(function () { knex("settings").select(),
// return importer("001", exportData); knex("tags").select()
// }).then(function () { ]);
// // Grab the data from tables }).then(function (importedData) {
// return when.all([
// knex("users").select(), should.exist(importedData);
// knex("posts").select(), importedData.length.should.equal(4, 'Did not get data successfully');
// knex("settings").select()
// ]); // we always have 0 users as there isn't one in fixtures
// }).then(function (importedData) { importedData[0].length.should.equal(0, 'There should not be a user');
// // import no longer requires all data to be dropped, and adds posts
// should.exist(importedData); importedData[1].length.should.equal(exportData.data.posts.length + 1, 'Wrong number of posts');
// importedData.length.should.equal(3);
// // test settings
// // we always have 0 users as there isn't one in fixtures importedData[2].length.should.be.above(0, 'Wrong number of settings');
// importedData[0].length.should.equal(0); _.findWhere(importedData[2], {key: "databaseVersion"}).value.should.equal("000", 'Wrong database version');
// importedData[1].length.should.equal(exportData.data.posts.length);
// importedData[2].length.should.be.above(0); // test tags
// importedData[3].length.should.equal(exportData.data.tags.length, 'no new tags');
// _.findWhere(exportData.data.settings, {key: "databaseVersion"}).value.should.equal("001");
// done();
// done(); }).then(null, done);
// }).then(null, done); });
// }); });
// }); });
//
// it("resolves 002", function (done) {
// var importStub = sinon.stub(Importer002, "importData", function () {
// return when.resolve();
// }),
// fakeData = { test: true };
//
// importer("002", fakeData).then(function () {
// importStub.calledWith(fakeData).should.equal(true);
//
// importStub.restore();
//
// done();
// }).then(null, done);
// });
//
// describe("002", function () {
// this.timeout(4000);
//
// should.exist(Importer002);
//
// it("imports data from 001", function (done) {
// var exportData;
//
// // initialise database to version 001 - confusingly we have to set the max version to be one higher
// // than the migration version we want
// migration.migrateUpFromVersion('001', '002').then(function () {
// return Settings.populateDefaults();
// }).then(function () {
// // export the version 001 data ready to import
// // TODO: Should have static test data here?
// return exporter("001");
// }).then(function (exported) {
// exportData = exported;
//
// // now migrate up to the proper version ready for importing - confusingly we have to set the max version
// // to be one higher than the migration version we want
// return migration.migrateUpFromVersion('002', '003');
// }).then(function () {
// return importer("002", exportData);
// }).then(function () {
// // Grab the data from tables
// return when.all([
// knex("users").select(),
// knex("posts").select(),
// knex("settings").select()
// ]);
// }).then(function (importedData) {
//
// should.exist(importedData);
// importedData.length.should.equal(3);
//
// // we always have 0 users as there isn't one in fixtures
// importedData[0].length.should.equal(0);
// // import no longer requires all data to be dropped, and adds posts
// importedData[1].length.should.equal(exportData.data.posts.length + 1);
// importedData[2].length.should.be.above(0);
//
// _.findWhere(importedData[2], {key: "databaseVersion"}).value.should.equal("002");
//
// done();
// }).then(null, done);
// });
//
// it("imports data from 002", function (done) {
// var exportData;
//
// // initialise database to version 001 - confusingly we have to set the max version to be one higher
// // than the migration version we want
// migration.migrateUpFromVersion('001', '003').then(function () {
// return Settings.populateDefaults();
// }).then(function () {
// // export the version 002 data ready to import
// // TODO: Should have static test data here?
// return exporter("002");
// }).then(function (exported) {
// exportData = exported;
//
// return importer("002", exportData);
// }).then(function () {
// // Grab the data from tables
// return when.all([
// knex("users").select(),
// knex("posts").select(),
// knex("settings").select()
// ]);
// }).then(function (importedData) {
//
// should.exist(importedData);
// importedData.length.should.equal(3);
//
// // we always have 0 users as there isn't one in fixtures
// importedData[0].length.should.equal(0);
// // import no longer requires all data to be dropped, and adds posts
// importedData[1].length.should.equal(exportData.data.posts.length + 1);
// importedData[2].length.should.be.above(0);
//
// _.findWhere(importedData[2], {key: "databaseVersion"}).value.should.equal("002");
//
// done();
// }).then(null, done);
// });
// });
//});

View File

@ -94,7 +94,7 @@ describe('permissions', function () {
.then(function (actionsMap) { .then(function (actionsMap) {
should.exist(actionsMap); should.exist(actionsMap);
actionsMap.edit.should.eql(['post', 'tag', 'user', 'page']); actionsMap.edit.sort().should.eql(['post', 'tag', 'user', 'page'].sort());
actionsMap.should.equal(permissions.actionsMap); actionsMap.should.equal(permissions.actionsMap);

View File

@ -189,10 +189,16 @@ describe('Core Helpers', function () {
}); });
it('returns meta tag string', function () { it('returns meta tag string', function () {
var rendered = handlebars.helpers.ghost_head.call({version: "0.3"}); var rendered = handlebars.helpers.ghost_head.call({version: "0.3.0"});
should.exist(rendered); should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.3" />\n<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss/">'); rendered.string.should.equal('<meta name="generator" content="Ghost 0.3" />\n<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss/">');
}); });
it('returns meta tag string even if version is invalid', function () {
var rendered = handlebars.helpers.ghost_head.call({version: "0.9"});
should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.9" />\n<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss/">');
});
}); });
describe('ghost_foot Helper', function () { describe('ghost_foot Helper', function () {

View File

@ -252,4 +252,19 @@ describe("Github showdown extensions", function () {
}); });
}); });
it("should not output image if there is no src", function () {
var testPhrases = [
{
input: "![anything here]()",
output: /^$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = _ConvertPhrase(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
}); });

View File

@ -37,7 +37,7 @@ sampleUser = function (i) {
email: "joe_" + i + "@bloggs.com", email: "joe_" + i + "@bloggs.com",
password: "$2a$10$c5G9RS5.dXRt3UqvZ5wNgOLQLc7ZFc2DJo01du0oLT1YYOM67KJMe", password: "$2a$10$c5G9RS5.dXRt3UqvZ5wNgOLQLc7ZFc2DJo01du0oLT1YYOM67KJMe",
created_by: 1, created_by: 1,
created_at: 1234567890 created_at: new Date()
}; };
}; };

View File

@ -2,7 +2,7 @@
// Orchestrates the loading of Ghost // Orchestrates the loading of Ghost
var configLoader = require('./core/config-loader.js'), var configLoader = require('./core/config-loader.js'),
error = require('./core/server/errorHandling'); error = require('./core/server/errorHandling');
// If no env is set, default to development // If no env is set, default to development
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';

View File

@ -1,6 +1,6 @@
{ {
"name": "ghost", "name": "ghost",
"version": "0.3.0", "version": "0.3.1",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node index", "start": "node index",