FEATURE: Reason and deleted content support in the review queue (#30295)

Add flag reason filter and improve handling of deleted content in review queue

This commit enhances the review queue with several key improvements:

1. Adds a new "Reason" filter to allow filtering flags by their score type
2. Improves UI for deleted content by:
   - Adding visual indication for deleted posts (red background)
   - Properly handling deleted content visibility for staff (category mods can not see deleted content)
3. Refactors reviewable score type handling for better code organization
4. Adds  tests for trashed topics/posts visibility

This change will help moderators more efficiently manage the review queue by
being able to focus on specific types of flags and better identify deleted
content.
This commit is contained in:
Sam 2024-12-17 11:44:46 +11:00 committed by GitHub
parent d43d8e0023
commit 55a8184231
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 158 additions and 6 deletions

View File

@ -59,9 +59,16 @@ export default class ReviewableItem extends Component {
"reviewable.type",
"reviewable.last_performing_username",
"siteSettings.blur_tl0_flagged_posts_media",
"reviewable.target_created_by_trust_level"
"reviewable.target_created_by_trust_level",
"reviewable.deleted_at"
)
customClasses(type, lastPerformingUsername, blurEnabled, trustLevel) {
customClasses(
type,
lastPerformingUsername,
blurEnabled,
trustLevel,
deletedAt
) {
let classes = dasherize(type);
if (lastPerformingUsername) {
@ -72,6 +79,10 @@ export default class ReviewableItem extends Component {
classes = `${classes} blur-images`;
}
if (deletedAt) {
classes = `${classes} reviewable-deleted`;
}
return classes;
}

View File

@ -20,6 +20,7 @@ export default class ReviewIndexController extends Controller {
"sort_order",
"additional_filters",
"flagged_by",
"score_type",
];
type = null;
@ -36,6 +37,7 @@ export default class ReviewIndexController extends Controller {
to_date = null;
sort_order = null;
additional_filters = null;
filterScoreType = null;
@discourseComputed("reviewableTypes")
allTypes() {
@ -49,6 +51,11 @@ export default class ReviewIndexController extends Controller {
});
}
@discourseComputed("scoreTypes")
allScoreTypes() {
return this.scoreTypes || [];
}
@discourseComputed
priorities() {
return ["any", "low", "medium", "high"].map((priority) => {
@ -164,6 +171,7 @@ export default class ReviewIndexController extends Controller {
username: this.filterUsername,
reviewed_by: this.filterReviewedBy,
flagged_by: this.filterFlaggedBy,
score_type: this.filterScoreType,
from_date: isPresent(this.filterFromDate)
? this.filterFromDate.toISOString(true).split("T")[0]
: null,

View File

@ -39,6 +39,7 @@ export default class ReviewIndex extends DiscourseRoute {
filterCategoryId: meta.category_id,
filterPriority: meta.priority,
reviewableTypes: meta.reviewable_types,
scoreTypes: meta.score_types,
filterUsername: meta.username,
filterReviewedBy: meta.reviewed_by,
filterFlaggedBy: meta.flagged_by,

View File

@ -53,6 +53,18 @@
/>
</div>
<div class="reviewable-filter">
<label class="filter-label">
{{i18n "review.filters.score_type.title"}}
</label>
<ComboBox
@value={{this.filterScoreType}}
@content={{this.allScoreTypes}}
@onChange={{fn (mut this.filterScoreType)}}
@options={{hash none="review.filters.score_type.all"}}
/>
</div>
<div class="reviewable-filter">
<label class="filter-label">
{{i18n "review.filters.priority.title"}}

View File

@ -305,6 +305,13 @@
opacity: 0.7;
}
.reviewable-deleted {
.reviewable-contents .post-contents .post-body {
background-color: var(--danger-low-mid);
padding: 0.5em;
}
}
.blur-images {
img:not(.avatar):not(.emoji) {
filter: blur(10px);

View File

@ -8,6 +8,10 @@ class ReviewablesController < ApplicationController
before_action :version_required, only: %i[update perform]
before_action :ensure_can_see, except: [:destroy]
around_action :with_deleted_content,
only: %i[index show],
if: ->(controller) { controller.guardian.is_staff? }
def index
offset = params[:offset].to_i
@ -41,6 +45,7 @@ class ReviewablesController < ApplicationController
type
sort_order
flagged_by
score_type
].each { |filter_key| filters[filter_key] = params[filter_key] }
total_rows = Reviewable.list_for(current_user, **filters).count
@ -69,6 +74,11 @@ class ReviewablesController < ApplicationController
total_rows_reviewables: total_rows,
types: meta_types,
reviewable_types: Reviewable.types,
score_types:
ReviewableScore
.types
.filter { |k, v| k != :notify_user }
.map { |k, v| { id: v, name: ReviewableScore.type_title(k) } },
reviewable_count: current_user.reviewable_count,
unseen_reviewable_count: Reviewable.unseen_reviewable_count(current_user),
),
@ -318,4 +328,8 @@ class ReviewablesController < ApplicationController
def ensure_can_see
Guardian.new(current_user).ensure_can_see_review_queue!
end
def with_deleted_content
Post.unscoped { Topic.unscoped { PostAction.unscoped { yield } } }
end
end

View File

@ -449,7 +449,8 @@ class Reviewable < ActiveRecord::Base
additional_filters: {},
preload: true,
include_claimed_by_others: true,
flagged_by: nil
flagged_by: nil,
score_type: nil
)
order =
case sort_order
@ -489,6 +490,16 @@ class Reviewable < ActiveRecord::Base
SQL
end
if score_type
score_type = score_type.to_i
result = result.where(<<~SQL, score_type: score_type)
EXISTS(
SELECT 1 FROM reviewable_scores
WHERE reviewable_scores.reviewable_id = reviewables.id AND reviewable_scores.reviewable_score_type = :score_type
)
SQL
end
if reviewed_by
reviewed_by_id = User.find_by_username(reviewed_by)&.id
return none if reviewed_by_id.nil?

View File

@ -14,6 +14,12 @@ class ReviewableScore < ActiveRecord::Base
@types ||= PostActionType.flag_types.merge(PostActionType.score_types)
end
def self.type_title(type)
I18n.t("post_action_types.#{type}.title", default: nil) ||
I18n.t("reviewable_score_types.#{type}.title", default: nil) ||
PostActionType.names[types[type]]
end
# When extending post action flags, we need to call this method in order to
# get the latests flags.
def self.reload_types

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class ReviewableFlaggedPostSerializer < ReviewableSerializer
target_attributes :cooked, :raw, :reply_count, :reply_to_post_number
target_attributes :cooked, :raw, :reply_count, :reply_to_post_number, :deleted_at
attributes :blank_post, :post_updated_at, :post_version
def created_from_flag?

View File

@ -9,8 +9,7 @@ class ReviewableScoreTypeSerializer < ApplicationSerializer
# Allow us to share post action type translations for backwards compatibility
def title
I18n.t("post_action_types.#{type}.title", default: nil) ||
I18n.t("reviewable_score_types.#{type}.title", default: nil) || PostActionType.names[id]
ReviewableScore.type_title(type)
end
def reviewable_priority

View File

@ -689,6 +689,9 @@ en:
refresh: "Refresh"
status: "Status"
category: "Category"
score_type:
title: "Reason"
all: "(all reasons)"
orders:
score: "Score"
score_asc: "Score (reverse)"

View File

@ -75,6 +75,86 @@ RSpec.describe ReviewablesController do
expect(json["meta"]["status"]).to eq("pending")
end
context "with trashed topics and posts" do
fab!(:post1) { Fabricate(:post) }
fab!(:reviewable) do
Fabricate(
:reviewable,
target_id: post1.id,
target_type: "Post",
topic: post1.topic,
type: "ReviewableFlaggedPost",
category: post1.topic.category,
)
end
fab!(:moderator)
let(:topic) { post1.topic }
fab!(:category_mod) { Fabricate(:user) }
fab!(:group)
fab!(:group_user) { GroupUser.create!(group_id: group.id, user_id: category_mod.id) }
fab!(:mod_group) do
CategoryModerationGroup.create!(category_id: post1.topic.category.id, group_id: group.id)
end
it "supports returning information for trashed topics and posts to staff" do
sign_in(moderator)
topic.trash!
post1.trash!
get "/review.json"
expect(response.code).to eq("200")
json = response.parsed_body
reviewable_json = json["reviewables"].find { |r| r["id"] == reviewable.id }
topic_json = json["topics"].find { |t| t["id"] == topic.id }
expect(reviewable_json["raw"]).to eq(post1.raw)
expect(reviewable_json["deleted_at"]).to be_present
expect(topic_json["title"]).to eq(topic.title)
end
it "does not return information for trashed topics and posts to category mods" do
SiteSetting.enable_category_group_moderation = true
sign_in(category_mod)
post1.trash!
topic.trash!
get "/review.json"
expect(response.code).to eq("200")
json = response.parsed_body
reviewable_json = json["reviewables"].find { |r| r["id"] == reviewable.id }
expect(reviewable_json["raw"]).to be_blank
end
end
it "supports filtering by flag reason" do
# this is not flagged by the user
reviewable = Fabricate(:reviewable)
reviewable.reviewable_scores.create!(
user: admin,
score: 1000,
status: "pending",
reviewable_score_type: 1,
)
reviewable = Fabricate(:reviewable)
user = Fabricate(:user)
reviewable.reviewable_scores.create!(
user: user,
score: 1000,
status: "pending",
reviewable_score_type: 2,
)
get "/review.json?score_type=1"
expect(response.code).to eq("200")
json = response.parsed_body
expect(json["reviewables"].length).to eq(1)
end
it "supports filtering by flagged_by" do
# this is not flagged by the user
reviewable = Fabricate(:reviewable)