Class: MakeTaggable::Tag

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/make_taggable/tag.rb

Overview

A tag name, shared by every record tagged with it.

Tags are found and created through the class methods here rather than directly, so that the configured case sensitivity is applied consistently. Subclass it to keep a separate vocabulary for one context, and point at the subclass from MakeTaggable::Taggable::Core#find_or_create_tags_from_list_with_context.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#nameString

The tag itself.

Returns:

  • (String)


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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/make_taggable/tag.rb', line 19

class Tag < ::ActiveRecord::Base
  self.table_name = MakeTaggable.tags_table

  ### ASSOCIATIONS:
  has_many :taggings, dependent: :destroy, class_name: "::MakeTaggable::Tagging"

  ### VALIDATIONS:
  validates_presence_of :name
  # Two declarations, one of which runs. A tags table with a `type` column is
  # being used for single table inheritance -- a Tag subclass per vocabulary --
  # and there a name is expected to repeat across subclasses: "energy" as a
  # Market and as a Genre. Scoping the check keeps it meaningful within a
  # subclass rather than making the whole thing something to switch off.
  #
  # The column is looked up per validation rather than when this class loads,
  # because the class can load before the migration that adds it has run.
  validates_uniqueness_of :name,
    if: -> { validates_name_uniqueness? && !self.class.tag_type_column? },
    case_sensitive: true

  validates_uniqueness_of :name,
    scope: :type,
    if: -> { validates_name_uniqueness? && self.class.tag_type_column? },
    case_sensitive: true
  validates_length_of :name, maximum: 255

  ##
  # Whether the uniqueness validation on `name` runs.
  #
  # Override this in a subclass to allow tag names to repeat.
  #
  # @return [TrueClass, FalseClass] always `true` here
  #
  def validates_name_uniqueness?
    true
  end

  ##
  # Whether the tags table carries a `type` column, and so is being used for single table
  # inheritance.
  #
  # @return [TrueClass, FalseClass]
  #
  # @api private
  #
  def self.tag_type_column?
    column_names.include?("type")
  end

  ### SCOPES:
  scope :most_used, ->(limit = 20) { order("taggings_count desc").limit(limit) }
  scope :least_used, ->(limit = 20) { order("taggings_count asc").limit(limit) }

  ##
  # Tags matching a name exactly, honouring the configured case sensitivity.
  #
  # @param name [String] the name to match
  # @return [ActiveRecord::Relation]
  #
  def self.named(name)
    if MakeTaggable.strict_case_match
      where(["name = #{binary}?", name.to_s])
    else
      where(["LOWER(name) = LOWER(?)", name.to_s.downcase])
    end
  end

  ##
  # Tags matching any of the given names exactly.
  #
  # @param list [Array<String>] the names to match
  # @return [ActiveRecord::Relation]
  #
  def self.named_any(list)
    clause = list.map { |tag|
      sanitize_sql_for_named_any(tag)
    }.join(" OR ")
    where(clause)
  end

  ##
  # Tags whose name contains the given fragment.
  #
  # Case insensitive on PostgreSQL, which uses `ILIKE`; otherwise it follows the column's
  # collation.
  #
  # @param name [String] the fragment to look for
  # @return [ActiveRecord::Relation]
  #
  def self.named_like(name)
    clause = ["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(name)}%"]
    where(clause)
  end

  ##
  # Tags whose name contains any of the given fragments.
  #
  # @param list [Array<String>] the fragments to look for
  # @return [ActiveRecord::Relation]
  #
  def self.named_like_any(list)
    clause = list.map { |tag|
      sanitize_sql(["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(tag.to_s)}%"])
    }.join(" OR ")
    where(clause)
  end

  ##
  # Tags used in a given context, whatever the record they were applied to.
  #
  # @param context [String, Symbol] the tagging context
  # @return [ActiveRecord::Relation]
  #
  # @example
  #   MakeTaggable::Tag.for_context(:skills)
  #
  def self.for_context(context)
    joins(:taggings)
      .where(["#{MakeTaggable.taggings_table}.context = ?", context])
      .select("DISTINCT #{MakeTaggable.tags_table}.*")
  end

  ### CLASS METHODS:

  ##
  # Finds a tag by name, creating it when it does not exist yet.
  #
  # The name is matched in full. Honours the configured case sensitivity.
  #
  # @param name [String] the tag name
  # @return [MakeTaggable::Tag]
  #
  # @example
  #   MakeTaggable::Tag.find_or_create_with_like_by_name("ruby")
  #
  def self.find_or_create_with_like_by_name(name)
    if MakeTaggable.strict_case_match
      find_or_create_all_with_like_by_name([name]).first
    else
      # Matching has to happen in Ruby's terms rather than the column's: the
      # MySQL migration collates tag names as utf8mb4_bin, which would make a
      # LIKE comparison case sensitive whatever strict_case_match says.
      named(name).first || create(name: name)
    end
  end

  ##
  # Finds every tag in a list by name, creating those that do not exist yet.
  #
  # A competing write that takes a name first is retried up to three times before giving up.
  # Each insert runs in a savepoint of its own, so a name lost to a race unwinds that insert
  # alone -- an enclosing transaction the caller opened is left untouched, along with everything
  # written into it.
  #
  # @param list [Array<String>] the tag names
  # @return [Array<MakeTaggable::Tag>] in the order the names were given
  # @raise [MakeTaggable::DuplicateTagError] when a name stays taken after three attempts
  #
  # @example
  #   MakeTaggable::Tag.find_or_create_all_with_like_by_name(%w[ruby rails])
  #
  def self.find_or_create_all_with_like_by_name(*list)
    list = Array(list).flatten

    return [] if list.empty?

    existing_tags = named_any(list).to_a
    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

      # Tags created earlier in this call have to stay visible to the names
      # that follow, or a list holding both "Ruby" and "ruby" resolves to two
      # rows even though the two names compare equal.
      #
      # The insert gets a savepoint of its own so that a RecordNotUnique
      # unwinds only the failed insert. Without one the caller's transaction
      # is left in an aborted state and everything it had done is lost.
      transaction(requires_new: true) { create(name: tag_name) }.tap { |tag| existing_tags << tag }
      # A deadlock counts as losing the race, the same as a duplicate key.
      # MySQL reports one or the other depending on how two inserts of the
      # same name interleave on the unique index, and both mean the work
      # should be re-read and retried rather than abandoned.
    rescue ActiveRecord::RecordNotUnique, ActiveRecord::Deadlocked
      if (tries -= 1).positive?
        existing_tags = named_any(list).to_a
        retry
      end

      raise DuplicateTagError.new("'#{tag_name}' has already been taken")
    end
  end

  ### INSTANCE METHODS:

  ##
  # Compares tags by name, so a saved tag and an unsaved one with the same name are equal.
  #
  # @param other [Object] the object to compare against
  # @return [TrueClass, FalseClass]
  #
  def ==(other)
    super || (other.is_a?(Tag) && name == other.name)
  end

  ##
  # The tag's name, so a tag renders as itself in a view or a string.
  #
  # @return [String]
  #
  def to_s
    name
  end

  ##
  # How many times this tag matched, on relations that select a count alongside the tag columns.
  #
  # Zero on a tag loaded without one.
  #
  # @return [Integer]
  #
  def count
    read_attribute(:count).to_i
  end

  class << self
    private

    def comparable_name(str)
      if MakeTaggable.strict_case_match
        str
      else
        str.to_s.downcase
      end
    end

    def binary
      MakeTaggable::Utils.using_mysql? ? "BINARY " : nil
    end

    def sanitize_sql_for_named_any(tag)
      if MakeTaggable.strict_case_match
        sanitize_sql(["name = #{binary}?", tag.to_s])
      else
        sanitize_sql(["LOWER(name) = LOWER(?)", tag.to_s.downcase])
      end
    end
  end
