more group progress, UI getting there, controller mostly done

changed it so notify moderators goes to the moderators group
allow admins to grant self moderation and revoke self moderation
This commit is contained in:
Sam 2013-05-09 17:37:34 +10:00
parent 4f328e3e45
commit 5280b3a01b
27 changed files with 224 additions and 61 deletions

View file

@ -1,9 +1,9 @@
Discourse.AdminGroupsController = Ember.ArrayController.extend({ Discourse.AdminGroupsController = Ember.Controller.extend({
itemController: 'adminGroup', itemController: 'adminGroup',
edit: function(group){ edit: function(group){
this.get('model').select(group); this.get('model').select(group);
group.loadUsers(); group.load();
}, },
refreshAutoGroups: function(){ refreshAutoGroups: function(){
@ -14,9 +14,31 @@ Discourse.AdminGroupsController = Ember.ArrayController.extend({
controller.set('model', Discourse.Group.findAll()); controller.set('model', Discourse.Group.findAll());
controller.set('refreshingAutoGroups',false); controller.set('refreshingAutoGroups',false);
}); });
},
newGroup: function(){
var group = Discourse.Group.create();
group.set("loaded", true);
var model = this.get("model");
model.addObject(group);
model.select(group);
},
save: function(group){
if(!group.get("id")){
group.create();
} else {
group.save();
}
},
destroy: function(group){
var list = this.get("model");
if(group.get("id")){
group.destroy().then(function(){
list.removeObject(group);
});
}
} }
}); });
Discourse.AdminGroupController = Ember.Controller.extend({
});

View file

@ -26,7 +26,8 @@ Discourse.FlaggedPost = Discourse.Post.extend({
if (a.message) { if (a.message) {
return r.push({ return r.push({
user: _this.userLookup[a.user_id], user: _this.userLookup[a.user_id],
message: a.message message: a.message,
permalink: a.permalink
}); });
} }
}); });

View file

@ -1,4 +1,6 @@
Discourse.Group = Discourse.Model.extend({ Discourse.Group = Discourse.Model.extend({
loaded: false,
userCountDisplay: function(){ userCountDisplay: function(){
var c = this.get('user_count'); var c = this.get('user_count');
// don't display zero its ugly // don't display zero its ugly
@ -7,16 +9,19 @@ Discourse.Group = Discourse.Model.extend({
} }
}.property('user_count'), }.property('user_count'),
loadUsers: function() { load: function() {
var group = this; var id = this.get('id');
if(id && !this.get('loaded')) {
Discourse.ajax('/admin/groups/' + this.get('id') + '/users').then(function(payload){ var group = this;
var users = Em.A() Discourse.ajax('/admin/groups/' + this.get('id') + '/users').then(function(payload){
payload.each(function(user){ var users = Em.A()
users.addObject(Discourse.User.create(user)); payload.each(function(user){
users.addObject(Discourse.User.create(user));
});
group.set('users', users)
group.set('loaded', true)
}); });
group.set('users', users) }
});
}, },
usernames: function() { usernames: function() {
@ -28,7 +33,32 @@ Discourse.Group = Discourse.Model.extend({
}).join(',') }).join(',')
} }
return usernames; return usernames;
}.property('users') }.property('users'),
destroy: function(){
var group = this;
group.set('disableSave', true);
return Discourse.ajax("/admin/groups/" + this.get("id"), {type: "DELETE"})
.then(function(){
group.set('disableSave', false);
});
},
create: function(){
var group = this;
group.set('disableSave', true);
return Discourse.ajax("/admin/groups", {type: "POST", data: {
group: {
name: this.get('name'),
usernames: this.get('usernames')
}
}}).then(function(r){
group.set('disableSave', false);
group.set('id', r.id);
});
}
}); });

View file

