Merge branch 'master' into ember

Conflicts:
	bower.json
	core/client/views/editor.js
This commit is contained in:
Hannah Wolfe 2014-03-16 20:52:44 +00:00
commit 79a333b480
20 changed files with 645 additions and 340 deletions

2
.gitignore vendored
View File

@ -14,7 +14,7 @@ results
npm-debug.log
node_modules
bower_components
.bowerrc
.idea/*
*.iml
projectFilesBackup

View File

@ -76,6 +76,8 @@ var path = require('path'),
files: [
// Theme CSS
'content/themes/casper/css/*.css',
// Ghost UI CSS
'bower_components/ghost-ui/dist/css/*.css',
// Theme JS
'content/themes/casper/js/*.js',
// Admin JS
@ -1021,4 +1023,4 @@ var path = require('path'),
grunt.registerTask('default', 'Build JS & templates for development', ['update_submodules', 'handlebars', 'concat', 'copy:dev', 'emberBuild']);
};
module.exports = configureGrunt;
module.exports = configureGrunt;

View File

@ -1,4 +1,4 @@
Copyright (c) 2014 Ghost Foundation - Released under The MIT License.
Copyright (c) 2013-2014 Ghost Foundation - Released under The MIT License.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
@ -19,4 +19,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
OTHER DEALINGS IN THE SOFTWARE.

View File

@ -83,4 +83,4 @@ Constructed with the following guidelines:
## Copyright & License
Copyright (C) 2014 Ghost Foundation - Released under the [MIT license](LICENSE).
Copyright (c) 2013-2014 Ghost Foundation - Released under the [MIT license](LICENSE).

View File

@ -7,10 +7,9 @@
"ember": "~1.4.0",
"ember-resolver": "git://github.com/stefanpenner/ember-jj-abrams-resolver.git#9805033c178e7f857f801359664adb599444b430",
"fastclick": "1.0.0",
"ghost-ui": "0.1.0",
"ghost-ui": "0.1.2",
"handlebars": "~1.1.2",
"ic-ajax": "1.0.1",
"iCheck": "1.0.1",
"jquery": "1.11.0",
"jquery-file-upload": "9.5.6",
"jquery-hammerjs": "1.0.1",

View File

@ -1,114 +1,69 @@
<section class="markdown-help-container">
<table class="modal-markdown-help-table">
<thead>
<tr>
<th>Result</th>
<th>Markdown</th>
<th>Shortcut</th>
</tr>
<tr>
<th>Result</th>
<th>Markdown</th>
<th>Shortcut</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Bold</strong></td>
<td>**text**</td>
<td>Ctrl / Cmd + B</td>
</tr>
<tr>
<td><em>Emphasize</em></td>
<td>*text*</td>
<td>Ctrl / Cmd + I</td>
</tr>
<tr>
<td><code>Inline Code</code></td>
<td>`code`</td>
<td>Cmd + K / Ctrl + Shift + K</td>
</tr>
<tr>
<td>Strike-through</td>
<td>~~text~~</td>
<td>Ctrl + Alt + U</td>
</tr>
<tr>
<td><a href="#">Link</a></td>
<td>[title](http://)</td>
<td>Ctrl + Shift + L</td>
</tr>
<tr>
<td>Image</td>
<td>![alt](http://)</td>
<td>Ctrl + Shift + I</td>
</tr>
<tr>
<td>List</td>
<td>* item</td>
<td>Ctrl + L</td>
</tr>
<tr>
<td>Blockquote</td>
<td>> quote</td>
<td>Ctrl + Q</td>
</tr>
<tr>
<td>H1</td>
<td># Heading</td>
<td>Ctrl + Alt + 1</td>
</tr>
<tr>
<td>H2</td>
<td>## Heading</td>
<td>Ctrl + Alt + 2</td>
</tr>
<tr>
<td>H3</td>
<td>### Heading</td>
<td>Ctrl + Alt + 3</td>
</tr>
<tr>
<td>H4</td>
<td>#### Heading</td>
<td>Ctrl + Alt + 4</td>
</tr>
<tr>
<td>H5</td>
<td>##### Heading</td>
<td>Ctrl + Alt + 5</td>
</tr>
<tr>
<td>H6</td>
<td>###### Heading</td>
<td>Ctrl + Alt + 6</td>
</tr>
<tr>
<td>Select Word</td>
<td></td>
<td>Ctrl + Alt + W</td>
</tr>
<tr>
<td>New Paragraph</td>
<td></td>
<td>Ctrl / Cmd + Enter</td>
</tr>
<tr>
<td>Uppercase</td>
<td></td>
<td>Ctrl + U</td>
</tr>
<tr>
<td>Lowercase</td>
<td></td>
<td>Ctrl + Shift + U</td>
</tr>
<tr>
<td>Titlecase</td>
<td></td>
<td>Ctrl + Alt + Shift + U</td>
</tr>
<tr>
<td>Insert Current Date</td>
<td></td>
<td>Ctrl + Shift + 1</td>
</tr>
<tr>
<td><strong>Bold</strong></td>
<td>**text**</td>
<td>Ctrl / Cmd + B</td>
</tr>
<tr>
<td><em>Emphasize</em></td>
<td>*text*</td>
<td>Ctrl / Cmd + I</td>
</tr>
<tr>
<td>Strike-through</td>
<td>~~text~~</td>
<td>Ctrl + Alt + U</td>
</tr>
<tr>
<td><a href="#">Link</a></td>
<td>[title](http://)</td>
<td>Ctrl + Shift + L</td>
</tr>
<tr>
<td>Image</td>
<td>![alt](http://)</td>
<td>Ctrl + Shift + I</td>
</tr>
<tr>
<td>List</td>
<td>* item</td>
<td>Ctrl + L</td>
</tr>
<tr>
<td>Blockquote</td>
<td>> quote</td>
<td>Ctrl + Q</td>
</tr>
<tr>
<td>H1</td>
<td># Heading</td>
<td>Ctrl + Alt + 1</td>
</tr>
<tr>
<td>H2</td>
<td>## Heading</td>
<td>Ctrl + Alt + 2</td>
</tr>
<tr>
<td>H3</td>
<td>### Heading</td>
<td>Ctrl + Alt + 3</td>
</tr>
<tr>
<td><code>Inline Code</code></td>
<td>`code`</td>
<td>Cmd + K / Ctrl + Shift + K</td>
</tr>
</tbody>
</table>
For further Markdown syntax reference: <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Documentation</a>
</section>
</section>

View File

@ -387,7 +387,6 @@
model: {
options: {
close: true,
type: "info",
style: ["wide"],
animation: 'fade'
},
@ -531,7 +530,6 @@
model: {
options: {
close: true,
type: "info",
style: ["wide"],
animation: 'fade'
},

View File

@ -2,17 +2,31 @@
// Orchestrates the loading of Ghost
// When run from command line.
var bootstrap = require('./bootstrap'),
errors = require('./server/errorHandling');
var when = require('when'),
bootstrap = require('./bootstrap');
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
function startGhost(options) {
// When we no longer need to require('./server')
// in a callback this extra deferred object
// won't be necessary, we'll just be able to return
// the server object directly.
var deferred = when.defer();
options = options || {};
bootstrap(options.config).then(function () {
var ghost = require('./server');
ghost(options.app);
}).otherwise(errors.logAndThrowError);
return ghost(options.app).then(deferred.resolve).otherwise(function (e) {
// We don't return the rejected promise to stop
// the propogation of the rejection and just
// allow the user to manage what to do.
deferred.reject(e);
});
});
return deferred.promise;
}
module.exports = startGhost;

View File

@ -14,6 +14,14 @@ var path = require('path'),
appRoot = path.resolve(__dirname, '../../../'),
corePath = path.resolve(appRoot, 'core/');
// Are we using sockets? Custom socket or the default?
function getSocket() {
if (ghostConfig.server.hasOwnProperty('socket')) {
return _.isString(ghostConfig.server.socket) ? ghostConfig.server.socket : path.join(ghostConfig.paths.contentPath, process.env.NODE_ENV + '.socket');
}
return false;
}
function updateConfig(config) {
var localPath,
contentPath,
@ -110,5 +118,6 @@ function config() {
module.exports = config;
module.exports.init = initConfig;
module.exports.theme = theme;
module.exports.getSocket = getSocket;
module.exports.urlFor = configUrl.urlFor;
module.exports.urlForPost = configUrl.urlForPost;

View File

@ -252,11 +252,16 @@ frontendControllers = {
'rss': function (req, res, next) {
// Initialize RSS
var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
feed;
tagParam = req.params.slug;
// No negative pages, or page 1
if (isNaN(pageParam) || pageParam < 1 || (pageParam === 1 && req.route.path === '/rss/:page/')) {
return res.redirect(config().paths.subdir + '/rss/');
if (isNaN(pageParam) || pageParam < 1 ||
(pageParam === 1 && (req.route.path === '/rss/:page/' || req.route.path === '/tag/:slug/rss/:page/'))) {
if (tagParam !== undefined) {
return res.redirect(config().paths.subdir + '/tag/' + tagParam + '/rss/');
} else {
return res.redirect(config().paths.subdir + '/rss/');
}
}
// TODO: needs refactor for multi user to not use first user as default
@ -266,25 +271,37 @@ frontendControllers = {
api.settings.read('description'),
api.settings.read('permalinks')
]).then(function (result) {
var user = result[0].value,
title = result[1].value.value,
description = result[2].value.value,
permalinks = result[3].value,
siteUrl = config.urlFor('home', null, true),
feedUrl = config.urlFor('rss', null, true);
feed = new RSS({
title: title,
description: description,
generator: 'Ghost v' + res.locals.version,
feed_url: feedUrl,
site_url: siteUrl,
ttl: '60'
});
var options = {};
if (pageParam) { options.page = pageParam; }
if (tagParam) { options.tag = tagParam; }
return api.posts.browse(options).then(function (page) {
var user = result[0].value,
title = result[1].value.value,
description = result[2].value.value,
permalinks = result[3].value,
siteUrl = config.urlFor('home', null, true),
feedUrl = config.urlFor('rss', null, true),
maxPage = page.pages,
feedItems = [],
feed;
if (tagParam) {
title = page.aspect.tag.name + ' - ' + title;
feedUrl = feedUrl + 'tag/' + page.aspect.tag.slug + '/';
}
feed = new RSS({
title: title,
description: description,
generator: 'Ghost v' + res.locals.version,
feed_url: feedUrl,
site_url: siteUrl,
ttl: '60'
});
return api.posts.browse({page: pageParam}).then(function (page) {
var maxPage = page.pages,
feedItems = [];
// A bit of a hack for situations with no content.
if (maxPage === 0) {
@ -294,14 +311,18 @@ frontendControllers = {
// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return res.redirect(config().paths.subdir + '/rss/' + maxPage + '/');
if (tagParam) {
return res.redirect(config().paths.subdir + '/tag/' + tagParam + '/rss/' + maxPage + '/');
} else {
return res.redirect(config().paths.subdir + '/rss/' + maxPage + '/');
}
}
filters.doFilter('prePostsRender', page.posts).then(function (posts) {
posts.forEach(function (post) {
var deferred = when.defer(),
item = {
title: _.escape(post.title),
title: post.title,
guid: post.uuid,
url: config.urlFor('post', {post: post, permalinks: permalinks}, true),
date: post.published_at,

View File

@ -4,7 +4,6 @@ var crypto = require('crypto'),
hbs = require('express-hbs'),
fs = require('fs'),
uuid = require('node-uuid'),
path = require('path'),
Polyglot = require('node-polyglot'),
semver = require('semver'),
_ = require('lodash'),
@ -22,7 +21,6 @@ var crypto = require('crypto'),
routes = require('./routes'),
packageInfo = require('../../package.json'),
// Variables
dbHash;
@ -107,21 +105,92 @@ function builtFilesExist() {
return when.all(deferreds);
}
function startGhost(deferred) {
return function () {
// Tell users if their node version is not supported, and exit
if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) {
console.log(
"\nERROR: Unsupported version of Node".red,
"\nGhost needs Node version".red,
packageInfo.engines.node.yellow,
"you are using version".red,
process.versions.node.yellow,
"\nPlease go to http://nodejs.org to get a supported version".green
);
process.exit(0);
}
// Startup & Shutdown messages
if (process.env.NODE_ENV === 'production') {
console.log(
"Ghost is running...".green,
"\nYour blog is now available on",
config().url,
"\nCtrl+C to shut down".grey
);
// ensure that Ghost exits correctly on Ctrl+C
process.on('SIGINT', function () {
console.log(
"\nGhost has shut down".red,
"\nYour blog is now offline"
);
process.exit(0);
});
} else {
console.log(
("Ghost is running in " + process.env.NODE_ENV + "...").green,
"\nListening on",
config.getSocket() || config().server.host + ':' + config().server.port,
"\nUrl configured as:",
config().url,
"\nCtrl+C to shut down".grey
);
// ensure that Ghost exits correctly on Ctrl+C
process.on('SIGINT', function () {
console.log(
"\nGhost has shutdown".red,
"\nGhost was running for",
Math.round(process.uptime()),
"seconds"
);
process.exit(0);
});
}
deferred.resolve();
};
}
// ## Initializes the ghost application.
// Sets up the express server instance.
// Instantiates the ghost singleton, helpers, routes, middleware, and apps.
// Finally it starts the http server.
function setup(server) {
function init(server) {
// create a hash for cache busting assets
var assetHash = (crypto.createHash('md5').update(packageInfo.version + Date.now()).digest('hex')).substring(0, 10);
// If no express instance is passed in
// then create our own
if (!server) {
server = express();
}
// Set up Polygot instance on the require module
Polyglot.instance = new Polyglot();
// ### Initialisation
// The server and its dependencies require a populated config
// It returns a promise that is resolved when the application
// has finished starting up.
// Initialise the models
models.init().then(function () {
// Make sure javascript files have been built via grunt concat
return builtFilesExist().then(function () {
// Initialise the models
return models.init();
}).then(function () {
// Populate any missing default settings
return models.Settings.populateDefaults();
}).then(function () {
@ -136,19 +205,17 @@ function setup(server) {
// Check for or initialise a dbHash.
initDbHashAndFirstRun(),
// Initialize the permissions actions and objects
permissions.init()
permissions.init(),
// Initialize mail
mailer.init(),
// Initialize apps
apps.init()
);
}).then(function () {
// Make sure javascript files have been built via grunt concat
return builtFilesExist();
}).then(function () {
// Initialize mail
return mailer.init();
}).then(function () {
var adminHbs = hbs.create();
var adminHbs = hbs.create(),
deferred = when.defer();
// ##Configuration
server.set('version hash', assetHash);
// return the correct mime type for woff filess
express['static'].mime.define({'application/font-woff': ['woff']});
@ -177,111 +244,37 @@ function setup(server) {
// Set up Frontend routes
routes.frontend(server);
// Are we using sockets? Custom socket or the default?
function getSocket() {
if (config().server.hasOwnProperty('socket')) {
return _.isString(config().server.socket) ? config().server.socket : path.join(config.path().contentPath, process.env.NODE_ENV + '.socket');
}
return false;
}
function startGhost() {
// Tell users if their node version is not supported, and exit
if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) {
console.log(
"\nERROR: Unsupported version of Node".red,
"\nGhost needs Node version".red,
packageInfo.engines.node.yellow,
"you are using version".red,
process.versions.node.yellow,
"\nPlease go to http://nodejs.org to get a supported version".green
);
process.exit(0);
}
// Startup & Shutdown messages
if (process.env.NODE_ENV === 'production') {
console.log(
"Ghost is running...".green,
"\nYour blog is now available on",
config().url,
"\nCtrl+C to shut down".grey
);
// ensure that Ghost exits correctly on Ctrl+C
process.on('SIGINT', function () {
console.log(
"\nGhost has shut down".red,
"\nYour blog is now offline"
);
process.exit(0);
});
} else {
console.log(
("Ghost is running in " + process.env.NODE_ENV + "...").green,
"\nListening on",
getSocket() || config().server.host + ':' + config().server.port,
"\nUrl configured as:",
config().url,
"\nCtrl+C to shut down".grey
);
// ensure that Ghost exits correctly on Ctrl+C
process.on('SIGINT', function () {
console.log(
"\nGhost has shutdown".red,
"\nGhost was running for",
Math.round(process.uptime()),
"seconds"
);
process.exit(0);
});
}
}
// Initialize apps then start the server
apps.init().then(function () {
// ## Start Ghost App
if (getSocket()) {
// Make sure the socket is gone before trying to create another
fs.unlink(getSocket(), function (err) {
/*jshint unused:false*/
server.listen(
getSocket(),
startGhost
);
fs.chmod(getSocket(), '0660');
});
} else {
server.listen(
config().server.port,
config().server.host,
startGhost
);
}
_.each(config().paths.availableThemes._messages.errors, function (error) {
errors.logError(error.message, error.context);
});
_.each(config().paths.availableThemes._messages.warns, function (warn) {
errors.logWarn(warn.message, warn.context);
});
// Log all theme errors and warnings
_.each(config().paths.availableThemes._messages.errors, function (error) {
errors.logError(error.message, error.context);
});
}, function (err) {
errors.logErrorAndExit(err, err.context, err.help);
_.each(config().paths.availableThemes._messages.warns, function (warn) {
errors.logWarn(warn.message, warn.context);
});
// ## Start Ghost App
if (config.getSocket()) {
// Make sure the socket is gone before trying to create another
fs.unlink(config.getSocket(), function (err) {
/*jshint unused:false*/
server.listen(
config.getSocket(),
startGhost(deferred)
);
fs.chmod(config.getSocket(), '0660');
});
} else {
server.listen(
config().server.port,
config().server.host,
startGhost(deferred)
);
}
return deferred.promise;
});
}
// Initializes the ghost application.
function init(app) {
if (!app) {
app = express();
}
// The server and its dependencies require a populated config
setup(app);
}
module.exports = init;