end

#taggings_countInteger

How many taggings reference this tag, maintained as a counter cache.

Returns:

  • (Integer)


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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/make_taggable/tag.rb', line 19

class Tag < ::ActiveRecord::Base
  self.table_name = MakeTaggable.tags_table

  ### ASSOCIATIONS:
  has_many :taggings, dependent: :destroy, class_name: "::MakeTaggable::Tagging"

  ### VALIDATIONS:
  validates_presence_of :name
  # Two declarations, one of which runs. A tags table with a `type` column is
  # being used for single table inheritance -- a Tag subclass per vocabulary --
  # and there a name is expected to repeat across subclasses: "energy" as a
  # Market and as a Genre. Scoping the check keeps it meaningful within a
  # subclass rather than making the whole thing something to switch off.
  #
  # The column is looked up per validation rather than when this class loads,
  # because the class can load before the migration that adds it has run.
  validates_uniqueness_of :name,
    if: -> { validates_name_uniqueness? && !self.class.tag_type_column? },
    case_sensitive: true

  validates_uniqueness_of :name,
    scope: :type,
    if: -> { validates_name_uniqueness? && self.class.tag_type_column? },
    case_sensitive: true
  validates_length_of :name, maximum: 255

  ##
  # Whether the uniqueness validation on `name` runs.
  #
  # Override this in a subclass to allow tag names to repeat.
  #
  # @return [TrueClass, FalseClass] always `true` here
  #
  def validates_name_uniqueness?
    true
  end

  ##
  # Whether the tags table carries a `type` column, and so is being used for single table
  # inheritance.
  #
  # @return [TrueClass, FalseClass]
  #
  # @api private
  #
  def self.tag_type_column?
    column_names.include?("type")
  end

  ### SCOPES:
  scope :most_used, ->(limit = 20) { order("taggings_count desc").limit(limit) }
  scope :least_used, ->(limit = 20) { order("taggings_count asc").limit(limit) }

  ##
  # Tags matching a name exactly, honouring the configured case sensitivity.
  #
  # @param name [String] the name to match
  # @return [ActiveRecord::Relation]
  #
  def self.named(name)
    if MakeTaggable.strict_case_match
      where(["name = #{binary}?", name.to_s])
    else
      where(["LOWER(name) = LOWER(?)", name.to_s.downcase])
    end
  end

  ##
  # Tags matching any of the given names exactly.
  #
  # @param list [Array<String>] the names to match
  # @return [ActiveRecord::Relation]
  #
  def self.named_any(list)
    clause = list.map { |tag|
      sanitize_sql_for_named_any(tag)
    }.join(" OR ")
    where(clause)
  end

  ##
  # Tags whose name contains the given fragment.
  #
  # Case insensitive on PostgreSQL, which uses `ILIKE`; otherwise it follows the column's
  # collation.
  #
  # @param name [String] the fragment to look for
  # @return [ActiveRecord::Relation]
  #
  def self.named_like(name)
    clause = ["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(name)}%"]
    where(clause)
  end

  ##
  # Tags whose name contains any of the given fragments.
  #
  # @param list [Array<String>] the fragments to look for
  # @return [ActiveRecord::Relation]
  #
  def self.named_like_any(list)
    clause = list.map { |tag|
      sanitize_sql(["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(tag.to_s)}%"])
    }.join(" OR ")
    where(clause)
  end

  ##
  # Tags used in a given context, whatever the record they were applied to.
  #
  # @param context [String, Symbol] the tagging context
  # @return [ActiveRecord::Relation]
  #
  # @example
  #   MakeTaggable::Tag.for_context(:skills)
  #
  def self.for_context(context)
    joins(:taggings)
      .where(["#{MakeTaggable.taggings_table}.context = ?", context])
      .select("DISTINCT #{MakeTaggable.tags_table}.*")
  end

  ### CLASS METHODS:

  ##
  # Finds a tag by name, creating it when it does not exist yet.
  #
  # The name is matched in full. Honours the configured case sensitivity.
  #
  # @param name [String] the tag name
  # @return [MakeTaggable::Tag]
  #
  # @example
  #   MakeTaggable::Tag.find_or_create_with_like_by_name("ruby")
  #
  def self.find_or_create_with_like_by_name(name)
    if MakeTaggable.strict_case_match
      find_or_create_all_with_like_by_name([name]).first
    else
      # Matching has to happen in Ruby's terms rather than the column's: the
      # MySQL migration collates tag names as utf8mb4_bin, which would make a
      # LIKE comparison case sensitive whatever strict_case_match says.
      named(name).first || create(name: name)
    end
  end

  ##
  # Finds every tag in a list by name, creating those that do not exist yet.
  #
  # A competing write that takes a name first is retried up to three times before giving up.
  # Each insert runs in a savepoint of its own, so a name lost to a race unwinds that insert
  # alone -- an enclosing transaction the caller opened is left untouched, along with everything
  # written into it.
  #
  # @param list [Array<String>] the tag names
  # @return [Array<MakeTaggable::Tag>] in the order the names were given
  # @raise [MakeTaggable::DuplicateTagError] when a name stays taken after three attempts
  #
  # @example
  #   MakeTaggable::Tag.find_or_create_all_with_like_by_name(%w[ruby rails])
  #
  def self.find_or_create_all_with_like_by_name(*list)
    list = Array(list).flatten

    return [] if list.empty?

    existing_tags = named_any(list).to_a
    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

      # Tags created earlier in this call have to stay visible to the names
      # that follow, or a list holding both "Ruby" and "ruby" resolves to two
      # rows even though the two names compare equal.
      #
      # The insert gets a savepoint of its own so that a RecordNotUnique
      # unwinds only the failed insert. Without one the caller's transaction
      # is left in an aborted state and everything it had done is lost.
      transaction(requires_new: true) { create(name: tag_name) }.tap { |tag| existing_tags << tag }
      # A deadlock counts as losing the race, the same as a duplicate key.
      # MySQL reports one or the other depending on how two inserts of the
      # same name interleave on the unique index, and both mean the work
      # should be re-read and retried rather than abandoned.
    rescue ActiveRecord::RecordNotUnique, ActiveRecord::Deadlocked
      if (tries -= 1).positive?
        existing_tags = named_any(list).to_a
        retry
      end

      raise DuplicateTagError.new("'#{tag_name}' has already been taken")
    end
  end

  ### INSTANCE METHODS:

  ##
  # Compares tags by name, so a saved tag and an unsaved one with the same name are equal.
  #
  # @param other [Object] the object to compare against
  # @return [TrueClass, FalseClass]
  #
  def ==(other)
    super || (other.is_a?(Tag) && name == other.name)
  end

  ##
  # The tag's name, so a tag renders as itself in a view or a string.
  #
  # @return [String]
  #
  def to_s
    name
  end

  ##
  # How many times this tag matched, on relations that select a count alongside the tag columns.
  #
  # Zero on a tag loaded without one.
  #
  # @return [Integer]
  #
  def count
    read_attribute(:count).to_i
  end

  class << self
    private

    def comparable_name(str)
      if MakeTaggable.strict_case_match
        str
      else
        str.to_s.downcase
      end
    end

    def binary
      MakeTaggable::Utils.using_mysql? ? "BINARY " : nil
    end

    def sanitize_sql_for_named_any(tag)
      if MakeTaggable.strict_case_match
        sanitize_sql(["name = #{binary}?", tag.to_s])
      else
        sanitize_sql(["LOWER(name) = LOWER(?)", tag.to_s.downcase])
      end
    end
  end
