FEATURE: Admin selector to choose a primary group for a user, display it
and apply a CSS class to their posts.
This commit is contained in:
parent
c97481623d
commit
b61df08d1b
|
@ -24,6 +24,8 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
|
||||||
return Discourse.SiteSettings.must_approve_users;
|
return Discourse.SiteSettings.must_approve_users;
|
||||||
}.property(),
|
}.property(),
|
||||||
|
|
||||||
|
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
toggleTitleEdit: function() {
|
toggleTitleEdit: function() {
|
||||||
this.toggleProperty('editingTitle');
|
this.toggleProperty('editingTitle');
|
||||||
|
@ -44,6 +46,22 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
|
||||||
this.get('model').generateApiKey();
|
this.get('model').generateApiKey();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
savePrimaryGroup: function() {
|
||||||
|
var self = this;
|
||||||
|
Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", {
|
||||||
|
type: 'PUT',
|
||||||
|
data: {primary_group_id: this.get('primary_group_id')}
|
||||||
|
}).then(function () {
|
||||||
|
self.set('originalPrimaryGroupId', self.get('primary_group_id'));
|
||||||
|
}).catch(function() {
|
||||||
|
bootbox.alert(I18n.t('generic_error'));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetPrimaryGroup: function() {
|
||||||
|
this.set('primary_group_id', this.get('originalPrimaryGroupId'));
|
||||||
|
},
|
||||||
|
|
||||||
regenerateApiKey: function() {
|
regenerateApiKey: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||||
|
|
|
@ -23,10 +23,16 @@ Discourse.AdminUserRoute = Discourse.Route.extend({
|
||||||
afterModel: function(adminUser) {
|
afterModel: function(adminUser) {
|
||||||
var controller = this.controllerFor('adminUser');
|
var controller = this.controllerFor('adminUser');
|
||||||
|
|
||||||
adminUser.loadDetails().then(function () {
|
return adminUser.loadDetails().then(function () {
|
||||||
adminUser.setOriginalTrustLevel();
|
adminUser.setOriginalTrustLevel();
|
||||||
controller.set('model', adminUser);
|
controller.set('model', adminUser);
|
||||||
window.scrollTo(0, 0);
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController: function(controller, model) {
|
||||||
|
controller.setProperties({
|
||||||
|
originalPrimaryGroupId: model.get('primary_group_id'),
|
||||||
|
model: model
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class='display-row'>
|
||||||
|
<div class='field'>{{i18n admin.groups.primary}}</div>
|
||||||
|
<div class='value'>
|
||||||
|
{{#if custom_groups}}
|
||||||
|
{{combobox content=custom_groups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
|
||||||
|
{{else}}
|
||||||
|
—
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class='controls'>
|
||||||
|
{{#if primaryGroupDirty}}
|
||||||
|
<div>
|
||||||
|
<button class='btn ok' {{action savePrimaryGroup}}><i class='fa fa-check'></i></button>
|
||||||
|
<button class='btn cancel' {{action resetPrimaryGroup}}><i class='fa fa-times'></i></button>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='display-row'>
|
<div class='display-row'>
|
||||||
<div class='field'>{{i18n user.ip_address.title}}</div>
|
<div class='field'>{{i18n user.ip_address.title}}</div>
|
||||||
<div class='value'>{{ip_address}}</div>
|
<div class='value'>{{ip_address}}</div>
|
||||||
|
@ -317,7 +336,7 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<hr/>
|
<hr/>
|
||||||
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}} {{bind-attr}}>
|
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}}>
|
||||||
<i class="fa fa-exclamation-triangle"></i>
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
{{i18n admin.user.delete}}
|
{{i18n admin.user.delete}}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
The view class for an Admin User
|
||||||
|
|
||||||
|
@class AdminUserView
|
||||||
|
@extends Discourse.View
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminUserView = Discourse.View.extend(Discourse.ScrollTop);
|
||||||
|
|
|
@ -25,7 +25,10 @@ Discourse.PostGapComponent = Ember.Component.extend({
|
||||||
if (this.get('loading')) {
|
if (this.get('loading')) {
|
||||||
buffer.push(I18n.t('loading'));
|
buffer.push(I18n.t('loading'));
|
||||||
} else {
|
} else {
|
||||||
buffer.push(I18n.t('post.gap', {count: this.get('gap.length')}));
|
var gapLength = this.get('gap.length');
|
||||||
|
if (gapLength) {
|
||||||
|
buffer.push(I18n.t('post.gap', {count: gapLength}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if user_title}}<div class="user-title" {{action showPosterExpansion this}}>{{user_title}}</div>{{/if}}
|
{{#if user_title}}<div class="user-title" {{action showPosterExpansion this}}>{{user_title}}</div>{{/if}}
|
||||||
|
{{#if primary_group_name}}<div><a href='/groups/{{unbound primary_group_name}}' class='user-group'>{{unbound primary_group_name}}</a></div>{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="contents">
|
<div class="contents">
|
||||||
|
|
|
@ -12,13 +12,21 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
||||||
classNameBindings: ['postTypeClass',
|
classNameBindings: ['postTypeClass',
|
||||||
'selected',
|
'selected',
|
||||||
'post.hidden:post-hidden',
|
'post.hidden:post-hidden',
|
||||||
'post.deleted'],
|
'post.deleted',
|
||||||
|
'groupNameClass'],
|
||||||
postBinding: 'content',
|
postBinding: 'content',
|
||||||
|
|
||||||
postTypeClass: function() {
|
postTypeClass: function() {
|
||||||
return this.get('post.post_type') === Discourse.Site.currentProp('post_types.moderator_action') ? 'moderator' : 'regular';
|
return this.get('post.post_type') === Discourse.Site.currentProp('post_types.moderator_action') ? 'moderator' : 'regular';
|
||||||
}.property('post.post_type'),
|
}.property('post.post_type'),
|
||||||
|
|
||||||
|
groupNameClass: function() {
|
||||||
|
var primaryGroupName = this.get('post.primary_group_name');
|
||||||
|
if (primaryGroupName) {
|
||||||
|
return "group-" + primaryGroupName;
|
||||||
|
}
|
||||||
|
}.property('post.primary_group_name'),
|
||||||
|
|
||||||
// If the cooked content changed, add the quote controls
|
// If the cooked content changed, add the quote controls
|
||||||
cookedChanged: function() {
|
cookedChanged: function() {
|
||||||
var postView = this;
|
var postView = this;
|
||||||
|
|
|
@ -461,17 +461,26 @@ iframe {
|
||||||
width: 45px;
|
width: 45px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contents {
|
.contents {
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.user-group {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
padding: 0px;
|
||||||
|
color: $primary_light;
|
||||||
|
font-size: 80%;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
h3 a {
|
h3 a {
|
||||||
display: inline;
|
display: inline;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -614,6 +623,7 @@ position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.user-title {
|
.user-title {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
color: $primary_light;
|
color: $primary_light;
|
||||||
|
|
|
@ -442,6 +442,7 @@ iframe {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.user-title {
|
.user-title {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
|
|
|
@ -17,6 +17,7 @@ class Admin::UsersController < Admin::AdminController
|
||||||
:block,
|
:block,
|
||||||
:unblock,
|
:unblock,
|
||||||
:trust_level,
|
:trust_level,
|
||||||
|
:primary_group,
|
||||||
:generate_api_key,
|
:generate_api_key,
|
||||||
:revoke_api_key]
|
:revoke_api_key]
|
||||||
|
|
||||||
|
@ -94,6 +95,13 @@ class Admin::UsersController < Admin::AdminController
|
||||||
render_serialized(@user, AdminUserSerializer)
|
render_serialized(@user, AdminUserSerializer)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def primary_group
|
||||||
|
guardian.ensure_can_change_primary_group!(@user)
|
||||||
|
@user.primary_group_id = params[:primary_group_id]
|
||||||
|
@user.save!
|
||||||
|
render nothing: true
|
||||||
|
end
|
||||||
|
|
||||||
def trust_level
|
def trust_level
|
||||||
guardian.ensure_can_change_trust_level!(@user)
|
guardian.ensure_can_change_trust_level!(@user)
|
||||||
logger = StaffActionLogger.new(current_user)
|
logger = StaffActionLogger.new(current_user)
|
||||||
|
|
|
@ -220,6 +220,7 @@ class Group < ActiveRecord::Base
|
||||||
if @deletions
|
if @deletions
|
||||||
@deletions.each do |gu|
|
@deletions.each do |gu|
|
||||||
gu.destroy
|
gu.destroy
|
||||||
|
User.update_all 'primary_group_id = NULL', ['id = ? AND primary_group_id = ?', gu.user_id, gu.group_id]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@deletions = nil
|
@deletions = nil
|
||||||
|
|
|
@ -14,12 +14,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||||
:private_topics_count,
|
:private_topics_count,
|
||||||
:can_delete_all_posts,
|
:can_delete_all_posts,
|
||||||
:can_be_deleted,
|
:can_be_deleted,
|
||||||
:suspend_reason
|
:suspend_reason,
|
||||||
|
:primary_group_id
|
||||||
|
|
||||||
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
||||||
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
|
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
|
||||||
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
|
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
|
||||||
has_one :leader_requirements, serializer: LeaderRequirementsSerializer, embed: :objects
|
has_one :leader_requirements, serializer: LeaderRequirementsSerializer, embed: :objects
|
||||||
|
has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer
|
||||||
|
|
||||||
def can_revoke_admin
|
def can_revoke_admin
|
||||||
scope.can_revoke_admin?(object)
|
scope.can_revoke_admin?(object)
|
||||||
|
|
|
@ -23,6 +23,7 @@ class PostSerializer < BasicPostSerializer
|
||||||
:topic_slug,
|
:topic_slug,
|
||||||
:topic_id,
|
:topic_id,
|
||||||
:display_username,
|
:display_username,
|
||||||
|
:primary_group_name,
|
||||||
:version,
|
:version,
|
||||||
:can_edit,
|
:can_edit,
|
||||||
:can_delete,
|
:can_delete,
|
||||||
|
@ -75,6 +76,11 @@ class PostSerializer < BasicPostSerializer
|
||||||
object.user.try(:name)
|
object.user.try(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def primary_group_name
|
||||||
|
return nil unless object.user
|
||||||
|
return @topic_view.primary_group_names[object.user.primary_group_id] if object.user.primary_group_id
|
||||||
|
end
|
||||||
|
|
||||||
def link_counts
|
def link_counts
|
||||||
return @single_post_link_counts if @single_post_link_counts.present?
|
return @single_post_link_counts if @single_post_link_counts.present?
|
||||||
|
|
||||||
|
|
|
@ -1267,6 +1267,8 @@ en:
|
||||||
other: "spam x{{count}}"
|
other: "spam x{{count}}"
|
||||||
|
|
||||||
groups:
|
groups:
|
||||||
|
primary: "Primary Group"
|
||||||
|
no_primary: "(no primary group)"
|
||||||
title: "Groups"
|
title: "Groups"
|
||||||
edit: "Edit Groups"
|
edit: "Edit Groups"
|
||||||
selector_placeholder: "add users"
|
selector_placeholder: "add users"
|
||||||
|
|
|
@ -58,6 +58,7 @@ Discourse::Application.routes.draw do
|
||||||
put "block"
|
put "block"
|
||||||
put "unblock"
|
put "unblock"
|
||||||
put "trust_level"
|
put "trust_level"
|
||||||
|
put "primary_group"
|
||||||
get "leader_requirements"
|
get "leader_requirements"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddPrimaryGroupIdToUsers < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :users, :primary_group_id, :integer, null: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -133,6 +133,10 @@ class Guardian
|
||||||
user && is_staff?
|
user && is_staff?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_change_primary_group?(user)
|
||||||
|
user && is_staff?
|
||||||
|
end
|
||||||
|
|
||||||
def can_change_trust_level?(user)
|
def can_change_trust_level?(user)
|
||||||
user && is_staff?
|
user && is_staff?
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,6 +111,22 @@ class TopicView
|
||||||
filter_posts_paged(opts[:page].to_i)
|
filter_posts_paged(opts[:page].to_i)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def primary_group_names
|
||||||
|
return @group_names if @group_names
|
||||||
|
|
||||||
|
primary_group_ids = Set.new
|
||||||
|
@posts.each do |p|
|
||||||
|
primary_group_ids << p.user.primary_group_id if p.user.try(:primary_group_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
unless primary_group_ids.empty?
|
||||||
|
Group.where(id: primary_group_ids.to_a).pluck(:id, :name).each do |g|
|
||||||
|
result[g[0]] = g[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
# Find the sort order for a post in the topic
|
# Find the sort order for a post in the topic
|
||||||
def sort_order_for_post_number(post_number)
|
def sort_order_for_post_number(post_number)
|
||||||
|
|
|
@ -140,6 +140,29 @@ describe Admin::UsersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context '.primary_group' do
|
||||||
|
before do
|
||||||
|
@another_user = Fabricate(:coding_horror)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "raises an error when the user doesn't have permission" do
|
||||||
|
Guardian.any_instance.expects(:can_change_primary_group?).with(@another_user).returns(false)
|
||||||
|
xhr :put, :primary_group, user_id: @another_user.id
|
||||||
|
response.should be_forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 404 if the user doesn't exist" do
|
||||||
|
xhr :put, :primary_group, user_id: 123123
|
||||||
|
response.should be_forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
it "chagnes the user's trust level" do
|
||||||
|
xhr :put, :primary_group, user_id: @another_user.id, primary_group_id: 2
|
||||||
|
@another_user.reload
|
||||||
|
@another_user.primary_group_id.should == 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context '.trust_level' do
|
context '.trust_level' do
|
||||||
before do
|
before do
|
||||||
@another_user = Fabricate(:coding_horror)
|
@another_user = Fabricate(:coding_horror)
|
||||||
|
|
|
@ -1050,4 +1050,35 @@ describe User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "primary_group_id" do
|
||||||
|
let!(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
it "has no primary_group_id by default" do
|
||||||
|
user.primary_group_id.should be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the user has a group" do
|
||||||
|
let!(:group) { Fabricate(:group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group.usernames = user.username
|
||||||
|
group.save
|
||||||
|
user.primary_group_id = group.id
|
||||||
|
user.save
|
||||||
|
user.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow us to use it as a primary group" do
|
||||||
|
user.primary_group_id.should == group.id
|
||||||
|
|
||||||
|
# If we remove the user from the group
|
||||||
|
group.usernames = ""
|
||||||
|
group.save
|
||||||
|
|
||||||
|
# It should unset it from the primary_group_id
|
||||||
|
user.reload
|
||||||
|
user.primary_group_id.should be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue