Ruby on Rails
ActsAsTaggablePluginHowto



sorry to top-post, but it should be known that someone named josh peek has a repository of this along w/ a broken migration file, and an MIT license under his name (why is he claiming copyright, exactly? does one get a copyright for tweaking DHH code?). if you get his copy it won’t work. his tests also have ‘require’ files on his machine’s path, e.g.

require ’/Users/josh/Source/rails/activerecord/lib/active_record’

that’s going to be a difficult task for the interpreter, eh?

i thought maybe it’s edge rails (i’m only 1.2.3)?

another e.g.: create_table :taggings do |t| t.integer :tag_id, :taggable_id t.string :taggable_type end

is this the future or a huge mistake?

This plugin worked great for me.
I recommend ignoring the warnings below and going straight to the howto – its very easy.

Before you read any of the info below:
I spent two days fooling around with the acts_as_taggable plugin, applying all of the modifications below and it still didn’t work very well. After searching around I found acts_as_taggable_on_steroids which is a complete rewrite, or version 2 of this plugin and works great. I highly recommend it if you want to save yourself from a major headache! Look in the README and Tests for instructions and usage examples.

Is someone actively maintaining this plugin? Why aren’t the cool hacks shown below already part of the plugin? There is also a great tag_cloud method described here

(another post on this plugin can be found at rails.co.za )

(Some french stuff on acts_as_taggable_on_steroids plugin can be read at this address : Nuage de tag avec RubyOnRails)

The Instructions
First, this plugin is different from the acts_as_taggable gem, foremost in that it uses features that are not in Rails 1.0 (like has_many :through and :as). The plugin lets you use tags across models, unlike the gem, and is preferable in most cases. So either upgrade to Rails 1.1, or if you can handle being on the bleeding edge, install the “edge version of rails”: EdgeRails. To switch to EdgeRails, in your application’s main directory do:

rake rails:freeze:edge

To undo this later, just type:

rake rails:unfreeze

Next, install the acts_as_taggable plugin:

script/plugin install acts_as_taggable

Now create a migration to add two tables to store the tagging information:

script/generate migration add_tag_support

Insert the following:

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

    # Index your tags/taggings
    add_index :tags, :name
    add_index :taggings, [:tag_id, :taggable_id, :taggable_type]
  end

  def self.down
    drop_table :taggings
    drop_table :tags
  end
end


Run Rake to add the tables to the database.

rake db:migrate


An example:

First create a model that you want to tag. In this example books. So we generate a database table books and generate the model:

script/generate model Book

Next we add the acts_as_taggable line to the modelfile (book.rb):


 class Book < ActiveRecord::Base
  acts_as_taggable
 end

Finally we should have created the tags and taggings tables so we can start tagging:


 mybook = Book.new
 mybook.tag_with('red library book')
 mybook.save
 mybook.tag_list #gives back 'red library book'
 Book.find_tagged_with('library') #gives your Book

This generates three tags with the names ‘red’, ‘library’ and ‘book’ in the tag table. Next it creates rows in the taggings table with the taggable_type set to ‘Book’ and the tag_id set to the corresponding tag in the tag table. The taggable_id is set to the id of the book in your books table.

Nice huh?

ALSO: The View (forms)

<%= text_field_tag 'tag_list', @item.tags.collect{|t| t.name}.join(" ") %>

if you are using the alias below, you can use this in the view instead and then you don’t have to do the controller-fu below.

<%= text_field 'item', 'tag_list' %>

AND The Controller (if using the default generated scaffolding).
For example in say books_controller.rb add the following to your ‘update’ and ‘create’ methods:

@book.tag_with(params[:tag_list])

- David Andrew Thompson

Question:
If tag_with was named tag_list= it would just work in most controllers/views with normal @model.update(params[:model]) calls, instead of having to do it manually like above.

Is there a reason why its not called tag_list=, or that the method isn’t aliased for this purpose?

If anyone wants to do this, just add:

alias tag_list= tag_with