end

Class Method Details

.find_or_create_all_with_like_by_name(*list) ⇒ Array<MakeTaggable::Tag>

Finds every tag in a list by name, creating those that do not exist yet.

A competing write that takes a name first is retried up to three times before giving up. Each insert runs in a savepoint of its own, so a name lost to a race unwinds that insert alone -- an enclosing transaction the caller opened is left untouched, along with everything written into it.

Examples:

MakeTaggable::Tag.find_or_create_all_with_like_by_name(%w[ruby rails])

Parameters:

  • list (Array<String>)

    the tag names

Returns:

Raises:



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/make_taggable/tag.rb', line 180

def self.find_or_create_all_with_like_by_name(*list)
  list = Array(list).flatten

  return [] if list.empty?

  existing_tags = named_any(list).to_a
  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

    # Tags created earlier in this call have to stay visible to the names
    # that follow, or a list holding both "Ruby" and "ruby" resolves to two
    # rows even though the two names compare equal.
    #
    # The insert gets a savepoint of its own so that a RecordNotUnique
    # unwinds only the failed insert. Without one the caller's transaction
    # is left in an aborted state and everything it had done is lost.
    transaction(requires_new: true) { create(name: tag_name) }.tap { |tag| existing_tags << tag }
    # A deadlock counts as losing the race, the same as a duplicate key.
    # MySQL reports one or the other depending on how two inserts of the
    # same name interleave on the unique index, and both mean the work
    # should be re-read and retried rather than abandoned.
  rescue ActiveRecord::RecordNotUnique, ActiveRecord::Deadlocked
    if (tries -= 1).positive?
      existing_tags = named_any(list).to_a
      retry
    end

    raise DuplicateTagError.new("'#{tag_name}' has already been taken")
  end