View File

@ -194,6 +194,41 @@ function checkSSL(req, res, next) {
next();
}
// ### Robots Middleware
// Handle requests to robots.txt and cache file
function robots() {
var content, // file cache
filePath = path.join(config().paths.corePath, '/shared/robots.txt');
return function robots(req, res, next) {
if ('/robots.txt' === req.url) {
if (content) {
res.writeHead(200, content.headers);
res.end(content.body);
} else {
fs.readFile(filePath, function (err, buf) {
if (err) {
return next(err);
}
content = {
headers: {
'Content-Type': 'text/plain',
'Content-Length': buf.length,
'Cache-Control': 'public, max-age=' + ONE_YEAR_MS / 1000
},
body: buf
};
res.writeHead(200, content.headers);
res.end(content.body);
});
}
} else {
next();
}
};
}
module.exports = function (server, dbHash) {
var logging = config().logging,
subdir = config().paths.subdir,
@ -229,7 +264,6 @@ module.exports = function (server, dbHash) {
// First determine whether we're serving admin or theme content
expressServer.use(manageAdminAndTheme);
// Admin only config
expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/clientold/assets'), {maxAge: ONE_YEAR_MS})));
expressServer.use(subdir + '/ghost/ember', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/client/assets'), {maxAge: ONE_YEAR_MS})));
@ -243,6 +277,9 @@ module.exports = function (server, dbHash) {
// Theme only config
expressServer.use(subdir, middleware.whenEnabled(expressServer.get('activeTheme'), middleware.staticTheme()));
// Serve robots.txt if not found in theme
expressServer.use(robots());
// Add in all trailing slashes
expressServer.use(slashes(true, {headers: {'Cache-Control': 'public, max-age=' + ONE_YEAR_S}}));

View File

@ -14,11 +14,6 @@ module.exports = {
init: function () {
return migrations.init();
},
reset: function () {
return migrations.reset().then(function () {
return migrations.init();
});
},
// ### deleteAllContent
// Delete all content from the database (posts, tags, tags_posts)
deleteAllContent: function () {

View File

@ -6,6 +6,8 @@ module.exports = function (server) {
// ### Frontend routes
server.get('/rss/', frontend.rss);
server.get('/rss/:page/', frontend.rss);
server.get('/tag/:slug/rss/', frontend.rss);
server.get('/tag/:slug/rss/:page/', frontend.rss);
server.get('/tag/:slug/page/:page/', frontend.tag);
server.get('/tag/:slug/', frontend.tag);
server.get('/page/:page/', frontend.homepage);

View File

@ -4,32 +4,34 @@
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="csrf-param" content="{{csrfToken}}">
<meta name="csrf-param" content="{{csrfToken}}" />
<title>Ghost Admin</title>
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui" />
<meta http-equiv="cleartype" content="on">
<meta http-equiv="cleartype" content="on" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Ghost" />
<link rel="shortcut icon" href="{{asset "favicon.ico"}}">
<link rel="shortcut icon" href="{{asset "favicon.ico"}}" />
<link rel="apple-touch-icon-precomposed" href="{{asset "img/touch-icon-iphone.png" ember="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{{asset "img/touch-icon-ipad.png" ember="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{{asset "img/small.png" ember="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{asset "img/medium.png" ember="true"}}" />
<meta name="application-name" content="Ghost"/>
<meta name="msapplication-TileColor" content="#ffffff"/>
<meta name="application-name" content="Ghost" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-square70x70logo" content="{{asset "img/small.png" ember="true"}}" />
<meta name="msapplication-square150x150logo" content="{{asset "img/medium.png" ember="true"}}" />
<meta name="msapplication-square310x310logo" content="{{asset "img/large.png" ember="true"}}" />
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans:400,300,700">
<link rel="stylesheet" href="{{asset "css/ghost-ui.min.css" ember="true"}}">
<link rel="stylesheet" href="{{asset "css/ember-hacks.css" ember="true"}}">
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans:400,300,700" />
<link rel="stylesheet" href="{{asset "css/ghost-ui.min.css" ember="true"}}" />
<link rel="stylesheet" href="{{asset "css/ember-hacks.css" ember="true"}}" />
</head>
<body class="{{bodyClass}}{{update_notification classOnly="true"}}">

View File

@ -4,31 +4,33 @@
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="csrf-param" content="{{csrfToken}}">
<meta name="csrf-param" content="{{csrfToken}}" />
<title>Ghost Admin</title>
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1, minimal-ui" />
<meta http-equiv="cleartype" content="on">
<meta http-equiv="cleartype" content="on" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="Ghost" />
<link rel="shortcut icon" href="{{asset "favicon.ico"}}">
<link rel="shortcut icon" href="{{asset "favicon.ico"}}" />
<link rel="apple-touch-icon-precomposed" href="{{asset "img/touch-icon-iphone.png" ghost="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{{asset "img/touch-icon-ipad.png" ghost="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{{asset "img/small.png" ghost="true"}}" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{asset "img/medium.png" ghost="true"}}" />
<meta name="application-name" content="Ghost"/>
<meta name="msapplication-TileColor" content="#ffffff"/>
<meta name="application-name" content="Ghost" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-square70x70logo" content="{{asset "img/small.png" ghost="true"}}" />
<meta name="msapplication-square150x150logo" content="{{asset "img/medium.png" ghost="true"}}" />
<meta name="msapplication-square310x310logo" content="{{asset "img/large.png" ghost="true"}}" />
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans:400,300,700">
<link rel="stylesheet" href="{{asset "css/ghost-ui.min.css" ghost="true"}}">
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Open+Sans:400,300,700" />
<link rel="stylesheet" href="{{asset "css/ghost-ui.min.css" ghost="true"}}" />
</head>
<body class="{{bodyClass}}{{update_notification classOnly="true"}}">
{{#unless hideNavbar}}

View File

@ -91,6 +91,81 @@ CasperTest.begin("Word count and plurality", 4, function suite(test) {
});
});
CasperTest.begin("Image Uploads", 14, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
// Test standard image upload modal
casper.then(function () {
casper.writeContentToCodeMirror("![]()");
});
function assertEmptyImageUploaderDisplaysCorrectly() {
test.assertExists(".entry-preview .js-upload-target", "Upload target exists");
test.assertExists(".entry-preview .js-fileupload", "File upload target exists");
test.assertExists(".entry-preview .image-url", "Image URL button exists");
};
casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", assertEmptyImageUploaderDisplaysCorrectly);
// Test image URL upload modal
casper.thenClick(".entry-preview .image-uploader a.image-url");
casper.waitForSelector(".image-uploader-url", function onSuccess() {
test.assertExists(".image-uploader-url .url.js-upload-url", "Image URL uploader exists")
test.assertExists(".image-uploader-url .button-save.js-button-accept", "Image URL accept button exists")
test.assertExists(".image-uploader-url .image-upload", "Back to normal image upload style button exists")
});
// Test image source location
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
var testFileLocation = "test/file/location";
casper.then(function () {
var markdownImageString = "![](" + testFileLocation + ")";
casper.writeContentToCodeMirror(markdownImageString);
});
casper.waitForSelector(".entry-preview .js-drop-zone.pre-image-uploader", function onSuccess() {
var imageJQuerySelector = ".entry-preview img.js-upload-target[src='" + testFileLocation + "']"
test.assertExists(imageJQuerySelector, "Uploaded image tag properly links to source location");
});
// Test cancel image button
casper.thenClick(".pre-image-uploader a.image-cancel.js-cancel");
casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", assertEmptyImageUploaderDisplaysCorrectly);
// Test image url source location
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.then(function () {
casper.writeContentToCodeMirror("![]()");
});
casper.waitForSelector(".entry-preview .js-drop-zone.image-uploader", function onSuccess() {
casper.thenClick(".entry-preview .image-uploader a.image-url");
});
var imageURL = "random.url";
casper.waitForSelector(".image-uploader-url", function onSuccess() {
casper.sendKeys(".image-uploader-url input.url.js-upload-url", imageURL);
casper.thenClick(".js-button-accept.button-save");
});
casper.waitForSelector(".entry-preview .js-drop-zone.pre-image-uploader", function onSuccess() {
var imageJQuerySelector = ".entry-preview img.js-upload-target[src='" + imageURL + "']"
test.assertExists(imageJQuerySelector, "Uploaded image tag properly links to inputted image URL");
});
});
CasperTest.begin('Required Title', 4, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
@ -131,6 +206,117 @@ CasperTest.begin('Title Trimming', 2, function suite(test) {
});
});
CasperTest.begin("Tag editor", 6, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
var tagName = "someTagName";
casper.then(function () {
test.assertExists("#entry-tags", "should have tag label area");
test.assertExists("#entry-tags .tag-label", "should have tag label icon");
test.assertExists("#entry-tags input.tag-input", "should have tag input area");
casper.sendKeys("#entry-tags input.tag-input", tagName);
casper.sendKeys("#entry-tags input.tag-input", casper.page.event.key.Enter);
});
var createdTagSelector = "#entry-tags .tags .tag";
casper.waitForSelector(createdTagSelector, function onSuccess() {
test.assertSelectorHasText(createdTagSelector, tagName, "typing enter after tag name should create tag");
});
casper.thenClick(createdTagSelector);
casper.then(function () {
test.assertDoesntExist(createdTagSelector, "clicking the tag should delete the tag");
});
});
CasperTest.begin("Post settings menu", 17, function suite(test) {
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
});
casper.then(function () {
test.assertExists("#publish-bar a.post-settings", "icon toggle should exist");
test.assertNotVisible("#publish-bar .post-settings-menu", "popup menu should not be visible at startup");
test.assertExists(".post-settings-menu input#url", "url field exists");
test.assertExists(".post-settings-menu input#pub-date", "publication date field exists");
test.assertExists(".post-settings-menu input#static-page", "static page checkbox field exists");
test.assertExists(".post-settings-menu a.delete", "delete post button exists")
});
casper.thenClick("#publish-bar a.post-settings");
casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() {
test.assert(true, "popup menu should be visible after clicking post-settings icon");
test.assertNotVisible(".post-settings-menu a.delete", "delete post btn shouldn't be visible on unsaved drafts");
});
casper.thenClick("#publish-bar a.post-settings");
casper.waitWhileVisible("#publish-bar .post-settings-menu", function onSuccess() {
test.assert(true, "popup menu should not be visible after clicking post-settings icon");
});
// Enter a title and save draft so converting to/from static post
// will result in notifications and 'Delete This Post' button appears
casper.then(function (){
casper.sendKeys("#entry-title", "aTitle");
casper.thenClick(".js-publish-button");
});
casper.thenClick("#publish-bar a.post-settings");
casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() {
test.assertVisible(".post-settings-menu a.delete", "delete post button should be visible for saved drafts");
});
// Test Static Page conversion
casper.thenClick(".post-settings-menu #static-page");
var staticPageConversionText = "Successfully converted to static page.";
casper.waitForText(staticPageConversionText, function onSuccess() {
test.assertSelectorHasText(
".notification-success", staticPageConversionText, "correct static page conversion notification appears");
})
casper.thenClick(".post-settings-menu #static-page");
var postConversionText = "Successfully converted to post.";
casper.waitForText(postConversionText, function onSuccess() {
test.assertSelectorHasText(
".notification-success", postConversionText, "correct post conversion notification appears");
});
// Test Delete Post Modal
casper.thenClick(".post-settings-menu a.delete");
casper.waitUntilVisible("#modal-container", function onSuccess() {
test.assert(true, "delete post modal is visible after clicking delete");
test.assertSelectorHasText(
"#modal-container .modal-header",
"Are you sure you want to delete this post?",
"delete post modal header has correct text");
});
casper.thenClick("#modal-container .js-button-reject");
casper.waitWhileVisible("#modal-container", function onSuccess() {
test.assert(true, "clicking cancel should close the delete post modal");
});
casper.thenClick("#publish-bar a.post-settings");
casper.thenClick(".post-settings-menu a.delete");
casper.thenClick("#modal-container .js-button-accept");
casper.waitForUrl(/ghost\/content\/$/, function onSuccess() {
test.assert(true, "clicking the delete post button should bring us to the content page");
});
});
CasperTest.begin('Publish menu - new post', 10, function suite(test) {
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
test.assertTitle("Ghost Admin", 'Ghost admin has no title');

View File

@ -154,6 +154,89 @@ CasperTest.begin('Ensure general blog description field length validation', 3, f
}, 2000);
});
CasperTest.begin('Ensure image upload modals display correctly', 6, function suite(test) {
casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
});
function assertImageUploaderModalThenClose() {
test.assertExists('.js-drop-zone.image-uploader', 'Image drop zone modal renders correctly');
this.click('#modal-container .js-button-accept');
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
}, function onTimeout() {
test.fail('No success notification');
}, 1000);
};
// Test Blog Logo Upload Button
casper.waitForSelector('#general', function then() {
this.click('#general .js-modal-logo');
});
casper.waitForSelector('#modal-container .modal-content', assertImageUploaderModalThenClose,
function onTimeout() {
test.fail('No upload logo modal container appeared');
}, 1000);
// Test Blog Cover Upload Button
casper.then(function() {
this.click('#general .js-modal-cover');
});
casper.waitForSelector('#modal-container .modal-content', assertImageUploaderModalThenClose,
function onTimeout() {
test.fail('No upload cover modal container appeared');
}, 1000);
});
CasperTest.begin("User settings screen validates email", 6, function suite(test) {
var email, brokenEmail;
casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time");
});
casper.then(function setEmailToInvalid() {
email = casper.getElementInfo('#user-email').attributes.value;
brokenEmail = email.replace('.', '-');
casper.fillSelectors('.user-profile', {
'#user-email': brokenEmail
}, false);
});
casper.thenClick('#user .button-save');
casper.waitForResource('/users/');
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'No error notification :(');
});
casper.then(function resetEmailToValid() {
casper.fillSelectors('.user-profile', {
'#user-email': email
}, false);
});
casper.thenClick('#user .button-save');
casper.waitForResource(/users/);
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
test.assertSelectorDoesntHaveText('.notification-success', '[object Object]');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
});
CasperTest.begin('Ensure postsPerPage number field form validation', 3, function suite(test) {
casper.thenOpen(url + "ghost/settings/general/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
@ -217,52 +300,6 @@ CasperTest.begin('Ensure postsPerPage min of 0', 3, function suite(test) {
}, 2000);
});
CasperTest.begin("User settings screen validates email", 6, function suite(test) {
var email, brokenEmail;
casper.thenOpen(url + "ghost/settings/user/", function testTitleAndUrl() {
test.assertTitle("Ghost Admin", "Ghost admin has no title");
test.assertUrlMatch(/ghost\/settings\/user\/$/, "Ghost doesn't require login this time");
});
casper.then(function setEmailToInvalid() {
email = casper.getElementInfo('#user-email').attributes.value;
brokenEmail = email.replace('.', '-');
casper.fillSelectors('.user-profile', {
'#user-email': brokenEmail
}, false);
});
casper.thenClick('#user .button-save');
casper.waitForResource('/users/');
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Got error notification');
test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
}, function onTimeout() {
test.assert(false, 'No error notification :(');
});
casper.then(function resetEmailToValid() {
casper.fillSelectors('.user-profile', {
'#user-email': email
}, false);
});
casper.thenClick('#user .button-save');
casper.waitForResource(/users/);
casper.waitForSelector('.notification-success', function onSuccess() {
test.assert(true, 'Got success notification');
test.assertSelectorDoesntHaveText('.notification-success', '[object Object]');
}, function onTimeout() {
test.assert(false, 'No success notification :(');
});
});
CasperTest.begin("User settings screen shows remaining characters for Bio properly", 4, function suite(test) {
function getRemainingBioCharacterCount() {

View File

@ -171,7 +171,7 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should redirect to last page is page too high', function (done) {
it('should redirect to last page if page too high', function (done) {
request.get('/page/4/')
.expect('Location', '/page/3/')
.expect('Cache-Control', cacheRules['public'])
@ -179,7 +179,7 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should redirect to first page is page too low', function (done) {
it('should redirect to first page if page too low', function (done) {
request.get('/page/0/')
.expect('Location', '/')
.expect('Cache-Control', cacheRules['public'])
@ -214,7 +214,7 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should redirect to last page is page too high', function (done) {
it('should redirect to last page if page too high', function (done) {
request.get('/rss/3/')
.expect('Location', '/rss/2/')
.expect('Cache-Control', cacheRules['public'])
@ -222,7 +222,7 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should redirect to first page is page too low', function (done) {
it('should redirect to first page if page too low', function (done) {
request.get('/rss/0/')
.expect('Location', '/rss/')
.expect('Cache-Control', cacheRules['public'])
@ -231,6 +231,49 @@ describe('Frontend Routing', function () {
});
});
describe('Tag based RSS pages', function () {
it('should redirect without slash', function (done) {
request.get('/tag/getting-started/rss')
.expect('Location', '/tag/getting-started/rss/')
.expect('Cache-Control', cacheRules.year)
.expect(301)
.end(doEnd(done));
});
it('should respond with xml', function (done) {
request.get('/tag/getting-started/rss/')
.expect('Content-Type', /xml/)
.expect('Cache-Control', cacheRules['public'])
.expect(200)
.end(doEnd(done));
});
it('should redirect page 1', function (done) {
request.get('/tag/getting-started/rss/1/')
.expect('Location', '/tag/getting-started/rss/')
.expect('Cache-Control', cacheRules['public'])
// TODO: This should probably be a 301?
.expect(302)
.end(doEnd(done));
});
it('should redirect to last page if page too high', function (done) {
request.get('/tag/getting-started/rss/2/')
.expect('Location', '/tag/getting-started/rss/1/')
.expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
it('should redirect to first page if page too low', function (done) {
request.get('/tag/getting-started/rss/0/')
.expect('Location', '/tag/getting-started/rss/')
.expect('Cache-Control', cacheRules['public'])
.expect(302)
.end(doEnd(done));
});
});
describe('Static page', function () {
it('should redirect without slash', function (done) {
request.get('/static-page-test')
@ -282,6 +325,13 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should retrieve default robots.txt', function (done) {
request.get('/robots.txt')
.expect('Cache-Control', cacheRules.year)
.expect(200)
.end(doEnd(done));
})
// at the moment there is no image fixture to test
// it('should retrieve image assets', function (done) {
// request.get('/content/images/some.jpg')
@ -335,7 +385,7 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should redirect to last page is page too high', function (done) {
it('should redirect to last page if page too high', function (done) {
request.get('/tag/injection/page/4/')
.expect('Location', '/tag/injection/page/2/')
.expect('Cache-Control', cacheRules['public'])
@ -343,7 +393,7 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
it('should redirect to first page is page too low', function (done) {
it('should redirect to first page if page too low', function (done) {
request.get('/tag/injection/page/0/')
.expect('Location', '/tag/injection/')
.expect('Cache-Control', cacheRules['public'])

View File

@ -2,6 +2,9 @@
// Orchestrates the loading of Ghost
// When run from command line.
var ghost = require('./core');
var ghost = require('./core'),
errors = require('./core/server/errorHandling');
ghost();
ghost().otherwise(function (err) {
errors.logErrorAndExit(err, err.context, err.help);
});