under the tag_with method in acts_as_taggable.rb

-Nate K.

AND The Fixtures. tags.yml looks like this:


big:
  id: 1
  name: Big

small:
  id: 2
  name: Small

taggings.yml looks like this:


big_elephant:
  id: 1
  tag_id: 1
  taggable_id: 50 # Elephant is an Animal record with id 50
  taggable_type: Animal

small_beer:
  id: 1
  tag_id: 1
  taggable_id: 20 # Beer is a Beverage record with id 20
  taggable_type: Beverage

Note:
This post has some useful information about how the code in /vendor/plugins/acts_as_taggable/lib works.

Note:
I was having issues with the tag.collect putting information in single quotes and on an update that information begin submitted to the database as ”’test” “series’” instead of “test series” so I corrected line 52:


 tags.collect { |tag| tag.name.include?(" ") ? "'#{tag.name}'" : tag.name }.join(" ")

so that the tags will be in double quotes and insert into the database correctly:


 tags.collect { |tag| tag.name.include?(" ") ? '"' +"#{tag.name}" + '"' : tag.name }.join(" ")

Note:
I needed the tags_count method, so I put “my own” in module SingletonMethods (in acts_as_taggable.rb), like this:


 def tags_count(options)
   sql = "SELECT  tags.id AS id, tags.name AS name, COUNT(*) AS count FROM tags, taggings, #{table_name} " 
   sql << "WHERE taggings.taggable_id = #{table_name}.#{primary_key} AND taggings.tag_id = tags.id " 
   sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions]
   sql << "GROUP BY tags.name " 
   sql << "HAVING count #{options[:count]} " if options[:count]
   sql << "ORDER BY #{options[:order]} " if options[:order]
   sql << "LIMIT #{options[:limit]} " if options[:limit]
   find_by_sql(sql)
end


For instance:

@tags = Post.tags_count :conditions => "posts.posttype = 'public'", :order => 'count DESC'

Note:
The tag_with is destructive, it deletes all previous tags which is not always what you want on an edit. I added the following method: EO


  def add_tags(list)          
    Tag.transaction do
      Tag.parse(list).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      

This should be added to acts_as_taggable.rb Further note: After much confusion, it was finally noted that add_tags was a deprecated association method (?!). Thus, no edits to it were working… Change the name of the above to add_tag, add_tag_list, or something similar, and all will be well.

another addition to the above:
In order to prevent multiple additions to taggable on the same tag change this:

 
  Tag.find_or_create_by_name(name).on(self)

To

 t = Tag.find_or_create_by_name(name)
 t.on(self) unless self.tags.include? t

Note:
ClassMethod for filtering tags by the Class of the item being tagged. This lets you filter tag to those available for a certain class. Used to assign roles to a users in a system. It is an ugly hack as it should be an instance method that uses self.class to find out the jind od the class it is after.


def class_tagged(class_type)
  @taggings = Tagging.find_all_by_taggable_type(class_type)
  @tags = Hash.new
  @taggings.each do |tagging|
    @tags[tagging.tag.name]= tagging.tag
  end
  return @tags.values
end

... response to above?
The above can be accomplished with a new ClassMethod. The following allows you to retrieve all unique tags for a given Class (as an array). I am guessing you would like to have this capability if you had multiple models that were acts_as_taggable, and wanted to list the tags for only a certain model. At the time of writing this my application only has one model, so I cannot test with multiple models. —markG


--- acts_as_taggable.rb ---
--- (in module ClassMethods) ---

def tag_list
  @taggings = Tagging.find_all_by_taggable_type(ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s)
  return @taggings.collect { |tagging| tagging.tag.name }.uniq
end

--- usage ---
Mymodel.tag_list

A Better Solution to Get Tags by Model?
Rather than implementing a whole new method, a line added to the existing self.tag method in tag.rb allows you to retrieve tags by model name.

By adding the following line between lines 4 and 5:


