acts_as_taggable 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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