discourse/app/models/user_badge.rb
Selase Krakani cdf1589a85
FEATURE: Add support for user badge revocation webhook events (#21204)
Currently, only user badge grants emit webhook events. This change
extends the `user_badge` webhook to emit user badge revocation events.

A new `user_badge_revoked` event has been introduced instead of relying
on the existing `user_badge_removed` event. `user_badge_removed` emitted
just the `badge_id` and `user_id` which aren't helpful for generating a
meaningful webhook payload for revoked(deleted) user badges.

The new event emits  the user badge object.
2023-04-24 20:36:40 +00:00

137 lines
4.9 KiB
Ruby

# frozen_string_literal: true
class UserBadge < ActiveRecord::Base
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,
-> {
group(:badge_id, :user_id)
.select_for_grouping
.order("MAX(featured_rank) ASC")
.includes(:user, :granted_by, { badge: :badge_type }, post: :topic)
}
scope :select_for_grouping,
-> {
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"',
)
}
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
# TODO: Follow up with a deprecation notice for `user_badge_removed`
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
# notification_id :integer
# seq :integer default(0), not null
# featured_rank :integer
# created_at :datetime not null
# is_favorite :boolean
#
# 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)
#