end

.find_or_create_with_like_by_name(name) ⇒ MakeTaggable::Tag

Finds a tag by name, creating it when it does not exist yet.

The name is matched in full. Honours the configured case sensitivity.

Examples:

MakeTaggable::Tag.find_or_create_with_like_by_name("ruby")

Parameters:

  • name (String)

    the tag name

Returns:



154
155
156
157
158
159
160
161
162
163
# File 'lib/make_taggable/tag.rb', line 154

def self.find_or_create_with_like_by_name(name)
  if MakeTaggable.strict_case_match
    find_or_create_all_with_like_by_name([name]).first
  else
    # Matching has to happen in Ruby's terms rather than the column's: the
    # MySQL migration collates tag names as utf8mb4_bin, which would make a
    # LIKE comparison case sensitive whatever strict_case_match says.
    named(name).first || create(name: name)
  end
end

.for_context(context) ⇒ ActiveRecord::Relation

Tags used in a given context, whatever the record they were applied to.

Examples:

MakeTaggable::Tag.for_context(:skills)

Parameters:

  • context (String, Symbol)

    the tagging context

Returns:

  • (ActiveRecord::Relation)


135
136
137
138
139
# File 'lib/make_taggable/tag.rb', line 135

def self.for_context(context)
  joins(:taggings)
    .where(["#{MakeTaggable.taggings_table}.context = ?", context])
    .select("DISTINCT #{MakeTaggable.tags_table}.*")
