FEATURE: can invite/revoke groups on private messages

This commit is contained in:
Sam 2016-06-20 16:29:11 +10:00
parent 94df22564f
commit 8866169879
16 changed files with 210 additions and 19 deletions

View File

@ -3,7 +3,15 @@ import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
export function actionDescriptionHtml(actionCode, createdAt, username) {
const dt = new Date(createdAt);
const when = autoUpdatingRelativeAge(dt, { format: 'medium-with-ago' });
const who = username ? `<a class="mention" href="/users/${username}">@${username}</a>` : "";
var who = "";
if (username) {
if (actionCode === "invited_group" || actionCode === "removed_group") {
who = `<a class="mention-group" href="/groups/${username}">@${username}</a>`;
} else {
who = `<a class="mention" href="/users/${username}">@${username}</a>`;
}
}
return I18n.t(`action_codes.${actionCode}`, { who, when }).htmlSafe();
}

View File

@ -13,10 +13,13 @@ export default TextField.extend({
allowedUsers = this.get('allowedUsers') === 'true';
function excludedUsernames() {
// hack works around some issues with allowAny eventing
const usernames = self.get('single') ? [] : selected;
if (currentUser && self.get('excludeCurrentUser')) {
return selected.concat([currentUser.get('username')]);
return usernames.concat([currentUser.get('username')]);
}
return selected;
return usernames;
}
this.$().val(this.get('usernames')).autocomplete({

View File

@ -121,6 +121,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
successMessage: function() {
if (this.get('model.inviteLink')) {
return I18n.t('user.invited.generated_link_message', {inviteLink: this.get('model.inviteLink'), invitedEmail: this.get('emailOrUsername')});
} else if (this.get('hasGroups')) {
return I18n.t('topic.invite_private.success_group');
} else if (this.get('isMessage')) {
return I18n.t('topic.invite_private.success');
} else if ( Discourse.Utilities.emailValid(this.get('emailOrUsername')) ) {
@ -171,6 +173,22 @@ export default Ember.Controller.extend(ModalFunctionality, {
model.setProperties({ saving: true, error: false });
const onerror = function(e) {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
} else {
self.set("errorMessage", self.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error'));
}
model.setProperties({ saving: false, error: true });
};
if (this.get('hasGroups')) {
return this.get('model').createGroupInvite(this.get('emailOrUsername').trim()).then(result => {
model.setProperties({ saving: false, finished: true });
}).catch(onerror);
} else {
return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames, this.get('customMessage')).then(result => {
model.setProperties({ saving: false, finished: true });
if (!this.get('invitingToTopic')) {
@ -181,14 +199,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
} else if (this.get('isMessage') && result && result.user) {
this.get('model.details.allowed_users').pushObject(Ember.Object.create(result.user));
}
}).catch(function(e) {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
} else {
self.set("errorMessage", self.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error'));
}).catch(onerror);
}
model.setProperties({ saving: false, error: true });
});
},
generateInvitelink() {

View File

@ -256,6 +256,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return this.get('model.details').removeAllowedUser(user);
},
removeAllowedGroup(group) {
return this.get('model.details').removeAllowedGroup(group);
},
deleteTopic() {
this.deleteTopic();
},

View File

@ -63,6 +63,18 @@ const TopicDetails = RestModel.extend({
});
},
removeAllowedGroup(group) {
const groups = this.get('allowed_groups');
const name = group.name;
return Discourse.ajax("/t/" + this.get('topic.id') + "/remove-allowed-group", {
type: 'PUT',
data: { name: name }
}).then(() => {
groups.removeObject(groups.findProperty('name', name));
});
},
removeAllowedUser(user) {
const users = this.get('allowed_users');
const username = user.get('username');

View File

@ -309,6 +309,13 @@ const Topic = RestModel.extend({
});
},
createGroupInvite(group) {
return Discourse.ajax("/t/" + this.get('id') + "/invite-group", {
type: 'POST',
data: { group }
});
},
createInvite(user, group_names, custom_message) {
return Discourse.ajax("/t/" + this.get('id') + "/invite", {
type: 'POST',

View File

@ -11,9 +11,22 @@
<label>{{inviteInstructions}}</label>
{{#if allowExistingMembers}}
{{#if isPrivateTopic}}
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername allowedUsers="true" topicId=topicId placeholderKey=placeholderKey}}
{{user-selector single="true"
allowAny=true
excludeCurrentUser="true"
usernames=emailOrUsername
allowedUsers="true"
topicId=topicId
placeholderKey=placeholderKey}}
{{else}}
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername placeholderKey=placeholderKey}}
{{user-selector
single="true"
allowAny=true
excludeCurrentUser="true"
includeMentionableGroups="true"
hasGroups=hasGroups
usernames=emailOrUsername
placeholderKey=placeholderKey}}
{{/if}}
{{else}}
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}

View File

@ -113,6 +113,7 @@
toggleWiki="toggleWiki"
toggleSummary="toggleSummary"
removeAllowedUser="removeAllowedUser"
removeAllowedGroup="removeAllowedGroup"
showInvite="showInvite"
topVisibleChanged="topVisibleChanged"
currentPostChanged="currentPostChanged"

View File

@ -20,7 +20,9 @@ const icons = {
'visible.disabled': 'eye-slash',
'split_topic': 'sign-out',
'invited_user': 'plus-circle',
'invited_group': 'plus-circle',
'removed_user': 'minus-circle',
'removed_group': 'minus-circle',
'public_topic': 'comment',
'private_topic': 'envelope'
};

View File

@ -3,12 +3,33 @@ import { createWidget } from 'discourse/widgets/widget';
import { h } from 'virtual-dom';
import { avatarFor } from 'discourse/widgets/post';
createWidget('pm-remove-group-link', {
tagName: 'a.remove-invited',
html() {
return iconNode('times');
},
click() {
bootbox.confirm(I18n.t("private_message_info.remove_allowed_group", {name: this.attrs.name}), (confirmed) => {
if (confirmed) { this.sendWidgetAction('removeAllowedGroup', this.attrs); }
});
}
});
createWidget('pm-map-user-group', {
tagName: 'div.user.group',
html(attrs) {
const link = h('a', { attributes: { href: Discourse.getURL(`/groups/${attrs.name}`) } }, attrs.name);
return [iconNode('users'), ' ', link];
const link = h('a', { attributes: { href: Discourse.getURL(`/groups/${attrs.group.name}`) } }, attrs.group.name);
const result = [iconNode('users'), ' ', link];
if (attrs.canRemoveAllowedUsers) {
result.push(' ');
result.push(this.attach('pm-remove-group-link', attrs.group));
}
return result;
}
});
@ -51,12 +72,12 @@ export default createWidget('private-message-map', {
const participants = [];
if (attrs.allowedGroups.length) {
participants.push(attrs.allowedGroups.map(ag => this.attach('pm-map-user-group', ag)));
participants.push(attrs.allowedGroups.map(ag => this.attach('pm-map-user-group', {group: ag, canRemoveAllowedUsers: attrs.canRemoveAllowedUsers})));
}
if (attrs.allowedUsers.length) {
participants.push(attrs.allowedUsers.map(ag => {
return this.attach('pm-map-user', { user: ag, canRemoveAllowedUsers: attrs.canRemoveAllowedUsers });
participants.push(attrs.allowedUsers.map(au => {
return this.attach('pm-map-user', { user: au, canRemoveAllowedUsers: attrs.canRemoveAllowedUsers });
}));
}

View File

@ -375,6 +375,34 @@ class TopicsController < ApplicationController
end
end
def remove_allowed_group
params.require(:name)
topic = Topic.find_by(id: params[:topic_id])
guardian.ensure_can_remove_allowed_users!(topic)
if topic.remove_allowed_group(current_user, params[:name])
render json: success_json
else
render json: failed_json, status: 422
end
end
def invite_group
group = Group.find_by(name: params[:group])
raise Discourse::NotFound unless group
topic = Topic.find_by(id: params[:topic_id])
if topic.private_message?
guardian.ensure_can_send_private_message!(group)
topic.invite_group(current_user, group)
render json: success_json
else
render json: failed_json, status: 422
end
end
def invite
username_or_email = params[:user] ? fetch_username : fetch_email

View File

@ -577,6 +577,19 @@ class Topic < ActiveRecord::Base
changed_to_category(cat)
end
def remove_allowed_group(removed_by, name)
if group = Group.find_by(name: name)
group_user = topic_allowed_groups.find_by(group_id: group.id)
if group_user
group_user.destroy
add_small_action(removed_by, "removed_group", group.name)
return true
end
end
false
end
def remove_allowed_user(removed_by, username)
if user = User.find_by(username: username)
topic_user = topic_allowed_users.find_by(user_id: user.id)
@ -590,6 +603,19 @@ class Topic < ActiveRecord::Base
false
end
def invite_group(user, group)
TopicAllowedGroup.create!(topic_id: id, group_id: group.id)
last_post = posts.order('post_number desc').where('not hidden AND posts.deleted_at IS NULL').first
if last_post
# ensure all the notifications are out
PostAlerter.new.after_save_post(last_post)
add_small_action(user, "invited_group", group.name)
end
true
end
# Invite a user to the topic by username or email. Returns success/failure
def invite(invited_by, username_or_email, group_ids=nil, custom_message=nil)
if private_message?

View File

@ -144,7 +144,9 @@ en:
private_topic: "made this topic private %{when}"
split_topic: "split this topic %{when}"
invited_user: "invited %{who} %{when}"
invited_group: "invited %{who} %{when}"
removed_user: "removed %{who} %{when}"
removed_group: "removed %{who} %{when}"
autoclosed:
enabled: 'closed %{when}'
disabled: 'opened %{when}'
@ -908,6 +910,7 @@ en:
title: "Message"
invite: "Invite Others..."
remove_allowed_user: "Do you really want to remove {{name}} from this message?"
remove_allowed_group: "Do you really want to remove {{name}} from this message?"
email: 'Email'
username: 'Username'
@ -1448,6 +1451,7 @@ en:
email_or_username_placeholder: "email address or username"
action: "Invite"
success: "We've invited that user to participate in this message."
success_group: "We've invited that group to participate in this message."
error: "Sorry, there was an error inviting that user."
group_name: "group name"

View File

@ -547,6 +547,7 @@ Discourse::Application.routes.draw do
put "t/:topic_id/make-banner" => "topics#make_banner", constraints: {topic_id: /\d+/}
put "t/:topic_id/remove-banner" => "topics#remove_banner", constraints: {topic_id: /\d+/}
put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", constraints: {topic_id: /\d+/}
put "t/:topic_id/remove-allowed-group" => "topics#remove_allowed_group", constraints: {topic_id: /\d+/}
put "t/:topic_id/recover" => "topics#recover", constraints: {topic_id: /\d+/}
get "t/:topic_id/:post_number" => "topics#show", constraints: {topic_id: /\d+/, post_number: /\d+/}
get "t/:topic_id/last" => "topics#show", post_number: 99999999, constraints: {topic_id: /\d+/}
@ -557,6 +558,7 @@ Discourse::Application.routes.draw do
get "t/:topic_id/posts" => "topics#posts", constraints: {topic_id: /\d+/}, format: :json
post "t/:topic_id/timings" => "topics#timings", constraints: {topic_id: /\d+/}
post "t/:topic_id/invite" => "topics#invite", constraints: {topic_id: /\d+/}
post "t/:topic_id/invite-group" => "topics#invite_group", constraints: {topic_id: /\d+/}
post "t/:topic_id/move-posts" => "topics#move_posts", constraints: {topic_id: /\d+/}
post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: {topic_id: /\d+/}
post "t/:topic_id/change-owner" => "topics#change_post_owners", constraints: {topic_id: /\d+/}

View File

@ -950,6 +950,35 @@ describe TopicsController do
end
end
describe 'invite_group' do
let :admins do
Group[:admins]
end
let! :admin do
log_in :admin
end
before do
admins.alias_level = Group::ALIAS_LEVELS[:everyone]
admins.save!
end
it "disallows inviting a group to a topic" do
topic = Fabricate(:topic)
xhr :post, :invite_group, topic_id: topic.id, group: 'admins'
expect(response.status).to eq(422)
end
it "allows inviting a group to a PM" do
topic = Fabricate(:private_message_topic)
xhr :post, :invite_group, topic_id: topic.id, group: 'admins'
expect(response.status).to eq(200)
expect(topic.allowed_groups.first.id).to eq(admins.id)
end
end
describe 'invite' do
describe "group invites" do

View File

@ -372,6 +372,25 @@ describe Topic do
context 'existing user' do
let(:walter) { Fabricate(:walter_white) }
context 'by group name' do
it 'can add admin to allowed groups' do
admins = Group[:admins]
admins.alias_level = Group::ALIAS_LEVELS[:everyone]
admins.save
expect(topic.invite_group(topic.user, admins)).to eq(true)
expect(topic.allowed_groups.include?(admins)).to eq(true)
expect(topic.remove_allowed_group(topic.user, 'admins')).to eq(true)
topic.reload
expect(topic.allowed_groups.include?(admins)).to eq(false)
end
end
context 'by username' do
it 'adds and removes walter to the allowed users' do