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:
Robin Ward 2014-02-10 16:59:36 -05:00
parent c97481623d
commit b61df08d1b
20 changed files with 185 additions and 10 deletions

View File

@ -24,6 +24,8 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
return Discourse.SiteSettings.must_approve_users;
}.property(),
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
actions: {
toggleTitleEdit: function() {
this.toggleProperty('editingTitle');
@ -44,6 +46,22 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
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() {
var self = this;
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {

View File

@ -23,10 +23,16 @@ Discourse.AdminUserRoute = Discourse.Route.extend({
afterModel: function(adminUser) {
var controller = this.controllerFor('adminUser');
adminUser.loadDetails().then(function () {
return adminUser.loadDetails().then(function () {
adminUser.setOriginalTrustLevel();
controller.set('model', adminUser);
window.scrollTo(0, 0);
});
},
setupController: function(controller, model) {
controller.setProperties({
originalPrimaryGroupId: model.get('primary_group_id'),
model: model
});
},

View File

@ -46,6 +46,25 @@
</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}}
&mdash;
{{/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='field'>{{i18n user.ip_address.title}}</div>
<div class='value'>{{ip_address}}</div>
@ -317,7 +336,7 @@
<section>
<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>
{{i18n admin.user.delete}}
</button>

View File

@ -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);

View File

@ -25,7 +25,10 @@ Discourse.PostGapComponent = Ember.Component.extend({
if (this.get('loading')) {
buffer.push(I18n.t('loading'));
} 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}));
}
}
},

View File

@ -29,6 +29,7 @@
{{/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>
{{else}}
<div class="contents">

View File

@ -12,13 +12,21 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
classNameBindings: ['postTypeClass',
'selected',
'post.hidden:post-hidden',
'post.deleted'],
'post.deleted',
'groupNameClass'],
postBinding: 'content',
postTypeClass: function() {
return this.get('post.post_type') === Discourse.Site.currentProp('post_types.moderator_action') ? 'moderator' : 'regular';
}.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
cookedChanged: function() {
var postView = this;

View File

@ -472,6 +472,15 @@ iframe {
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 {
display: inline;
width: auto;
@ -614,6 +623,7 @@ position: relative;
}
}
.user-title {
margin-top: 8px;
color: $primary_light;

View File

@ -442,6 +442,7 @@ iframe {
float: left;
}
.user-title {
color: #aaa;
padding-top: 2px;

View File

@ -17,6 +17,7 @@ class Admin::UsersController < Admin::AdminController
:block,
:unblock,
:trust_level,
:primary_group,
:generate_api_key,
:revoke_api_key]
@ -94,6 +95,13 @@ class Admin::UsersController < Admin::AdminController
render_serialized(@user, AdminUserSerializer)
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
guardian.ensure_can_change_trust_level!(@user)
logger = StaffActionLogger.new(current_user)

View File

@ -220,6 +220,7 @@ class Group < ActiveRecord::Base
if @deletions
@deletions.each do |gu|
gu.destroy
User.update_all 'primary_group_id = NULL', ['id = ? AND primary_group_id = ?', gu.user_id, gu.group_id]
end
end
@deletions = nil

View File

@ -14,12 +14,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:private_topics_count,
:can_delete_all_posts,
:can_be_deleted,
:suspend_reason
:suspend_reason,
:primary_group_id
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
has_one :leader_requirements, serializer: LeaderRequirementsSerializer, embed: :objects
has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer
def can_revoke_admin
scope.can_revoke_admin?(object)

View File

@ -23,6 +23,7 @@ class PostSerializer < BasicPostSerializer
:topic_slug,
:topic_id,
:display_username,
:primary_group_name,
:version,
:can_edit,
:can_delete,
@ -75,6 +76,11 @@ class PostSerializer < BasicPostSerializer
object.user.try(:name)
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
return @single_post_link_counts if @single_post_link_counts.present?

View File

@ -1267,6 +1267,8 @@ en:
other: "spam x{{count}}"
groups:
primary: "Primary Group"
no_primary: "(no primary group)"
title: "Groups"
edit: "Edit Groups"
selector_placeholder: "add users"

View File

@ -58,6 +58,7 @@ Discourse::Application.routes.draw do
put "block"
put "unblock"
put "trust_level"
put "primary_group"
get "leader_requirements"
end

View File

@ -0,0 +1,5 @@
class AddPrimaryGroupIdToUsers < ActiveRecord::Migration
def change
add_column :users, :primary_group_id, :integer, null: true
end
end

View File

@ -133,6 +133,10 @@ class Guardian
user && is_staff?
end
def can_change_primary_group?(user)
user && is_staff?
end
def can_change_trust_level?(user)
user && is_staff?
end

View File

@ -111,6 +111,22 @@ class TopicView
filter_posts_paged(opts[:page].to_i)
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
def sort_order_for_post_number(post_number)

View File

@ -140,6 +140,29 @@ describe Admin::UsersController do
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
before do
@another_user = Fabricate(:coding_horror)

View File

@ -1050,4 +1050,35 @@ describe User do
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