discourse/app/controllers/user_badges_controller.rb
Alan Guo Xiang Tan 3e4eac0fed
PERF: Enqueue Job::BackfillBadge in Jobs::BadgeGrant (#30945)
This commit updates the `Jobs::BadgeGrant` scheduled job to enqueue on
`Job::BackfillBadge` regular job for each enabled badge on the site.

The rationale for this change is that we started seeing the
`Jobs::BadgeGrant` job taking hours on sites with lots of enabled badges
as well as users because the job was backfilling all enabled badges
serially within the job. This is bad as it means that a `mini_scheduler`
thread is tied up
by this job thus reducing the overall capacity of `mini_scheduler` for
hours.
2025-01-24 09:35:01 +08:00

186 lines
5.4 KiB
Ruby

# frozen_string_literal: true
class UserBadgesController < ApplicationController
MAX_BADGES = 96 # This was limited in PR#2360 to make it divisible by 8
before_action :ensure_badges_enabled
def index
params.permit %i[granted_before offset username]
badge = fetch_badge_from_params
user_badges = badge.user_badges.order("granted_at DESC, id DESC").limit(MAX_BADGES)
user_badges =
user_badges.includes(
:user,
:granted_by,
badge: :badge_type,
post: :topic,
user: %i[primary_group flair_group],
)
grant_count = nil
if params[:username]
user_id = User.where(username_lower: params[:username].downcase).pick(:id)
user_badges = user_badges.where(user_id: user_id) if user_id
grant_count = badge.user_badges.where(user_id: user_id).count
end
offset = fetch_int_from_params(:offset, default: 0)
user_badges = user_badges.offset(offset) if offset > 0
user_badges_topic_ids = user_badges.map { |user_badge| user_badge.post&.topic_id }.compact
user_badges =
UserBadges.new(
user_badges: user_badges,
username: params[:username],
grant_count: grant_count,
)
render_serialized(
user_badges,
UserBadgesSerializer,
root: :user_badge_info,
include_long_description: true,
allowed_user_badge_topic_ids: guardian.can_see_topic_ids(topic_ids: user_badges_topic_ids),
)
end
def username
params.permit [:grouped]
user =
fetch_user_from_params(
include_inactive:
current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts),
)
raise Discourse::NotFound unless guardian.can_see_profile?(user)
user_badges = user.user_badges
user_badges = user_badges.group(:badge_id).select_for_grouping if params[:grouped]
user_badges =
user_badges
.includes(badge: %i[badge_grouping badge_type image_upload])
.includes(post: :topic)
.includes(:granted_by)
user_badges_topic_ids = user_badges.map { |user_badge| user_badge.post&.topic_id }.compact
render_serialized(
user_badges,
DetailedUserBadgeSerializer,
allowed_user_badge_topic_ids: guardian.can_see_topic_ids(topic_ids: user_badges_topic_ids),
root: :user_badges,
)
end
def create
params.require(:username)
user = fetch_user_from_params
return render json: failed_json, status: 403 unless can_assign_badge_to_user?(user)
badge = fetch_badge_from_params
post_id = nil
if params[:reason].present?
unless is_badge_reason_valid? params[:reason]
return(
render json: failed_json.merge(message: I18n.t("invalid_grant_badge_reason_link")),
status: 400
)
end
if route = Discourse.route_for(params[:reason])
if route[:controller] == "topics" && route[:action] == "show"
topic_id = (route[:id] || route[:topic_id]).to_i
post_number = route[:post_number] || 1
post_id = Post.find_by(topic_id: topic_id, post_number: post_number)&.id if topic_id > 0
end
end
end
grant_opts_from_params =
DiscoursePluginRegistry.apply_modifier(
:user_badges_badge_grant_opts,
{ granted_by: current_user, post_id: post_id },
{ param: params },
)
user_badge = BadgeGranter.grant(badge, user, grant_opts_from_params)
render_serialized(user_badge, DetailedUserBadgeSerializer, root: "user_badge")
end
def destroy
params.require(:id)
user_badge = UserBadge.find(params[:id])
unless can_assign_badge_to_user?(user_badge.user)
render json: failed_json, status: 403
return
end
BadgeGranter.revoke(user_badge, revoked_by: current_user)
render json: success_json
end
def toggle_favorite
params.require(:user_badge_id)
user_badge = UserBadge.find(params[:user_badge_id])
user_badges = user_badge.user.user_badges
return render json: failed_json, status: 403 unless can_favorite_badge?(user_badge)
if !user_badge.is_favorite &&
user_badges.select(:badge_id).distinct.where(is_favorite: true).count >=
SiteSetting.max_favorite_badges
return render json: failed_json, status: 400
end
UserBadge.where(user_id: user_badge.user_id, badge_id: user_badge.badge_id).update_all(
is_favorite: !user_badge.is_favorite,
)
UserBadge.update_featured_ranks!([user_badge.user_id])
end
private
# Get the badge from either the badge name or id specified in the params.
def fetch_badge_from_params
badge = nil
params.permit(:badge_name)
if params[:badge_name].nil?
params.require(:badge_id)
badge = Badge.find_by(id: params[:badge_id], enabled: true)
else
badge = Badge.find_by(name: params[:badge_name], enabled: true)
end
raise Discourse::NotFound if badge.blank?
badge
end
def can_assign_badge_to_user?(user)
master_api_call = current_user.nil? && is_api?
master_api_call || guardian.can_grant_badges?(user)
end
def can_favorite_badge?(user_badge)
current_user == user_badge.user && !(1..4).include?(user_badge.badge_id)
end
def ensure_badges_enabled
raise Discourse::NotFound unless SiteSetting.enable_badges?
end
def is_badge_reason_valid?(reason)
route = Discourse.route_for(reason)
route && (route[:controller] == "posts" || route[:controller] == "topics")
end
end