Recipe 14.5. Simplifying Folksonomy with the acts_as_taggable


Problem

You want to make it easier to assign tags to your content and then to search for records by their tags. You may also have more than one model in your application that you want to associate with tags.

Solution

Install and modify the acts_as_taggable plug-in, especially if you have more than one model that needs tagging. The plug-in ships with a broken instance method definition, but it can easily be modified to work as advertised. Start by downloading and installing the plug-in into your application:

$ ruby script/plugin install acts_as_taggable             

The tag_list instance method needs to be defined as follows for it to work correctly. The tag_with method has also been customized to behave more naturally when assigning tags to objects.

vendor/plugins/acts_as_taggable/lib/acts_as_taggable.rb:

module ActiveRecord   module Acts #:nodoc:     module Taggable #:nodoc:       def self.included(base)         base.extend(ClassMethods)         end              module ClassMethods         def acts_as_taggable(options = {})           write_inheritable_attribute(:acts_as_taggable_options, {             :taggable_type => ActiveRecord::Base.\                     send(:class_name_of_active_record_descendant, self).to_s,             :from => options[:from]           })                      class_inheritable_reader :acts_as_taggable_options           has_many :taggings, :as => :taggable, :dependent => true           has_many :tags, :through => :taggings           include ActiveRecord::Acts::Taggable::InstanceMethods           extend ActiveRecord::Acts::Taggable::SingletonMethods                   end       end              module SingletonMethods         def find_tagged_with(list)           find_by_sql([             "SELECT #{table_name}.* FROM #{table_name}, tags, taggings " +             "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " +             "AND taggings.taggable_type = ? " +             "AND taggings.tag_id = tags.id AND tags.name IN (?)",             acts_as_taggable_options[:taggable_type], list           ])         end       end              module InstanceMethods         def tag_with(list)           Tag.transaction do             curr_tags = self.tag_list             taggings.destroy_all             uniq_tags = (list + ' ' + curr_tags).split(/\s+/).uniq.join(" ")             Tag.parse(uniq_tags).sort.each do |name|               if acts_as_taggable_options[:from]                 send(acts_as_taggable_options[:from]).tags.\                                 find_or_create_by_name(name).on(self)               else                 Tag.find_or_create_by_name(name).on(self)               end             end           end         end         def tag_list           self.reload           tags.collect do |tag|             tag.name.include?(" ") ? "'#{tag.name}'" : tag.name           end.join(" ")         end       end     end   end end

Your application contains articles and announcements. You want the ability to tag objects from both models. Start by creating a migration to build these tables:

db/migrate/001_add_articles_add_announcements.rb:

class AddArticles < ActiveRecord::Migration   def self.up     create_table :articles do |t|       t.column :title, :text       t.column :body, :text       t.column :created_on, :date       t.column :updated_on, :date     end     create_table :announcements do |t|       t.column :body, :text       t.column :created_on, :date       t.column :updated_on, :date     end   end   def self.down     drop_table :articles     drop_table :announcements   end end

Next, generate a migration to set up the necessary tags and taggings tables, as required by the plug-in.

db/migrate/002_add_tag_support.rb:

class AddTagSupport < ActiveRecord::Migration   def self.up     # Table for your Tags     create_table :tags do |t|       t.column :name, :string     end     create_table :taggings do |t|       t.column :tag_id, :integer       # id of tagged object       t.column :taggable_id, :integer       # type of object tagged       t.column :taggable_type, :string     end   end   def self.down     drop_table :tags     drop_table :taggings   end end

Finally, in article.rb and announcement.rb, declare both the Article and Announcement models as taggable:

app/models/article.rb:

class Article < ActiveRecord::Base   acts_as_taggable end

app/models/announcement.rb:

class Announcement < ActiveRecord::Base   acts_as_taggable end

You can now use the tag_with method provided by the plug-in to associate tags with both Article and Announcement objects. You can view the assigned tags of an object with the tag_list method.

Once you have some content associated with tags, you can use those tags to help users search for relevant content. Use find_tagged_with to find all articles tagged with "indispensable", for example:

Article.find_tagged_with("indispensable")

This returns an array of objects associated with that tag. There's no method to find all object types by tag name but there's no reason you couldn't add such a method to the Tag class.

Discussion

To demonstrate how to use this plug-in, create some fixtures, and load them into your database with rake db:fixtures:load:

