This commit is contained in:
Sam
2013-06-20 17:42:15 +10:00
parent 6491bda4ab
commit 4a8a663a67
12 changed files with 172 additions and 36 deletions

View File

@ -14,15 +14,32 @@ Discourse.AdminFlagsController = Ember.ArrayController.extend({
@method clearFlags @method clearFlags
@param {Discourse.FlaggedPost} item The post whose flags we want to clear @param {Discourse.FlaggedPost} item The post whose flags we want to clear
**/ **/
clearFlags: function(item) { disagreeFlags: function(item) {
var adminFlagsController = this; var adminFlagsController = this;
item.clearFlags().then((function() { item.disagreeFlags().then((function() {
adminFlagsController.removeObject(item); adminFlagsController.removeObject(item);
}), function() { }), function() {
bootbox.alert(Em.String.i18n("admin.flags.error")); bootbox.alert(Em.String.i18n("admin.flags.error"));
}); });
}, },
agreeFlags: function(item) {
var adminFlagsController = this;
item.agreeFlags().then((function() {
adminFlagsController.removeObject(item);
}), function() {
bootbox.alert(Em.String.i18n("admin.flags.error"));
});
},
deferFlags: function(item) {
var adminFlagsController = this;
item.deferFlags().then((function() {
adminFlagsController.removeObject(item);
}), function() {
bootbox.alert(Em.String.i18n("admin.flags.error"));
});
},
/** /**
Deletes a post Deletes a post

View File

@ -63,10 +63,22 @@ Discourse.FlaggedPost = Discourse.Post.extend({
} }
}, },
clearFlags: function() { disagreeFlags: function() {
return Discourse.ajax("/admin/flags/clear/" + this.id, { type: 'POST', cache: false }); return Discourse.ajax("/admin/flags/disagree/" + this.id, { type: 'POST', cache: false });
}, },
deferFlags: function() {
return Discourse.ajax("/admin/flags/defer/" + this.id, { type: 'POST', cache: false });
},
agreeFlags: function() {
return Discourse.ajax("/admin/flags/agree/" + this.id, { type: 'POST', cache: false });
},
postHidden: function() {
return (this.get('hidden') === "t");
}.property(),
hiddenClass: function() { hiddenClass: function() {
if (this.get('hidden') === "t") return "hidden-post"; if (this.get('hidden') === "t") return "hidden-post";
}.property() }.property()

View File

@ -23,7 +23,7 @@
</thead> </thead>
<tbody> <tbody>
{{#each flag in content}} {{#each flag in content}}
<tr {{bindAttr class="hiddenClass"}}> <tr {{bindAttr class="flag.hiddenClass"}}>
<td class='user'>{{#linkTo 'adminUser' flag.user}}{{avatar flag.user imageSize="small"}}{{/linkTo}}</td> <td class='user'>{{#linkTo 'adminUser' flag.user}}{{avatar flag.user imageSize="small"}}{{/linkTo}}</td>
<td class='excerpt'>{{#if flag.topicHidden}}<i title='{{i18n topic_statuses.invisible.help}}' class='icon icon-eye-close'></i> {{/if}}<h3><a href='{{unbound flag.url}}'>{{flag.title}}</a></h3><br>{{{flag.excerpt}}} <td class='excerpt'>{{#if flag.topicHidden}}<i title='{{i18n topic_statuses.invisible.help}}' class='icon icon-eye-close'></i> {{/if}}<h3><a href='{{unbound flag.url}}'>{{flag.title}}</a></h3><br>{{{flag.excerpt}}}
</td> </td>
@ -33,8 +33,15 @@
<td class='last-flagged'>{{date flag.lastFlagged}}</td> <td class='last-flagged'>{{date flag.lastFlagged}}</td>
<td class='action'> <td class='action'>
{{#if adminActiveFlagsView}} {{#if adminActiveFlagsView}}
<button title='{{i18n admin.flags.clear_title}}' class='btn' {{action clearFlags flag}}>{{i18n admin.flags.clear}}</button> {{#if flag.postHidden}}
<button title='{{i18n admin.flags.delete_title}}' class='btn' {{action deletePost flag}}>{{i18n admin.flags.delete}}</button> <button title='{{i18n admin.flags.disagree_unhide_title}}' class='btn' {{action disagreeFlags flag}}>{{i18n admin.flags.disagree_unhide}}</button>
<button title='{{i18n admin.flags.defer_title}}' class='btn' {{action deferFlags flag}}>{{i18n admin.flags.defer}}</button>
{{else}}
<button title='{{i18n admin.flags.agree_hide_title}}' class='btn' {{action agreeFlags flag}}>{{i18n admin.flags.agree_hide}}</button>
<button title='{{i18n admin.flags.disagree_title}}' class='btn' {{action disagreeFlags flag}}>{{i18n admin.flags.disagree}}</button>
{{/if}}
<button title='{{i18n admin.flags.delete_post_title}}' class='btn' {{action deletePost flag}}>{{i18n admin.flags.delete_post}}</button>
{{/if}} {{/if}}
</td> </td>
</tr> </tr>

View File

@ -286,6 +286,10 @@ table {
.flaggers { padding: 0 10px; } .flaggers { padding: 0 10px; }
.last-flagged { padding: 0 10px; } .last-flagged { padding: 0 10px; }
.flag-summary { font-size: 11px; } .flag-summary { font-size: 11px; }
.action {
button { margin: 4px; display: block;}
padding-bottom: 20px;
}
} }
/* Dashboard */ /* Dashboard */

View File

@ -1,6 +1,5 @@
class Admin::FlagsController < Admin::AdminController class Admin::FlagsController < Admin::AdminController
def index def index
# we may get out of sync, fix it here # we may get out of sync, fix it here
PostAction.update_flagged_posts_count PostAction.update_flagged_posts_count
posts, users = PostAction.flagged_posts_report(params[:filter]) posts, users = PostAction.flagged_posts_report(params[:filter])
@ -12,11 +11,24 @@ class Admin::FlagsController < Admin::AdminController
end end
end end
def clear def disagree
p = Post.find(params[:id]) p = Post.find(params[:id])
PostAction.clear_flags!(p, current_user.id) PostAction.clear_flags!(p, current_user.id)
p.reload p.reload
p.unhide! p.unhide!
render nothing: true render nothing: true
end end
def agree
p = Post.find(params[:id])
PostAction.defer_flags!(p, current_user.id)
PostAction.hide_post!(p)
render nothing: true
end
def defer
p = Post.find(params[:id])
PostAction.defer_flags!(p, current_user.id)
render nothing: true
end
end end

View File

@ -32,6 +32,8 @@ end
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# reply_key :string(32) # reply_key :string(32)
# post_id :integer
# topic_id :integer
# #
# Indexes # Indexes
# #

View File

@ -21,6 +21,7 @@ class PostAction < ActiveRecord::Base
def self.update_flagged_posts_count def self.update_flagged_posts_count
posts_flagged_count = PostAction.joins(post: :topic) posts_flagged_count = PostAction.joins(post: :topic)
.where('defer = false or defer IS NULL')
.where('post_actions.post_action_type_id' => PostActionType.notify_flag_type_ids, .where('post_actions.post_action_type_id' => PostActionType.notify_flag_type_ids,
'posts.deleted_at' => nil, 'posts.deleted_at' => nil,
'topics.deleted_at' => nil) 'topics.deleted_at' => nil)
@ -70,6 +71,25 @@ class PostAction < ActiveRecord::Base
update_flagged_posts_count update_flagged_posts_count
end end
def self.defer_flags!(post, moderator_id)
actions = PostAction.where(
defer: nil,
post_id: post.id,
post_action_type_id:
PostActionType.flag_types.values,
deleted_at: nil
)
actions.each do |a|
a.defer = true
a.defer_by = moderator_id
# so callback is called
a.save
end
update_flagged_posts_count
end
def self.act(user, post, post_action_type_id, opts={}) def self.act(user, post, post_action_type_id, opts={})
begin begin
title, target_usernames, target_group_names, subtype, body = nil title, target_usernames, target_group_names, subtype, body = nil
@ -179,12 +199,12 @@ class PostAction < ActiveRecord::Base
# can weigh flags differently. # can weigh flags differently.
def self.flag_counts_for(post_id) def self.flag_counts_for(post_id)
flag_counts = exec_sql("SELECT SUM(CASE flag_counts = exec_sql("SELECT SUM(CASE
WHEN pa.deleted_at IS NULL AND pa.staff_took_action THEN :flags_required_to_hide_post WHEN pa.deleted_at IS NULL AND (pa.staff_took_action) THEN :flags_required_to_hide_post
WHEN pa.deleted_at IS NULL AND (NOT pa.staff_took_action) THEN 1 WHEN pa.deleted_at IS NULL AND (NOT pa.staff_took_action) THEN 1
ELSE 0 ELSE 0
END) AS new_flags, END) AS new_flags,
SUM(CASE SUM(CASE
WHEN pa.deleted_at IS NOT NULL AND pa.staff_took_action THEN :flags_required_to_hide_post WHEN pa.deleted_at IS NOT NULL AND (pa.staff_took_action) THEN :flags_required_to_hide_post
WHEN pa.deleted_at IS NOT NULL AND (NOT pa.staff_took_action) THEN 1 WHEN pa.deleted_at IS NOT NULL AND (NOT pa.staff_took_action) THEN 1
ELSE 0 ELSE 0
END) AS old_flags END) AS old_flags
@ -228,27 +248,52 @@ class PostAction < ActiveRecord::Base
PostAction.update_flagged_posts_count PostAction.update_flagged_posts_count
end end
if PostActionType.auto_action_flag_types.include?(post_action_type) && SiteSetting.flags_required_to_hide_post > 0 PostAction.auto_hide_if_needed(post, post_action_type)
# automatic hiding of posts
SpamRulesEnforcer.enforce!(post.user) if post_action_type == :spam
end
def self.auto_hide_if_needed(post, post_action_type)
return if post.hidden
if PostActionType.auto_action_flag_types.include?(post_action_type) &&
SiteSetting.flags_required_to_hide_post > 0
old_flags, new_flags = PostAction.flag_counts_for(post.id) old_flags, new_flags = PostAction.flag_counts_for(post.id)
if new_flags >= SiteSetting.flags_required_to_hide_post if new_flags >= SiteSetting.flags_required_to_hide_post
reason = old_flags > 0 ? Post.hidden_reasons[:flag_threshold_reached_again] : Post.hidden_reasons[:flag_threshold_reached] hide_post!(post, guess_hide_reason(old_flags))
Post.update_all(["hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)", reason], id: post_id) end
end
end
def self.hide_post!(post, reason=nil)
return if post.hidden
unless reason
old_flags,_ = PostAction.flag_counts_for(post.id)
reason = guess_hide_reason(old_flags)
end
Post.update_all(["hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)", reason], id: post.id)
Topic.update_all({ visible: false }, Topic.update_all({ visible: false },
["id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", ["id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)",
topic_id: post.topic_id]) topic_id: post.topic_id])
# inform user # inform user
if post.user if post.user
SystemMessage.create(post.user, :post_hidden, SystemMessage.create(post.user,
:post_hidden,
url: post.url, url: post.url,
edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts) edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts)
end end
end end
end
SpamRulesEnforcer.enforce!(post.user) if post_action_type == :spam def self.guess_hide_reason(old_flags)
old_flags > 0 ?
Post.hidden_reasons[:flag_threshold_reached_again] :
Post.hidden_reasons[:flag_threshold_reached]
end end
def self.flagged_posts_report(filter) def self.flagged_posts_report(filter)
@ -268,7 +313,11 @@ class PostAction < ActiveRecord::Base
post_actions = PostAction.includes({:related_post => :topic}) post_actions = PostAction.includes({:related_post => :topic})
.where(post_action_type_id: PostActionType.notify_flag_type_ids) .where(post_action_type_id: PostActionType.notify_flag_type_ids)
.where(post_id: post_lookup.keys) .where(post_id: post_lookup.keys)
post_actions = post_actions.with_deleted if filter == 'old' if filter == 'old'
post_actions = post_actions.with_deleted.where('deleted_at IS NOT NULL OR defer = true')
else
post_actions = post_actions.where('defer IS NULL OR defer = false')
end
post_actions.each do |pa| post_actions.each do |pa|
post = post_lookup[pa.post_id] post = post_lookup[pa.post_id]
@ -309,13 +358,12 @@ class PostAction < ActiveRecord::Base
# it may make sense to add a view that shows flags on deleted posts, # it may make sense to add a view that shows flags on deleted posts,
# we don't clear the flags on post deletion, just supress counts # we don't clear the flags on post deletion, just supress counts
# they may have deleted_at on the action not set
if filter == 'old' if filter == 'old'
sql.where2 "deleted_at is not null" sql.where2 "deleted_at is not null OR defer = true"
sql.order_by "max desc" sql.order_by "max desc"
else else
sql.where "p.deleted_at is null and t.deleted_at is null" sql.where "p.deleted_at is null and t.deleted_at is null"
sql.where2 "deleted_at is null" sql.where2 "deleted_at is null and (defer IS NULL OR defer = false)"
sql.order_by "cnt desc, max asc" sql.order_by "cnt desc, max asc"
end end
@ -343,6 +391,8 @@ end
# message :text # message :text
# related_post_id :integer # related_post_id :integer
# staff_took_action :boolean default(FALSE), not null # staff_took_action :boolean default(FALSE), not null
# defer :boolean
# defer_by :integer
# #
# Indexes # Indexes
# #

View File

@ -653,6 +653,7 @@ end
# likes_received :integer default(0), not null # likes_received :integer default(0), not null
# topic_reply_count :integer default(0), not null # topic_reply_count :integer default(0), not null
# blocked :boolean default(FALSE) # blocked :boolean default(FALSE)
# dynamic_favicon :boolean default(FALSE), not null
# #
# Indexes # Indexes
# #

View File

@ -1033,10 +1033,18 @@ en:
title: "Flags" title: "Flags"
old: "Old" old: "Old"
active: "Active" active: "Active"
clear: "Clear Flags"
clear_title: "dismiss all flags on this post (will unhide hidden posts)" agree_hide: "Agree (Hide + PM)"
delete: "Delete Post" agree_hide_title: "Agree with flags, hide post and send user a private message"
delete_title: "delete post (if its the first post delete topic)" defer: "Defer"
defer_title: "Defer flag handling to the system"
delete_post: "Delete Post"
delete_post_title: "delete post (if its the first post delete topic)"
disagree_unhide: "Disagree (Unhide)"
disagree_unhide_title: "Disagree with flag, remove flags from post and show post"
disagree: "Disagree"
disagree_title: "Disagree with flag, remove flags from post"
flagged_by: "Flagged by" flagged_by: "Flagged by"
error: "Something went wrong" error: "Something went wrong"
view_message: "view message" view_message: "view message"

View File

@ -69,7 +69,9 @@ Discourse::Application.routes.draw do
get 'customize' => 'site_customizations#index', constraints: AdminConstraint.new get 'customize' => 'site_customizations#index', constraints: AdminConstraint.new
get 'flags' => 'flags#index' get 'flags' => 'flags#index'
get 'flags/:filter' => 'flags#index' get 'flags/:filter' => 'flags#index'
post 'flags/clear/:id' => 'flags#clear' post 'flags/agree/:id' => 'flags#agree'
post 'flags/disagree/:id' => 'flags#disagree'
post 'flags/defer/:id' => 'flags#defer'
resources :site_customizations, constraints: AdminConstraint.new resources :site_customizations, constraints: AdminConstraint.new
resources :site_contents, constraints: AdminConstraint.new resources :site_contents, constraints: AdminConstraint.new
resources :site_content_types, constraints: AdminConstraint.new resources :site_content_types, constraints: AdminConstraint.new

View File

@ -0,0 +1,7 @@
class AddDeferToPostActions < ActiveRecord::Migration
def change
# an action can be deferred by a moderator, used for flags
add_column :post_actions, :defer, :boolean
add_column :post_actions, :defer_by, :int
end
end

View File

@ -84,6 +84,20 @@ describe PostAction do
PostAction.flagged_posts_count.should == 0 PostAction.flagged_posts_count.should == 0
end end
it "should ignore validated flags" do
admin = Fabricate(:admin)
PostAction.act(codinghorror, post, PostActionType.types[:off_topic])
post.hidden.should be_false
PostAction.defer_flags!(post, admin.id)
PostAction.flagged_posts_count.should == 0
post.reload
post.hidden.should be_false
PostAction.hide_post!(post)
post.reload
post.hidden.should be_true
end
end end
describe "when a user bookmarks something" do describe "when a user bookmarks something" do
@ -220,7 +234,7 @@ describe PostAction do
u2 = Fabricate(:walter_white) u2 = Fabricate(:walter_white)
admin = Fabricate(:admin) # we need an admin for the messages admin = Fabricate(:admin) # we need an admin for the messages
SiteSetting.flags_required_to_hide_post = 2 SiteSetting.stubs(:flags_required_to_hide_post).returns(2)
PostAction.act(u1, post, PostActionType.types[:spam]) PostAction.act(u1, post, PostActionType.types[:spam])
PostAction.act(u2, post, PostActionType.types[:spam]) PostAction.act(u2, post, PostActionType.types[:spam])