From 8c7cc426b7064523925fae817ef9a5a138a63d50 Mon Sep 17 00:00:00 2001 From: janzenisaac <50783505+janzenisaac@users.noreply.github.com> Date: Wed, 24 Nov 2021 09:28:20 -0600 Subject: [PATCH] FEATURE: Notify responders of post removal (#15049) - Notify users whose posts were cascade deleted due to a flagged post --- config/locales/server.en.yml | 24 +++++++++++++ config/site_settings.yml | 2 ++ lib/post_destroyer.rb | 19 +++++++---- spec/fabricators/reviewable_fabricator.rb | 3 ++ .../reviewable_score_fabricator.rb | 10 ++++++ spec/models/reviewable_flagged_post_spec.rb | 21 +++++++++--- .../topic_view_posts_serializer_spec.rb | 34 +++++++------------ 7 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 spec/fabricators/reviewable_score_fabricator.rb diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6db14aa86bb..c090367f9e5 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1498,6 +1498,7 @@ en: tl2_post_edit_time_limit: "A tl2+ author can edit their post for (n) minutes after posting. Set to 0 for forever." edit_history_visible_to_public: "Allow everyone to see previous versions of an edited post. When disabled, only staff members can view." delete_removed_posts_after: "Posts removed by the author will be automatically deleted after (n) hours. If set to 0, posts will be deleted immediately." + notify_users_after_responses_deleted_on_flagged_post: "When a post is flagged and then removed, all users that responded to the post and had their responses removed will be notified." max_image_width: "Maximum thumbnail width of images in a post" max_image_height: "Maximum thumbnail height of images in a post" responsive_post_image_sizes: "Resize lightbox preview images to allow for high DPI screens of the following pixel ratios. Remove all values to disable responsive images." @@ -2791,6 +2792,11 @@ en: inappropriate: "Your post was flagged as **inappropriate**: the community feels it is offensive, abusive, or a violation of [our community guidelines](%{base_path}/guidelines)." spam: "Your post was flagged as **spam**: the community feels it is an advertisement, something that is overly promotional in nature instead of being useful or relevant to the topic as expected." notify_moderators: "Your post was flagged **for moderator attention**: the community feels something about the post requires manual intervention by a staff member." + responder: + off_topic: "The post was flagged as **off-topic**: the community feels it is not a good fit for the topic, as currently defined by the title and the first post." + inappropriate: "The post was flagged as **inappropriate**: the community feels it is offensive, abusive, or a violation of [our community guidelines](%{base_path}/guidelines)." + spam: "The post was flagged as **spam**: the community feels it is an advertisement, something that is overly promotional in nature instead of being useful or relevant to the topic as expected." + notify_moderators: "The post was flagged **for moderator attention**: the community feels something about the post requires manual intervention by a staff member." flags_dispositions: agreed: "Thanks for letting us know. We agree there is an issue and we're looking into it." @@ -2889,6 +2895,24 @@ en: Please review our [community guidelines](%{base_url}/guidelines) for details. + flags_agreed_and_post_deleted_for_responders: + title: "Reply removed from flagged post by staff" + subject_template: "Reply removed from flagged post by staff" + text_body_template: | + Hello, + + This is an automated message from %{site_name} to let you know that a [post](%{base_url}%{url}) you replied to was removed. + + %{flag_reason} + + This post was flagged by the community and a staff member opted to remove it. + + ``` markdown + %{flagged_post_raw_content} + ``` + + For more details on the reason for removal, please review our [community guidelines](%{base_url}/guidelines). + usage_tips: text_body_template: | For a few quick tips on getting started as a new user, [check out this blog post](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/). diff --git a/config/site_settings.yml b/config/site_settings.yml index 9ea8cf78497..508981517d4 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -828,6 +828,8 @@ posting: delete_removed_posts_after: client: true default: 24 + notify_users_after_responses_deleted_on_flagged_post: + default: false traditional_markdown_linebreaks: client: true default: false diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 9d09c7ee32d..427fd6f08dd 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -43,7 +43,10 @@ class PostDestroyer reply_ids = post.reply_ids(Guardian.new(performed_by), only_replies_to_single_post: false) replies = Post.where(id: reply_ids.map { |r| r[:id] }) PostDestroyer.new(performed_by, post, reviewable: reviewable).destroy - replies.each { |reply| PostDestroyer.new(performed_by, reply, defer_flags: defer_reply_flags).destroy } + + options = { defer_flags: defer_reply_flags } + options.merge!({ reviewable: reviewable, notify_responders: true, parent_post: post }) if SiteSetting.notify_users_after_responses_deleted_on_flagged_post + replies.each { |reply| PostDestroyer.new(performed_by, reply, options).destroy } end def initialize(user, post, opts = {}) @@ -179,7 +182,7 @@ class PostDestroyer DB.after_commit do if @opts[:reviewable] - notify_deletion(@opts[:reviewable]) + notify_deletion(@opts[:reviewable], { notify_responders: @opts[:notify_responders], parent_post: @opts[:parent_post] }) elsif reviewable = @post.reviewable_flag @opts[:defer_flags] ? ignore(reviewable) : agree(reviewable) end @@ -315,21 +318,23 @@ class PostDestroyer reviewable.transition_to(:ignored, @user) end - def notify_deletion(reviewable) + def notify_deletion(reviewable, options = {}) return if @post.user.blank? allowed_user = @user.human? && @user.staff? return unless allowed_user && rs = reviewable.reviewable_scores.order('created_at DESC').first + notify_responders = options[:notify_responders] + Jobs.enqueue( :send_system_message, user_id: @post.user_id, - message_type: :flags_agreed_and_post_deleted, + message_type: notify_responders ? :flags_agreed_and_post_deleted_for_responders : :flags_agreed_and_post_deleted, message_options: { - flagged_post_raw_content: @post.raw, - url: @post.url, + flagged_post_raw_content: notify_responders ? options[:parent_post].raw : @post.raw, + url: notify_responders ? options[:parent_post].url : @post.url, flag_reason: I18n.t( - "flag_reasons.#{PostActionType.types[rs.reviewable_score_type]}", + "flag_reasons#{".responder" if notify_responders}.#{PostActionType.types[rs.reviewable_score_type]}", locale: SiteSetting.default_locale, base_path: Discourse.base_path ) diff --git a/spec/fabricators/reviewable_fabricator.rb b/spec/fabricators/reviewable_fabricator.rb index bbf825d105a..b2f26120406 100644 --- a/spec/fabricators/reviewable_fabricator.rb +++ b/spec/fabricators/reviewable_fabricator.rb @@ -57,6 +57,9 @@ Fabricator(:reviewable_flagged_post) do topic target_type 'Post' target { Fabricate(:post) } + reviewable_scores { |p| [ + Fabricate.build(:reviewable_score, reviewable_id: p[:id]), + ]} end Fabricator(:reviewable_user) do diff --git a/spec/fabricators/reviewable_score_fabricator.rb b/spec/fabricators/reviewable_score_fabricator.rb new file mode 100644 index 00000000000..81e781baaef --- /dev/null +++ b/spec/fabricators/reviewable_score_fabricator.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Fabricator(:reviewable_score) do + reviewable { Fabricate(:reviewable) } + user { Fabricate(:user) } + reviewable_score_type { 4 } + status { 1 } + score { 11.0 } + reviewed_by { Fabricate(:user) } +end diff --git a/spec/models/reviewable_flagged_post_spec.rb b/spec/models/reviewable_flagged_post_spec.rb index 8b36f825cac..f322024d84e 100644 --- a/spec/models/reviewable_flagged_post_spec.rb +++ b/spec/models/reviewable_flagged_post_spec.rb @@ -283,16 +283,27 @@ RSpec.describe ReviewableFlaggedPost, type: :model do end describe "#perform_delete_and_agree_replies" do - it 'ignore flagged replies' do - flagged_post = Fabricate(:reviewable_flagged_post) - reply = create_reply(flagged_post.target) - flagged_post.target.update(reply_count: 1) - flagged_reply = Fabricate(:reviewable_flagged_post, target: reply) + let(:flagged_post) { Fabricate(:reviewable_flagged_post) } + let!(:reply) { create_reply(flagged_post.target) } + before do + flagged_post.target.update(reply_count: 1) + end + + it 'ignore flagged replies' do + flagged_reply = Fabricate(:reviewable_flagged_post, target: reply) flagged_post.perform(moderator, :delete_and_agree_replies) expect(flagged_reply.reload.status).to eq(Reviewable.statuses[:ignored]) end + + it 'notifies users that responded to flagged post' do + SiteSetting.notify_users_after_responses_deleted_on_flagged_post = true + flagged_post.perform(moderator, :delete_and_agree_replies) + + expect(Jobs::SendSystemMessage.jobs.size).to eq(2) + expect(Jobs::SendSystemMessage.jobs.last["args"].first["message_type"]).to eq("flags_agreed_and_post_deleted_for_responders") + end end describe "#perform_disagree_and_restore" do diff --git a/spec/serializers/topic_view_posts_serializer_spec.rb b/spec/serializers/topic_view_posts_serializer_spec.rb index 55be89f36da..04c43ffdf16 100644 --- a/spec/serializers/topic_view_posts_serializer_spec.rb +++ b/spec/serializers/topic_view_posts_serializer_spec.rb @@ -4,28 +4,20 @@ require 'rails_helper' describe TopicViewPostsSerializer do + let(:user) { Fabricate(:user) } + let(:post) { Fabricate(:post) } + let(:topic) { post.topic } + let!(:reviewable) { Fabricate(:reviewable_flagged_post, + created_by: user, + target: post, + topic: topic, + reviewable_scores: [ + Fabricate(:reviewable_score, reviewable_score_type: 0, status: ReviewableScore.statuses[:pending]), + Fabricate(:reviewable_score, reviewable_score_type: 0, status: ReviewableScore.statuses[:ignored]) + ] + )} + it 'should return the right attributes' do - - user = Fabricate(:user) - post = Fabricate(:post) - topic = post.topic - - reviewable = Fabricate(:reviewable_flagged_post, created_by: user, target: post, topic: topic) - - ReviewableScore.create!( - reviewable_id: reviewable.id, - user_id: user.id, - reviewable_score_type: 0, - status: ReviewableScore.statuses[:pending] - ) - - ReviewableScore.create!( - reviewable_id: reviewable.id, - user_id: user.id, - reviewable_score_type: 0, - status: ReviewableScore.statuses[:ignored] - ) - topic_view = TopicView.new(topic, user, post_ids: [post.id]) serializer = TopicViewPostsSerializer.new(