1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
|
# frozen_string_literal: true
module ActsAsTaggableOn
class Tag < ActsAsTaggableOn.base_class.constantize
self.table_name = ActsAsTaggableOn.tags_table
### ASSOCIATIONS:
has_many :taggings, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
### VALIDATIONS:
validates_presence_of :name
validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
validates_length_of :name, maximum: 255
# monkey patch this method if don't need name uniqueness validation
def validates_name_uniqueness?
true
end
### SCOPES:
scope :most_used, ->(limit = 20) { order('taggings_count desc').limit(limit) }
scope :least_used, ->(limit = 20) { order('taggings_count asc').limit(limit) }
def self.named(name)
if ActsAsTaggableOn.strict_case_match
where(["name = #{binary}?", as_8bit_ascii(name)])
else
where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
end
end
def self.named_any(list)
clause = list.map do |tag|
sanitize_sql_for_named_any(tag).force_encoding('BINARY')
end.join(' OR ')
where(clause)
end
def self.named_like(name)
clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
"%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
where(clause)
end
def self.named_like_any(list)
clause = list.map do |tag|
sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
"%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
end.join(' OR ')
where(clause)
end
def self.for_context(context)
joins(:taggings)
.where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context])
.select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
end
def self.for_tenant(tenant)
joins(:taggings)
.where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s)
.select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
end
### CLASS METHODS:
def self.find_or_create_with_like_by_name(name)
if ActsAsTaggableOn.strict_case_match
find_or_create_all_with_like_by_name([name]).first
else
named_like(name).first || create(name: name)
end
end
def self.find_or_create_all_with_like_by_name(*list)
list = Array(list).flatten
return [] if list.empty?
existing_tags = named_any(list)
list.map do |tag_name|
tries ||= 3
comparable_tag_name = comparable_name(tag_name)
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
next existing_tag if existing_tag
transaction(requires_new: true) { create(name: tag_name) }
rescue ActiveRecord::RecordNotUnique
if (tries -= 1).positive?
existing_tags = named_any(list)
retry
end
raise DuplicateTagError, "'#{tag_name}' has already been taken"
end
end
### INSTANCE METHODS:
def ==(other)
super || (other.is_a?(Tag) && name == other.name)
end
def to_s
name
end
def count
read_attribute(:count).to_i
end
class << self
private
def comparable_name(str)
if ActsAsTaggableOn.strict_case_match
str
else
unicode_downcase(str.to_s)
end
end
def binary
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
end
def as_8bit_ascii(string)
string.to_s.mb_chars
end
def unicode_downcase(string)
as_8bit_ascii(string).downcase
end
def sanitize_sql_for_named_any(tag)
if ActsAsTaggableOn.strict_case_match
sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
else
sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
end
end
end
end
end
|