discourse/app/models/user_badge.rb
Bianca Nenciu 8016fcab33
DEV: Drop old notification id columns (#28550)
The `id` column of `notifications` table and `notification_id` columns
of the other tables have been migrated to bigint in previous commits
(for example, 799a45a).

In order to run the migrations with zero downtime, the data had to be
copied to new columns and swapped, but the old columns have been kept
to allow for rollback. They are no longer needed now.
2024-10-15 11:58:57 +03:00

140 lines
5.0 KiB
Ruby

# frozen_string_literal: true
class UserBadge < ActiveRecord::Base
self.ignored_columns = [
:old_notification_id, # TODO: Remove once 20240829140226_drop_old_notification_id_columns has been promoted to pre-deploy
]
belongs_to :badge
belongs_to :user
belongs_to :granted_by, class_name: "User"
belongs_to :notification, dependent: :destroy
belongs_to :post
BOOLEAN_ATTRIBUTES = %w[is_favorite]
scope :grouped_with_count,
-> do
group(:badge_id, :user_id)
.select_for_grouping
.order("MAX(featured_rank) ASC")
.includes(:user, :granted_by, { badge: :badge_type }, post: :topic)
end
scope :select_for_grouping,
-> do
select(
UserBadge.attribute_names.map do |name|
operation = BOOLEAN_ATTRIBUTES.include?(name) ? "BOOL_OR" : "MAX"
"#{operation}(user_badges.#{name}) AS #{name}"
end,
'COUNT(*) AS "count"',
)
end
scope :for_enabled_badges,
-> { where("user_badges.badge_id IN (SELECT id FROM badges WHERE enabled)") }
validates :badge_id, presence: true, uniqueness: { scope: :user_id }, if: :single_grant_badge?
validates :user_id, presence: true
validates :granted_at, presence: true
validates :granted_by, presence: true
after_create do
Badge.increment_counter "grant_count", self.badge_id
UserStat.update_distinct_badge_count self.user_id
UserBadge.update_featured_ranks! self.user_id
self.trigger_user_badge_granted_event
end
after_destroy do
Badge.decrement_counter "grant_count", self.badge_id
UserStat.update_distinct_badge_count self.user_id
UserBadge.update_featured_ranks! self.user_id
DiscourseEvent.trigger(:user_badge_removed, self.badge_id, self.user_id)
DiscourseEvent.trigger(:user_badge_revoked, user_badge: self)
end
def self.ensure_consistency!
self.update_featured_ranks!
end
def self.update_featured_ranks!(user_id = nil)
query = <<~SQL
WITH featured_tl_badge AS -- Find the best trust level badge for each user
(
SELECT user_id, max(badge_id) as badge_id
FROM user_badges
WHERE badge_id IN (1,2,3,4)
#{"AND user_id = #{user_id.to_i}" if user_id}
GROUP BY user_id
),
ranks AS ( -- Take all user badges, group by user_id and badge_id, and calculate a rank for each one
SELECT
user_badges.user_id,
user_badges.badge_id,
RANK() OVER (
PARTITION BY user_badges.user_id -- Do a separate rank for each user
ORDER BY BOOL_OR(badges.enabled) DESC, -- Disabled badges last
MAX(featured_tl_badge.user_id) NULLS LAST, -- Best tl badge first
BOOL_OR(user_badges.is_favorite) DESC NULLS LAST, -- Favorite badges next
CASE WHEN user_badges.badge_id IN (1,2,3,4) THEN 1 ELSE 0 END ASC, -- Non-featured tl badges last
MAX(badges.badge_type_id) ASC,
MAX(badges.grant_count) ASC,
user_badges.badge_id DESC
) rank_number
FROM user_badges
INNER JOIN badges ON badges.id = user_badges.badge_id
LEFT JOIN featured_tl_badge ON featured_tl_badge.user_id = user_badges.user_id AND featured_tl_badge.badge_id = user_badges.badge_id
#{"WHERE user_badges.user_id = #{user_id.to_i}" if user_id}
GROUP BY user_badges.user_id, user_badges.badge_id
)
-- Now use that data to update the featured_rank column
UPDATE user_badges SET featured_rank = rank_number
FROM ranks WHERE ranks.badge_id = user_badges.badge_id AND ranks.user_id = user_badges.user_id AND featured_rank IS DISTINCT FROM rank_number
SQL
DB.exec query
end
def self.trigger_user_badge_granted_event(badge_id, user_id)
DiscourseEvent.trigger(:user_badge_granted, badge_id, user_id)
end
private
def trigger_user_badge_granted_event
self.class.trigger_user_badge_granted_event(self.badge_id, self.user_id)
end
def single_grant_badge?
self.badge ? self.badge.single_grant? : true
end
end
# == Schema Information
#
# Table name: user_badges
#
# id :integer not null, primary key
# badge_id :integer not null
# user_id :integer not null
# granted_at :datetime not null
# granted_by_id :integer not null
# post_id :integer
# seq :integer default(0), not null
# featured_rank :integer
# created_at :datetime not null
# is_favorite :boolean
# notification_id :bigint
#
# Indexes
#
# index_user_badges_on_badge_id_and_user_id (badge_id,user_id)
# index_user_badges_on_badge_id_and_user_id_and_post_id (badge_id,user_id,post_id) UNIQUE WHERE (post_id IS NOT NULL)
# index_user_badges_on_badge_id_and_user_id_and_seq (badge_id,user_id,seq) UNIQUE WHERE (post_id IS NULL)
# index_user_badges_on_user_id (user_id)
#