query << " AND taggings.taggable_type = #{options[:taggable_type]}'" if options[:taggable_type] != nil

Our method will now look like this:


  def self.tags(options = {})
    query = "select tags.id, name, count(*) as count" 
    query << " from taggings, tags" 
    query << " where tags.id = tag_id" 
    query << " AND taggings.taggable_type = #{options[:taggable_type]}'" if options[:taggable_type] != nil
    query << " group by tag_id" 
    query << " order by #{options[:order]}" if options[:order] != nil
    query << " limit #{options[:limit]}" if options[:limit] != nil
    tags = Tag.find_by_sql(query)
  end

We can call this as so:

@tags = Tag.tags(:order => "name", :taggable_type => "post")

I’ve fully documented this in my article, Modifiy Acts_As_Taggable to Return Tags By Model, including both this version and a version that follows Rails conventions more closely.

Note
User-specific acts_as_taggable functionality

Note
I wanted to allow multiple word tags (like “New York Yankees”). This little modification in tags.rb lets you send in an array of anything (preferably strings) to use as your tags, instead of breaking up a string into single-word tags.


  def self.parse(list)
    unless list.kind_of? Array
      tag_names = []

      # first, pull out the quoted tags
      list.gsub!(/"(.*?)"s*/ ) { tag_names << $1; "" }

      # then, replace all commas with a space
      list.gsub!(/,/, " ")

      # then, get whatever's left
      tag_names.concat list.split(/s/)

      # strip whitespace from the names
      tag_names = tag_names.map { |t| t.strip }

      # delete any blank tag names
      tag_names = tag_names.delete_if { |t| t.empty? }

      return tag_names
    else
      tag_names = list.collect {|tag| tag.nil? ? nil : tag.to_s}
      return tag_names.compact
    end
  end

find_tagged_with

On ALL tags, instead of ANY

The default method find_tagged_with will search for any of the tags, and doesn’t return unique results.

This method will do a strict tag search and only return unique results. (works in mysql… does it work in other db engines?)


   def find_tagged_with!(list)
    find_by_sql([
      "SELECT #{table_name}.* FROM #{table_name} " +
      "WHERE #{table_name}.#{primary_key} in (" +
      "  SELECT taggable_id FROM taggings, tags " +
      "    WHERE tags.id=taggings.tag_id " +
      "     AND taggings.taggable_type = ? " +
      "     AND tags.name IN (?) " + 
      "    GROUP BY taggable_id " + 
      "    HAVING count(tags.id) >= ? " + 
      "  )",
      acts_as_taggable_options[:taggable_type], list, list.to_a.length
    ])
  end

find_tagged_with with duplicate tags gotcha

The above method find_tagged_with! will behave strangely when duplicate tags are introduced (acts_as_taggable doesn’t check/remove for dupes by default and they’re easy to get in a large list if users are entering tags). Changing the last line of Tag.parse like this solves the issue:


-    return tag_names
+    return tag_names.uniq

find_tagged_with on ALL tags, unique results, sorted by relevance


        def find_tagged_with(list)
          find_by_sql([
            "SELECT #{table_name}.*, COUNT(tags.id) AS count 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 (?)" +
                        "GROUP BY posts.id ORDER BY count DESC",
            acts_as_taggable_options[:taggable_type], list
          ])
        end

Acts_as_taggable – Pagination

The following opens the pagination class back up adds the abilty to filter based on a single tag. Pass a :tag => “tag” and the pagination with be base on though s items that are tagged with it. Belongs in acts_as_taggable.rb. EO


module ActionController
   module Pagination
       MY_OPTIONS = {
         :tag        => 'All'
       }
     DEFAULT_OPTIONS.merge!(MY_OPTIONS) {|key, old, new| old} 
     alias old_find_collection_for_pagination find_collection_for_pagination
     def find_collection_for_pagination(model, options, paginator)
       models = old_find_collection_for_pagination(model, options, paginator)
       if options[:tag]=="All" 
         models
       else
         models.delete_if {|m| !m.is_tagged_with?(options[:tag])}
       end
     end
   end
