mirror of
https://github.com/discourse/discourse.git
synced 2024-12-19 13:30:13 +08:00
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:
parent
d43d8e0023
commit
55a8184231
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"}}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user