test/fixtures/articles.yml:

first:   id: 1   title: Vim 7.0 Released!   body: Vim 7 adds native spell checking, tabs and the app... another:   id: 2   title: Foo Camp   body: The bar at Foo Camp is appropriately named Foo Bar... third:   id: 3   title: Web 4.0    body: Time to refactor...

test/fixtures/announcements.yml:

first:   id: 1   body: Classes will start in November. second:   id: 2   body: There will be a concert at noon in the quad.

Now, open a Rails console session and instantiate an Article object. Assign a few tags with tag_with, then list them with tag_list. Next, add an additional tag with tag_with. Now, tag_list shows all four tags. This behaviorappending new tags to the listis the result of our modified version of tag_with. The unmodified version removes existing tags whenever you add new ones.

$ ./script/console  Loading development environment. >> article = Article.find(1) => #<Article:0x25909f4 @attributes={"created_on"=>nil,  "body"=>"Vim 7 adds native spell checking, tabs and the app...",  "title"=>"Vim 7.0 Released!", "updated_on"=>nil, "id"=>"1"}> >> article.tag_with('editor bram uganda')    => ["bram", "editor", "uganda"] >> article.tag_list => "bram editor uganda" >> article.tag_with('productivity') => ["bram", "editor", "productivity", "uganda"] >> article.tag_list => "bram editor uganda productivity"

Now create an Announcement object, and assign it a couple of tags:

>> announcement = Announcement.find(1) => #<Announcement:0x25054a8 @attributes={"created_on"=>nil,  "body"=>"Classes will start in November.", "updated_on"=>nil, "id"=>"1"}> >> announcement.tag_with('important schedule') => ["important", "schedule"] >> announcement.tag_list => "important schedule"

The plug-in allows you to assign tags to any number of models as long as they are declared as taggable (as in the solution with acts_as_taggable in the model class definitions). This is due to a polymorphic association with the taggable interface as set up by the following lines of the acts_as_taggable class method in acts_as_taggable.rb:

def acts_as_taggable(options = {})   write_inheritable_attribute(:acts_as_taggable_options, {     :taggable_type => ActiveRecord::Base.\             send(:class_name_of_active_record_descendant, self).to_s,     :from => options[:from]   })      class_inheritable_reader :acts_as_taggable_options   has_many :taggings, :as => :taggable, :dependent => true   has_many :tags, :through => :taggings   include ActiveRecord::Acts::Taggable::InstanceMethods   extend ActiveRecord::Acts::Taggable::SingletonMethods           end

...along with the corresponding association method calls in the tagging.rb and tag.rb:

class Tagging < ActiveRecord::Base   belongs_to :tag   belongs_to :taggable, :polymorphic => true   ... end

class Tag < ActiveRecord::Base   has_many :taggings   ... end

The taggings table stores all the associations between tags and objects being tagged. The taggable_id and taggable_type columns differentiate between the different object type associations. Here is the contents of this table after we've assigned tags to Article and Announcement objects:

mysql> select * from taggings; +----+--------+-------------+---------------+ | id | tag_id | taggable_id | taggable_type | +----+--------+-------------+---------------+ |  4 |      1 |           1 | Article       |  |  5 |      2 |           1 | Article       |  |  6 |      4 |           1 | Article       |  |  7 |      3 |           1 | Article       |  |  8 |      5 |           1 | Announcement  |  |  9 |      6 |           1 | Announcement  |  +----+--------+-------------+---------------+

The specific modifications made to the plug-in's default instance methods include fixing what looks to be a typo in tag_list, but also adding the call to self.reload in that method. Calling self.reload allows you to view all current tags on an object with tag_list immediately after adding more tags with tag_with. The other significant addition is to the tag_with method. The method has been altered to save all current tags, then destroy all taggings with taggings.destroy_all, and finally to create a new list of taggings that merges the existing taggings with those being added as parameters. The end result is that tag_with now has a cumulative effect when tags are added.

See Also

  • For more information on tag clouds with acts_as_taggable, see

    http://blog.craz8.com/articles/2005/10/28/acts_as_taggable-is-a-cool-piece-of-code



Rails Cookbook
Rails Cookbook (Cookbooks (OReilly))
ISBN: 0596527314
EAN: 2147483647
Year: 2007
Pages: 250
Authors: Rob Orsini

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net