module ActiveRecord::Acts::Taggable::ClassMethods
This mixin provides an easy way for adding tagging capabilities (also known as folksnomy) to your active record objects. It allows you to add tags to your objects as well as search for tagged objects.
It assumes you are using a fully-normalized tagging database schema. For
that, you need a table (by default, named tags
) to hold all
tags in your application and this table must have a primary key (normally
a id
int autonumber column) and a name
varchar
column. You must also define a model class related to this table (by
default, named Tag
).
All tag names will be stored in this tags table. Taggable objects should reside in their own
tables, like any other object. Tagging objects is performed by the
acts_as_taggable
mixin using a
has_and_belong_to_many
relationship that is automatically
created on the taggable class, and as so, a join table must exist between
the tags table and the taggable object table.
The name of the join table follows the standards for rails
Unless the join table is explicitly specified as an option, it is guessed using the lexical order of the class names.
The join table must be composed of the foreign keys from the tags table and
the taggable object table, so for instance, if we have a tags table named
tags
(related to a Tag
model) and a taggable
photos
table (related to a Photo
model), there
should be a join table tags_photos
with int FK columns
photo_id
and tag_id
. If you dont use a explicit
full model related to the join table (through the
:join_class_name
option), you must not add a primary key to
the join table.
The acts_as_taggable
adds the instance methods
tag
, tag_names
, +tag_names= +,
+tag_names<< +, +tagged_with? + for adding tags to the object and
also the class method find_tagged_with
method for search
tagged objects.
Examples:
class Photo < ActiveRecord::Base # this creates a 'tags' collection, through a has_and_belongs_to_many # relationship that utilizes the join table 'photos_tags'. acts_as_taggable :normalizer => Proc.new {|name| name.downcase} end photo = Photo.new # splits and adds to the tags collection photo.tag "wine beer alcohol" # don't need to split since it's an array, but replaces the tags collection # trailing and leading spaces are properly removed photo.tag [ 'wine ', ' vodka'], :clear => true photo.tag_names # => [ 'wine', 'vodka' ] # You can remove tags one at a time or in a group photo.tag_remove 'wine' photo.tag_remove 'wine beer alcohol' # appends new tags with a different separator # the 'wine' tag wont be duplicated photo.tag_names << 'wine, beer, alcohol', :separator => ',' # The difference between +tag_names+ and +tags+ is that +tag_names+ # holds an array of String objects, mapped from +tags+, while +tags+ # holds the actual +has_and_belongs_to_many+ collection, and so, is # composed of +Tag+ objects. photo.tag_names.size # => 4 photo.tags.size # => 4 # Now you can clear all tags in one call photo.clear_tags! # Find photos with 'wine' OR 'whisky' Photo.find_tagged_with :any => [ 'wine', 'whisky' ] # Finds photos with 'wine' AND 'whisky' using a different separator. # This is also known as tag combos. Photo.find_tagged_with(:all => 'wine+whisky', :separator => '+' # Gets the top 10 tags for all photos Photo.tags_count :limit => 10 # => { 'beer' => 68, 'wine' => 37, 'vodka' => '22', ... } # Gets the tags count that are greater than 30 Photo.tags_count :count => '> 30' # => { 'beer' => 68, 'wine' => 37 } # Replace allows you to find_tagged_with, remove the old tags and add the new ones Photo.replace_tag("beer whisky","wine vodka") # Display the photos returned from the tags_count call using 9 different CSS classes <% Photo.cloud(@photo_tags, %w(cloud1 cloud2 cloud3 cloud4 cloud5 cloud6 cloud7 cloud8 cloud9)) do |tag, cloud_class| %> <%= link_to(h("<#{tag}>"), tag_photos_url(:name => tag), { :class => cloud_class } ) -%> <% end %> # Display the photos returned from the tags_count call using 5 different font sizes <% Photo.cloud(@photo_tags, %w(x-small small medium large x-large)) do |tag, font_size| %> <%= link_to(h("<#{tag}>"), tag_photos_url(:name => tag), { style: => "font-size: #{font_size}" } ) -%> <% end %>
You can also use full join models if you want to take advantage of
ActiveRecords callbacks, timestamping, inheritance and other features on
the join records as well. For that, you use the
:join_class_name
option. In this case, the join table must
have a primary key.
class Person # This defines a class +TagPerson+ automagically. acts_as_taggable :join_class_name => 'TagPerson' end # We can open the +TagPerson+ class and add features to it. class TagPerson acts_as_list :scope => :person belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_id' before_save :do_some_validation after_save :do_some_stats end # We can do some interesting things with it now person = Person.new person.tag "wine beer alcohol", :attributes => { :created_by_id => 1 } Person.find_tagged_with(:any => 'wine', :condition => "tags_people.created_by_id = 1 AND tags_people.position = 1")
Public Instance Methods
This method defines a has_and_belongs_to_many
relationship
between the target class and the tag model class. It also adds several
instance methods for tagging objects of the target class, as well as a
class method for searching objects that contains specific tags.
The options are:
The :collection
parameter receives a symbol defining the name
of the tag collection method and it defaults to :tags
.
The :tag_class_name
parameter receives the tag model class
name and it defaults to +'Tag'+.
The :tag_class_column_name
parameter receives the tag model
class name attribute and it defaults to +'name'+.
The +:normalizer + paramater takes a Procs. This is used to normalize all tags Simple example
:normalizer => Proc.new {|name| name.capitalize}
The :join_class_name
parameter receives the model class name
that joins the tag model and the taggable model. This automagically defines
the join model class that can be opened and extended.
The remaining options are passed on to the
has_and_belongs_to_many
declaration. The
:join_table
parameter is defined by default using the standard
has_and_belongs_to_many
behavior.
# File lib/taggable.rb, line 165 def acts_as_taggable(options = {}) options = { :collection => :tags, :tag_class_name => 'Tag', :tag_class_column_name => 'name', :normalizer=> Proc.new {|name| name}}.merge(options) collection_name = options[:collection] tag_model = options[:tag_class_name].constantize tag_model_name = options[:tag_class_column_name] normalizer = options[:normalizer] if tag_model.table_name < self.table_name default_join_table = "#{tag_model.table_name}_#{self.table_name}" else default_join_table = "#{self.table_name}_#{tag_model.table_name}" end options[:join_table] ||= default_join_table options[:foreign_key] ||= self.name.to_s.foreign_key options[:association_foreign_key] ||= tag_model.to_s.foreign_key # not using a simple has_and_belongs_to_many but a full model # for joining the tags table and the taggable object table if join_class_name = options[:join_class_name] Object.class_eval "class #{join_class_name} < ActiveRecord::Base; set_table_name '#{options[:join_table]}' end" unless Object.const_defined?(join_class_name) join_model = join_class_name.constantize tagged = self join_model.class_eval do belongs_to :tag, :class_name => tag_model.to_s belongs_to :tagged, :class_name => tagged.name.to_s define_method(:normalizer, normalizer) define_method(tag_model_name.to_sym) { self[tag_model_name] ||= normalizer(tag.send(tag_model_name.to_sym)) } end options[:class_name] ||= join_model.to_s tag_pk, tag_fk = tag_model.primary_key, options[:association_foreign_key] t, tn, jt = tag_model.table_name, tag_model_name, join_model.table_name options[:finder_sql] ||= "SELECT #{jt}.*, #{t}.#{tn} AS #{tn} FROM #{jt}, #{t} WHERE #{jt}.#{tag_fk} = #{t}.#{tag_pk} AND #{jt}.#{options[:foreign_key]} = \#{quoted_id}" else join_model = nil end # set some class-wide attributes needed in class and instance methods write_inheritable_attribute(:tag_foreign_key, options[:association_foreign_key]) write_inheritable_attribute(:taggable_foreign_key, options[:foreign_key]) write_inheritable_attribute(:normalizer, normalizer) write_inheritable_attribute(:tag_collection_name, collection_name) write_inheritable_attribute(:tag_model, tag_model) write_inheritable_attribute(:tag_model_name, tag_model_name) write_inheritable_attribute(:tags_join_model, join_model) write_inheritable_attribute(:tags_join_table, options[:join_table]) write_inheritable_attribute(:tag_options, options) [ :collection, :tag_class_name, :tag_class_column_name, :join_class_name,:normalizer].each { |key| options.delete(key) } # remove these, we don't need it anymore [ :join_table, :association_foreign_key ].each { |key| options.delete(key) } if join_model # dont need this for has_many # now, finally add the proper relationships class_eval do include ActiveRecord::Acts::Taggable::InstanceMethods extend ActiveRecord::Acts::Taggable::SingletonMethods class_inheritable_reader :tag_collection_name, :tag_model, :tag_model_name, :tags_join_model, :tags_options, :tags_join_table, :tag_foreign_key, :taggable_foreign_key,:normalizer if join_model has_many collection_name, options else has_and_belongs_to_many collection_name, options end end end