# frozen_string_literal: true class Badge < ActiveRecord::Base # TODO: Drop in July 2021 self.ignored_columns = %w{image} include GlobalPath # NOTE: These badge ids are not in order! They are grouped logically. # When picking an id, *search* for it. BasicUser = 1 Member = 2 Regular = 3 Leader = 4 Welcome = 5 NicePost = 6 GoodPost = 7 GreatPost = 8 Autobiographer = 9 Editor = 10 WikiEditor = 48 FirstLike = 11 FirstShare = 12 FirstFlag = 13 FirstLink = 14 FirstQuote = 15 FirstMention = 40 FirstEmoji = 41 FirstOnebox = 42 FirstReplyByEmail = 43 ReadGuidelines = 16 Reader = 17 NiceTopic = 18 GoodTopic = 19 GreatTopic = 20 NiceShare = 21 GoodShare = 22 GreatShare = 23 Anniversary = 24 Promoter = 25 Campaigner = 26 Champion = 27 PopularLink = 28 HotLink = 29 FamousLink = 30 Appreciated = 36 Respected = 37 Admired = 31 OutOfLove = 33 HigherLove = 34 CrazyInLove = 35 ThankYou = 38 GivesBack = 32 Empathetic = 39 Enthusiast = 45 Aficionado = 46 Devotee = 47 NewUserOfTheMonth = 44 # other consts AutobiographerMinBioLength = 10 # used by serializer attr_accessor :has_badge def self.trigger_hash Hash[*( Badge::Trigger.constants.map { |k| [k.to_s.underscore, Badge::Trigger.const_get(k)] }.flatten )] end module Trigger None = 0 PostAction = 1 PostRevision = 2 TrustLevelChange = 4 UserChange = 8 PostProcessed = 16 # deprecated def self.is_none?(trigger) [None].include? trigger end def self.uses_user_ids?(trigger) [TrustLevelChange, UserChange].include? trigger end def self.uses_post_ids?(trigger) [PostAction, PostRevision].include? trigger end end belongs_to :badge_type belongs_to :badge_grouping belongs_to :image_upload, class_name: 'Upload' has_many :user_badges, dependent: :destroy validates :name, presence: true, uniqueness: true validates :badge_type, presence: true validates :allow_title, inclusion: [true, false] validates :multiple_grant, inclusion: [true, false] scope :enabled, -> { where(enabled: true) } before_create :ensure_not_system after_commit do SvgSprite.expire_cache UserStat.update_distinct_badge_count if saved_change_to_enabled? UserBadge.ensure_consistency! if saved_change_to_enabled? end # fields that can not be edited on system badges def self.protected_system_fields [ :name, :badge_type_id, :multiple_grant, :target_posts, :show_posts, :query, :trigger, :auto_revoke, :listable ] end def self.trust_level_badge_ids (1..4).to_a end def self.like_badge_counts @like_badge_counts ||= { NicePost => 10, GoodPost => 25, GreatPost => 50, NiceTopic => 10, GoodTopic => 25, GreatTopic => 50 } end def self.ensure_consistency! DB.exec <<~SQL DELETE FROM user_badges USING user_badges ub LEFT JOIN users u ON u.id = ub.user_id WHERE u.id IS NULL AND user_badges.id = ub.id SQL DB.exec <<~SQL WITH X AS ( SELECT badge_id , COUNT(user_id) users FROM user_badges GROUP BY badge_id ) UPDATE badges SET grant_count = X.users FROM X WHERE id = X.badge_id AND grant_count <> X.users SQL end def clear_user_titles! DB.exec(<<~SQL, badge_id: self.id, updated_at: Time.zone.now) UPDATE users AS u SET title = '', updated_at = :updated_at FROM user_profiles AS up WHERE up.user_id = u.id AND up.granted_title_badge_id = :badge_id SQL DB.exec(<<~SQL, badge_id: self.id) UPDATE user_profiles AS up SET badge_granted_title = false, granted_title_badge_id = NULL WHERE up.granted_title_badge_id = :badge_id SQL end ## # Update all user titles based on a badge to the new name def update_user_titles!(new_title) DB.exec(<<~SQL, granted_title_badge_id: self.id, title: new_title, updated_at: Time.zone.now) UPDATE users AS u SET title = :title, updated_at = :updated_at FROM user_profiles AS up WHERE up.user_id = u.id AND up.granted_title_badge_id = :granted_title_badge_id SQL end ## # When a badge has its TranslationOverride cleared, reset # all user titles granted to the standard name. def reset_user_titles! DB.exec(<<~SQL, granted_title_badge_id: self.id, updated_at: Time.zone.now) UPDATE users AS u SET title = badges.name, updated_at = :updated_at FROM user_profiles AS up INNER JOIN badges ON badges.id = up.granted_title_badge_id WHERE up.user_id = u.id AND up.granted_title_badge_id = :granted_title_badge_id SQL end def self.i18n_name(name) name.downcase.tr(' ', '_') end def self.display_name(name) I18n.t(i18n_key(name), default: name) end def self.i18n_key(name) "badges.#{i18n_name(name)}.name" end def self.find_system_badge_id_from_translation_key(translation_key) return unless translation_key.starts_with?('badges.') badge_name_klass = translation_key.split('.').second.camelize Badge.const_defined?(badge_name_klass) ? "Badge::#{badge_name_klass}".constantize : nil end def awarded_for_trust_level? id <= 4 end def reset_grant_count! self.grant_count = UserBadge.where(badge_id: id).count save! end def single_grant? !self.multiple_grant? end def default_icon=(val) if self.image_upload_id.blank? self.icon ||= val self.icon = val if self.icon == "fa-certificate" end end def default_allow_title=(val) return unless self.new_record? self.allow_title ||= val end def default_badge_grouping_id=(val) # allow to correct orphans if !self.badge_grouping_id || self.badge_grouping_id <= BadgeGrouping::Other self.badge_grouping_id = val end end def display_name self.class.display_name(name) end def translation_key self.class.i18n_key(name) end def long_description key = "badges.#{i18n_name}.long_description" I18n.t(key, default: self[:long_description] || '', base_uri: Discourse.base_path, max_likes_per_day: SiteSetting.max_likes_per_day) end def long_description=(val) self[:long_description] = val if val != long_description val end def description key = "badges.#{i18n_name}.description" I18n.t(key, default: self[:description] || '', base_uri: Discourse.base_path, max_likes_per_day: SiteSetting.max_likes_per_day) end def description=(val) self[:description] = val if val != description val end def slug Slug.for(self.display_name, '-') end def manually_grantable? query.blank? && !system? end def i18n_name @i18n_name ||= self.class.i18n_name(name) end def image_url if image_upload_id.present? upload_cdn_path(image_upload.url) end end def for_beginners? id == Welcome || (badge_grouping_id == BadgeGrouping::GettingStarted && id != NewUserOfTheMonth) end protected def ensure_not_system self.id = [Badge.maximum(:id) + 1, 100].max unless id end end # == Schema Information # # Table name: badges # # id :integer not null, primary key # name :string not null # description :text # badge_type_id :integer not null # grant_count :integer default(0), not null # created_at :datetime not null # updated_at :datetime not null # allow_title :boolean default(FALSE), not null # multiple_grant :boolean default(FALSE), not null # icon :string default("fa-certificate") # listable :boolean default(TRUE) # target_posts :boolean default(FALSE) # query :text # enabled :boolean default(TRUE), not null # auto_revoke :boolean default(TRUE), not null # badge_grouping_id :integer default(5), not null # trigger :integer # show_posts :boolean default(FALSE), not null # system :boolean default(FALSE), not null # long_description :text # image_upload_id :integer # # Indexes # # index_badges_on_badge_type_id (badge_type_id) # index_badges_on_name (name) UNIQUE #