From 8fcad73b363d4ea5e42442a38a231fd40bf167d7 Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Thu, 11 Mar 2021 08:21:24 -0300 Subject: [PATCH] FEATURE: Admins can flag posts so they can review them later. (#12311) Staff can send a post to the review queue by clicking the "Flag Post" button next to "Take Action...". Clicking it flags the post using the "Notify moderators" score type and hides it. A custom message will be sent to the user. --- .../app/components/flag-selection.js | 5 ++- .../discourse/app/controllers/flag.js | 11 ++++++ .../discourse/app/models/action-summary.js | 1 + .../discourse/app/templates/modal/flag.hbs | 7 ++++ app/controllers/post_actions_controller.rb | 3 +- app/models/post.rb | 9 ++++- config/locales/client.en.yml | 1 + config/locales/server.en.yml | 15 +++++++ lib/post_action_creator.rb | 25 +++++++++++- spec/components/post_action_creator_spec.rb | 39 +++++++++++++++++++ 10 files changed, 110 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/flag-selection.js b/app/assets/javascripts/discourse/app/components/flag-selection.js index ebb7209e4f8..1c43f4b0059 100644 --- a/app/assets/javascripts/discourse/app/components/flag-selection.js +++ b/app/assets/javascripts/discourse/app/components/flag-selection.js @@ -12,7 +12,10 @@ export default Component.extend({ return; } - this.element.querySelector("#radio_" + nameKey).checked = "true"; + const selector = this.element.querySelector("#radio_" + nameKey); + if (selector) { + selector.checked = "true"; + } }, @observes("nameKey") diff --git a/app/assets/javascripts/discourse/app/controllers/flag.js b/app/assets/javascripts/discourse/app/controllers/flag.js index 21c41e973b0..4872698dc65 100644 --- a/app/assets/javascripts/discourse/app/controllers/flag.js +++ b/app/assets/javascripts/discourse/app/controllers/flag.js @@ -266,6 +266,17 @@ export default Controller.extend(ModalFunctionality, { this.set("model.hidden", true); }, + flagForReview() { + const notifyModeratorsID = 7; + const notifyModerators = this.flagsAvailable.find( + (f) => f.id === notifyModeratorsID + ); + this.set("selected", notifyModerators); + + this.send("createFlag", { queue_for_review: true }); + this.set("model.hidden", true); + }, + changePostActionType(action) { this.set("selected", action); }, diff --git a/app/assets/javascripts/discourse/app/models/action-summary.js b/app/assets/javascripts/discourse/app/models/action-summary.js index 2bf455aaded..6c76700a06e 100644 --- a/app/assets/javascripts/discourse/app/models/action-summary.js +++ b/app/assets/javascripts/discourse/app/models/action-summary.js @@ -53,6 +53,7 @@ export default RestModel.extend({ message: opts.message, is_warning: opts.isWarning, take_action: opts.takeAction, + queue_for_review: opts.queue_for_review, flag_topic: this.flagTopic ? true : false, }, returnXHR: true, diff --git a/app/assets/javascripts/discourse/app/templates/modal/flag.hbs b/app/assets/javascripts/discourse/app/templates/modal/flag.hbs index 3b2284ccbac..0cea23d91f0 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/flag.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/flag.hbs @@ -40,6 +40,13 @@ performAction=(action "takeAction") reviewableUpdating=submitDisabled }} + + {{d-button + class="btn-danger" + action=(action "flagForReview") + icon="exclamation-triangle" + label="flagging.flag_for_review" + }} {{/if}} {{#if showDeleteSpammer}} diff --git a/app/controllers/post_actions_controller.rb b/app/controllers/post_actions_controller.rb index 594c7062ce1..9e757970446 100644 --- a/app/controllers/post_actions_controller.rb +++ b/app/controllers/post_actions_controller.rb @@ -16,7 +16,8 @@ class PostActionsController < ApplicationController is_warning: params[:is_warning], message: params[:message], take_action: params[:take_action] == 'true', - flag_topic: params[:flag_topic] == 'true' + flag_topic: params[:flag_topic] == 'true', + queue_for_review: params[:queue_for_review] == 'true' ) result = creator.perform diff --git a/app/models/post.rb b/app/models/post.rb index 3d835009136..5afec60316f 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -522,7 +522,7 @@ class Post < ActiveRecord::Base (topic.present? && (topic.private_message? || topic.category&.read_restricted)) end - def hide!(post_action_type_id, reason = nil) + def hide!(post_action_type_id, reason = nil, custom_message: nil) return if hidden? reason ||= hidden_at ? @@ -554,11 +554,16 @@ class Post < ActiveRecord::Base ) } + message = custom_message + if message.nil? + message = hiding_again ? :post_hidden_again : :post_hidden + end + Jobs.enqueue_in( 5.seconds, :send_system_message, user_id: user.id, - message_type: hiding_again ? :post_hidden_again : :post_hidden, + message_type: message, message_options: options ) end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3b19fe7f84e..f317a7a2a91 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3246,6 +3246,7 @@ en: notify_action: "Message" official_warning: "Official Warning" delete_spammer: "Delete Spammer" + flag_for_review: "Flag Post" # keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details delete_confirm_MF: "You are about to delete {POSTS, plural, one {# post} other {# posts}} and {TOPICS, plural, one {# topic} other {# topics}} from this user, remove their account, block signups from their IP address {ip_address}, and add their email address {email} to a permanent block list. Are you sure this user is really a spammer?" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 28344bb5df3..ada1d6e7278 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2820,6 +2820,20 @@ en: The community flagged this post and now it is hidden. **Because this post has been hidden more than once, your post will now remain hidden until it is handled by a staff member.** For additional guidance, please refer to our [community guidelines](%{base_url}/guidelines). + + queued_by_staff: + title: "Post Needs Approval" + subject_template: "Post hidden by staff, awaiting approval" + text_body_template: | + Hello, + + This is an automated message from %{site_name} to let you know that your post was hidden. + + <%{base_url}%{url}> + + Your post will remain hidden until a staff member reviews it. + + For additional guidance, please refer to our [community guidelines](%{base_url}/guidelines). flags_disagreed: title: "Flagged post restored by staff" @@ -4907,6 +4921,7 @@ en: email_spam: "This email was flagged as spam by the header defined in `email_in_spam_header`." suspect_user: "This new user entered profile information without reading any topics or posts, which strongly suggests they may be a spammer. See `approve_suspect_users`." contains_media: "This post includes embedded media. See `review_media_unless_trust_level`." + queued_by_staff: "A staff member thinks this post needs review. It'll remain hidden until then." actions: agree: diff --git a/lib/post_action_creator.rb b/lib/post_action_creator.rb index f64f860f8c4..fc7b2297181 100644 --- a/lib/post_action_creator.rb +++ b/lib/post_action_creator.rb @@ -39,6 +39,7 @@ class PostActionCreator take_action: false, flag_topic: false, created_at: nil, + queue_for_review: false, reason: nil ) @created_by = created_by @@ -54,7 +55,13 @@ class PostActionCreator @message = message @flag_topic = flag_topic @meta_post = nil + @reason = reason + @queue_for_review = queue_for_review + + if reason.nil? && @queue_for_review + @reason = 'queued_by_staff' + end end def post_can_act? @@ -71,7 +78,7 @@ class PostActionCreator def perform result = CreateResult.new - unless post_can_act? + if !post_can_act? || (@queue_for_review && !guardian.is_staff?) result.forbidden = true result.add_error(I18n.t("invalid_access")) return result @@ -186,7 +193,20 @@ private def auto_hide_if_needed return if @post.hidden? return if !@created_by.staff? && @post.user&.staff? - return unless PostActionType.auto_action_flag_types.include?(@post_action_name) + + not_auto_action_flag_type = !PostActionType.auto_action_flag_types.include?(@post_action_name) + return if not_auto_action_flag_type && !@queue_for_review + + if @queue_for_review + @post.topic.update_status('visible', false, @created_by) if @post.is_first_post? + + @post.hide!( + @post_action_type_id, + Post.hidden_reasons[:flag_threshold_reached], + custom_message: :queued_by_staff + ) + return + end if trusted_spam_flagger? @post.hide!(@post_action_type_id, Post.hidden_reasons[:flagged_by_tl3_user]) @@ -312,6 +332,7 @@ private targets_topic: @targets_topic } ) + result.reviewable_score = result.reviewable.add_score( @created_by, @post_action_type_id, diff --git a/spec/components/post_action_creator_spec.rb b/spec/components/post_action_creator_spec.rb index a68b874c035..63e5117537a 100644 --- a/spec/components/post_action_creator_spec.rb +++ b/spec/components/post_action_creator_spec.rb @@ -219,4 +219,43 @@ describe PostActionCreator do expect(reviewable.reload).to be_approved end end + + context "queue_for_review" do + fab!(:admin) { Fabricate(:admin) } + + it 'fails if the user is not a staff member' do + creator = PostActionCreator.new( + user, post, + PostActionType.types[:notify_moderators], queue_for_review: true + ) + result = creator.perform + + expect(result.success?).to eq(false) + end + + it 'creates a new reviewable and hides the post' do + result = build_creator.perform + + expect(result.success?).to eq(true) + + score = result.reviewable.reviewable_scores.last + expect(score.reason).to eq('queued_by_staff') + expect(post.reload.hidden?).to eq(true) + end + + it 'hides the topic even if it has replies' do + Fabricate(:post, topic: post.topic) + + result = build_creator.perform + + expect(post.topic.reload.visible?).to eq(false) + end + + def build_creator + PostActionCreator.new( + admin, post, + PostActionType.types[:notify_moderators], queue_for_review: true + ) + end + end end