acts_as_taggable 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,70 @@
1
+ = The Acts As Taggable Mixin
2
+
3
+ == Installation
4
+
5
+ To install or update the gem, simply execute:
6
+
7
+ gem install acts_as_taggable
8
+
9
+ To use the 'acts_as_taggable' library in your Rails application after installing
10
+ the gem, add this line at the end of your 'config/environment.rb' file:
11
+
12
+ require_gem 'acts_as_taggable'
13
+
14
+ == Usage Instructions
15
+
16
+ To use the acts_as_taggable mixin with your ActiveRecord objects, you must use
17
+ a normalized database schema for tagging (also know as folksnomies).
18
+
19
+ This means that you must have a table solely for holding tag names. Usually,
20
+ this table is named 'tags' having at least 2 columns: primary key (usually
21
+ an autoincrement integer called 'id' - the AR standard for PKs) and a 'name'
22
+ columns, usually a varchar. You must also have a defined ActiveRecord model
23
+ class that relates to this table, by default called 'Tag'.
24
+
25
+ For associating tags to your objects you also must have join tables that are
26
+ composed of at least 2 columns: the tags table foreign key (by default 'tag_id')
27
+ and your taggable object table foreign key.
28
+
29
+ If you�re using the simple has_and_belongs_to_many model, you must NOT have a
30
+ primary key (usually an 'id' column) defined on the join table. If you�re using
31
+ a full join model, you must add a primary key column to the join table. Please
32
+ see the RDoc documentation on acts_as_taggable macro and the :join_class_name
33
+ option for the differences between these two approaches.
34
+
35
+ For example, suppose you are tagging photos and you hold your photo data thru
36
+ the Photo model and on the 'photos' table. Your database schema would look
37
+ something like this (example suited for MySQL):
38
+
39
+ CREATE TABLE `tags` (
40
+ `id` int(11) NOT NULL auto_increment,
41
+ `name` varchar(255) default NULL,
42
+ PRIMARY KEY (`id`)
43
+ )
44
+
45
+ CREATE TABLE `tags_photos` (
46
+ `tag_id` int(11) NOT NULL,
47
+ `photo_id` int(11) NOT NULL
48
+ )
49
+
50
+ CREATE TABLE `photos` (
51
+ `id` int(11) NOT NULL auto_increment,
52
+ `title` varchar(255) default NULL,
53
+ `author_name` varchar(255) default NULL,
54
+ `image_path` varchar(255) default NULL,
55
+ PRIMARY KEY (`id`)
56
+ )
57
+
58
+ You would normally define 2 models to relate to these tables:
59
+
60
+ class Tag < ActiveRecord::Base
61
+ end
62
+
63
+ class Photo < ActiveRecord::Base
64
+ acts_as_taggable
65
+ end
66
+
67
+ Now you can easily apply and search for tags on photos in your Rails application.
68
+
69
+ This assumes you�re using only default naming conventions. For using the mix-in
70
+ with non-standard naming conventions, please see the proper RDoc documentation.
data/lib/taggable.rb ADDED
@@ -0,0 +1,467 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+
4
+ module ActiveRecord
5
+ module Acts #:nodoc:
6
+ module Taggable #:nodoc:
7
+
8
+ def self.append_features(base)
9
+ super
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ def self.split_tag_names(tags, separator)
14
+ tag_names = []
15
+ if tags.is_a?(Array)
16
+ tag_names << tags
17
+ elsif tags.is_a?(String)
18
+ tag_names << (separator.is_a?(Proc) ? separator.call(tags) : tags.split(separator))
19
+ end
20
+ tag_names = tag_names.flatten.map { |name| name.strip }.uniq.compact #straight 'em up
21
+ end
22
+
23
+ # This mixin provides an easy way for addind tagging capabilities (also
24
+ # known as folksnomy) to your active record objects. It allows you to add
25
+ # tags to your objects as well as search for tagged objects.
26
+ #
27
+ # It assumes you are using a fully-normalized tagging database schema. For
28
+ # that, you need a table (by default, named +tags+) to hold all tags in your
29
+ # application and this table must have a primary key (normally a +id+ int
30
+ # autonumber column) and a +name+ varchar column. You must also define a model class
31
+ # related to this table (by default, named +Tag+).
32
+ #
33
+ # All tag names will be stored in this tags table. Taggable objects should reside
34
+ # in their own tables, like any other object. Tagging objects is perfomed by
35
+ # the +acts_as_taggable+ mixin using a +has_and_belong_to_many+ relationship that is
36
+ # automatically created on the taggable class, and as so, a join table must exist
37
+ # between the tags table and the taggable object table.
38
+ #
39
+ # The name of the join table, by default, always follow the form
40
+ # '[tags_table_name]_[taggable_object_table_name]' even if the taggable object
41
+ # table name precedes the tags table name alphabetically (for example, tags_photos).
42
+ # This is different from the regular +has_and_belongs_to_many+ convention and
43
+ # allows all your join tables to share a common prefix (which is the tags table name).
44
+ #
45
+ # The join table must be composed of the foreign keys from the tags table and the
46
+ # taggable object table, so for instance, if we have a tags table named +tags+ (related
47
+ # to a +Tag+ model) and a taggable +photos+ table (related to a +Photo+ model),
48
+ # there should be a join table +tags_photos+ with int FK columns +photo_id+ and +tag_id+.
49
+ # If you don�t use a explicit full model related to the join table (thru the
50
+ # +:join_class_name+ option), you must not add a primary key to the join table.
51
+ #
52
+ # The +acts_as_taggable+ adds the instance methods +tag+, +tag_names+,
53
+ # +tag_names= +, +tag_names<< +, +tagged_with? + for adding tags to the object
54
+ # and also the class method +find_tagged_with+ method for search tagged objects.
55
+ #
56
+ # Examples:
57
+ #
58
+ # class Photo < ActiveRecord::Base
59
+ # # this creates a 'tags' collection, thru a has_and_belongs_to_many
60
+ # # relationship that utilizes the join table 'tags_photos'.
61
+ # acts_as_taggable
62
+ # end
63
+ #
64
+ # photo = Photo.new
65
+ #
66
+ # # splits and adds to the tags collection
67
+ # photo.tag "wine beer alcohol"
68
+ #
69
+ # # don't need to split since it's an array, but replaces the tags collection
70
+ # # trailing and leading spaces are properly removed
71
+ # photo.tag [ 'wine ', ' vodka'], :clear => true
72
+ #
73
+ # photo.tag_names # => [ 'wine', 'vodka' ]
74
+ #
75
+ # # appends new tags with a different separator
76
+ # # the 'wine' tag won�t be duplicated
77
+ # photo.tag_names << 'wine, beer, alcohol', :separator => ','
78
+ #
79
+ # # The difference between +tag_names+ and +tags+ is that +tag_names+
80
+ # # holds an array of String objects, mapped from +tags+, while +tags+
81
+ # # holds the actual +has_and_belongs_to_many+ collection, and so, is
82
+ # # composed of +Tag+ objects.
83
+ # photo.tag_names.size # => 4
84
+ # photo.tags.size # => 4
85
+ #
86
+ # # Find photos with 'wine' OR 'whisky'
87
+ # Photo.find_tagged_with :any => [ 'wine', 'whisky' ]
88
+ #
89
+ # # Finds photos with 'wine' AND 'whisky' using a different separator.
90
+ # # This is also known as tag combos.
91
+ # Photo.find_tagged_with(:all => 'wine+whisky', :separator => '+'
92
+ #
93
+ # # Gets the top 10 tags for all photos
94
+ # Photo.tags_count :limit => 10 # => { 'beer' => 68, 'wine' => 37, 'vodka' => '22', ... }
95
+ #
96
+ # # Gets the tags count that are greater than 30
97
+ # Photo.tags_count :count => '> 30' # => { 'beer' => 68, 'wine' => 37 }
98
+ #
99
+ # You can also use full join models if you want to take advantage of
100
+ # ActiveRecord�s callbacks, timestamping, inheritance and other features
101
+ # on the join records as well. For that, you use the +:join_class_name+ option.
102
+ # In this case, the join table must have a primary key.
103
+ #
104
+ # class Person
105
+ # # This defines a class +TagPerson+ automagically.
106
+ # acts_as_taggable :join_class_name => 'TagPerson'
107
+ # end
108
+ #
109
+ # # We can open the +TagPerson+ class and add features to it.
110
+ # class TagPerson
111
+ # acts_as_list :scope => :person
112
+ # belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_id'
113
+ # before_save :do_some_validation
114
+ # after_save :do_some_stats
115
+ # end
116
+ #
117
+ # # We can do some interesting things with it now
118
+ # person = Person.new
119
+ # person.tag "wine beer alcohol", :attributes => { :created_by_id => 1 }
120
+ # Person.find_tagged_with(:any => 'wine', :condition => "tags_people.created_by_id = 1 AND tags_people.position = 1")
121
+ module ClassMethods
122
+
123
+ # This method defines a +has_and_belongs_to_many+ relationship between
124
+ # the target class and the tag model class. It also adds several instance methods
125
+ # for tagging objects of the target class, as well as a class method for searching
126
+ # objects that contains specific tags.
127
+ #
128
+ # The options are:
129
+ #
130
+ # The +:collection+ parameter receives a symbol defining
131
+ # the name of the tag collection method and it defaults to +:tags+.
132
+ #
133
+ # The +:tag_class_name+ parameter receives the tag model class name and
134
+ # it defaults to +'Tag'+.
135
+ #
136
+ # THe +:join_class_name+ parameter receives the model class name that joins
137
+ # the tag model and the taggable model. This automagically defines the join model
138
+ # class that can be opened and extended.
139
+ #
140
+ # The remaining options are passed on to the +has_and_belongs_to_many+ declaration.
141
+ # The +:join_table+ parameter is defined by default using the form
142
+ # of '[tags_table_name]_[target_class_table_name]', example: +tags_photos+,
143
+ # which differs from the standard +has_and_belongs_to_many+ behavior.
144
+ def acts_as_taggable(options = {})
145
+
146
+ options = { :collection => :tags, :tag_class_name => 'Tag' }.merge(options)
147
+ collection_name = options[:collection]
148
+ tag_model = options[:tag_class_name].constantize
149
+
150
+ default_join_table = "#{tag_model.table_name}_#{self.table_name}"
151
+ options[:join_table] ||= default_join_table
152
+ options[:foreign_key] ||= self.name.to_s.foreign_key
153
+ options[:association_foreign_key] ||= tag_model.to_s.foreign_key
154
+
155
+ # not using a simple has_and_belongs_to_many but a full model
156
+ # for joining the tags table and the taggable object table
157
+ if join_class_name = options[:join_class_name]
158
+ Object.class_eval "class #{join_class_name} < ActiveRecord::Base; set_table_name '#{options[:join_table]}' end" unless Object.const_defined?(join_class_name)
159
+
160
+ join_model = join_class_name.constantize
161
+ tagged = self
162
+ join_model.class_eval do
163
+ belongs_to :tag, :class_name => tag_model.to_s
164
+ belongs_to :tagged, :class_name => tagged.name.to_s
165
+
166
+ define_method(:name) { self['name'] ||= tag.name }
167
+ end
168
+
169
+
170
+ options[:class_name] ||= join_model.to_s
171
+ tag_pk, tag_fk = tag_model.primary_key, options[:association_foreign_key]
172
+ t, jt = tag_model.table_name, join_model.table_name
173
+ options[:finder_sql] ||= "SELECT #{jt}.*, #{t}.name AS name FROM #{jt}, #{t} WHERE #{jt}.#{tag_fk} = #{t}.#{tag_pk} AND #{jt}.#{options[:foreign_key]} = \#{quoted_id}"
174
+ else
175
+ join_model = nil
176
+ end
177
+
178
+ # set some class-wide attributes needed in class and instance methods
179
+ write_inheritable_attribute(:tag_foreign_key, options[:association_foreign_key])
180
+ write_inheritable_attribute(:taggable_foreign_key, options[:foreign_key])
181
+ write_inheritable_attribute(:tag_collection_name, collection_name)
182
+ write_inheritable_attribute(:tag_model, tag_model)
183
+ write_inheritable_attribute(:tags_join_model, join_model)
184
+ write_inheritable_attribute(:tags_join_table, options[:join_table])
185
+ write_inheritable_attribute(:tag_options, options)
186
+
187
+ [ :collection, :tag_class_name, :join_class_name ].each { |key| options.delete(key) } # remove these, we don't need it anymore
188
+ [ :join_table, :association_foreign_key ].each { |key| options.delete(key) } if join_model # don�t need this for has_many
189
+
190
+ # now, finally add the proper relationships
191
+ class_eval do
192
+ include ActiveRecord::Acts::Taggable::InstanceMethods
193
+ extend ActiveRecord::Acts::Taggable::SingletonMethods
194
+
195
+ class_inheritable_reader :tag_collection_name, :tag_model, :tags_join_model,
196
+ :tags_options, :tags_join_table,
197
+ :tag_foreign_key, :taggable_foreign_key
198
+ if join_model
199
+ has_many collection_name, options
200
+ else
201
+ has_and_belongs_to_many collection_name, options
202
+ end
203
+ end
204
+
205
+ end
206
+ end
207
+
208
+ module SingletonMethods
209
+ # This method searches for objects of the taggable class and subclasses that
210
+ # contains specific tags associated to them. The tags to be searched for can
211
+ # be passed to the +:any+ or +:all+ options, either as a String or an Array.
212
+ #
213
+ # The options are:
214
+ #
215
+ # +:any+: searches objects that are related to ANY of the given tags
216
+ #
217
+ # +:all+: searcher objects that are related to ALL of the the given tags
218
+ #
219
+ # +:separator+: a string, regex or Proc object that will be used to split the
220
+ # tags string passed to +:any+ or +:all+ using a regular +String#split+ method.
221
+ # If a Proc is passed, the proc should split the string in any way it wants
222
+ # and return an array of strings.
223
+ #
224
+ # +:conditions+: any additional conditions that should be appended to the
225
+ # WHERE clause of the finder SQL. Just like regular +ActiveRecord::Base#find+ methods.
226
+ #
227
+ # +:order+: the same as used in regular +ActiveRecord::Base#find+ methods.
228
+ #
229
+ # +:limit+: the same as used in regular +ActiveRecord::Base#find+ methods.
230
+ def find_tagged_with(options = {})
231
+ options = { :separator => ' ' }.merge(options)
232
+
233
+ tag_names = ActiveRecord::Acts::Taggable.split_tag_names(options[:any] || options[:all], options[:separator])
234
+ raise "No tags were passed to :any or :all options" if tag_names.empty?
235
+
236
+ o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
237
+ sql = "SELECT #{o}.* FROM #{jt}, #{o}, #{t} WHERE #{jt}.#{t_fk} = #{t}.#{t_pk}
238
+ AND (#{t}.name = '#{tag_names.join("' OR #{t}.name='")}')
239
+ AND #{o}.#{o_pk} = #{jt}.#{o_fk}"
240
+ sql << " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
241
+ sql << " GROUP BY #{o}.#{o_pk}"
242
+ sql << " HAVING COUNT(#{o}.#{o_pk}) = #{tag_names.length}" if options[:all]
243
+ sql << " ORDER BY #{options[:order]} " if options[:order]
244
+ add_limit!(sql, options)
245
+
246
+ find_by_sql(sql)
247
+ end
248
+
249
+ # This method counts the number of times the tags have been applied to your objects
250
+ # and, by default, returns a hash in the form of { 'tag_name' => count, ... }
251
+ #
252
+ # The options are:
253
+ #
254
+ # +:raw+: If you just want to get the raw output of the SQL statement (Array of Hashes), instead of the regular tags count Hash, set this to +true+.
255
+ #
256
+ # +:conditions+: any additional conditions that should be appended to the
257
+ # WHERE clause of the SQL. Just like in regular +ActiveRecord::Base#find+ methods.
258
+ #
259
+ # +:order+: The same as used in +ActiveRecord::Base#find+ methods. By default, this is 'count DESC'.
260
+ #
261
+ # +:count+: Adds a HAVING clause to the SQL statement, where you can set conditions for the 'count' column. For example: '> 50'
262
+ #
263
+ # +:limit+: the same as used in regular +ActiveRecord::Base#find+ methods.
264
+ def tags_count(options = {})
265
+ options = {:order => 'count DESC'}.merge(options)
266
+
267
+ o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
268
+ sql = "SELECT #{t}.#{t_pk} AS id, #{t}.name AS name, COUNT(*) AS count FROM #{jt}, #{o}, #{t} WHERE #{jt}.#{t_fk} = #{t}.#{t_pk}
269
+ AND #{jt}.#{o_fk} = #{o}.#{o_pk}"
270
+ sql << " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
271
+ sql << " GROUP BY #{t}.name"
272
+ sql << " HAVING count #{options[:count]} " if options[:count]
273
+ sql << " ORDER BY #{options[:order]} " if options[:order]
274
+ add_limit!(sql, options)
275
+ result = connection.select_all(sql)
276
+ count = result.inject({}) { |hsh, row| hsh[row['name']] = row['count'].to_i; hsh } unless options[:raw]
277
+
278
+ count || result
279
+ end
280
+
281
+ # Alias for +tags_count+
282
+ alias_method :tag_count, :tags_count
283
+
284
+ # Finds other records that share the most tags with the record passed
285
+ # as the +related+ parameter. Useful for constructing 'Related' or
286
+ # 'See Also' boxes and lists.
287
+ #
288
+ # The options are:
289
+ #
290
+ # +:limit+: defaults to 5, which means the method will return the top 5 records
291
+ # that share the greatest number of tags with the passed one.
292
+ def find_related_tagged(related, options = {})
293
+ related_id = related.is_a?(self) ? related.id : related
294
+ options = { :limit => 5 }.merge(options)
295
+
296
+ o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
297
+ sql = "SELECT o.*, COUNT(jt2.#{o_fk}) AS count FROM #{o} o, #{jt} jt, #{t} t, #{jt} jt2
298
+ WHERE jt.#{o_fk}=#{related_id} AND t.#{t_pk} = jt.#{t_fk}
299
+ AND jt2.#{o_fk} != jt.#{o_fk}
300
+ AND jt2.#{t_fk}=jt.#{t_fk} AND o.#{o_pk} = jt2.#{o_fk}
301
+ GROUP BY jt2.#{o_fk} ORDER BY count DESC"
302
+ add_limit!(sql, options)
303
+
304
+ find_by_sql(sql)
305
+ end
306
+
307
+ # Finds other tags that are related to the tags passed thru the +tags+
308
+ # parameter, by finding common records that share similar sets of tags.
309
+ # Useful for constructing 'Related tags' lists.
310
+ #
311
+ # The options are:
312
+ #
313
+ # +:separator+ => defines the separator (String or Regex) used to split
314
+ # the tags parameter and defaults to ' ' (space and line breaks).
315
+ #
316
+ # +:raw+: If you just want to get the raw output of the SQL statement (Array of Hashes), instead of the regular tags count Hash, set this to +true+.
317
+ #
318
+ # +:limit+: the same as used in regular +ActiveRecord::Base#find+ methods.
319
+ def find_related_tags(tags, options = {})
320
+ tag_names = ActiveRecord::Acts::Taggable.split_tag_names(tags, options[:separator])
321
+ o, o_pk, o_fk, t, t_pk, t_fk, jt = set_locals_for_sql
322
+
323
+ sql = "SELECT jt.#{o_fk} AS o_id FROM #{jt} jt, #{t} t
324
+ WHERE jt.#{t_fk} = t.#{t_pk}
325
+ AND (t.name IN ('#{tag_names.uniq.join("', '")}'))
326
+ GROUP BY jt.#{o_fk}
327
+ HAVING COUNT(jt.#{o_fk})=#{tag_names.length}"
328
+
329
+ o_ids = connection.select_all(sql).map { |row| row['o_id'] }
330
+ return options[:raw] ? [] : {} if o_ids.length < 1
331
+
332
+ sql = "SELECT t.#{t_pk} AS id, t.name AS name, COUNT(jt.#{o_fk}) AS count FROM #{jt} jt, #{t} t
333
+ WHERE jt.#{o_fk} IN (#{o_ids.join(",")})
334
+ AND t.#{t_pk} = jt.#{t_fk}
335
+ GROUP BY jt.#{t_fk}
336
+ ORDER BY count DESC"
337
+ add_limit!(sql, options)
338
+
339
+ result = connection.select_all(sql).delete_if { |row| tag_names.include?(row['name']) }
340
+ count = result.inject({}) { |hsh, row| hsh[row['name']] = row['count'].to_i; hsh } unless options[:raw]
341
+
342
+ count || result
343
+ end
344
+
345
+ private
346
+ def set_locals_for_sql
347
+ [ table_name, primary_key, taggable_foreign_key,
348
+ tag_model.table_name, tag_model.primary_key, tag_foreign_key,
349
+ tags_join_model ? tags_join_model.table_name : tags_join_table ]
350
+ end
351
+
352
+ end
353
+
354
+ module InstanceMethods
355
+
356
+ # This method applies tags to the target object, by parsing the tags parameter
357
+ # into Tag object instances and adding them to the tag collection of the object.
358
+ # If the tag name already exists in the tags table, it just adds a relationship
359
+ # to the existing tag record. If it doesn't exist, it then creates a new
360
+ # Tag record for it.
361
+ #
362
+ # The +tags+ parameter can be a +String+, +Array+ or a +Proc+ object.
363
+ # If it's a +String+, it's splitted using the +:separator+ specified in
364
+ # the +options+ hash. If it's an +Array+ it is flattened and compacted.
365
+ # Duplicate entries will be removed as well. Tag names are also stripped
366
+ # of trailing and leading whitespaces. If a Proc is passed,
367
+ # the proc should split the string in any way it wants and return an array of strings.
368
+ #
369
+ # The +options+ hash has the following parameters:
370
+ #
371
+ # +:separator+ => defines the separator (String or Regex) used to split
372
+ # the tags parameter and defaults to ' ' (space and line breaks).
373
+ #
374
+ # +:clear+ => defines whether the existing tag collection will be cleared before
375
+ # applying the new +tags+ passed. Defaults to +false+.
376
+ def tag(tags, options = {})
377
+
378
+ options = { :separator => ' ', :clear => false }.merge(options)
379
+ attributes = options[:attributes] || {}
380
+
381
+ # parse the tags parameter
382
+ tag_names = ActiveRecord::Acts::Taggable.split_tag_names(tags, options[:separator])
383
+
384
+ # clear the collection if appropriate
385
+ tag_collection.clear if options[:clear]
386
+
387
+ # append the tag names to the collection
388
+ tag_names.each do |name|
389
+ # ensure that tag names don't get duplicated
390
+ tag_record = tag_model.find_by_name(name) || tag_model.new(:name => name)
391
+ if tags_join_model
392
+ tag_join_record = tags_join_model.new(attributes)
393
+ tag_join_record.tag = tag_record
394
+ tag_join_record.tagged = self
395
+ tag_collection << tag_join_record unless tagged_with?(name)
396
+ else
397
+ tag_collection.push_with_attributes(tag_record, attributes) unless tagged_with?(name)
398
+ end
399
+ end
400
+
401
+ end
402
+
403
+ # Clears the current tags collection and sets the tag names for this object.
404
+ # Equivalent of calling #tag(..., :clear => true)
405
+ #
406
+ # Another way of appending tags to a existing tags collection is by using
407
+ # the +<<+ or +concat+ method on +tag_names+, which is equivalent of calling
408
+ # #tag(..., :clear => false).
409
+ def tag_names=(tags, options = {})
410
+ tag(tags, options.merge(:clear => true))
411
+ end
412
+
413
+ # Returns an array of strings containing the tags applied to this object.
414
+ # If +reload+ is +true+, the tags collection is reloaded.
415
+ def tag_names(reload = false)
416
+ ary = tag_collection(reload).map { |tag| tag.name }
417
+ ary.extend(TagNamesMixin)
418
+ ary.set_tag_container(self)
419
+ ary
420
+ end
421
+
422
+ # Checks to see if this object has been tagged with +tag_name+.
423
+ # If +reload+ is true, reloads the tag collection before doing the check.
424
+ def tagged_with?(tag_name, reload = false)
425
+ tag_names(reload).include?(tag_name)
426
+ end
427
+
428
+ # Calls +find_related_tagged+ passing +self+ as the +related+ parameter.
429
+ def tagged_related(options = {})
430
+ self.class.find_related_tagged(self.id, options)
431
+ end
432
+
433
+ private
434
+ def tag_model
435
+ self.class.tag_model
436
+ end
437
+
438
+ def tag_collection(reload = false)
439
+ send(self.class.tag_collection_name, reload)
440
+ end
441
+
442
+ def tags_join_model
443
+ self.class.tags_join_model
444
+ end
445
+
446
+ end
447
+
448
+ module TagNamesMixin #:nodoc:
449
+
450
+ def set_tag_container(tag_container)
451
+ @tag_container = tag_container
452
+ end
453
+
454
+ def <<(tags, options = {})
455
+ @tag_container.tag(tags, options.merge(:clear => false))
456
+ end
457
+
458
+ alias_method :concat, :<<
459
+ end
460
+
461
+ end
462
+ end
463
+ end
464
+
465
+ ActiveRecord::Base.class_eval do
466
+ include ActiveRecord::Acts::Taggable
467
+ end