@ -38,7 +38,7 @@
<tr> <tr>
<td></td> <td></td>
<td class='message'> <td class='message'>
<div>{{#linkTo 'adminUser' user}}{{avatar user imageSize="small"}}{{/linkTo}} {{message}}</div> <div>{{#linkTo 'adminUser' user}}{{avatar user imageSize="small"}}{{/linkTo}} {{message}} <a href="{{unbound permalink}}">{{i18n admin.flags.view_message}}</a></div>
</td> </td>
<td></td> <td></td>
<td></td> <td></td>

View file

@ -1,5 +1,5 @@
<!-- work in progress, please ignore --> <!-- work in progress, please ignore -->
<div class='row'> <div class='row groups'>
<div class='content-list span6'> <div class='content-list span6'>
<h3>{{i18n admin.groups.edit}}</h3> <h3>{{i18n admin.groups.edit}}</h3>
<ul> <ul>
@ -9,19 +9,35 @@
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
<div> <div class='controls'>
<button {{bindAttr disabled="refreshingAutoGroups"}} {{action "refreshAutoGroups"}}>Refresh Automatic Groups</button> <button class='btn' {{bindAttr disabled="refreshingAutoGroups"}} {{action "refreshAutoGroups"}}>Refresh</button>
<button class='btn' {{action newGroup}}>New</button>
</div> </div>
</div> </div>
<div class='content-editor'> <div class='content-editor'>
{{#if model.active}} {{#if model.active}}
{{#with model.active}} {{#if model.active.loaded}}
<h3>{{name}}</h3> {{#with model.active}}
{{view Discourse.UserSelector id="private-message-users" class="span8" placeholderKey="admin.groups.selector_placeholder" tabindex="1" usernamesBinding="usernames"}} {{#if automatic}}
<button {{bindAttr disabled="allowSave"}}>Save</button> <h3>{{name}}</h3>
{{else}}
{{view Discourse.TextField valueBinding="name" placeholderKey="admin.groups.name_placeholder"}}
{{/if}}
{{/with}} {{view Discourse.UserSelector id="group-users" placeholderKey="admin.groups.selector_placeholder" tabindex="1" usernamesBinding="usernames"}}
<div class='controls'>
<button {{action save this}} {{bindAttr disabled="disableSave"}} class='btn'>{{i18n admin.customize.save}}</button>
{{#unless automatic}}
{{#if id}}
<a {{action destroy this}} class='delete-link'>{{i18n admin.customize.delete}}</a>
{{/if}}
{{/unless}}
</div>
{{/with}}
{{else}}
<div class='spinner'>{{i18n loading}}</div>
{{/if}}
{{else}} {{else}}
nothing here yet nothing here yet
{{/if}} {{/if}}

View file

@ -18,5 +18,13 @@ Discourse.SelectableArray = Em.ArrayProxy.extend({
} }
}); });
this.set("active", selected); this.set("active", selected);
},
removeObject: function(object) {
if(object === this.get("active")){
this.set("active", null);
Em.set(object, "active", false);
}
this._super(object);
} }
}); });

View file

@ -1,5 +1,10 @@
<h3><i class='icon icon-envelope-alt'></i> {{i18n private_message_info.title}}</h3> <h3><i class='icon icon-envelope-alt'></i> {{i18n private_message_info.title}}</h3>
<div class='participants clearfix'> <div class='participants clearfix'>
{{#each content.allowed_groups}}
<div class='user group'>
#{{unbound name}}
</div>
{{/each}}
{{#each content.allowed_users}} {{#each content.allowed_users}}
<div class='user'> <div class='user'>
<a href='/users/{{lower username}}'> <a href='/users/{{lower username}}'>

View file

@ -654,3 +654,12 @@ table {
} }
} }
.row.groups {
input[type='text'] {
width: 500px;
}
input#group-users {
width: 600px;
}
}

View file

@ -451,7 +451,12 @@ kbd {
.private_message .participants .user { .private_message .participants .user {
float: left; float: left;
display: block; display: block;
line-height: 35px;
margin-right: 15px; margin-right: 15px;
margin-top: 8px;
&.group {
color: black;
}
} }
.posts-wrapper .spinner { .posts-wrapper .spinner {

View file

@ -56,20 +56,23 @@ limit 100
map[p["id"]] = p map[p["id"]] = p
} }
sql = SqlBuilder.new "select a.id, a.user_id, post_action_type_id, a.created_at, post_id, a.message sql = SqlBuilder.new "select a.id, a.user_id, post_action_type_id, a.created_at, post_id, a.message, p.topic_id, t.slug
from post_actions a from post_actions a
left join posts p on p.id = related_post_id
left join topics t on t.id = p.topic_id
/*where*/ /*where*/
" "
sql.where("post_action_type_id in (:flag_types)", flag_types: PostActionType.notify_flag_types.values) sql.where("post_action_type_id in (:flag_types)", flag_types: PostActionType.notify_flag_types.values)
sql.where("post_id in (:posts)", posts: posts.map{|p| p["id"].to_i}) sql.where("post_id in (:posts)", posts: posts.map{|p| p["id"].to_i})
if params[:filter] == 'old' if params[:filter] == 'old'
sql.where('deleted_at is not null') sql.where('a.deleted_at is not null')
else else
sql.where('deleted_at is null') sql.where('a.deleted_at is null')
end end
sql.exec.each do |action| sql.exec.each do |action|
action["permalink"] = Topic.url(action["topic_id"],action["slug"]) if action["slug"].present?
p = map[action["post_id"]] p = map[action["post_id"]]
p[:post_actions] ||= [] p[:post_actions] ||= []
p[:post_actions] << action p[:post_actions] << action

View file

@ -1,7 +1,7 @@
class Admin::GroupsController < Admin::AdminController class Admin::GroupsController < Admin::AdminController
def index def index
groups = Group.order(:name).all groups = Group.order(:name).all
render_serialized(groups, AdminGroupSerializer) render_serialized(groups, BasicGroupSerializer)
end end
def refresh_automatic_groups def refresh_automatic_groups
@ -11,7 +11,7 @@ class Admin::GroupsController < Admin::AdminController
def users def users
group = Group.find(params[:group_id].to_i) group = Group.find(params[:group_id].to_i)
render_serialized(group.users, BasicUserSerializer) render_serialized(group.users.limit(100).to_a, BasicUserSerializer)
end end
def update def update
@ -28,7 +28,7 @@ class Admin::GroupsController < Admin::AdminController
group.name = params[:group][:name] group.name = params[:group][:name]
group.usernames = params[:group][:usernames] if params[:group][:usernames] group.usernames = params[:group][:usernames] if params[:group][:usernames]
group.save! group.save!
render_serialized(group, AdminGroupSerializer) render_serialized(group, BasicGroupSerializer)
end end
def destroy def destroy

View file

@ -7,6 +7,8 @@ class Group < ActiveRecord::Base
after_save :destroy_deletions after_save :destroy_deletions
validate :name_format_validator
AUTO_GROUPS = { AUTO_GROUPS = {
:admins => 1, :admins => 1,
:moderators => 2, :moderators => 2,
@ -26,8 +28,8 @@ class Group < ActiveRecord::Base
id = AUTO_GROUPS[name] id = AUTO_GROUPS[name]
unless group = self[name] unless group = self.lookup_group(name)
group = Group.new(name: "", automatic: true) group = Group.new(name: name.to_s, automatic: true)
group.id = id group.id = id
group.save! group.save!
end end
@ -61,6 +63,8 @@ class Group < ActiveRecord::Base
# we want to ensure consistency # we want to ensure consistency
Group.reset_counters(group.id, :group_users) Group.reset_counters(group.id, :group_users)
group
end end
def self.refresh_automatic_groups!(*args) def self.refresh_automatic_groups!(*args)
@ -73,9 +77,15 @@ class Group < ActiveRecord::Base
end end
def self.[](name) def self.[](name)
raise ArgumentError, "unknown group" unless id = AUTO_GROUPS[name] unless g = lookup_group(name)
g = refresh_automatic_group!(name)
end
g
end
Group.where(id: id).first def self.lookup_group(name)
raise ArgumentError, "unknown group" unless id = AUTO_GROUPS[name]
g = Group.where(id: id).first
end end
@ -84,7 +94,7 @@ class Group < ActiveRecord::Base
GroupUser.where(group_id: trust_group_ids, user_id: user_id).delete_all GroupUser.where(group_id: trust_group_ids, user_id: user_id).delete_all
if group = Group[name] if group = lookup_group(name)
group.group_users.build(user_id: user_id) group.group_users.build(user_id: user_id)
group.save! group.save!
else else
@ -92,6 +102,7 @@ class Group < ActiveRecord::Base
end end
end end
def self.builtin def self.builtin
Enum.new(:moderators, :admins, :trust_level_1, :trust_level_2) Enum.new(:moderators, :admins, :trust_level_1, :trust_level_2)
end end
@ -131,9 +142,15 @@ class Group < ActiveRecord::Base
def add(user) def add(user)
self.users.push(user) self.users.push(user)
end end
protected protected
def name_format_validator
validator = UsernameValidator.new(name)
unless validator.valid_format?
validator.errors.each { |e| errors.add(:name, e) }
end
end
# hack around AR # hack around AR
def destroy_deletions def destroy_deletions
if @deletions if @deletions

View file

@ -74,12 +74,16 @@ class PostAction < ActiveRecord::Base
def self.act(user, post, post_action_type_id, message = nil) def self.act(user, post, post_action_type_id, message = nil)
begin begin
title, target_usernames, subtype, body = nil title, target_usernames, target_group_names, subtype, body = nil
if message if message
[:notify_moderators, :notify_user].each do |k| [:notify_moderators, :notify_user].each do |k|
if post_action_type_id == PostActionType.types[k] if post_action_type_id == PostActionType.types[k]
target_usernames = k == :notify_moderators ? target_moderators(user) : post.user.username if k == :notify_moderators
target_group_names = target_moderators
else
target_usernames = post.user.username
end
title = I18n.t("post_action_types.#{k}.email_title", title = I18n.t("post_action_types.#{k}.email_title",
title: post.topic.title) title: post.topic.title)
body = I18n.t("post_action_types.#{k}.email_body", body = I18n.t("post_action_types.#{k}.email_body",
@ -91,9 +95,10 @@ class PostAction < ActiveRecord::Base
end end
related_post_id = nil related_post_id = nil
if target_usernames.present? if target_usernames.present? || target_group_names.present?
related_post_id = PostCreator.new(user, related_post_id = PostCreator.new(user,
target_usernames: target_usernames, target_usernames: target_usernames,
target_group_names: target_group_names,
archetype: Archetype.private_message, archetype: Archetype.private_message,
subtype: subtype, subtype: subtype,
title: title, title: title,
@ -209,13 +214,8 @@ class PostAction < ActiveRecord::Base
protected protected
def self.target_moderators(me) def self.target_moderators
User Group[:moderators].name
.where("moderator = 't' or admin = 't'")
.where('id <> ?', [me.id])
.select('username')
.map{|u| u.username}
.join(',')
end end
end end

View file

@ -641,9 +641,16 @@ class Topic < ActiveRecord::Base
"/t/#{slug}/#{id}/#{posts_count}" "/t/#{slug}/#{id}/#{posts_count}"
end end
def self.url(id, slug, post_number=nil)
url = "#{Discourse.base_url}/t/#{slug}/#{id}"
url << "/#{post_number}" if post_number.to_i > 1
url
end
def relative_url(post_number=nil) def relative_url(post_number=nil)
url = "/t/#{slug}/#{id}" url = "/t/#{slug}/#{id}"
url << "/#{post_number}" if post_number.present? && post_number.to_i > 1 url << "/#{post_number}" if post_number.to_i > 1
url url
end end

View file

@ -1,3 +1,3 @@
class AdminGroupSerializer < ApplicationSerializer class BasicGroupSerializer < ApplicationSerializer
attributes :id, :automatic, :name, :user_count attributes :id, :automatic, :name, :user_count
end end

View file

@ -50,6 +50,7 @@ class TopicViewSerializer < ApplicationSerializer
has_one :created_by, serializer: BasicUserSerializer, embed: :objects has_one :created_by, serializer: BasicUserSerializer, embed: :objects
has_one :last_poster, serializer: BasicUserSerializer, embed: :objects has_one :last_poster, serializer: BasicUserSerializer, embed: :objects
has_many :allowed_users, serializer: BasicUserSerializer, embed: :objects has_many :allowed_users, serializer: BasicUserSerializer, embed: :objects
has_many :allowed_groups, serializer: BasicGroupSerializer, embed: :objects
has_many :links, serializer: TopicLinkSerializer, embed: :objects has_many :links, serializer: TopicLinkSerializer, embed: :objects
has_many :participants, serializer: TopicPostCountSerializer, embed: :objects has_many :participants, serializer: TopicPostCountSerializer, embed: :objects
@ -172,6 +173,10 @@ class TopicViewSerializer < ApplicationSerializer
object.topic.allowed_users object.topic.allowed_users
end end
def allowed_groups
object.topic.allowed_groups
end
def include_links? def include_links?
object.links.present? object.links.present?
end end

View file

@ -895,11 +895,13 @@ en:
delete_title: "delete post (if its the first post delete topic)" delete_title: "delete post (if its the first post delete topic)"
flagged_by: "Flagged by" flagged_by: "Flagged by"
error: "Something went wrong" error: "Something went wrong"
view_message: "view message"
groups: groups:
title: "Groups" title: "Groups"
edit: "Edit Groups" edit: "Edit Groups"
selector_placeholder: "add users" selector_placeholder: "add users"
name_placeholder: "Group name, no spaces, same as username rule"
api: api:
title: "API" title: "API"

0
config/locales/server.es.yml Executable file → Normal file
View file

View file

@ -0,0 +1,11 @@
class UpdateSequenceForGroups < ActiveRecord::Migration
def up
# even if you alter a sequence you still need to set the seq
execute <<SQL
SELECT setval('groups_id_seq', 40)
SQL
end
def down
end
end

View file

@ -0,0 +1,5 @@
class AddUniqueNameToGroups < ActiveRecord::Migration
def change
add_index :groups, [:name], unique: true
end
end

View file

@ -141,15 +141,16 @@ class Guardian
def can_revoke_moderation?(moderator) def can_revoke_moderation?(moderator)
return false unless is_admin? return false unless is_admin?
return false if moderator.blank? return false if moderator.blank?
return false if @user.id == moderator.id return false if @user.id == moderator.id && !is_admin?
return false unless moderator.moderator?
true true
end end
def can_grant_moderation?(user) def can_grant_moderation?(user)
return false unless is_admin? return false unless is_admin?
return false if user.blank? return false unless user
return false if @user.id == user.id return false if @user.id == user.id && !is_admin?
return false if user.staff? return false if user.moderator?
true true
end end

View file

@ -21,6 +21,7 @@ class PostCreator
# archetype - Topic archetype # archetype - Topic archetype
# category - Category to assign to topic # category - Category to assign to topic
# target_usernames - comma delimited list of usernames for membership (private message) # target_usernames - comma delimited list of usernames for membership (private message)
# target_group_names - comma delimited list of groups for membership (private message)
# meta_data - Topic meta data hash # meta_data - Topic meta data hash
def initialize(user, opts) def initialize(user, opts)
# TODO: we should reload user in case it is tainted, should take in a user_id as opposed to user # TODO: we should reload user in case it is tainted, should take in a user_id as opposed to user

View file

@ -1,3 +1,3 @@
Fabricator(:group) do Fabricator(:group) do
name 'my group' name 'my_group'
end end

View file

@ -2,7 +2,22 @@ require 'spec_helper'
describe Group do describe Group do
describe "validation" do
let(:group) { build(:group) }
it "is invalid for blank" do
group.name = ""
group.valid?.should be_false
end
it "is valid for a longer name" do
group.name = "this_is_a_name"
group.valid?.should be_true
end
end
it "Can update moderator/staff/admin groups correctly" do it "Can update moderator/staff/admin groups correctly" do
admin = Fabricate(:admin) admin = Fabricate(:admin)
moderator = Fabricate(:moderator) moderator = Fabricate(:moderator)

View file

@ -37,7 +37,7 @@ describe PostAction do
describe 'notify_moderators' do describe 'notify_moderators' do
before do before do
PostAction.stubs(:create) PostAction.stubs(:create)
PostAction.expects(:target_moderators).returns("bob") PostAction.expects(:target_moderators).returns("moderators")
end end
it "sends an email to all moderators if selected" do it "sends an email to all moderators if selected" do