FEATURE: Notify responders of post removal (#15049)

- Notify users whose posts were cascade deleted due to a flagged post
This commit is contained in:
janzenisaac 2021-11-24 09:28:20 -06:00 committed by GitHub
parent 9105163882
commit 8c7cc426b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 80 additions and 33 deletions

View File

@ -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/).

View File

@ -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

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(