Fix #4669: Track moderation report status.
* Add ability to mark moderation reports as 'handled' or 'rejected'. * Automatically mark reports as handled when the comment or forum post is deleted. * Send a dmail to the reporter when their report is handled. * Don't show the report notice on comments or forum posts when all reports against it have been handled or rejected. * Add a fix script to mark all existing reports for deleted comments, forum posts, or dmails as handled.
This commit is contained in:
@@ -42,6 +42,6 @@ class CommentComponent < ApplicationComponent
|
||||
end
|
||||
|
||||
def reported?
|
||||
policy(ModerationReport).can_see_moderation_reports? && comment.moderation_reports.present?
|
||||
policy(ModerationReport).can_see_moderation_reports? && comment.pending_moderation_reports.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
<% if reported? %>
|
||||
<li class="moderation-report-notice">
|
||||
Reported (<%= link_to pluralize(comment.moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "Comment", model_id: comment.id }) %>)
|
||||
Reported (<%= link_to pluralize(comment.pending_moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "Comment", model_id: comment.id, status: "pending" }) %>)
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class CommentSectionComponent < ApplicationComponent
|
||||
@comments = @post.comments.order(id: :asc)
|
||||
@comments = @comments.includes(:creator)
|
||||
@comments = @comments.includes(:votes) if !current_user.is_anonymous?
|
||||
@comments = @comments.includes(:moderation_reports) if policy(ModerationReport).can_see_moderation_reports?
|
||||
@comments = @comments.includes(:pending_moderation_reports) if policy(ModerationReport).can_see_moderation_reports?
|
||||
@comments = @comments.last(limit) if limit.present?
|
||||
|
||||
@dtext_data = DText.preprocess(@comments.map(&:body))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ForumPostComponent < ApplicationComponent
|
||||
attr_reader :forum_post, :original_forum_post_id, :dtext_data, :moderation_reports, :current_user
|
||||
attr_reader :forum_post, :original_forum_post_id, :dtext_data, :current_user
|
||||
|
||||
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :policy, :data_attributes_for, to: :helpers
|
||||
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :data_attributes_for, to: :helpers
|
||||
|
||||
with_collection_parameter :forum_post
|
||||
|
||||
@@ -12,7 +12,7 @@ class ForumPostComponent < ApplicationComponent
|
||||
original_forum_post_id = forum_topic.original_post&.id
|
||||
|
||||
forum_posts = forum_posts.includes(:creator, :bulk_update_request)
|
||||
forum_posts = forum_posts.includes(:moderation_reports) if Pundit.policy!(current_user, ModerationReport).show?
|
||||
forum_posts = forum_posts.includes(:pending_moderation_reports) if Pundit.policy(current_user, ModerationReport).can_see_moderation_reports?
|
||||
|
||||
super(forum_posts, dtext_data: dtext_data, original_forum_post_id: original_forum_post_id, current_user: current_user)
|
||||
end
|
||||
@@ -29,7 +29,7 @@ class ForumPostComponent < ApplicationComponent
|
||||
policy(forum_post).show_deleted?
|
||||
end
|
||||
|
||||
def has_moderation_reports?
|
||||
policy(ModerationReport).can_see_moderation_reports? && forum_post.moderation_reports.present?
|
||||
def reported?
|
||||
policy(ModerationReport).can_see_moderation_reports? && forum_post.pending_moderation_reports.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= tag.article class: "forum-post message", id: "forum_post_#{forum_post.id}", data: { "is-reported": has_moderation_reports?, **data_attributes_for(forum_post, "", forum_post.html_data_attributes) } do %>
|
||||
<%= tag.article class: "forum-post message", id: "forum_post_#{forum_post.id}", data: { "is-reported": reported?, **data_attributes_for(forum_post, "", forum_post.html_data_attributes) } do %>
|
||||
<div class="author">
|
||||
<div class="author-name">
|
||||
<%= link_to_user forum_post.creator %>
|
||||
@@ -24,8 +24,10 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if has_moderation_reports? %>
|
||||
<li class="moderation-report-notice">Reported (<%= link_to pluralize(forum_post.moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "ForumPost", model_id: forum_post.id }) %>)</li>
|
||||
<% if reported? %>
|
||||
<li class="moderation-report-notice">
|
||||
Reported (<%= link_to pluralize(forum_post.pending_moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "ForumPost", model_id: forum_post.id, status: "pending" }) %>)
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<%= render PopupMenuComponent.new do |menu| %>
|
||||
|
||||
@@ -29,4 +29,12 @@ class ModerationReportsController < ApplicationController
|
||||
flash.now[:notice] = @moderation_report.valid? ? "Report submitted" : @moderation_report.errors.full_messages.join("; ")
|
||||
respond_with(@moderation_report)
|
||||
end
|
||||
|
||||
def update
|
||||
@moderation_report = authorize ModerationReport.find(params[:id])
|
||||
@moderation_report.update(permitted_attributes(@moderation_report))
|
||||
|
||||
flash.now[:notice] = @moderation_report.valid? ? "Report updated" : @moderation_report.errors.full_messages.join("; ")
|
||||
respond_with(@moderation_report)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,11 +6,13 @@ class Comment < ApplicationRecord
|
||||
belongs_to_updater
|
||||
|
||||
has_many :moderation_reports, as: :model, dependent: :destroy
|
||||
has_many :pending_moderation_reports, -> { pending }, as: :model, class_name: "ModerationReport"
|
||||
has_many :votes, class_name: "CommentVote", dependent: :destroy
|
||||
|
||||
validates :body, presence: true, length: { maximum: 15_000 }, if: :body_changed?
|
||||
|
||||
before_create :autoreport_spam
|
||||
before_save :handle_reports_on_deletion
|
||||
after_create :update_last_commented_at_on_create
|
||||
after_update(:if => ->(rec) {(!rec.is_deleted? || !rec.saved_change_to_is_deleted?) && CurrentUser.id != rec.creator_id}) do |rec|
|
||||
ModAction.log("comment ##{rec.id} updated by #{CurrentUser.user.name}", :comment_update)
|
||||
@@ -78,6 +80,13 @@ class Comment < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def handle_reports_on_deletion
|
||||
return unless Pundit.policy!(updater, ModerationReport).update?
|
||||
return unless moderation_reports.pending.present? && is_deleted_change == [false, true]
|
||||
|
||||
moderation_reports.pending.update!(status: :handled)
|
||||
end
|
||||
|
||||
def quoted_response
|
||||
DText.quote(body, creator.name)
|
||||
end
|
||||
|
||||
@@ -8,6 +8,7 @@ class ForumPost < ApplicationRecord
|
||||
belongs_to :topic, class_name: "ForumTopic", inverse_of: :forum_posts
|
||||
|
||||
has_many :moderation_reports, as: :model
|
||||
has_many :pending_moderation_reports, -> { pending }, as: :model, class_name: "ModerationReport"
|
||||
has_many :votes, class_name: "ForumPostVote"
|
||||
has_one :tag_alias
|
||||
has_one :tag_implication
|
||||
@@ -16,6 +17,7 @@ class ForumPost < ApplicationRecord
|
||||
validates :body, presence: true, length: { maximum: 200_000 }, if: :body_changed?
|
||||
|
||||
before_create :autoreport_spam
|
||||
before_save :handle_reports_on_deletion
|
||||
after_create :update_topic_updated_at_on_create
|
||||
after_update :update_topic_updated_at_on_update_for_original_posts
|
||||
after_destroy :update_topic_updated_at_on_destroy
|
||||
@@ -157,6 +159,12 @@ class ForumPost < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def handle_reports_on_deletion
|
||||
return unless moderation_reports.pending.present? && is_deleted_change == [false, true]
|
||||
|
||||
moderation_reports.pending.update!(status: :handled)
|
||||
end
|
||||
|
||||
def async_send_discord_notification
|
||||
DiscordNotificationJob.perform_later(forum_post: self)
|
||||
end
|
||||
|
||||
@@ -12,12 +12,19 @@ class ModerationReport < ApplicationRecord
|
||||
|
||||
after_create :create_forum_post!
|
||||
after_create :autoban_reported_user
|
||||
after_save :notify_reporter
|
||||
|
||||
scope :dmail, -> { where(model_type: "Dmail") }
|
||||
scope :comment, -> { where(model_type: "Comment") }
|
||||
scope :forum_post, -> { where(model_type: "ForumPost") }
|
||||
scope :recent, -> { where("moderation_reports.created_at >= ?", 1.week.ago) }
|
||||
|
||||
enum status: {
|
||||
pending: 0,
|
||||
rejected: 1,
|
||||
handled: 2,
|
||||
}
|
||||
|
||||
def self.model_types
|
||||
MODEL_TYPES
|
||||
end
|
||||
@@ -73,6 +80,15 @@ class ModerationReport < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def notify_reporter
|
||||
return if creator == User.system
|
||||
return unless handled? && status_before_last_save != :handled
|
||||
|
||||
Dmail.create_automated(to: creator, title: "Thank you for reporting #{model.dtext_shortlink}", body: <<~EOS)
|
||||
Thank you for reporting #{model.dtext_shortlink}. Action has been taken against the user.
|
||||
EOS
|
||||
end
|
||||
|
||||
def reported_user
|
||||
case model
|
||||
when Comment, ForumPost
|
||||
@@ -85,7 +101,7 @@ class ModerationReport < ApplicationRecord
|
||||
end
|
||||
|
||||
def self.search(params)
|
||||
q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator, :model)
|
||||
q = search_attributes(params, :id, :created_at, :updated_at, :reason, :creator, :model, :status)
|
||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||
|
||||
q.apply_default_order(params)
|
||||
|
||||
@@ -13,11 +13,19 @@ class ModerationReportPolicy < ApplicationPolicy
|
||||
unbanned? && policy(record.model).reportable?
|
||||
end
|
||||
|
||||
def update?
|
||||
user.is_moderator?
|
||||
end
|
||||
|
||||
def can_see_moderation_reports?
|
||||
user.is_moderator?
|
||||
end
|
||||
|
||||
def permitted_attributes
|
||||
def permitted_attributes_for_create
|
||||
[:model_type, :model_id, :reason]
|
||||
end
|
||||
|
||||
def permitted_attributes_for_update
|
||||
[:status]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<%= f.input :reason_matches, label: "Reason", input_html: { value: params[:search][:reason_matches] } %>
|
||||
<%= f.input :model_id, label: "ID", input_html: { value: params[:search][:model_id] } %>
|
||||
<%= f.input :model_type, label: "Type", collection: ModerationReport.model_types, include_blank: true, selected: params[:search][:model_type] %>
|
||||
<%= f.input :status, label: "Status", collection: ModerationReport.statuses.keys, include_blank: true, selected: params[:search][:status] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
@@ -26,6 +27,10 @@
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Status" do |report| %>
|
||||
<%= link_to report.status.capitalize, moderation_reports_path(search: { status: report.status }) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Reported user" do |report| %>
|
||||
<%= link_to_user report.reported_user %>
|
||||
<% end %>
|
||||
@@ -35,6 +40,18 @@
|
||||
<%= link_to "»", moderation_reports_path(search: { creator_name: report.creator.name }) %>
|
||||
<div><%= time_ago_in_words_tagged(report.created_at) %></div>
|
||||
<% end %>
|
||||
|
||||
<% t.column column: "control" do |report| %>
|
||||
<%= render PopupMenuComponent.new do |menu| %>
|
||||
<% menu.item do %>
|
||||
<%= link_to "Mark as handled", moderation_report_path(report.id), method: :put, remote: true, "data-params": "moderation_report[status]=handled" %>
|
||||
<% end %>
|
||||
|
||||
<% menu.item do %>
|
||||
<%= link_to "Reject", moderation_report_path(report.id), method: :put, remote: true, "data-params": "moderation_report[status]=rejected" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= numbered_paginator(@moderation_reports) %>
|
||||
|
||||
1
app/views/moderation_reports/update.js
Normal file
1
app/views/moderation_reports/update.js
Normal file
@@ -0,0 +1 @@
|
||||
location.reload();
|
||||
@@ -158,7 +158,7 @@ Rails.application.routes.draw do
|
||||
resources :media_assets, only: [:index, :show]
|
||||
resources :media_metadata, only: [:index]
|
||||
resources :mod_actions
|
||||
resources :moderation_reports, only: [:new, :create, :index, :show]
|
||||
resources :moderation_reports, only: [:new, :create, :index, :show, :update]
|
||||
resources :modqueue, only: [:index]
|
||||
resources :news_updates
|
||||
resources :notes do
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddStatusToModerationReports < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :moderation_reports, :status, :integer, null: false, default: 0
|
||||
add_index :moderation_reports, :status
|
||||
end
|
||||
end
|
||||
@@ -1202,7 +1202,8 @@ CREATE TABLE public.moderation_reports (
|
||||
model_type character varying NOT NULL,
|
||||
model_id bigint NOT NULL,
|
||||
creator_id integer NOT NULL,
|
||||
reason text NOT NULL
|
||||
reason text NOT NULL,
|
||||
status integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -3847,6 +3848,13 @@ CREATE INDEX index_moderation_reports_on_creator_id ON public.moderation_reports
|
||||
CREATE INDEX index_moderation_reports_on_model_type_and_model_id ON public.moderation_reports USING btree (model_type, model_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_moderation_reports_on_status; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_moderation_reports_on_status ON public.moderation_reports USING btree (status);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_news_updates_on_created_at; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -5688,6 +5696,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20220110171021'),
|
||||
('20220110171022'),
|
||||
('20220110171023'),
|
||||
('20220110171024');
|
||||
('20220110171024'),
|
||||
('20220120233850');
|
||||
|
||||
|
||||
|
||||
9
script/fixes/094_mark_modreports_as_handled.rb
Executable file
9
script/fixes/094_mark_modreports_as_handled.rb
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "base"
|
||||
|
||||
with_confirmation do
|
||||
ModerationReport.pending.where(model: Comment.deleted).update_all(status: :handled, updated_at: Time.zone.now)
|
||||
ModerationReport.pending.where(model: ForumPost.deleted).update_all(status: :handled, updated_at: Time.zone.now)
|
||||
ModerationReport.pending.where(model: Dmail.deleted).update_all(status: :handled, updated_at: Time.zone.now)
|
||||
end
|
||||
@@ -2,5 +2,6 @@ FactoryBot.define do
|
||||
factory(:moderation_report) do
|
||||
creator
|
||||
reason {"xxx"}
|
||||
status { :pending }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -243,6 +243,18 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_equal(true, @comment.reload.is_deleted)
|
||||
assert_redirected_to @comment
|
||||
end
|
||||
|
||||
should "mark all pending moderation reports against the comment as handled" do
|
||||
@comment = create(:comment, post: @post)
|
||||
report1 = create(:moderation_report, model: @comment, status: :pending)
|
||||
report2 = create(:moderation_report, model: @comment, status: :rejected)
|
||||
delete_auth comment_path(@comment.id), @mod
|
||||
|
||||
assert_redirected_to @comment
|
||||
assert_equal(true, @comment.reload.is_deleted)
|
||||
assert_equal(true, report1.reload.handled?)
|
||||
assert_equal(true, report2.reload.rejected?)
|
||||
end
|
||||
end
|
||||
|
||||
context "undelete action" do
|
||||
|
||||
@@ -229,6 +229,17 @@ class ForumPostsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response 403
|
||||
assert_equal(false, @forum_post.reload.is_deleted?)
|
||||
end
|
||||
|
||||
should "mark all pending moderation reports against the post as handled" do
|
||||
report1 = create(:moderation_report, model: @forum_post, status: :pending)
|
||||
report2 = create(:moderation_report, model: @forum_post, status: :rejected)
|
||||
delete_auth forum_post_path(@forum_post), @mod
|
||||
|
||||
assert_redirected_to(forum_post_path(@forum_post))
|
||||
assert_equal(true, @forum_post.reload.is_deleted?)
|
||||
assert_equal(true, report1.reload.handled?)
|
||||
assert_equal(true, report2.reload.rejected?)
|
||||
end
|
||||
end
|
||||
|
||||
context "undelete action" do
|
||||
|
||||
@@ -104,5 +104,32 @@ class ModerationReportsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "update action" do
|
||||
should "not allow non-mods to update moderation reports" do
|
||||
report = create(:moderation_report, model: @comment, creator: @user)
|
||||
put_auth moderation_report_path(report), @user, params: { moderation_report: { status: "handled" }}, xhr: true
|
||||
|
||||
assert_response 403
|
||||
end
|
||||
|
||||
should "allow a moderator to mark a moderation report as handled" do
|
||||
report = create(:moderation_report, model: @comment, creator: @user)
|
||||
put_auth moderation_report_path(report), @mod, params: { moderation_report: { status: "handled" }}, xhr: true
|
||||
|
||||
assert_response :success
|
||||
assert_equal("handled", report.reload.status)
|
||||
assert_equal(true, @user.dmails.received.exists?(from: User.system, title: "Thank you for reporting comment ##{@comment.id}"))
|
||||
end
|
||||
|
||||
should "allow a moderator to mark a moderation report as rejected" do
|
||||
report = create(:moderation_report, model: @comment, creator: @user)
|
||||
put_auth moderation_report_path(report), @mod, params: { moderation_report: { status: "rejected" }}, xhr: true
|
||||
|
||||
assert_response :success
|
||||
assert_equal("rejected", report.reload.status)
|
||||
assert_equal(false, @user.dmails.received.exists?(from: User.system))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user