diff --git a/.gitignore b/.gitignore
index f24e41bb3d6e..bb8005fc7f54 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,7 +14,7 @@ results
npm-debug.log
node_modules
bower_components
-
+.bowerrc
.idea/*
*.iml
projectFilesBackup
diff --git a/Gruntfile.js b/Gruntfile.js
index bb677f518b80..086c83271cbe 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -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;
\ No newline at end of file
+module.exports = configureGrunt;
diff --git a/LICENSE b/LICENSE
index 06189940a230..8d0dbdd59321 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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.
\ No newline at end of file
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
index 63eb30a17676..a5b046387f8f 100644
--- a/README.md
+++ b/README.md
@@ -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).
diff --git a/bower.json b/bower.json
index 7d5712f6f7d4..03f1d6848597 100644
--- a/bower.json
+++ b/bower.json
@@ -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",
diff --git a/core/clientold/tpl/modals/markdown.hbs b/core/clientold/tpl/modals/markdown.hbs
index b758838d0a2f..1d6383ea14fa 100644
--- a/core/clientold/tpl/modals/markdown.hbs
+++ b/core/clientold/tpl/modals/markdown.hbs
@@ -1,114 +1,69 @@
-
- Result
- Markdown
- Shortcut
-
+
+ Result
+ Markdown
+ Shortcut
+
-
- Bold
- **text**
- Ctrl / Cmd + B
-
-
- Emphasize
- *text*
- Ctrl / Cmd + I
-
-
- Inline Code
- `code`
- Cmd + K / Ctrl + Shift + K
-
-
- Strike-through
- ~~text~~
- Ctrl + Alt + U
-
-
- Link
- [title](http://)
- Ctrl + Shift + L
-
-
- Image
- ![alt](http://)
- Ctrl + Shift + I
-
-
- List
- * item
- Ctrl + L
-
-
- Blockquote
- > quote
- Ctrl + Q
-
-
- H1
- # Heading
- Ctrl + Alt + 1
-
-
- H2
- ## Heading
- Ctrl + Alt + 2
-
-
- H3
- ### Heading
- Ctrl + Alt + 3
-
-
- H4
- #### Heading
- Ctrl + Alt + 4
-
-
- H5
- ##### Heading
- Ctrl + Alt + 5
-
-
- H6
- ###### Heading
- Ctrl + Alt + 6
-
-
- Select Word
-
- Ctrl + Alt + W
-
-
- New Paragraph
-
- Ctrl / Cmd + Enter
-
-
- Uppercase
-
- Ctrl + U
-
-
- Lowercase
-
- Ctrl + Shift + U
-
-
- Titlecase
-
- Ctrl + Alt + Shift + U
-
-
- Insert Current Date
-
- Ctrl + Shift + 1
-
+
+ Bold
+ **text**
+ Ctrl / Cmd + B
+
+
+ Emphasize
+ *text*
+ Ctrl / Cmd + I
+
+
+ Strike-through
+ ~~text~~
+ Ctrl + Alt + U
+
+
+ Link
+ [title](http://)
+ Ctrl + Shift + L
+
+
+ Image
+ ![alt](http://)
+ Ctrl + Shift + I
+
+
+ List
+ * item
+ Ctrl + L
+
+
+ Blockquote
+ > quote
+ Ctrl + Q
+
+
+ H1
+ # Heading
+ Ctrl + Alt + 1
+
+
+ H2
+ ## Heading
+ Ctrl + Alt + 2
+
+
+ H3
+ ### Heading
+ Ctrl + Alt + 3
+
+
+ Inline Code
+ `code`
+ Cmd + K / Ctrl + Shift + K
+
For further Markdown syntax reference: Markdown Documentation
-
+
\ No newline at end of file
diff --git a/core/clientold/views/editor.js b/core/clientold/views/editor.js
index aeb26f58739d..35b29605c65b 100644
--- a/core/clientold/views/editor.js
+++ b/core/clientold/views/editor.js
@@ -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'
},
diff --git a/core/index.js b/core/index.js
index e65a07260945..28932730334a 100644
--- a/core/index.js
+++ b/core/index.js
@@ -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;
\ No newline at end of file
diff --git a/core/server/config/index.js b/core/server/config/index.js
index 8bcfa90d0aca..cee5842865e6 100644
--- a/core/server/config/index.js
+++ b/core/server/config/index.js
@@ -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;
diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js
index 49c7c70de608..7c220e032507 100644
--- a/core/server/controllers/frontend.js
+++ b/core/server/controllers/frontend.js
@@ -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'
- });
- return api.posts.browse({page: pageParam}).then(function (page) {
- var maxPage = page.pages,
- feedItems = [];
+ 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'
+ });
+
// 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,
diff --git a/core/server/index.js b/core/server/index.js
index e417a990ed7a..3d559dcef549 100644
--- a/core/server/index.js
+++ b/core/server/index.js
@@ -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);
- });
- }
-
- }
+ // Log all theme errors and warnings
+ _.each(config().paths.availableThemes._messages.errors, function (error) {
+ errors.logError(error.message, error.context);
+ });
- // 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');
- });
+ _.each(config().paths.availableThemes._messages.warns, function (warn) {
+ errors.logWarn(warn.message, warn.context);
+ });
- } else {
+ // ## 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().server.port,
- config().server.host,
- startGhost
+ config.getSocket(),
+ startGhost(deferred)
);
- }
- _.each(config().paths.availableThemes._messages.errors, function (error) {
- errors.logError(error.message, error.context);
+ fs.chmod(config.getSocket(), '0660');
});
- _.each(config().paths.availableThemes._messages.warns, function (warn) {
- errors.logWarn(warn.message, warn.context);
- });
- });
- }, function (err) {
- errors.logErrorAndExit(err, err.context, err.help);
- });
-}
-// Initializes the ghost application.
-function init(app) {
- if (!app) {
- app = express();
- }
+ } else {
+ server.listen(
+ config().server.port,
+ config().server.host,
+ startGhost(deferred)
+ );
+ }
- // The server and its dependencies require a populated config
- setup(app);
+ return deferred.promise;
+ });
}
module.exports = init;
diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js
index 70023b297bee..9050743fbc02 100644
--- a/core/server/middleware/index.js
+++ b/core/server/middleware/index.js
@@ -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}}));
diff --git a/core/server/models/index.js b/core/server/models/index.js
index f0b764d8c59b..16de3246ef49 100644
--- a/core/server/models/index.js
+++ b/core/server/models/index.js
@@ -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 () {
diff --git a/core/server/routes/frontend.js b/core/server/routes/frontend.js
index f3149b195a6b..84aa5cf358e3 100644
--- a/core/server/routes/frontend.js
+++ b/core/server/routes/frontend.js
@@ -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);
diff --git a/core/server/views/default-ember.hbs b/core/server/views/default-ember.hbs
index f265b4a6491e..63b4360b223e 100644
--- a/core/server/views/default-ember.hbs
+++ b/core/server/views/default-ember.hbs
@@ -4,32 +4,34 @@
-
+
Ghost Admin
-
-
-
+
+
+
-
+
+
+
-
+
-
-
+
+
-
-
-
+
+
+
diff --git a/core/server/views/default.hbs b/core/server/views/default.hbs
index 898205d0d943..24428df33fc2 100644
--- a/core/server/views/default.hbs
+++ b/core/server/views/default.hbs
@@ -4,31 +4,33 @@
-
+
Ghost Admin
-
-
-
+
+
+
-
+
+
+
-
+
-
-
+
+
-
-
+
+
{{#unless hideNavbar}}
diff --git a/core/test/functional/admin/editor_test.js b/core/test/functional/admin/editor_test.js
index 5ae016b9ea95..cbe8e98aeb54 100644
--- a/core/test/functional/admin/editor_test.js
+++ b/core/test/functional/admin/editor_test.js
@@ -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');
diff --git a/core/test/functional/admin/settings_test.js b/core/test/functional/admin/settings_test.js
index ea6bfa6b88d2..48ec2d50bba1 100644
--- a/core/test/functional/admin/settings_test.js
+++ b/core/test/functional/admin/settings_test.js
@@ -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() {
diff --git a/core/test/functional/routes/frontend_test.js b/core/test/functional/routes/frontend_test.js
index dddab36c58fb..616d4042d87d 100644
--- a/core/test/functional/routes/frontend_test.js
+++ b/core/test/functional/routes/frontend_test.js
@@ -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'])
diff --git a/index.js b/index.js
index 6fb52b3b6d2f..6cfca38bc025 100644
--- a/index.js
+++ b/index.js
@@ -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();
\ No newline at end of file
+ghost().otherwise(function (err) {
+ errors.logErrorAndExit(err, err.context, err.help);
+});
\ No newline at end of file