discourse/app/models/badge.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

370 lines
8.9 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2014-03-05 20:52:20 +08:00
class Badge < ActiveRecord::Base
# TODO: Drop in July 2021
self.ignored_columns = %w{image}
include GlobalPath
include HasSanitizableFields
# 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
2014-07-07 15:55:25 +08:00
Editor = 10
WikiEditor = 48
FirstLike = 11
FirstShare = 12
FirstFlag = 13
2014-07-10 09:18:02 +08:00
FirstLink = 14
2014-07-11 12:17:01 +08:00
FirstQuote = 15
FirstMention = 40
FirstEmoji = 41
2016-04-13 02:09:59 +08:00
FirstOnebox = 42
FirstReplyByEmail = 43
2014-07-16 14:26:22 +08:00
ReadGuidelines = 16
Reader = 17
NiceTopic = 18
GoodTopic = 19
GreatTopic = 20
NiceShare = 21
GoodShare = 22
GreatShare = 23
Anniversary = 24
2016-03-17 01:48:14 +08:00
Promoter = 25
Campaigner = 26
Champion = 27
2016-03-17 01:48:14 +08:00
PopularLink = 28
HotLink = 29
FamousLink = 30
2016-03-17 01:48:14 +08:00
2016-03-17 01:03:17 +08:00
Appreciated = 36
Respected = 37
Admired = 31
2016-03-17 01:48:14 +08:00
OutOfLove = 33
HigherLove = 34
CrazyInLove = 35
2016-03-17 01:48:14 +08:00
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
@trigger_hash ||= Badge::Trigger.constants.map do |k|
name = k.to_s.underscore
[name, Badge::Trigger.const_get(k)] unless name =~ /deprecated/
end.compact.to_h
end
2014-07-22 10:46:31 +08:00
module Trigger
None = 0
PostAction = 1
PostRevision = 2
TrustLevelChange = 4
UserChange = 8
DeprecatedPostProcessed = 16 # No longer in use
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
2014-03-05 20:52:20 +08:00
belongs_to :badge_type
belongs_to :badge_grouping
belongs_to :image_upload, class_name: 'Upload'
has_many :user_badges, dependent: :destroy
has_many :upload_references, as: :target, dependent: :destroy
2014-03-05 20:52:20 +08:00
validates :name, presence: true, uniqueness: true
validates :badge_type, presence: true
validates :allow_title, inclusion: [true, false]
2014-05-24 10:33:46 +08:00
validates :multiple_grant, inclusion: [true, false]
scope :enabled, -> { where(enabled: true) }
before_create :ensure_not_system
before_save :sanitize_description
after_save do
if saved_change_to_image_upload_id?
UploadReference.ensure_exist!(upload_ids: [self.image_upload_id], target: self)
end
end
Upgrade to FontAwesome 5 (take two) (#6673) * Add missing icons to set * Revert FA5 revert This reverts commit 42572ff * use new SVG syntax in locales * Noscript page changes (remove login button, center "powered by" footer text) * Cast wider net for SVG icons in settings - include any _icon setting for SVG registry (offers better support for plugin settings) - let themes store multiple pipe-delimited icons in a setting - also replaces broken onebox image icon with SVG reference in cooked post processor * interpolate icons in locales * Fix composer whisper icon alignment * Add support for stacked icons * SECURITY: enforce hostname to match discourse hostname This ensures that the hostname rails uses for various helpers always matches the Discourse hostname * load SVG sprite with pre-initializers * FIX: enable caching on SVG sprites * PERF: use JSONP for SVG sprites so they are served from CDN This avoids needing to deal with CORS for loading of the SVG Note, added the svg- prefix to the filename so we can quickly tell in dev tools what the file is * Add missing SVG sprite JSONP script to CSP * Upgrade to FA 5.5.0 * Add support for all FA4.7 icons - adds complete frontend and backend for renamed FA4.7 icons - improves performance of SvgSprite.bundle and SvgSprite.all_icons * Fix group avatar flair preview - adds an endpoint at /svg-sprites/search/:keyword - adds frontend ajax call that pulls icon in avatar flair preview even when it is not in subset * Remove FA 4.7 font files
2018-11-27 05:49:57 +08:00
after_commit do
SvgSprite.expire_cache
UserStat.update_distinct_badge_count if saved_change_to_enabled?
UserBadge.ensure_consistency! if saved_change_to_enabled?
Upgrade to FontAwesome 5 (take two) (#6673) * Add missing icons to set * Revert FA5 revert This reverts commit 42572ff * use new SVG syntax in locales * Noscript page changes (remove login button, center "powered by" footer text) * Cast wider net for SVG icons in settings - include any _icon setting for SVG registry (offers better support for plugin settings) - let themes store multiple pipe-delimited icons in a setting - also replaces broken onebox image icon with SVG reference in cooked post processor * interpolate icons in locales * Fix composer whisper icon alignment * Add support for stacked icons * SECURITY: enforce hostname to match discourse hostname This ensures that the hostname rails uses for various helpers always matches the Discourse hostname * load SVG sprite with pre-initializers * FIX: enable caching on SVG sprites * PERF: use JSONP for SVG sprites so they are served from CDN This avoids needing to deal with CORS for loading of the SVG Note, added the svg- prefix to the filename so we can quickly tell in dev tools what the file is * Add missing SVG sprite JSONP script to CSP * Upgrade to FA 5.5.0 * Add support for all FA4.7 icons - adds complete frontend and backend for renamed FA4.7 icons - improves performance of SvgSprite.bundle and SvgSprite.all_icons * Fix group avatar flair preview - adds an endpoint at /svg-sprites/search/:keyword - adds frontend ajax call that pulls icon in avatar flair preview even when it is not in subset * Remove FA 4.7 font files
2018-11-27 05:49:57 +08:00
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,
2014-09-11 11:30:47 +08:00
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)
FIX: Badge and user title interaction fixes (#8282) * Fix user title logic when badge name customized * Fix an issue where a user's title was not considered a badge granted title when the user used a badge for their title and the badge name was customized. this affected the effectiveness of revoke_ungranted_titles! which only operates on badge_granted_titles. * When a user's title is set now it is considered a badge_granted_title if the badge name OR the badge custom name from TranslationOverride is the same as the title * When a user's badge is revoked we now also revoke their title if the user's title matches the badge name OR the badge custom name from TranslationOverride * Add a user history log when the title is revoked to remove confusion about why titles are revoked * Add granted_title_badge_id to user_profile, now when we set badge_granted_title on a user profile when updating a user's title based on a badge, we also remember which badge matched the title * When badge name (or custom text) changes update titles of users in a background job * When the name of a badge changes, or in the case of system badges when their custom translation text changes, then we need to update the title of all corresponding users who have a badge_granted_title and matching granted_title_badge_id. In the case of system badges we need to first get the proper badge ID based on the translation key e.g. badges.regular.name * Add migration to backfill all granted_title_badge_ids for both normal badge name titles and titles using custom badge text.
2019-11-08 13:34:24 +08:00
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
2014-05-21 15:22:42 +08:00
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
2014-07-30 06:46:46 +08:00
def default_allow_title=(val)
return if !self.new_record?
self.allow_title = val
end
def default_enabled=(val)
return if !self.new_record?
self.enabled = val
2014-07-30 06:46:46 +08:00
end
2014-07-17 14:10:44 +08:00
def default_badge_grouping_id=(val)
2014-07-18 13:55:42 +08:00
# allow to correct orphans
if !self.badge_grouping_id || self.badge_grouping_id <= BadgeGrouping::Other
2014-07-18 13:55:42 +08:00
self.badge_grouping_id = val
end
2014-07-17 14:10:44 +08:00
end
def display_name
self.class.display_name(name)
FIX: Badge and user title interaction fixes (#8282) * Fix user title logic when badge name customized * Fix an issue where a user's title was not considered a badge granted title when the user used a badge for their title and the badge name was customized. this affected the effectiveness of revoke_ungranted_titles! which only operates on badge_granted_titles. * When a user's title is set now it is considered a badge_granted_title if the badge name OR the badge custom name from TranslationOverride is the same as the title * When a user's badge is revoked we now also revoke their title if the user's title matches the badge name OR the badge custom name from TranslationOverride * Add a user history log when the title is revoked to remove confusion about why titles are revoked * Add granted_title_badge_id to user_profile, now when we set badge_granted_title on a user profile when updating a user's title based on a badge, we also remember which badge matched the title * When badge name (or custom text) changes update titles of users in a background job * When the name of a badge changes, or in the case of system badges when their custom translation text changes, then we need to update the title of all corresponding users who have a badge_granted_title and matching granted_title_badge_id. In the case of system badges we need to first get the proper badge ID based on the translation key e.g. badges.regular.name * Add migration to backfill all granted_title_badge_ids for both normal badge name titles and titles using custom badge text.
2019-11-08 13:34:24 +08:00
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
def sanitize_description
if description_changed?
self.description = sanitize_field(self.description)
end
end
2014-03-05 20:52:20 +08:00
end
# == Schema Information
#
# Table name: badges
#
2014-07-17 14:10:44 +08:00
# id :integer not null, primary key
2019-01-12 03:29:56 +08:00
# name :string not null
2014-07-17 14:10:44 +08:00
# 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
2014-07-17 14:10:44 +08:00
# allow_title :boolean default(FALSE), not null
# multiple_grant :boolean default(FALSE), not null
2019-01-12 03:29:56 +08:00
# icon :string default("fa-certificate")
2014-07-17 14:10:44 +08:00
# listable :boolean default(TRUE)
# target_posts :boolean default(FALSE)
# query :text
# enabled :boolean default(TRUE), not null
# auto_revoke :boolean default(TRUE), not null
2014-07-22 10:46:31 +08:00
# badge_grouping_id :integer default(5), not null
# trigger :integer
# show_posts :boolean default(FALSE), not null
2014-08-07 11:33:11 +08:00
# system :boolean default(FALSE), not null
2015-09-18 08:41:10 +08:00
# long_description :text
# image_upload_id :integer
2014-03-05 20:52:20 +08:00
#
# Indexes
#
2019-01-12 03:29:56 +08:00
# index_badges_on_badge_type_id (badge_type_id)
# index_badges_on_name (name) UNIQUE
2014-03-05 20:52:20 +08:00
#