end

Note
I tried the acts as taggable pagination fix as described above and it didn’t work for me. However, I did solve my problem with the following code when paginating through a list of tags:

def show_all_tags
    per_page = 12
    if params[:id] then
      @tag_title = params[:id]
      @tags = params[:id].split.map {|o| CGI::unescape(o)}
      @members = Member.find_tagged_with(@tags)
      @members_pages, @members = paginate(Member, {:conditions => ['tags.name = ?', @tag_title], :include => [:tags], :per_page => per_page})
    else
      redirect_to :action => 'list'
    end
  end

-Dave Hoefler

Dave, it probably didn’t work because you also need to add an is_tagged_with? method under the InstanceMethods of acts_as_taggable.rb like this:


      module InstanceMethods
        ... 

        def is_tagged_with?(tag)
          tags.include?(tag)
        end
      end


-JB

Yet Another Pagination Solution for acts_as_taggable

This uses the more powerful AR find method. Replace the find_tagged_with method on the act_as_taggable.rb with:


def find_tagged_with(list, options = {})
  find(:all,
  :from => "#{table_name}, tags, taggings", 
  :conditions => ["#{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], 
  :offset => options[:offset], 
  :limit => options[:limit], 
  :order => options[:order])
end

Enjoy,
Sergio Bayona

And another method to paginate
This uses will_paginate and the Paginator gem, described here

Question:
Does anyone have a method for tag clouds with the plugin?

-Doug M.

Answer:

Here ’s a tag cloud that will work with the plugin.

Tom Fakes has an article on creating tag clouds for the acts_as_taggable gem, NOT the plugin.

I have an article with another way to do tag clouds
blog.wolfman.com

General Notes on Usage

When to Save

Do not try to apply tags to a Model during save. You will end up with a “stack too deep” error. You’re best to apply them :after_save

If you are seeing entries in the tagging table which are being added but the taggable_id is not filled in, it is because you are trying to add the tags too early. Follow the above advice and call tag_with only after the object has been saved.

Question:
I have overidden both acts_as_taggable.rb and tag.rb with slightly different versions in my application. When I make a call to acts_as_taggable from my web app all is peachy. However when I make the same call using script/runner it uses my acts_as_taggable but the original tag.rb. Any ideas? Is there a hard-coded reference to the original tag.rb

Adding count cache field

If you want to keep track and easily display the number of objects the tag is refering to, all you need to do is add the automagic field taggings_count to your tag table and enable cache in the belongs_to field. After that you can get the number of references by querying tag.taggings_count. For full writeup, go here: Counter for acts as taggable

Question:
Post has_many :comments
Comment belongs_to :Post, :counter_cache=>true acts_as_taggable #alos enable counter cache

I find comments_count cannot work properly after added acts_as_taggable plugin
who can tell me why?

Question:
How do you delete a tag?

Question:
How do you validate that a tag was entered when adding from a form?

A. Add the following to tag.rb: validates_presence_of :name

Question:
I tried the plugin. I’m sure a add the line acts_as_taggable in the model I would like to tag. But I got a method missing error when I’m testing tag_with(). Any idea what I missed? THX

A. I fixed it with a server reload.

Question:
How do you use this pluging with STI and record the :taggable_type as the subclass, not the base class? Example….

class Foo < ActiveRecord::Base
end

class SubFoo < Foo acts_as_taggable
end

In script/console,

c = SubFoo.create(.....)
c.tag_with(“mysubfoo stuff tag”)

Then in the database I execute the query

“select distinct taggable_type from taggings;
mysql output → Foo

Thanks in advance!

Question:
Is there a way to have multiple groups of tags on the same model. Say I want a set of tags that relates to how the model would be displayed in the layout and another that would be used for related links and be actually shown to the user. If I just group them all together I’d end up having a lot of very cryptic tags showing up in my list to the user. I’ve looked and cannot find anything about this.

-Christopher G.