FEATURE: admins can invite users to groups via the web UI

This commit is contained in:
Sam
2014-05-09 18:22:15 +10:00
parent 3f07c1d0a1
commit 084ec87850
16 changed files with 118 additions and 26 deletions

View File

@ -0,0 +1,52 @@
Discourse.GroupSelectorComponent = Em.Component.extend({
placeholder: function(){
return I18n.t(this.get("placeholderKey"));
}.property("placeholderKey"),
didInsertElement: function() {
var self = this;
var selectedGroups;
self.$('input').autocomplete({
allowAny: false,
onChangeItems: function(items){
selectedGroups = items;
self.set("groupNames", items.join(","));
},
transformComplete: function(g) {
return g.name;
},
dataSource: function(term) {
return Discourse.Group.findAll({search: term, ignore_automatic: true}).then(function(groups){
if(!selectedGroups){
return groups;
}
return groups.filter(function(group){
return !selectedGroups.any(function(s){return s === group.name});
});
});
},
template: Discourse.GroupSelectorComponent.templateFunction()
});
}
});
// TODO autocomplete should become an ember component, then we don't need this
Discourse.GroupSelectorComponent.reopenClass({
templateFunction: function() {
this.compiled = this.compiled || Handlebars.compile(
"<div class='autocomplete'>" +
"<ul>" +
"{{#each options}}" +
"<li>" +
"<a href=''>{{this.name}}</a>" +
"</li>" +
"{{/each}}" +
"</ul>" +
"</div>"
);
return this.compiled;
}
});

View File

