Merge pull request #5359 from josephwegner/onboarding-invites

invite users after signing up during setup
This commit is contained in:
Jason Williams 2015-06-09 21:56:54 -05:00
commit 9e5d9e975f
6 changed files with 174 additions and 35 deletions

View File

@ -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')
});
}
}
});

View File

@ -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');

View File

@ -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;

View File

@ -12,7 +12,10 @@
sally.sanders@example.com" value=users}}
</form>
<button {{action "signin"}} class="btn btn-default btn-lg btn-block {{buttonClass}}">
<button {{action 'invite'}} class="btn btn-default btn-lg btn-block {{buttonClass}}">
{{buttonText}}
</button>
</section>
{{#link-to "posts" class="gh-flow-skip"}}
I'll do this later, take me to my blog!
{{/link-to}}
</section>

View File

@ -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 || {});

View File

@ -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) {