end

.named(name) ⇒ ActiveRecord::Relation

Tags matching a name exactly, honouring the configured case sensitivity.

Parameters:

  • name (String)

    the name to match

Returns:

  • (ActiveRecord::Relation)


78
79
80
81
82
83
84
# File 'lib/make_taggable/tag.rb', line 78

def self.named(name)
  if MakeTaggable.strict_case_match
    where(["name = #{binary}?", name.to_s])
  else
    where(["LOWER(name) = LOWER(?)", name.to_s.downcase])
  end
end

.named_any(list) ⇒ ActiveRecord::Relation

Tags matching any of the given names exactly.

Parameters:

  • list (Array<String>)

    the names to match

Returns:

  • (ActiveRecord::Relation)


92
93
94
95
96
97
# File 'lib/make_taggable/tag.rb', line 92

def self.named_any(list)
  clause = list.map { |tag|
    sanitize_sql_for_named_any(tag)
  }.join(" OR ")
  where(clause)
end

.named_like(name) ⇒ ActiveRecord::Relation

Tags whose name contains the given fragment.

Case insensitive on PostgreSQL, which uses ILIKE; otherwise it follows the column's collation.

Parameters:

  • name (String)

    the fragment to look for

Returns:

  • (ActiveRecord::Relation)


108
109
110
111
# File 'lib/make_taggable/tag.rb', line 108

def self.named_like(name)
  clause = ["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(name)}%"]
  where(clause)
end

.named_like_any(list) ⇒ ActiveRecord::Relation

Tags whose name contains any of the given fragments.

Parameters:

  • list (Array<String>)

    the fragments to look for

Returns:

  • (ActiveRecord::Relation)


119
120
121
122
123
124
# File 'lib/make_taggable/tag.rb', line 119

def self.named_like_any(list)
  clause = list.map { |tag|
    sanitize_sql(["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(tag.to_s)}%"])
  }.join(" OR ")
  where(clause)
end

.tag_type_column?TrueClass, FalseClass

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether the tags table carries a type column, and so is being used for single table inheritance.

Returns:

  • (TrueClass, FalseClass)


64
65
66
# File 'lib/make_taggable/tag.rb', line 64

def self.tag_type_column?
  column_names.include?("type")
end

Instance Method Details

#==(other) ⇒ TrueClass, FalseClass

Compares tags by name, so a saved tag and an unsaved one with the same name are equal.

Parameters:

  • other (Object)

    the object to compare against

Returns:

  • (TrueClass, FalseClass)


222
223
224
# File 'lib/make_taggable/tag.rb', line 222

def ==(other)
  super || (other.is_a?(Tag) && name == other.name)
end

#countInteger

How many times this tag matched, on relations that select a count alongside the tag columns.

Zero on a tag loaded without one.

Returns:

  • (Integer)


242
243
244
# File 'lib/make_taggable/tag.rb', line 242

def count
  read_attribute(:count).to_i
end

#to_sString

The tag's name, so a tag renders as itself in a view or a string.

Returns:

  • (String)


231
232
233
# File 'lib/make_taggable/tag.rb', line 231

def to_s
  name
end

#validates_name_uniqueness?TrueClass, FalseClass

Whether the uniqueness validation on name runs.

Override this in a subclass to allow tag names to repeat.

Returns:

  • (TrueClass, FalseClass)

    always true here



52
53
54
# File 'lib/make_taggable/tag.rb', line 52

def validates_name_uniqueness?
  true
end