@ -10,6 +10,10 @@
export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, { export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
modalClass: 'invite', modalClass: 'invite',
isAdmin: function(){
return Discourse.User.currentProp("admin");
}.property(),
onShow: function(){ onShow: function(){
this.set('controllers.modal.modalClass', 'invite-modal'); this.set('controllers.modal.modalClass', 'invite-modal');
this.set('emailOrUsername', ''); this.set('emailOrUsername', '');

View File

@ -9,6 +9,10 @@
**/ **/
export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, { export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
isAdmin: function(){
return Discourse.User.currentProp("admin");
}.property(),
/** /**
Can we submit the form? Can we submit the form?
@ -89,8 +93,10 @@ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
if (this.get('disabled')) { return; } if (this.get('disabled')) { return; }
var self = this; var self = this;
var groupNames = this.get("groupNames");
this.setProperties({ saving: true, error: false }); this.setProperties({ saving: true, error: false });
this.get('model').createInvite(this.get('email')).then(function() { this.get('model').createInvite(this.get('email'), groupNames).then(function() {
self.setProperties({ saving: false, finished: true }); self.setProperties({ saving: false, finished: true });
}).catch(function() { }).catch(function() {
self.setProperties({ saving: false, error: true }); self.setProperties({ saving: false, error: true });

View File

@ -244,7 +244,6 @@ $.fn.autocomplete = function(options) {
} }
}; };
// chain to allow multiples // chain to allow multiples
var oldClose = me.data("closeAutocomplete"); var oldClose = me.data("closeAutocomplete");
me.data("closeAutocomplete", function() { me.data("closeAutocomplete", function() {

View File

@ -72,8 +72,8 @@ Discourse.Group = Discourse.Model.extend({
}); });
Discourse.Group.reopenClass({ Discourse.Group.reopenClass({
findAll: function(){ findAll: function(opts){
return Discourse.ajax("/admin/groups.json").then(function(groups){ return Discourse.ajax("/admin/groups.json", { data: opts }).then(function(groups){
return groups.map(function(g) { return Discourse.Group.create(g); }); return groups.map(function(g) { return Discourse.Group.create(g); });
}); });
}, },

View File

@ -217,10 +217,10 @@ Discourse.Topic = Discourse.Model.extend({
@method createInvite @method createInvite
@param {String} emailOrUsername The email or username of the user to be invited @param {String} emailOrUsername The email or username of the user to be invited
**/ **/
createInvite: function(emailOrUsername) { createInvite: function(emailOrUsername, groupNames) {
return Discourse.ajax("/t/" + this.get('id') + "/invite", { return Discourse.ajax("/t/" + this.get('id') + "/invite", {
type: 'POST', type: 'POST',
data: { user: emailOrUsername } data: { user: emailOrUsername, group_names: groupNames }
}); });
}, },

View File

@ -375,10 +375,10 @@ Discourse.User = Discourse.Model.extend({
@param {String} email The email address of the user to invite to the site @param {String} email The email address of the user to invite to the site
@returns {Promise} the result of the server call @returns {Promise} the result of the server call
**/ **/
createInvite: function(email) { createInvite: function(email, groupNames) {
return Discourse.ajax('/invites', { return Discourse.ajax('/invites', {
type: 'POST', type: 'POST',
data: {email: email} data: {email: email, group_names: groupNames}
}); });
}, },

View File

@ -0,0 +1 @@
<input class='ember-text-field group-names' type="text" {{bind-attr placeholder="placeholder"}} name="groups">

View File

@ -1,4 +1,4 @@
<div class="modal-body"> <div class="modal-body invite-modal">
{{#if error}} {{#if error}}
<div class="alert alert-error"> <div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button> <button class="close" data-dismiss="alert">×</button>
@ -12,6 +12,11 @@
<label>{{inviteInstructions}}</label> <label>{{inviteInstructions}}</label>
{{textField value=email placeholderKey="topic.invite_reply.email_placeholder"}} {{textField value=email placeholderKey="topic.invite_reply.email_placeholder"}}
{{#if isAdmin}}
<label>{{i18n topic.automatically_add_to_groups}}</label>
{{group-selector includeAuto=false groupNames=groupNames placeholderKey="topic.invite_private.group_name"}}
{{/if}}
{{/if}} {{/if}}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -1,6 +1,13 @@
class Admin::GroupsController < Admin::AdminController class Admin::GroupsController < Admin::AdminController
def index def index
groups = Group.order(:name).to_a groups = Group.order(:name)
if search = params[:search]
search = search.to_s
groups = groups.where("name ilike ?", "%#{search}%")
end
if params[:ignore_automatic].to_s == "true"
groups = groups.where(automatic: false)
end
render_serialized(groups, BasicGroupSerializer) render_serialized(groups, BasicGroupSerializer)
end end

View File

@ -30,9 +30,11 @@ class InvitesController < ApplicationController
def create def create
params.require(:email) params.require(:email)
guardian.ensure_can_invite_to_forum! group_ids = Group.lookup_group_ids(params)
if Invite.invite_by_email(params[:email], current_user) guardian.ensure_can_invite_to_forum!(group_ids)
if Invite.invite_by_email(params[:email], current_user, topic=nil, group_ids)
render json: success_json render json: success_json
else else
render json: failed_json, status: 422 render json: failed_json, status: 422

View File

@ -203,11 +203,7 @@ class TopicsController < ApplicationController
topic = Topic.find_by(id: params[:topic_id]) topic = Topic.find_by(id: params[:topic_id])
if group_ids = params[:group_ids] group_ids = Group.lookup_group_ids(params)
group_ids = group_ids.split(",").map(&:to_i)
group_ids = Group.where(id: group_ids).pluck(:id)
end
guardian.ensure_can_invite_to!(topic,group_ids) guardian.ensure_can_invite_to!(topic,group_ids)
if topic.invite(current_user, username_or_email, group_ids) if topic.invite(current_user, username_or_email, group_ids)

View File

@ -164,6 +164,24 @@ class Group < ActiveRecord::Base
end end
end end
def self.lookup_group_ids(opts)
if group_ids = opts[:group_ids]
group_ids = group_ids.split(",").map(&:to_i)
group_ids = Group.where(id: group_ids).pluck(:id)
end
group_ids ||= []
if group_names = opts[:group_names]
group_names = group_names.split(",")
if group_names.present?
group_ids += Group.where(name: group_names).pluck(:id)
end
end
group_ids
end
def self.user_trust_level_change!(user_id, trust_level) def self.user_trust_level_change!(user_id, trust_level)
name = "trust_level_#{trust_level}".to_sym name = "trust_level_#{trust_level}".to_sym

View File

@ -834,6 +834,7 @@ en:
success_message: 'You successfully flagged this topic.' success_message: 'You successfully flagged this topic.'
inviting: "Inviting..." inviting: "Inviting..."
automatically_add_to_groups: "Upon registration, automatically add user to groups. (optional, admin only)"
invite_private: invite_private:
title: 'Invite to Private Message' title: 'Invite to Private Message'
@ -842,6 +843,7 @@ en:
action: "Invite" action: "Invite"
success: "Thanks! We've invited that user to participate in this private message." success: "Thanks! We've invited that user to participate in this private message."
error: "Sorry, there was an error inviting that user." error: "Sorry, there was an error inviting that user."
group_name: "group name"
invite_reply: invite_reply:
title: 'Invite' title: 'Invite'

View File

@ -187,13 +187,14 @@ class Guardian
is_me?(user) is_me?(user)
end end
def can_invite_to_forum? def can_invite_to_forum?(groups=nil)
authenticated? && authenticated? &&
!SiteSetting.enable_sso && !SiteSetting.enable_sso &&
( (
(!SiteSetting.must_approve_users? && @user.has_trust_level?(:regular)) || (!SiteSetting.must_approve_users? && @user.has_trust_level?(:regular)) ||
is_staff? is_staff?
) ) &&
(groups.blank? || is_admin?)
end end
def can_invite_to?(object, group_ids=nil) def can_invite_to?(object, group_ids=nil)

View File

@ -45,21 +45,20 @@ describe InvitesController do
end end
context 'while logged in' do context 'while logged in' do
let!(:user) { log_in }
let(:email) { 'jake@adventuretime.ooo' } let(:email) { 'jake@adventuretime.ooo' }
it "fails if you can't invite to the forum" do it "fails if you can't invite to the forum" do
Guardian.any_instance.stubs(:can_invite_to_forum?).returns(false) log_in
Invite.expects(:invite_by_email).never
post :create, email: email post :create, email: email
response.should_not be_success response.should_not be_success
end end
it "delegates to Invite#invite_by_email and returns success if you can invite" do it "allows admins to invite to groups" do
Guardian.any_instance.stubs(:can_invite_to_forum?).returns(true) group = Fabricate(:group)
Invite.expects(:invite_by_email).with(email, user).returns(Invite.new) log_in(:admin)
post :create, email: email post :create, email: email, group_names: group.name
response.should be_success response.should be_success
Invite.find_by(email: email).invited_groups.count.should == 1
end end
end end