Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
move to a separate Invite model
Browse files Browse the repository at this point in the history
requires #tbd
- adds a new `Invite` model with associated serializer and test setup
- updates team screen to use invites rather than existing users with the "invited" property
- updates signup process to work with new invite model
  • Loading branch information
kevinansfield committed Sep 22, 2016
1 parent bb63ee3 commit feee8a2
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 77 deletions.
8 changes: 7 additions & 1 deletion app/components/gh-user-invited.js
Expand Up @@ -10,12 +10,18 @@ export default Component.extend({

notifications: service(),

createdAtUTC: computed('user.createdAtUTC', function () {
createdAt: computed('user.createdAtUTC', function () {
let createdAtUTC = this.get('user.createdAtUTC');

return createdAtUTC ? moment(createdAtUTC).fromNow() : '';
}),

expiresAt: computed('user.expires', function () {
let expires = this.get('user.expires');

return expires ? moment(expires).fromNow() : '';
}),

actions: {
resend() {
let user = this.get('user');
Expand Down
33 changes: 18 additions & 15 deletions app/components/modals/invite-new-user.js
Expand Up @@ -53,15 +53,19 @@ export default ModalComponent.extend(ValidationEngine, {
// the API should return an appropriate error when attempting to save
return new Promise((resolve, reject) => {
return this._super().then(() => {
this.get('store').findAll('user', {reload: true}).then((result) => {
let invitedUser = result.findBy('email', email);

if (invitedUser) {
return RSVP.hash({
users: this.get('store').findAll('user', {reload: true}),
invites: this.get('store').findAll('invite', {reload: true})
}).then((data) => {
let existingUser = data.users.findBy('email', email);
let existingInvite = data.invites.findBy('email', email);

if (existingUser || existingInvite) {
this.get('errors').clear('email');
if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') {
this.get('errors').add('email', 'A user with that email address was already invited.');
} else {
if (existingUser) {
this.get('errors').add('email', 'A user with that email address already exists.');
} else {
this.get('errors').add('email', 'A user with that email address was already invited.');
}

// TODO: this shouldn't be needed, ValidationEngine doesn't mark
Expand Down Expand Up @@ -90,29 +94,28 @@ export default ModalComponent.extend(ValidationEngine, {
let email = this.get('email');
let role = this.get('role');
let notifications = this.get('notifications');
let newUser;
let invite;

this.validate().then(() => {
this.set('submitting', true);

newUser = this.get('store').createRecord('user', {
invite = this.get('store').createRecord('invite', {
email,
role,
status: 'invited'
role
});

newUser.save().then(() => {
invite.save().then(() => {
let notificationText = `Invitation sent! (${email})`;

// If sending the invitation email fails, the API will still return a status of 201
// but the user's status in the response object will be 'invited-pending'.
if (newUser.get('status') === 'invited-pending') {
// but the invite's status in the response object will be 'invited-pending'.
if (invite.get('status') === 'pending') {
notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'});
} else {
notifications.showNotification(notificationText, {key: 'invite.send.success'});
}
}).catch((error) => {
newUser.deleteRecord();
invite.deleteRecord();
notifications.showAPIError(error, {key: 'invite.send'});
}).finally(() => {
this.send('closeModal');
Expand Down
14 changes: 2 additions & 12 deletions app/controllers/team/index.js
@@ -1,25 +1,15 @@
import Controller from 'ember-controller';
import {alias, filter} from 'ember-computed';
import injectService from 'ember-service/inject';

export default Controller.extend({

showInviteUserModal: false,

users: alias('model'),
users: null,
invites: null,

session: injectService(),

activeUsers: filter('users', function (user) {
return /^active|warn-[1-4]|locked$/.test(user.get('status'));
}),

invitedUsers: filter('users', function (user) {
let status = user.get('status');

return status === 'invited' || status === 'invited-pending';
}),

actions: {
toggleInviteUserModal() {
this.toggleProperty('showInviteUserModal');
Expand Down
3 changes: 3 additions & 0 deletions app/mirage/config.js
@@ -1,4 +1,5 @@
import mockAuthentication from './config/authentication';
import mockInvites from './config/invites';
import mockPosts from './config/posts';
import mockRoles from './config/roles';
import mockSettings from './config/settings';
Expand All @@ -19,6 +20,7 @@ export default function () {
// this.put('/posts/:id/', versionMismatchResponse);
// mockSubscribers(this);
this.loadFixtures('settings');
mockInvites(this);
mockSettings(this);
mockThemes(this);

Expand All @@ -38,6 +40,7 @@ export function testConfig() {
// this.logging = true;

mockAuthentication(this);
mockInvites(this);
mockPosts(this);
mockRoles(this);
mockSettings(this);
Expand Down
52 changes: 52 additions & 0 deletions app/mirage/config/invites.js
@@ -0,0 +1,52 @@
import Mirage from 'ember-cli-mirage';
import {paginatedResponse} from '../utils';

export default function mockInvites(server) {
server.get('/invites/', function (db, request) {
let response = paginatedResponse('invites', db.invites, request);
return response;
});

server.get('/invites/:id', function (db, request) {
let {id} = request.params;
let invite = db.invites.find(id);

if (!invite) {
return new Mirage.Response(404, {}, {
errors: [{
errorType: 'NotFoundError',
message: 'Invite not found.'
}]
});
} else {
return {invites: [invite]};
}
});

server.post('/invites/', function (db, request) {
let [attrs] = JSON.parse(request.requestBody).invites;
let invite;

/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
attrs.token = `${db.invites.length}-token`;
attrs.expires = moment.utc().add(1, 'day').unix();
attrs.created_at = moment.utc().format();
attrs.created_by = 1;
attrs.updated_at = moment.utc().format();
attrs.updated_by = 1;
attrs.status = 'sent';
/* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */

invite = db.invites.insert(attrs);

return {
invite
};
});

server.del('/invites/:id/', function (db, request) {
db.invites.remove(request.params.id);

return new Mirage.Response(204, {}, {});
});
}
14 changes: 14 additions & 0 deletions app/mirage/factories/invite.js
@@ -0,0 +1,14 @@
/* jscs:disable */
import Mirage from 'ember-cli-mirage';

export default Mirage.Factory.extend({
token(i) { return `${i}-token`; },
email(i) { return `invited-user-${i}@example.com`; },
expires() { return moment.utc().add(1, 'day').unix(); },
created_at() { return moment.utc().format(); },
created_by() { return 1; },
updated_at() { return moment.utc().format(); },
updated_by() { return 1; },
status() { return 'sent'; },
roles() { return []; }
});
55 changes: 55 additions & 0 deletions app/models/invite.js
@@ -0,0 +1,55 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import {hasMany} from 'ember-data/relationships';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';

export default Model.extend({
token: attr('string'),
email: attr('string'),
expires: attr('number'),
createdAtUTC: attr('moment-utc'),
createdBy: attr('number'),
updatedAtUTC: attr('moment-utc'),
updatedBy: attr('number'),
status: attr('string'),
roles: hasMany('role', {
embedded: 'always',
async: false
}),

// TODO: remove once `gh-user-invited` is updated to work with invite
// models instead of the current hacks which make invites look like
// users
invited: true,

ajax: injectService(),
ghostPaths: injectService(),

role: computed('roles', {
get() {
return this.get('roles.firstObject');
},
set(key, value) {
// Only one role per user, so remove any old data.
this.get('roles').clear();
this.get('roles').pushObject(value);

return value;
}
}),

resendInvite() {
let fullInviteData = this.toJSON();
let inviteData = {
email: fullInviteData.email,
roles: fullInviteData.roles
};
let inviteUrl = this.get('ghostPaths.url').api('invites');

return this.get('ajax').post(inviteUrl, {
data: JSON.stringify({invites: [inviteData]}),
contentType: 'application/json'
});
}
});
39 changes: 21 additions & 18 deletions app/routes/signup.js
Expand Up @@ -47,24 +47,27 @@ export default Route.extend(styleBody, {
model.set('token', params.token);
model.set('errors', Errors.create());

let authUrl = this.get('ghostPaths.url').api('authentication', 'invitation');

return this.get('ajax').request(authUrl, {
dataType: 'json',
data: {
email
}
}).then((response) => {
if (response && response.invitation && response.invitation[0].valid === false) {
this.get('notifications').showAlert('The invitation does not exist or is no longer valid.', {type: 'warn', delayed: true, key: 'signup.create.invalid-invitation'});

return resolve(this.transitionTo('signin'));
}

resolve(model);
}).catch(() => {
resolve(model);
});
// TODO: re-enable invite validation check once implemented server-side
resolve(model);

// let authUrl = this.get('ghostPaths.url').api('authentication', 'invitation');
//
// return this.get('ajax').request(authUrl, {
// dataType: 'json',
// data: {
// email
// }
// }).then((response) => {
// if (response && response.invitation && response.invitation[0].valid === false) {
// this.get('notifications').showAlert('The invitation does not exist or is no longer valid.', {type: 'warn', delayed: true, key: 'signup.create.invalid-invitation'});
//
// return resolve(this.transitionTo('signin'));
// }
//
// resolve(model);
// }).catch(() => {
// resolve(model);
// });
});
},

Expand Down
25 changes: 18 additions & 7 deletions app/routes/team/index.js
Expand Up @@ -2,6 +2,8 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
import PaginationMixin from 'ghost-admin/mixins/pagination';
import styleBody from 'ghost-admin/mixins/style-body';
import RSVP from 'rsvp';
import {isBlank} from 'ember-utils';

export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, PaginationMixin, {
titleToken: 'Team',
Expand All @@ -10,20 +12,29 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, Paginat

paginationModel: 'user',
paginationSettings: {
status: 'active',
status: 'all',
limit: 20
},

model() {
this.loadFirstPage();

return this.store.query('user', {limit: 'all', status: 'invited'}).then(() => {
return this.store.filter('user', () => {
return true;
});
return RSVP.hash({
users: this.loadFirstPage().then(() => {
return this.store.filter('user', (user) => {
return !user.get('isNew') && !isBlank(user.get('status'));
});
}),
invites: this.store.query('invite', {limit: 'all'}).then(() => {
return this.store.filter('invite', (invite) => {
return !invite.get('isNew');
});
})
});
},

setupController(controller, models) {
controller.setProperties(models);
},

actions: {
reload() {
this.refresh();
Expand Down
10 changes: 10 additions & 0 deletions app/serializers/invite.js
@@ -0,0 +1,10 @@
import ApplicationSerializer from 'ghost-admin/serializers/application';
import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin';

export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
attrs: {
roles: {embedded: 'always'},
createdAtUTC: {key: 'created_at'},
updatedAtUTC: {key: 'updated_at'}
}
});
15 changes: 8 additions & 7 deletions app/templates/team/index.hbs
Expand Up @@ -23,22 +23,23 @@
}}
{{!-- Do not show invited users to authors --}}
{{#unless session.user.isAuthor}}
{{#if invitedUsers}}
{{#if invites}}
<section class="user-list invited-users">
<h4 class="user-list-title">Invited users</h4>
{{#each invitedUsers as |user|}}
{{#gh-user-invited user=user reload="reload" as |component|}}
{{#each invites as |invite|}}
{{#gh-user-invited user=invite reload="reload" as |component|}}
<div class="user-list-item">
<span class="user-list-item-icon icon-mail">ic</span>
<div class="user-list-item-body">
<span class="name">{{user.email}}</span><br>
{{#if user.pending}}
<span class="name">{{invite.email}}</span><br>
{{#if invite.pending}}
<span class="description-error">
Invitation not sent - please try again
</span>
{{else}}
<span class="description">
Invitation sent: {{component.createdAtUTC}}
Invitation sent: {{component.createdAt}},
expires {{component.expiresAt}}
</span>
{{/if}}
</div>
Expand Down Expand Up @@ -66,7 +67,7 @@

<section class="user-list active-users">
<h4 class="user-list-title">Active users</h4>
{{#each activeUsers key="id" as |user|}}
{{#each users key="id" as |user|}}
{{!-- For authors only shows users as a list, otherwise show users with links to user page --}}
{{#unless session.user.isAuthor}}
{{#gh-user-active user=user as |component|}}
Expand Down

0 comments on commit feee8a2

Please sign in to comment.