diff --git a/app/assets/javascripts/discourse/components/small-action.js.es6 b/app/assets/javascripts/discourse/components/small-action.js.es6
index b66024987eb..8a17c41031c 100644
--- a/app/assets/javascripts/discourse/components/small-action.js.es6
+++ b/app/assets/javascripts/discourse/components/small-action.js.es6
@@ -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 ? `@${username}` : "";
+
+ var who = "";
+ if (username) {
+ if (actionCode === "invited_group" || actionCode === "removed_group") {
+ who = `@${username}`;
+ } else {
+ who = `@${username}`;
+ }
+ }
return I18n.t(`action_codes.${actionCode}`, { who, when }).htmlSafe();
}
diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6
index aa1c4b7e81c..ab695e8b2b0 100644
--- a/app/assets/javascripts/discourse/components/user-selector.js.es6
+++ b/app/assets/javascripts/discourse/components/user-selector.js.es6
@@ -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({
diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6
index c00311a9f9f..6747f1449c3 100644
--- a/app/assets/javascripts/discourse/controllers/invite.js.es6
+++ b/app/assets/javascripts/discourse/controllers/invite.js.es6
@@ -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,7 +173,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
model.setProperties({ saving: true, error: false });
- return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames, this.get('customMessage')).then(result => {
+ 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')) {
Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => {
@@ -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'));
- }
- model.setProperties({ saving: false, error: true });
- });
+ }).catch(onerror);
+ }
},
generateInvitelink() {
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index 2c6d56a021f..0062c13bda5 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -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();
},
diff --git a/app/assets/javascripts/discourse/models/topic-details.js.es6 b/app/assets/javascripts/discourse/models/topic-details.js.es6
index 3be04f6ec31..833af1e1988 100644
--- a/app/assets/javascripts/discourse/models/topic-details.js.es6
+++ b/app/assets/javascripts/discourse/models/topic-details.js.es6
@@ -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');
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index 8e8fd7c8ef4..53cf24f2a8a 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -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',
diff --git a/app/assets/javascripts/discourse/templates/modal/invite.hbs b/app/assets/javascripts/discourse/templates/modal/invite.hbs
index 98b1eb7890d..e7215807c08 100644
--- a/app/assets/javascripts/discourse/templates/modal/invite.hbs
+++ b/app/assets/javascripts/discourse/templates/modal/invite.hbs
@@ -11,9 +11,22 @@
{{#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"}}
diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs
index a173827ec85..086fc13642f 100644
--- a/app/assets/javascripts/discourse/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/templates/topic.hbs
@@ -113,6 +113,7 @@
toggleWiki="toggleWiki"
toggleSummary="toggleSummary"
removeAllowedUser="removeAllowedUser"
+ removeAllowedGroup="removeAllowedGroup"
showInvite="showInvite"
topVisibleChanged="topVisibleChanged"
currentPostChanged="currentPostChanged"
diff --git a/app/assets/javascripts/discourse/widgets/post-small-action.js.es6 b/app/assets/javascripts/discourse/widgets/post-small-action.js.es6
index 4f56c01ddb5..882cbc9746b 100644
--- a/app/assets/javascripts/discourse/widgets/post-small-action.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-small-action.js.es6
@@ -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'
};
diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6
index 7d19c8ed4e1..7d311b70b3a 100644
--- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6
+++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6
@@ -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 });
}));
}
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 3ffc9a86a56..24f7626e3e2 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -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
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 77f2e0afd75..e793f7c7422 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -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?
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index e53038c639a..3d142ef2c0e 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -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"
diff --git a/config/routes.rb b/config/routes.rb
index 10694913693..ee654ac8a64 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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+/}
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index e66a8085c5e..ecfd606346e 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -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
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index e1f48980ffd..75fc7beb581 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -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