Allow groups to be used as aliases for user mention
when configured by the admin a group can be found through the @mentions feature in both the compose/reply and the private message user-selectors and once selected the mention will be replaced by the list of users in the group
This commit is contained in:
parent
fd36fa1c2f
commit
c743a985a4
|
@ -35,6 +35,16 @@ Discourse.Group = Discourse.Model.extend({
|
|||
return usernames;
|
||||
}.property('users'),
|
||||
|
||||
validValues: function() {
|
||||
return Em.A([
|
||||
{ name: I18n.t("admin.groups.alias_levels.nobody"), value: 0},
|
||||
{ name: I18n.t("admin.groups.alias_levels.only_admins"), value: 1},
|
||||
{ name: I18n.t("admin.groups.alias_levels.mods_and_admins"), value: 2},
|
||||
{ name: I18n.t("admin.groups.alias_levels.members_mods_and_admins"), value: 3},
|
||||
{ name: I18n.t("admin.groups.alias_levels.everyone"), value: 99}
|
||||
]);
|
||||
}.property(),
|
||||
|
||||
destroy: function(){
|
||||
if(!this.id) return;
|
||||
|
||||
|
@ -58,6 +68,7 @@ Discourse.Group = Discourse.Model.extend({
|
|||
return Discourse.ajax("/admin/groups", {type: "POST", data: {
|
||||
group: {
|
||||
name: this.get('name'),
|
||||
alias_level: this.get('alias_level'),
|
||||
usernames: this.get('usernames')
|
||||
}
|
||||
}}).then(function(resp) {
|
||||
|
@ -83,6 +94,7 @@ Discourse.Group = Discourse.Model.extend({
|
|||
data: {
|
||||
group: {
|
||||
name: this.get('name'),
|
||||
alias_level: this.get('alias_level'),
|
||||
usernames: this.get('usernames')
|
||||
}
|
||||
},
|
||||
|
|
|
@ -24,15 +24,23 @@
|
|||
{{textField value=name placeholderKey="admin.groups.name_placeholder"}}
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">Group members
|
||||
</label>
|
||||
<div class="controls">
|
||||
{{userSelector usernames=usernames id="group-users" placeholderKey="admin.groups.selector_placeholder" tabindex="1" disabledBinding="automatic"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n admin.groups.alias_levels.title}}</label>
|
||||
<div class="controls">
|
||||
{{combobox valueAttribute="value" value=alias_level content=validValues}}
|
||||
</div>
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{#unless automatic}}
|
||||
<button {{action save this}} {{bindAttr disabled="disableSave"}} class='btn'>{{i18n admin.customize.save}}</button>
|
||||
{{#if id}}
|
||||
{{#unless automatic}}
|
||||
<a {{action destroy this}} class='delete-link'>{{i18n admin.customize.delete}}</a>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{i18n admin.groups.can_not_edit_automatic}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{/with}}
|
||||
|
|
|
@ -92,19 +92,26 @@ $.fn.autocomplete = function(options) {
|
|||
// dump what we have in single mode, just in case
|
||||
inputSelectedItems = [];
|
||||
}
|
||||
var d = $("<div class='item'><span>" + (transformed || item) + "<a class='remove' href='#'><i class='fa fa-times'></i></a></span></div>");
|
||||
if (!_.isArray(transformed)) {
|
||||
transformed = [transformed || item];
|
||||
}
|
||||
var divs = transformed.map(function(itm) {
|
||||
var d = $("<div class='item'><span>" + (itm) + "<a href='#'><i class='fa fa-times'></i></a></span></div>");
|
||||
var prev = me.parent().find('.item:last');
|
||||
if (prev.length === 0) {
|
||||
me.parent().prepend(d);
|
||||
} else {
|
||||
prev.after(d);
|
||||
}
|
||||
inputSelectedItems.push(item);
|
||||
inputSelectedItems.push(itm);
|
||||
return divs;
|
||||
});
|
||||
|
||||
if (options.onChangeItems) {
|
||||
options.onChangeItems(inputSelectedItems);
|
||||
}
|
||||
|
||||
d.find('a').click(function() {
|
||||
$(divs).find('a').click(function() {
|
||||
closeAutocomplete();
|
||||
inputSelectedItems.splice($.inArray(item, inputSelectedItems), 1);
|
||||
$(this).parent().parent().remove();
|
||||
|
|
|
@ -9,11 +9,12 @@ var cache = {};
|
|||
var cacheTopicId = null;
|
||||
var cacheTime = null;
|
||||
|
||||
var debouncedSearch = Discourse.debouncePromise(function(term, topicId) {
|
||||
var debouncedSearch = Discourse.debouncePromise(function(term, topicId, include_groups) {
|
||||
return Discourse.ajax('/users/search/users', {
|
||||
data: {
|
||||
term: term,
|
||||
topic_id: topicId
|
||||
topic_id: topicId,
|
||||
include_groups: include_groups
|
||||
}
|
||||
}).then(function (r) {
|
||||
cache[term] = r;
|
||||
|
@ -26,6 +27,7 @@ Discourse.UserSearch = {
|
|||
|
||||
search: function(options) {
|
||||
var term = options.term || "";
|
||||
var include_groups = options.include_groups || false;
|
||||
var exclude = options.exclude || [];
|
||||
var topicId = options.topicId;
|
||||
var limit = options.limit || 5;
|
||||
|
@ -46,21 +48,35 @@ Discourse.UserSearch = {
|
|||
cacheTopicId = topicId;
|
||||
|
||||
var organizeResults = function(r) {
|
||||
var result = [];
|
||||
var users = [], groups = [], results = [];
|
||||
_.each(r.users,function(u) {
|
||||
if (exclude.indexOf(u.username) === -1) {
|
||||
result.push(u);
|
||||
users.push(u);
|
||||
results.push(u);
|
||||
}
|
||||
if (result.length > limit) return false;
|
||||
if (results.length > limit) return false;
|
||||
return true;
|
||||
});
|
||||
promise.resolve(result);
|
||||
|
||||
_.each(r.groups,function(g) {
|
||||
if (results.length > limit) return false;
|
||||
if (exclude.indexOf(g.name) === -1) {
|
||||
groups.push(g);
|
||||
results.push(g);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
results.users = users;
|
||||
results.groups = groups;
|
||||
|
||||
promise.resolve(results);
|
||||
};
|
||||
|
||||
if (cache[term]) {
|
||||
organizeResults(cache[term]);
|
||||
} else {
|
||||
debouncedSearch(term, topicId).then(organizeResults);
|
||||
debouncedSearch(term, topicId, include_groups).then(organizeResults);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
{{userSelector topicId=controller.controllers.topic.model.id
|
||||
excludeCurrentUser="true"
|
||||
id="private-message-users"
|
||||
include_groups="true"
|
||||
class="span8"
|
||||
placeholderKey="composer.users_placeholder"
|
||||
tabindex="1"
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{{i18n topic.invite_private.success}}
|
||||
{{else}}
|
||||
<label>{{i18n topic.invite_private.email_or_username}}</label>
|
||||
{{userSelector single=true allowAny=true usernames=emailOrUsername placeholderKey="topic.invite_private.email_or_username_placeholder"}}
|
||||
{{userSelector single=true allowAny=true usernames=emailOrUsername include_groups="true" placeholderKey="topic.invite_private.email_or_username_placeholder"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
@ -183,11 +183,18 @@ Discourse.ComposerView = Discourse.View.extend(Ember.Evented, {
|
|||
dataSource: function(term) {
|
||||
return Discourse.UserSearch.search({
|
||||
term: term,
|
||||
topicId: composerView.get('controller.controllers.topic.model.id')
|
||||
topicId: composerView.get('controller.controllers.topic.model.id'),
|
||||
include_groups: true
|
||||
});
|
||||
},
|
||||
key: "@",
|
||||
transformComplete: function(v) { return v.username; }
|
||||
transformComplete: function(v) {
|
||||
if (v.username) {
|
||||
return v.username;
|
||||
} else {
|
||||
return v.usernames.join(", @");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.editor = editor = Discourse.Markdown.createEditor({
|
||||
|
|
|
@ -4,7 +4,6 @@ Discourse.UserSelector = Discourse.TextField.extend({
|
|||
|
||||
var userSelectorView = this;
|
||||
var selected = [];
|
||||
var transformTemplate = Handlebars.compile("{{avatar this imageSize=\"tiny\"}} {{this.username}}");
|
||||
|
||||
$(this.get('element')).val(this.get('usernames')).autocomplete({
|
||||
template: Discourse.UserSelector.templateFunction(),
|
||||
|
@ -20,9 +19,17 @@ Discourse.UserSelector = Discourse.TextField.extend({
|
|||
return Discourse.UserSearch.search({
|
||||
term: term,
|
||||
topicId: userSelectorView.get('topicId'),
|
||||
exclude: exclude
|
||||
exclude: exclude,
|
||||
include_groups: userSelectorView.get('include_groups')
|
||||
});
|
||||
},
|
||||
transformComplete: function(v) {
|
||||
if (v.username) {
|
||||
return v.username;
|
||||
} else {
|
||||
return v.usernames;
|
||||
}
|
||||
},
|
||||
|
||||
onChangeItems: function(items) {
|
||||
items = _.map(items, function(i) {
|
||||
|
@ -36,8 +43,6 @@ Discourse.UserSelector = Discourse.TextField.extend({
|
|||
selected = items;
|
||||
},
|
||||
|
||||
transformComplete: transformTemplate,
|
||||
|
||||
reverseTransform: function(i) {
|
||||
return { username: i };
|
||||
}
|
||||
|
@ -47,19 +52,39 @@ Discourse.UserSelector = Discourse.TextField.extend({
|
|||
|
||||
});
|
||||
|
||||
Handlebars.registerHelper("showMax", function(context, block){
|
||||
var maxLength = parseInt(block.hash.max) || 3;
|
||||
if (context.length > maxLength){
|
||||
return context.slice(0, maxLength).join(", ") + ", +" + (context.length - maxLength);
|
||||
} else {
|
||||
return context.join(", ");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
Discourse.UserSelector.reopenClass({
|
||||
// I really want to move this into a template file, but I need a handlebars template here, not an ember one
|
||||
templateFunction: function(){
|
||||
this.compiled = this.compiled || Handlebars.compile("<div class='autocomplete'>" +
|
||||
"<ul>" +
|
||||
"{{#each options}}" +
|
||||
"{{#each options.users}}" +
|
||||
"<li>" +
|
||||
"<a href='#'>{{avatar this imageSize=\"tiny\"}} " +
|
||||
"<span class='username'>{{this.username}}</span> " +
|
||||
"<span class='name'>{{this.name}}</span></a>" +
|
||||
"</li>" +
|
||||
"{{/each}}" +
|
||||
"{{#if options.groups}}" +
|
||||
"{{#if options.users}}<hr>{{/if}}"+
|
||||
"{{#each options.groups}}" +
|
||||
"<li>" +
|
||||
"<a href=''><i class='icon-group'></i>" +
|
||||
"<span class='username'>{{this.name}}</span> " +
|
||||
"<span class='name'>{{showMax this.usernames max=3}}</span></a>" +
|
||||
"</li>" +
|
||||
"{{/each}}" +
|
||||
"{{/if}}" +
|
||||
"</ul>" +
|
||||
"</div>");
|
||||
return this.compiled;
|
||||
|
|
|
@ -16,18 +16,22 @@ class Admin::GroupsController < Admin::AdminController
|
|||
|
||||
def update
|
||||
group = Group.find(params[:id].to_i)
|
||||
|
||||
if group.automatic
|
||||
can_not_modify_automatic
|
||||
# we can only change the alias level on automatic groups
|
||||
group.alias_level = params[:group][:alias_level]
|
||||
else
|
||||
group.usernames = params[:group][:usernames]
|
||||
group.alias_level = params[:group][:alias_level]
|
||||
group.name = params[:group][:name] if params[:group][:name]
|
||||
end
|
||||
|
||||
if group.save
|
||||
render json: success_json
|
||||
else
|
||||
render_json_error group
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
group = Group.new
|
||||
|
|
|
@ -264,7 +264,13 @@ class UsersController < ApplicationController
|
|||
user_fields = [:username, :use_uploaded_avatar, :upload_avatar_template, :uploaded_avatar_id]
|
||||
user_fields << :name if SiteSetting.enable_names?
|
||||
|
||||
render json: { users: results.as_json(only: user_fields, methods: :avatar_template) }
|
||||
to_render = { users: results.as_json(only: user_fields, methods: :avatar_template) }
|
||||
|
||||
if params[:include_groups] == "true"
|
||||
to_render[:groups] = Group.search_group(term, current_user).map {|m| {:name=>m.name, :usernames=> m.usernames.split(",")} }
|
||||
end
|
||||
|
||||
render json: to_render
|
||||
end
|
||||
|
||||
# [LEGACY] avatars in quotes/oneboxes might still be pointing to this route
|
||||
|
|
|
@ -99,6 +99,23 @@ class Group < ActiveRecord::Base
|
|||
lookup_group(name) || refresh_automatic_group!(name)
|
||||
end
|
||||
|
||||
def self.search_group(name, current_user)
|
||||
|
||||
levels = [99]
|
||||
if current_user.admin?
|
||||
levels = [99, 1, 2, 3]
|
||||
elsif current_user.moderator?
|
||||
levels = [99, 2, 3]
|
||||
end
|
||||
|
||||
return Group.where("name LIKE :term_like AND (" +
|
||||
" alias_level in (:levels)" +
|
||||
" OR (alias_level = 3 AND id in (" +
|
||||
"SELECT group_id FROM group_users WHERE user_id= :user_id)" +
|
||||
")" +
|
||||
")", term_like: "#{name.downcase}%", levels: levels, user_id: current_user.id)
|
||||
end
|
||||
|
||||
def self.lookup_group(name)
|
||||
if id = AUTO_GROUPS[name]
|
||||
Group.where(id: id).first
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
class BasicGroupSerializer < ApplicationSerializer
|
||||
attributes :id, :automatic, :name, :user_count
|
||||
attributes :id, :automatic, :name, :user_count, :alias_level
|
||||
end
|
||||
|
|
|
@ -1219,10 +1219,16 @@ en:
|
|||
selector_placeholder: "add users"
|
||||
name_placeholder: "Group name, no spaces, same as username rule"
|
||||
about: "Edit your group membership and names here"
|
||||
can_not_edit_automatic: "Automatic group membership is determined automatically, administer users to assign roles and trust levels"
|
||||
delete: "Delete"
|
||||
delete_confirm: "Delete this group?"
|
||||
delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed."
|
||||
alias_levels:
|
||||
title: "Who can use this group as an alias?"
|
||||
nobody: "Nobody"
|
||||
only_admins: "Only admins"
|
||||
mods_and_admins: "Only moderators and Admins"
|
||||
members_mods_and_admins: "Only group members, moderators and admins"
|
||||
everyone: "Everyone"
|
||||
|
||||
api:
|
||||
generate_master: "Generate Master API Key"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddAliasLevelToGroups < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :groups, :alias_level, :integer, default: 0
|
||||
end
|
||||
end
|
|
@ -21,7 +21,8 @@ describe Admin::GroupsController do
|
|||
"id"=>group.id,
|
||||
"name"=>group.name,
|
||||
"user_count"=>1,
|
||||
"automatic"=>false
|
||||
"automatic"=>false,
|
||||
"alias_level"=>0
|
||||
}]
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue