diff --git a/app/components/comment_component.rb b/app/components/comment_component.rb index 7d10555d6..78edb9c83 100644 --- a/app/components/comment_component.rb +++ b/app/components/comment_component.rb @@ -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 diff --git a/app/components/comment_component/comment_component.html.erb b/app/components/comment_component/comment_component.html.erb index a37a7a1ad..50c92bd29 100644 --- a/app/components/comment_component/comment_component.html.erb +++ b/app/components/comment_component/comment_component.html.erb @@ -85,7 +85,7 @@ <% if reported? %>
  • - 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" }) %>)
  • <% end %> diff --git a/app/components/comment_section_component.rb b/app/components/comment_section_component.rb index 5c3110bc0..0dec08161 100644 --- a/app/components/comment_section_component.rb +++ b/app/components/comment_section_component.rb @@ -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)) diff --git a/app/components/forum_post_component.rb b/app/components/forum_post_component.rb index 87c4f3b71..2e09a1072 100644 --- a/app/components/forum_post_component.rb +++ b/app/components/forum_post_component.rb @@ -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 diff --git a/app/components/forum_post_component/forum_post_component.html.erb b/app/components/forum_post_component/forum_post_component.html.erb index 8005ca173..cd1ce382f 100644 --- a/app/components/forum_post_component/forum_post_component.html.erb +++ b/app/components/forum_post_component/forum_post_component.html.erb @@ -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 %>
    <%= link_to_user forum_post.creator %> @@ -24,8 +24,10 @@ <% end %> <% end %> - <% if has_moderation_reports? %> -
  • Reported (<%= link_to pluralize(forum_post.moderation_reports.length, "report"), moderation_reports_path(search: { model_type: "ForumPost", model_id: forum_post.id }) %>)
  • + <% if reported? %> +
  • + 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" }) %>) +
  • <% end %> <%= render PopupMenuComponent.new do |menu| %> diff --git a/app/controllers/moderation_reports_controller.rb b/app/controllers/moderation_reports_controller.rb index 79fcade0e..99eefa52e 100644 --- a/app/controllers/moderation_reports_controller.rb +++ b/app/controllers/moderation_reports_controller.rb @@ -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 diff --git a/app/models/comment.rb b/app/models/comment.rb index 9f2b09a11..8454569e0 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -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 diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb index d9b4ce309..b87892bc9 100644 --- a/app/models/forum_post.rb +++ b/app/models/forum_post.rb @@ -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 diff --git a/app/models/moderation_report.rb b/app/models/moderation_report.rb index 0e658ec31..69cd54bfa 100644 --- a/app/models/moderation_report.rb +++ b/app/models/moderation_report.rb @@ -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) diff --git a/app/policies/moderation_report_policy.rb b/app/policies/moderation_report_policy.rb index be2d041ef..5169e69fc 100644 --- a/app/policies/moderation_report_policy.rb +++ b/app/policies/moderation_report_policy.rb @@ -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 diff --git a/app/views/moderation_reports/index.html.erb b/app/views/moderation_reports/index.html.erb index 12c22cad9..0a6e68ea8 100644 --- a/app/views/moderation_reports/index.html.erb +++ b/app/views/moderation_reports/index.html.erb @@ -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 @@ <% 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 }) %>
    <%= time_ago_in_words_tagged(report.created_at) %>
    <% 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) %> diff --git a/app/views/moderation_reports/update.js b/app/views/moderation_reports/update.js new file mode 100644 index 000000000..345366b9b --- /dev/null +++ b/app/views/moderation_reports/update.js @@ -0,0 +1 @@ +location.reload(); diff --git a/config/routes.rb b/config/routes.rb index 24b79b6b9..950025f90 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20220120233850_add_status_to_moderation_reports.rb b/db/migrate/20220120233850_add_status_to_moderation_reports.rb new file mode 100644 index 000000000..a64087b21 --- /dev/null +++ b/db/migrate/20220120233850_add_status_to_moderation_reports.rb @@ -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 diff --git a/db/structure.sql b/db/structure.sql index f3496187f..21e65aa6e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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'); diff --git a/script/fixes/094_mark_modreports_as_handled.rb b/script/fixes/094_mark_modreports_as_handled.rb new file mode 100755 index 000000000..6d7ba87c7 --- /dev/null +++ b/script/fixes/094_mark_modreports_as_handled.rb @@ -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 diff --git a/test/factories/moderation_report.rb b/test/factories/moderation_report.rb index 6c439d7c2..d2d668406 100644 --- a/test/factories/moderation_report.rb +++ b/test/factories/moderation_report.rb @@ -2,5 +2,6 @@ FactoryBot.define do factory(:moderation_report) do creator reason {"xxx"} + status { :pending } end end diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb index 43cf2bacc..b54060c39 100644 --- a/test/functional/comments_controller_test.rb +++ b/test/functional/comments_controller_test.rb @@ -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 diff --git a/test/functional/forum_posts_controller_test.rb b/test/functional/forum_posts_controller_test.rb index 4a527f49c..fc3d4b370 100644 --- a/test/functional/forum_posts_controller_test.rb +++ b/test/functional/forum_posts_controller_test.rb @@ -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 diff --git a/test/functional/moderation_reports_controller_test.rb b/test/functional/moderation_reports_controller_test.rb index ece56f024..846c5d428 100644 --- a/test/functional/moderation_reports_controller_test.rb +++ b/test/functional/moderation_reports_controller_test.rb @@ -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