mirror of
https://github.com/discourse/discourse.git
synced 2025-06-10 17:23:49 +08:00
flagging workflow changes per http://meta.discourse.org/t/we-need-an-archive-flag-notification-button/7450
This commit is contained in:
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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>
|
||||||
|
@ -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 */
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
7
db/migrate/20130619063902_add_defer_to_post_actions.rb
Normal file
7
db/migrate/20130619063902_add_defer_to_post_actions.rb
Normal 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
|
@ -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])
|
||||||
|
Reference in New Issue
Block a user