mirror of
https://github.com/discourse/discourse.git
synced 2025-02-24 07:42:11 +08:00

Plugins like for example AI or Akismet create reviewable items. When the plugin is disabled, then we cannot properly handle those items. In that situation, we should display warnings about unhandled types. Instruct admin to reenable plugins. In addition, we should allow the admin to delete all pending reviews from disabled plugins.
337 lines
9.9 KiB
Ruby
337 lines
9.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ReviewablesController < ApplicationController
|
|
requires_login
|
|
|
|
PER_PAGE = 10
|
|
|
|
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
|
|
|
|
if params[:type].present?
|
|
raise Discourse::InvalidParameters.new(:type) unless Reviewable.valid_type?(params[:type])
|
|
end
|
|
|
|
status = (params[:status] || "pending").to_sym
|
|
raise Discourse::InvalidParameters.new(:status) if allowed_statuses.exclude?(status)
|
|
|
|
topic_id = params[:topic_id] ? params[:topic_id].to_i : nil
|
|
category_id = params[:category_id] ? params[:category_id].to_i : nil
|
|
|
|
custom_keys = Reviewable.custom_filters.map(&:first)
|
|
additional_filters =
|
|
JSON.parse(params.fetch(:additional_filters, {}), symbolize_names: true).slice(*custom_keys)
|
|
filters = {
|
|
ids: params[:ids],
|
|
status: status,
|
|
category_id: category_id,
|
|
topic_id: topic_id,
|
|
additional_filters: additional_filters.reject { |_, v| v.blank? },
|
|
}
|
|
|
|
%i[
|
|
priority
|
|
username
|
|
reviewed_by
|
|
from_date
|
|
to_date
|
|
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
|
|
reviewables =
|
|
Reviewable.list_for(current_user, **filters.merge(limit: PER_PAGE, offset: offset)).to_a
|
|
|
|
claimed_topics = ReviewableClaimedTopic.claimed_hash(reviewables.map { |r| r.topic_id }.uniq)
|
|
|
|
# This is a bit awkward, but ActiveModel serializers doesn't seem to serialize STI. Note `hash`
|
|
# is mutated by the serializer and contains the side loaded records which must be merged in the end.
|
|
hash = {}
|
|
json = {
|
|
reviewables:
|
|
reviewables.map! do |r|
|
|
result =
|
|
r
|
|
.serializer
|
|
.new(r, root: nil, hash: hash, scope: guardian, claimed_topics: claimed_topics)
|
|
.as_json
|
|
hash[:bundled_actions].uniq!
|
|
(hash["actions"] || []).uniq!
|
|
result
|
|
end,
|
|
meta:
|
|
filters.merge(
|
|
total_rows_reviewables: total_rows,
|
|
types: meta_types,
|
|
reviewable_types: Reviewable.types,
|
|
unknown_reviewable_types: Reviewable.unknown_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),
|
|
),
|
|
}
|
|
if (offset + PER_PAGE) < total_rows
|
|
json[:meta][:load_more_reviewables] = review_path(filters.merge(offset: offset + PER_PAGE))
|
|
end
|
|
json.merge!(hash)
|
|
|
|
render_json_dump(json, rest_serializer: true)
|
|
end
|
|
|
|
def user_menu_list
|
|
json = {
|
|
reviewables:
|
|
Reviewable.basic_serializers_for_list(
|
|
Reviewable.user_menu_list_for(current_user),
|
|
current_user,
|
|
).as_json,
|
|
reviewable_count: current_user.reviewable_count,
|
|
}
|
|
render_json_dump(json, rest_serializer: true)
|
|
end
|
|
|
|
def count
|
|
render_json_dump(count: Reviewable.pending_count(current_user))
|
|
end
|
|
|
|
def topics
|
|
topic_ids = Set.new
|
|
|
|
stats = {}
|
|
unique_users = {}
|
|
|
|
# topics isn't indexed on `reviewable_score` and doesn't know what the current user can see,
|
|
# so let's query from the inside out.
|
|
pending = Reviewable.viewable_by(current_user).pending
|
|
pending = pending.where("score >= ?", Reviewable.min_score_for_priority)
|
|
|
|
pending.each do |r|
|
|
topic_ids << r.topic_id
|
|
|
|
meta = stats[r.topic_id] ||= { count: 0, unique_users: 0 }
|
|
users = unique_users[r.topic_id] ||= Set.new
|
|
|
|
r.reviewable_scores.each do |rs|
|
|
users << rs.user_id
|
|
meta[:count] += 1
|
|
end
|
|
meta[:unique_users] = users.size
|
|
end
|
|
|
|
topics = Topic.where(id: topic_ids).order("reviewable_score DESC")
|
|
render_serialized(
|
|
topics,
|
|
ReviewableTopicSerializer,
|
|
root: "reviewable_topics",
|
|
stats: stats,
|
|
claimed_topics: ReviewableClaimedTopic.claimed_hash(topic_ids),
|
|
rest_serializer: true,
|
|
meta: {
|
|
types: meta_types,
|
|
},
|
|
)
|
|
end
|
|
|
|
def explain
|
|
reviewable = find_reviewable
|
|
|
|
render_serialized(
|
|
{ reviewable: reviewable, scores: reviewable.explain_score },
|
|
ReviewableExplanationSerializer,
|
|
rest_serializer: true,
|
|
root: "reviewable_explanation",
|
|
)
|
|
end
|
|
|
|
def show
|
|
reviewable = find_reviewable
|
|
|
|
render_serialized(
|
|
reviewable,
|
|
reviewable.serializer,
|
|
rest_serializer: true,
|
|
claimed_topics: ReviewableClaimedTopic.claimed_hash([reviewable.topic_id]),
|
|
root: "reviewable",
|
|
meta: {
|
|
types: meta_types,
|
|
},
|
|
)
|
|
end
|
|
|
|
def destroy
|
|
user =
|
|
if is_api?
|
|
if @guardian.is_admin?
|
|
fetch_user_from_params
|
|
else
|
|
raise Discourse::InvalidAccess
|
|
end
|
|
else
|
|
current_user
|
|
end
|
|
|
|
reviewable =
|
|
Reviewable.find_by_flagger_or_queued_post_creator(
|
|
id: params[:reviewable_id],
|
|
user_id: user.id,
|
|
)
|
|
raise Discourse::NotFound.new if reviewable.blank?
|
|
|
|
reviewable.perform(current_user, :delete, { guardian: @guardian })
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def update
|
|
reviewable = find_reviewable
|
|
if error = claim_error?(reviewable)
|
|
return render_json_error(error)
|
|
end
|
|
|
|
editable = reviewable.editable_for(guardian)
|
|
raise Discourse::InvalidAccess.new if editable.blank?
|
|
|
|
# Validate parameters are all editable
|
|
edit_params = params[:reviewable] || {}
|
|
edit_params.each do |name, value|
|
|
if value.is_a?(ActionController::Parameters)
|
|
value.each do |pay_name, pay_value|
|
|
raise Discourse::InvalidAccess.new unless editable.has?("#{name}.#{pay_name}")
|
|
end
|
|
else
|
|
raise Discourse::InvalidAccess.new unless editable.has?(name)
|
|
end
|
|
end
|
|
|
|
begin
|
|
if reviewable.update_fields(edit_params, current_user, version: params[:version].to_i)
|
|
result = edit_params.merge(version: reviewable.version)
|
|
render json: result
|
|
else
|
|
render_json_error(reviewable.errors)
|
|
end
|
|
rescue Reviewable::UpdateConflict
|
|
render_json_error(I18n.t("reviewables.conflict"), status: 409)
|
|
end
|
|
end
|
|
|
|
def perform
|
|
args = { version: params[:version].to_i }
|
|
|
|
result = nil
|
|
begin
|
|
reviewable = find_reviewable
|
|
|
|
if error = claim_error?(reviewable)
|
|
return render_json_error(error)
|
|
end
|
|
|
|
if reviewable.type_class.respond_to?(:additional_args)
|
|
args.merge!(reviewable.type_class.additional_args(params) || {})
|
|
end
|
|
|
|
plugin_params =
|
|
DiscoursePluginRegistry.reviewable_params.select do |reviewable_param|
|
|
reviewable.type == reviewable_param[:type].to_s.classify
|
|
end
|
|
args.merge!(params.slice(*plugin_params.map { |pp| pp[:param] }).permit!)
|
|
|
|
result = reviewable.perform(current_user, params[:action_id].to_sym, args)
|
|
rescue Reviewable::InvalidAction => e
|
|
if reviewable.type == "ReviewableUser" && !reviewable.pending? && reviewable.target.blank?
|
|
raise Discourse::NotFound.new(
|
|
e.message,
|
|
custom_message: "reviewables.already_handled_and_user_not_exist",
|
|
)
|
|
else
|
|
# Consider InvalidAction an InvalidAccess
|
|
raise Discourse::InvalidAccess.new(e.message)
|
|
end
|
|
rescue Reviewable::UpdateConflict
|
|
return render_json_error(I18n.t("reviewables.conflict"), status: 409)
|
|
end
|
|
|
|
if result.success?
|
|
render_serialized(result, ReviewablePerformResultSerializer)
|
|
else
|
|
render_json_error(result)
|
|
end
|
|
end
|
|
|
|
def settings
|
|
raise Discourse::InvalidAccess.new unless current_user.admin?
|
|
|
|
post_action_types = PostActionType.where(id: PostActionType.flag_types.values).order("id")
|
|
|
|
if request.put?
|
|
params[:reviewable_priorities].each do |id, priority|
|
|
if !priority.nil? && Reviewable.priorities.has_value?(priority.to_i)
|
|
# For now, the score bonus is equal to the priority. In the future we might want
|
|
# to calculate it a different way.
|
|
PostActionType.where(id: id).update_all(
|
|
reviewable_priority: priority.to_i,
|
|
score_bonus: priority.to_f,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
data = { reviewable_score_types: post_action_types }
|
|
render_serialized(data, ReviewableSettingsSerializer, rest_serializer: true)
|
|
end
|
|
|
|
protected
|
|
|
|
def claim_error?(reviewable)
|
|
return if SiteSetting.reviewable_claiming == "disabled" || reviewable.topic_id.blank?
|
|
|
|
claimed_by_id = ReviewableClaimedTopic.where(topic_id: reviewable.topic_id).pluck(:user_id)[0]
|
|
|
|
if SiteSetting.reviewable_claiming == "required" && claimed_by_id.blank?
|
|
I18n.t("reviewables.must_claim")
|
|
elsif claimed_by_id.present? && claimed_by_id != current_user.id
|
|
I18n.t("reviewables.user_claimed")
|
|
end
|
|
end
|
|
|
|
def find_reviewable
|
|
reviewable = Reviewable.viewable_by(current_user).where(id: params[:reviewable_id]).first
|
|
raise Discourse::NotFound.new if reviewable.blank?
|
|
reviewable
|
|
end
|
|
|
|
def allowed_statuses
|
|
@allowed_statuses ||= (%i[reviewed all] + Reviewable.statuses.symbolize_keys.keys)
|
|
end
|
|
|
|
def version_required
|
|
render_json_error(I18n.t("reviewables.missing_version"), status: 422) if params[:version].blank?
|
|
end
|
|
|
|
def meta_types
|
|
{ created_by: "user", target_created_by: "user", reviewed_by: "user", claimed_by: "user" }
|
|
end
|
|
|
|
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
|