diff --git a/core/client/app/controllers/setup/three.js b/core/client/app/controllers/setup/three.js index 55d93317e4..e05006227e 100644 --- a/core/client/app/controllers/setup/three.js +++ b/core/client/app/controllers/setup/three.js @@ -1,41 +1,134 @@ import Ember from 'ember'; -import ValidationEngine from 'ghost/mixins/validation-engine'; -var SetupThreeController = Ember.Controller.extend(ValidationEngine, { +var SetupThreeController = Ember.Controller.extend({ + notifications: Ember.inject.service(), users: '', usersArray: Ember.computed('users', function () { - return this.get('users').split('\n').filter(function (user) { + var users = this.get('users').split('\n').filter(function (email) { + return email.trim().length > 0; + }); + + return users.uniq(); + }), + validUsersArray: Ember.computed('usersArray', function () { + return this.get('usersArray').filter(function (user) { return validator.isEmail(user); }); }), - numUsers: Ember.computed('usersArray', function () { - return this.get('usersArray').length; + validateUsers: Ember.computed('usersArray', function () { + var errors = []; + + this.get('usersArray').forEach(function (user) { + if (!validator.isEmail(user)) { + errors.push({ + user: user, + error: 'email' + }); + } + }); + + return errors.length === 0 ? true : errors; }), - buttonText: Ember.computed('numUsers', function () { - var user = this.get('numUsers') === 1 ? 'user' : 'users'; - return this.get('numUsers') > 0 ? - 'Invite ' + this.get('numUsers') + ' ' + user : 'I\'ll do this later, take me to my blog!'; + numUsers: Ember.computed('validUsersArray', function () { + return this.get('validUsersArray').length; }), - buttonClass: Ember.computed('numUsers', function () { - return this.get('numUsers') > 0 ? 'btn-green' : 'btn-minor'; + buttonText: Ember.computed('usersArray', function () { + var num = this.get('usersArray').length, + user; + + if (num > 0) { + user = num === 1 ? 'user' : 'users'; + user = num + ' ' + user; + } else { + user = 'some users'; + } + + return 'Invite ' + user; + }), + buttonClass: Ember.computed('validateUsers', 'numUsers', function () { + if (this.get('validateUsers') === true && this.get('numUsers') > 0) { + return 'btn-green'; + } else { + return 'btn-minor'; + } + }), + authorRole: Ember.computed(function () { + return this.store.find('role').then(function (roles) { + return roles.findBy('name', 'Author'); + }); }), actions: { invite: function () { - console.log('inviting', this.get('usersArray')); + var self = this, + validationErrors = this.get('validateUsers'), + users = this.get('usersArray'), + errorMessages, + notifications = this.get('notifications'), + invitationsString; - if (this.get('numUsers') === 0) { - this.sendAction('signin'); + if (validationErrors === true && users.length > 0) { + this.get('authorRole').then(function (authorRole) { + Ember.RSVP.Promise.all( + users.map(function (user) { + var newUser = self.store.createRecord('user', { + email: user, + status: 'invited', + role: authorRole + }); + + return newUser.save().then(function () { + return { + email: user, + success: newUser.get('status') === 'invited' + }; + }).catch(function () { + return { + email: user, + success: false + }; + }); + }) + ).then(function (invites) { + var successCount = 0, + erroredEmails = [], + message; + + invites.forEach(function (invite) { + if (invite.success) { + successCount++; + } else { + erroredEmails.push(invite.email); + } + }); + + if (erroredEmails.length > 0) { + message = 'Failed to send ' + erroredEmails.length + ' invitations: '; + message += erroredEmails.join(', '); + notifications.showError(message, {delayed: successCount > 0}); + } + + if (successCount > 0) { + // pluralize + invitationsString = successCount > 1 ? 'invitations' : 'invitation'; + + notifications.showSuccess(successCount + ' ' + invitationsString + ' sent!', {delayed: true}); + self.transitionTo('posts.index'); + } + }); + }); + } else if (users.length === 0) { + notifications.showError('No users to invite.'); + } else { + errorMessages = validationErrors.map(function (error) { + // Only one error type here so far, but one day the errors might be more detailed + switch (error.error) { + case 'email': + return {message: error.user + ' is not a valid email.'}; + } + }); + + notifications.showErrors(errorMessages); } - - // TODO: do invites - }, - signin: function () { - var self = this; - - this.get('session').authenticate('simple-auth-authenticator:oauth2-password-grant', { - identification: self.get('email'), - password: self.get('password') - }); } } }); diff --git a/core/client/app/controllers/setup/two.js b/core/client/app/controllers/setup/two.js index cf61173325..b14d547d82 100644 --- a/core/client/app/controllers/setup/two.js +++ b/core/client/app/controllers/setup/two.js @@ -14,6 +14,7 @@ export default Ember.Controller.extend(ValidationEngine, { ghostPaths: Ember.inject.service('ghost-paths'), notifications: Ember.inject.service(), + application: Ember.inject.controller(), gravatarUrl: Ember.computed('email', function () { var email = this.get('email'), @@ -55,9 +56,15 @@ export default Ember.Controller.extend(ValidationEngine, { }] } }).then(function () { + // Don't call the success handler, otherwise we will be redirected to admin + self.get('application').set('skipAuthSuccessHandler', true); + self.get('session').authenticate('simple-auth-authenticator:oauth2-password-grant', { identification: self.get('email'), password: self.get('password') + }).then(function () { + self.set('password', ''); + self.transitionToRoute('setup.three'); }); }).catch(function (resp) { self.toggleProperty('submitting'); diff --git a/core/client/app/styles/layouts/flow.css b/core/client/app/styles/layouts/flow.css index eab9ec6d31..566a49c4bd 100644 --- a/core/client/app/styles/layouts/flow.css +++ b/core/client/app/styles/layouts/flow.css @@ -208,6 +208,13 @@ max-width: 400px; } +.gh-flow-content .gh-flow-skip { + display: inline-block; + margin-top: 5px; + color: #7d878a; + font-size: 1.2rem; +} + .gh-flow-content .gh-flow-create { position: relative; margin: 70px auto 30px; diff --git a/core/client/app/templates/setup/three.hbs b/core/client/app/templates/setup/three.hbs index 8bd10736c2..1f2298da31 100644 --- a/core/client/app/templates/setup/three.hbs +++ b/core/client/app/templates/setup/three.hbs @@ -12,7 +12,10 @@ sally.sanders@example.com" value=users}} - - \ No newline at end of file + {{#link-to "posts" class="gh-flow-skip"}} + I'll do this later, take me to my blog! + {{/link-to}} + diff --git a/core/server/api/index.js b/core/server/api/index.js index 8ab6175f0b..f0029b985a 100644 --- a/core/server/api/index.js +++ b/core/server/api/index.js @@ -239,7 +239,7 @@ http = function http(apiMethod) { return apiMethod(object, options).tap(function onSuccess(response) { // Add X-Cache-Invalidate, Location, and Content-Disposition headers - return addHeaders(apiMethod, req, res, response); + return addHeaders(apiMethod, req, res, (response || {})); }).then(function then(response) { // Send a properly formatting HTTP response containing the data with correct headers res.json(response || {}); diff --git a/core/test/functional/setup/setup_test.js b/core/test/functional/setup/setup_test.js index 0d5e87f65d..f9d17c044e 100644 --- a/core/test/functional/setup/setup_test.js +++ b/core/test/functional/setup/setup_test.js @@ -1,9 +1,8 @@ -// # Setup Test // Test that setup works correctly /*global CasperTest, casper, email, user, password */ -CasperTest.begin('Ghost setup fails properly', 5, function suite(test) { +CasperTest.begin('Ghost setup fails properly', 12, function suite(test) { casper.thenOpenAndWaitForPageLoad('setup', function then() { test.assertUrlMatch(/ghost\/setup\/one\/$/, 'Landed on the correct URL'); }); @@ -27,12 +26,42 @@ CasperTest.begin('Ghost setup fails properly', 5, function suite(test) { // This can take quite a long time casper.wait(5000); - casper.waitForResource(/\d+/, function testForDashboard() { - test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL'); - test.assertExists('.gh-nav-main-content.active', 'Now we are on Content'); - }, function onTimeOut() { - test.fail('Failed to signin'); - }, 20000); + casper.waitForScreenLoad('setup.three', function inviteUsers() { + casper.thenClick('.gh-flow-content .btn'); + }); + + casper.waitForSelector('.notification-error', function onSuccess() { + test.assert(true, 'Got error notification'); + test.assertSelectorHasText('.notification-error', 'No users to invite.'); + + test.assertExists('.gh-flow-content .btn-minor', 'Submit button is not minor'); + test.assertSelectorHasText('.gh-flow-content .btn', 'Invite some users', 'Submit button has wrong text'); + }, function onTimeout() { + test.assert(false, 'No error notification for empty invitation list'); + }); + + casper.then(function fillInvitationForm() { + casper.fill('form.gh-flow-invite', {users: 'test@example.com'}); + test.assertSelectorHasText('.gh-flow-content .btn', 'Invite 1 user', 'One invitation button text is incorrect'); + + test.assertExists('.gh-flow-content .btn-green', 'Submit button is not green'); + + casper.fill('form.gh-flow-invite', {users: 'test@example.com\ntest2@example.com'}); + test.assertSelectorHasText('.gh-flow-content .btn', 'Invite 2 users', 'Two invitations button text is incorrect'); + }); + + casper.thenClick('.gh-flow-content .btn'); + + // This might take awhile + casper.wait(5000); + + // These invitations will fail, because Casper can't send emails + casper.waitForSelector('.notification-error', function onSuccess() { + test.assert(true, 'Got error notification'); + test.assertSelectorHasText('.notification-error', 'Failed to send 2 invitations: test@example.com, test2@example.com'); + }, function onTimeout() { + test.assert(false, 'No error notification after invite.'); + }); }, true); CasperTest.begin('Authenticated user is redirected', 6, function suite(test) {