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, {
modalClass: 'invite',
isAdmin: function(){
return Discourse.User.currentProp("admin");
}.property(),
onShow: function(){
this.set('controllers.modal.modalClass', 'invite-modal');
this.set('emailOrUsername', '');

View File

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

View File

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

View File

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

View File

@ -217,10 +217,10 @@ Discourse.Topic = Discourse.Model.extend({
@method createInvite
@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", {
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
@returns {Promise} the result of the server call
**/
createInvite: function(email) {
createInvite: function(email, groupNames) {
return Discourse.ajax('/invites', {
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}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
@ -12,6 +12,11 @@
<label>{{inviteInstructions}}</label>
{{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}}
</div>
<div class="modal-footer">

View File

@ -1,6 +1,13 @@
class Admin::GroupsController < Admin::AdminController
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)
end

View File

@ -30,9 +30,11 @@ class InvitesController < ApplicationController
def create
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
else
render json: failed_json, status: 422

View File

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

View File

@ -164,6 +164,24 @@ class Group < ActiveRecord::Base
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)
name = "trust_level_#{trust_level}".to_sym

View File

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

View File

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

View File

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