diff --git a/app/assets/javascripts/admin/controllers/admin_flags_controller.js b/app/assets/javascripts/admin/controllers/admin_flags_controller.js index 6b1bfcc84ce..f1843e93d2a 100644 --- a/app/assets/javascripts/admin/controllers/admin_flags_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_flags_controller.js @@ -14,15 +14,32 @@ Discourse.AdminFlagsController = Ember.ArrayController.extend({ @method clearFlags @param {Discourse.FlaggedPost} item The post whose flags we want to clear **/ - clearFlags: function(item) { + disagreeFlags: function(item) { var adminFlagsController = this; - item.clearFlags().then((function() { + item.disagreeFlags().then((function() { adminFlagsController.removeObject(item); }), function() { 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 diff --git a/app/assets/javascripts/admin/models/flagged_post.js b/app/assets/javascripts/admin/models/flagged_post.js index 9392786e5a8..a93d74a3293 100644 --- a/app/assets/javascripts/admin/models/flagged_post.js +++ b/app/assets/javascripts/admin/models/flagged_post.js @@ -63,10 +63,22 @@ Discourse.FlaggedPost = Discourse.Post.extend({ } }, - clearFlags: function() { - return Discourse.ajax("/admin/flags/clear/" + this.id, { type: 'POST', cache: false }); + disagreeFlags: function() { + 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() { if (this.get('hidden') === "t") return "hidden-post"; }.property() diff --git a/app/assets/javascripts/admin/templates/flags.js.handlebars b/app/assets/javascripts/admin/templates/flags.js.handlebars index b38a67f519d..e63bd7e84d7 100644 --- a/app/assets/javascripts/admin/templates/flags.js.handlebars +++ b/app/assets/javascripts/admin/templates/flags.js.handlebars @@ -23,7 +23,7 @@ {{#each flag in content}} - + {{#linkTo 'adminUser' flag.user}}{{avatar flag.user imageSize="small"}}{{/linkTo}} {{#if flag.topicHidden}} {{/if}}

{{flag.title}}


{{{flag.excerpt}}} @@ -33,8 +33,15 @@ {{date flag.lastFlagged}} {{#if adminActiveFlagsView}} - - + {{#if flag.postHidden}} + + + {{else}} + + + {{/if}} + + {{/if}} diff --git a/app/assets/stylesheets/admin/admin_base.scss b/app/assets/stylesheets/admin/admin_base.scss index a6a4b4a8c5e..e1b599745e9 100644 --- a/app/assets/stylesheets/admin/admin_base.scss +++ b/app/assets/stylesheets/admin/admin_base.scss @@ -286,6 +286,10 @@ table { .flaggers { padding: 0 10px; } .last-flagged { padding: 0 10px; } .flag-summary { font-size: 11px; } + .action { + button { margin: 4px; display: block;} + padding-bottom: 20px; + } } /* Dashboard */ diff --git a/app/controllers/admin/flags_controller.rb b/app/controllers/admin/flags_controller.rb index 7bbd5fdaec4..0dae7442419 100644 --- a/app/controllers/admin/flags_controller.rb +++ b/app/controllers/admin/flags_controller.rb @@ -1,6 +1,5 @@ class Admin::FlagsController < Admin::AdminController def index - # we may get out of sync, fix it here PostAction.update_flagged_posts_count posts, users = PostAction.flagged_posts_report(params[:filter]) @@ -12,11 +11,24 @@ class Admin::FlagsController < Admin::AdminController end end - def clear + def disagree p = Post.find(params[:id]) PostAction.clear_flags!(p, current_user.id) p.reload p.unhide! render nothing: true 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 diff --git a/app/models/email_log.rb b/app/models/email_log.rb index 9b912fdb6ed..2efbdaf01a0 100644 --- a/app/models/email_log.rb +++ b/app/models/email_log.rb @@ -32,6 +32,8 @@ end # created_at :datetime not null # updated_at :datetime not null # reply_key :string(32) +# post_id :integer +# topic_id :integer # # Indexes # diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 6547cd7fde5..358fb655748 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -21,6 +21,7 @@ class PostAction < ActiveRecord::Base def self.update_flagged_posts_count 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, 'posts.deleted_at' => nil, 'topics.deleted_at' => nil) @@ -70,6 +71,25 @@ class PostAction < ActiveRecord::Base update_flagged_posts_count 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={}) begin title, target_usernames, target_group_names, subtype, body = nil @@ -179,12 +199,12 @@ class PostAction < ActiveRecord::Base # can weigh flags differently. def self.flag_counts_for(post_id) 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 ELSE 0 END) AS new_flags, 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 ELSE 0 END) AS old_flags @@ -228,27 +248,52 @@ class PostAction < ActiveRecord::Base PostAction.update_flagged_posts_count end - if PostActionType.auto_action_flag_types.include?(post_action_type) && SiteSetting.flags_required_to_hide_post > 0 - # automatic hiding of posts + PostAction.auto_hide_if_needed(post, post_action_type) + + 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) 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] - Post.update_all(["hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)", reason], id: post_id) - Topic.update_all({ visible: false }, - ["id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", - topic_id: post.topic_id]) - - # inform user - if post.user - SystemMessage.create(post.user, :post_hidden, - url: post.url, - edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts) - end + hide_post!(post, guess_hide_reason(old_flags)) end end + end - SpamRulesEnforcer.enforce!(post.user) if post_action_type == :spam + + 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 }, + ["id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", + topic_id: post.topic_id]) + + # inform user + if post.user + SystemMessage.create(post.user, + :post_hidden, + url: post.url, + edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts) + end + end + + def self.guess_hide_reason(old_flags) + old_flags > 0 ? + Post.hidden_reasons[:flag_threshold_reached_again] : + Post.hidden_reasons[:flag_threshold_reached] end def self.flagged_posts_report(filter) @@ -268,7 +313,11 @@ class PostAction < ActiveRecord::Base post_actions = PostAction.includes({:related_post => :topic}) .where(post_action_type_id: PostActionType.notify_flag_type_ids) .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 = 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, # 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' - sql.where2 "deleted_at is not null" + sql.where2 "deleted_at is not null OR defer = true" sql.order_by "max desc" else 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" end @@ -343,6 +391,8 @@ end # message :text # related_post_id :integer # staff_took_action :boolean default(FALSE), not null +# defer :boolean +# defer_by :integer # # Indexes # diff --git a/app/models/user.rb b/app/models/user.rb index 2be3b030679..9a41d5a6d06 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -653,6 +653,7 @@ end # likes_received :integer default(0), not null # topic_reply_count :integer default(0), not null # blocked :boolean default(FALSE) +# dynamic_favicon :boolean default(FALSE), not null # # Indexes # diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5e2bc97438e..fe19d5e12b2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1033,10 +1033,18 @@ en: title: "Flags" old: "Old" active: "Active" - clear: "Clear Flags" - clear_title: "dismiss all flags on this post (will unhide hidden posts)" - delete: "Delete Post" - delete_title: "delete post (if its the first post delete topic)" + + agree_hide: "Agree (Hide + PM)" + agree_hide_title: "Agree with flags, hide post and send user a private message" + 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" error: "Something went wrong" view_message: "view message" diff --git a/config/routes.rb b/config/routes.rb index 93470f04d8e..424ce3a3612 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,7 +69,9 @@ Discourse::Application.routes.draw do get 'customize' => 'site_customizations#index', constraints: AdminConstraint.new get 'flags' => '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_contents, constraints: AdminConstraint.new resources :site_content_types, constraints: AdminConstraint.new diff --git a/db/migrate/20130619063902_add_defer_to_post_actions.rb b/db/migrate/20130619063902_add_defer_to_post_actions.rb new file mode 100644 index 00000000000..ac290b92d8a --- /dev/null +++ b/db/migrate/20130619063902_add_defer_to_post_actions.rb @@ -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 diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index bac455e97c1..8100525e3b1 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -84,6 +84,20 @@ describe PostAction do PostAction.flagged_posts_count.should == 0 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 describe "when a user bookmarks something" do @@ -220,7 +234,7 @@ describe PostAction do u2 = Fabricate(:walter_white) 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(u2, post, PostActionType.types[:spam])