Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GSoC19] Redifine roles in Ghost #7

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion app/adapters/user.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import ApplicationAdapter from 'ghost-admin/adapters/application';
import IdUrl from 'ghost-admin/mixins/id-url';
import SlugUrl from 'ghost-admin/mixins/slug-url';

export default ApplicationAdapter.extend(SlugUrl, {
export default ApplicationAdapter.extend(SlugUrl, IdUrl, {
queryRecord(store, type, query) {
if (!query || query.id !== 'me') {
return this._super(...arguments);
Expand Down
134 changes: 133 additions & 1 deletion app/components/gh-post-settings-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default Component.extend(SettingsMenuMixin, {
session: service(),
settings: service(),
ui: service(),
rcServices: service('rc-services'),

post: null,

Expand All @@ -36,14 +37,24 @@ export default Component.extend(SettingsMenuMixin, {
ogTitleScratch: alias('post.ogTitleScratch'),
twitterDescriptionScratch: alias('post.twitterDescriptionScratch'),
twitterTitleScratch: alias('post.twitterTitleScratch'),
rcDescriptionScratch: alias('post.rcDescriptionScratch'),
rcTitleScratch: alias('post.rcTitleScratch'),
roomNameScratch: alias('post.roomNameScratch'),
slugValue: boundOneWay('post.slug'),

allowAnnouncements: boundOneWay('settings.isAnnounced'),
allowAuthorRooms: boundOneWay('settings.isAuthorsRooms'),
announce: boundOneWay('allowAnnouncements'),

roomName: or('roomNameScratch', 'settings.roomName'),
facebookDescription: or('ogDescriptionScratch', 'customExcerptScratch', 'seoDescription'),
facebookImage: or('post.ogImage', 'post.featureImage'),
facebookTitle: or('ogTitleScratch', 'seoTitle'),
twitterDescription: or('twitterDescriptionScratch', 'customExcerptScratch', 'seoDescription'),
twitterImage: or('post.twitterImage', 'post.featureImage'),
twitterTitle: or('twitterTitleScratch', 'seoTitle'),
rcDescription: or('rcDescriptionScratch', 'customExcerptScratch', 'seoDescription'),
rcImage: or('post.rcImage', 'post.featureImage'),
rcTitle: or('rcTitleScratch', 'seoTitle'),

seoTitle: computed('metaTitleScratch', 'post.titleScratch', function () {
return this.metaTitleScratch || this.post.titleScratch || '(Untitled)';
Expand Down Expand Up @@ -116,6 +127,11 @@ export default Component.extend(SettingsMenuMixin, {
post.validate({attribute: 'publishedAtBlog'});
}

if (errors.has('roomName')) {
post.set('roomNameScratch', post.get('roomName'));
post.validate({attribute: 'roomName'});
}

// remove throbbers
this.set('_showThrobbers', false);
}
Expand Down Expand Up @@ -163,6 +179,50 @@ export default Component.extend(SettingsMenuMixin, {
});
},

/**
* triggered by user manually changing announce-setting
*/
toggleAnnounce() {
let post = this.post;
let announce = this.announce;
this.toggleProperty('announce');
post.set('announce', !announce);
post.set('announceChanged', true);
},

/**
* triggered by user manually changing room-name
*/
validateRoom(newRoom) {
let oldRoom = this.roomName;
let post = this.post;
let errMessage = 'Room does not exist';

// reset errors and validation
post.get('errors').remove('roomName');

if (!newRoom) {
newRoom = oldRoom;
}
this.rcServices.getRoom(newRoom)
.then((room) => {
const existingRCRoom = room.data[0].exist && room.data[0].roomname === newRoom;

if (!existingRCRoom) {
throw errMessage;
}
post.set('roomName', newRoom);
post.set('roomId', room.data[0].rid);
})
.catch((e) => {
if (e === errMessage){
post.get('errors').add('roomName', errMessage);
return;
}
throw e;
});
},

/**
* triggered by user manually changing slug
*/
Expand Down Expand Up @@ -402,6 +462,52 @@ export default Component.extend(SettingsMenuMixin, {
});
},

setRcTitle(rcTitle) {
// Grab the post and current stored twitter title
let post = this.post;
let currentTitle = post.get('rcTitle');

// If the title entered matches the stored twitter title, do nothing
if (currentTitle === rcTitle) {
return;
}

// If the title entered is different, set it as the new twitter title
post.set('rcTitle', rcTitle);

// Make sure the twitter title is valid and if so, save it into the post
return post.validate({property: 'rcTitle'}).then(() => {
if (post.get('isNew')) {
return;
}

return this.savePost.perform();
});
},

setRcDescription(rcDescription) {
// Grab the post and current stored twitter description
let post = this.post;
let currentDescription = post.get('rcDescription');

// If the description entered matches the stored twitter description, do nothing
if (currentDescription === rcDescription) {
return;
}

// If the description entered is different, set it as the new twitter description
post.set('rcDescription', rcDescription);

// Make sure the twitter description is valid and if so, save it into the post
return post.validate({property: 'rcDescription'}).then(() => {
if (post.get('isNew')) {
return;
}

return this.savePost.perform();
});
},

setCoverImage(image) {
this.set('post.featureImage', image);

Expand Down Expand Up @@ -480,6 +586,32 @@ export default Component.extend(SettingsMenuMixin, {
});
},

setRcImage(image) {
this.set('post.rcImage', image);

if (this.get('post.isNew')) {
return;
}

this.savePost.perform().catch((error) => {
this.showError(error);
this.post.rollbackAttributes();
});
},

clearRcImage() {
this.set('post.rcImage', '');

if (this.get('post.isNew')) {
return;
}

this.savePost.perform().catch((error) => {
this.showError(error);
this.post.rollbackAttributes();
});
},

changeAuthors(newAuthors) {
let post = this.post;

Expand Down
7 changes: 7 additions & 0 deletions app/components/gh-posts-list-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default Component.extend({

post: null,
active: false,
currentUserId: null,

// closure actions
onClick() {},
Expand All @@ -31,6 +32,12 @@ export default Component.extend({
return authors.map(author => author.get('name') || author.get('email')).join(', ');
}),

canEdit: computed('post.authors.[]', 'currentUserId', function () {
let authors = this.get('post.authors');
authors = authors.map(author => author.get('id'));
return authors.includes(this.currentUserId);
}),

subText: computed('post.{excerpt,customExcerpt,metaDescription}', function () {
let text = this.get('post.excerpt') || '';
let customExcerpt = this.get('post.customExcerpt');
Expand Down
56 changes: 25 additions & 31 deletions app/components/modal-invite-new-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ const {Promise} = RSVP;
export default ModalComponent.extend(ValidationEngine, {
notifications: service(),
store: service(),
rcService: service('rc-services'),

classNames: 'modal-content invite-new-user',

role: null,
roles: null,
authorRole: null,
defaultRole: null,

validationType: 'inviteUser',

Expand Down Expand Up @@ -48,82 +49,75 @@ export default ModalComponent.extend(ValidationEngine, {
},

validate() {
let email = this.email;
let username = this.username;

// TODO: either the validator should check the email's existence or
// TODO: either the validator should check the username's existence or
// the API should return an appropriate error when attempting to save
return new Promise((resolve, reject) => this._super().then(() => RSVP.hash({
users: this.store.findAll('user', {reload: true}),
invites: this.store.findAll('invite', {reload: true})
rc_users: this.rcService.getUser(username),
users: this.store.findAll('user', {reload: true})
}).then((data) => {
let existingUser = data.users.findBy('email', email);
let existingInvite = data.invites.findBy('email', email);
let existingRCUser = data.rc_users.data[0].exist;
let existingUser = data.users.findBy('rc_username', username);

if (existingUser || existingInvite) {
this.errors.clear('email');
if (existingUser || !existingRCUser) {
this.errors.clear('username');
if (existingUser) {
this.errors.add('email', 'A user with that email address already exists.');
this.errors.add('username', 'A user with that username already exists.');
} else {
this.errors.add('email', 'A user with that email address was already invited.');
this.errors.add('username', 'Username doesnot exist');
}

// TODO: this shouldn't be needed, ValidationEngine doesn't mark
// properties as validated when validating an entire object
this.hasValidated.addObject('email');
this.hasValidated.addObject('username');
reject();
} else {
resolve();
}
}), () => {
// TODO: this shouldn't be needed, ValidationEngine doesn't mark
// properties as validated when validating an entire object
this.hasValidated.addObject('email');
this.hasValidated.addObject('username');
reject();
}));
},

fetchRoles: task(function * () {
let roles = yield this.store.query('role', {permissions: 'assign'});
let authorRole = roles.findBy('name', 'Author');
let defaultRole = roles.findBy('name', 'Author');
defaultRole = defaultRole ? defaultRole : roles.findBy('name', 'Contributor');

this.set('roles', roles);
this.set('authorRole', authorRole);
this.set('defaultRole', defaultRole);

if (!this.role) {
this.set('role', authorRole);
this.set('role', defaultRole);
}
}),

sendInvitation: task(function* () {
let email = this.email;
let username = this.username;
let role = this.role;
let notifications = this.notifications;
let notificationText = `Invitation sent! (${email})`;
let invite;

try {
yield this.validate();

invite = this.store.createRecord('invite', {
email,
role
});
const user = yield this.rcService.addUser(username, role);

yield invite.save();

// If sending the invitation email fails, the API will still return a status of 201
// 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'});
// Check and notify if user is added
if (user.invitation && user.invitation[0].message === 'User Added') {
notifications.showNotification(user.invitation[0].message, {key: 'invite.send.success'});
} else {
notifications.showNotification(notificationText, {key: 'invite.send.success'});
notifications.showAlert('Unable to add user', {type: 'error', key: 'invite.send.failed'});
}

this.send('closeModal');
} catch (error) {
// validation will reject and cause this to be called with no error
if (error) {
invite.deleteRecord();
// invite.deleteRecord();
notifications.showAPIError(error, {key: 'invite.send'});
this.send('closeModal');
}
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export default Controller.extend({
}),

_availableAuthors: computed(function () {
this.store.query('user', {limit: 'all'});
return this.get('store').peekAll('user');
}),

Expand All @@ -116,6 +117,10 @@ export default Controller.extend({
return authors.findBy('slug', author);
}),

currentUserId: computed('session.user.id', function () {
return this.get('session.user.id');
}),

actions: {
changeType(type) {
this.set('type', get(type, 'value'));
Expand Down
Loading