acts_as_taggable per user tagging
David Heinemeier Hansson created the acts_as_taggable Ruby on Rails plugin to allow tagging of any Active Record object. This is great for tagging objects application-wide, but I required user specific tags for my Rails app. Hopefully the following outlines the exact changes required to add the same functionality to your own Rails app – post a comment if you have any problems.
Note: There is also an acts_as_taggable gem which is different than the plugin. The acts_as_taggable plugin is only available for Rails 1.1.
Install acts_as_taggable plugin
script/plugin install acts_as_taggable
Create database tables
The following are mysql specific create table definitions:
-- tags DROP TABLE IF EXISTS tags; CREATE TABLE IF NOT EXISTS tags ( id int(11) unsigned NOT NULL auto_increment, name varchar(255) NOT NULL, created_at datetime NOT NULL, updated_at datetime NOT NULL, PRIMARY KEY (id) ) TYPE=InnoDB; -- taggings DROP TABLE IF EXISTS taggings; CREATE TABLE taggings ( id int(11) unsigned NOT NULL auto_increment, tag_id int(11) unsigned NOT NULL, taggable_id int(11) unsigned NOT NULL, taggable_type varchar(255), user_id int(11) unsigned NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB;
The above taggings table differs from the normal acts_as_taggable plugin requirement by adding a user_id column to associate a tagging with an individual user. I am assuming that your Rails application already has a users table (I am using the login_engine to provide user authentication).
Update Rails objects
Add acts_as_taggable to the class you want to be able to tag
class ItemToTag < ActiveRecord::Base acts_as_taggable end
class User < ActiveRecord::Base has_many :taggings has_many :tags, :through => :taggings, :select => "DISTINCT tags.*" end
Modify acts_as_taggable Plugin
To add per-user taggings we need to modify the acts_as_taggable plugin itself. First we add belongs_to :user to associate a tagging with a user.
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
belongs_to :user
def self.tagged_class(taggable)
ActiveRecord::Base.send(:class_name_of_active_record_descendant, taggable.class).to_s
end
def self.find_taggable(tagged_class, tagged_id)
tagged_class.constantize.find(tagged_id)
end
end
Next replace the existing on method with the following in the Tag class (tag.rb file).
def on(taggable, user) tagging = taggings.create :taggable => taggable, :user => user end
Update acts_as_taggable.rb class by replacing each of the following methods with those given below:
def find_tagged_with(list, user)
find_by_sql([
"SELECT #{table_name}.* FROM #{table_name}, tags, taggings " +
"WHERE #{table_name}.#{primary_key} = taggings.taggable_id " +
"AND taggings.user_id = ? " +
"AND taggings.taggable_type = ? " +
"AND taggings.tag_id = tags.id AND tags.name IN (?)",
user.id, acts_as_taggable_options[:taggable_type], list
])
end
def tag_with(list, user)
Tag.transaction do
Tagging.destroy_all(" taggable_id = #{self.id} and taggable_type = '#{self.class}' and user_id = #{user.id}")
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, user)
else
Tag.find_or_create_by_name(name).on(self, user)
end
end
end
end
def tag_list
tags.collect { |tag| tag.name.include?(" ") ? "'#{tag.name}'" : tag.name }.join(" ")
end
Usage
item_to_tag.tag_with('ruby rails', current_user)
item_to_tag.save
item_to_tag.tag_list # => 'ruby rails'
current_user.tags # => 'ruby rails'
ItemToTag.find_tagged_with('ruby', current_user)
Note: The tag_with is destructive, it deletes all previous tags.
Update
Previously the tags collection for the User object would return ALL taggings (with duplicates if multiple objects had been tagged with the same tag). Thanks to Ian Li who kindly suggested the use of :select => "DISTINCT tags.*" for the tags has_many relationship in the User class, duplicates are now filtered at the database.
Update II
I’ve replaced the taggings.destroy_all with the following in the tag_with method. This is in response to feedback from Bryan and Anna regarding the destructive nature of the previous tag_with method. Thanks for the comments.
Tagging.destroy_all(" taggable_id = #{self.id} and taggable_type = '#{self.class}' and user_id = #{user.id}")
Since writing this post back in April 2006 I’ve started a new project with slightly different tagging requirements, I’ll create a new article when I’m done and update the content here.
References
Pretty much all of this information came from the Ruby on Rails wiki and Rails Weenie so props to them.
Trackbacks
Use the following link to trackback from your own site:
http://www.slashdotdash.net/articles/trackback/12


So that you don’t have to use .uniq to remove duplicates, e.g.
Change User to:
I tried your tutorial but never was able to create a working app;
I always end up with the error that say wrong number of arguments either wrong number of arguments (2 for 1) or wrong number of arguments (1 for 2)
I think you’d better add the “DISTINCT” keyword in the find_by_sql statement in the find_tagged_with function.
Hi, I notice that the destory is deleting all taggings for a given taggable_id.
With the user, this is not the behavior you want. When a given user edits a tag, the destructive behavior should delete existing tags based on taggable_id and user_id. Could you please fix this?
Thanks.
Thanks, this is exactly what I was looking for!
Hi, I want to add a function in Tag.rb, how can i do this without changing original tag.rb(in plugin)
has_many :through now supports :uniq
check out this article. http://blog.hasmanythrough.com/articles/category/associations
To avoid destroying everything automatically, you can change the tag_with method in act_as_taggable.rb.
instead of: taggings.destroy_all
change to: user.taggings.destroy_all
that should do the trick.
ok, sorry about the last entry, after some more thoughts, I have decided to remove that .destroy_all from act_as_taggable.rb altogether.
The problem, as I see it, should be fixed directly in tag.rb, at the on method. Before we add any taggings, there should be a check first to make sure that there are no duplicates.
So in tag.rb, at the on method, replace it with:
taggings.find_or_create_by_tag_id_and_taggable_id_and_taggable_type_and_person_id(id, taggable.id, taggable.class.to_s, person.id)
This is totally UGLY, but works for now, until some Ruby guru can show this noob a more elegant way.
People were telling me that this doesn’t work. ok, well, the following should, since I have tested it and I swear :) its fine:
change in the infamous destroy_all to:
Tagging.destroy_all(” taggable_id = #{self.id} and user_id = #{user.id} “)
Love, Anna.
Thanks for the comments Anna, I’ll have a quick look at the changes and update the post appropriately.
Ben
Ben, thanks for the post! I was about to try to do something similar, but I’m glad you already did the hard part for me..
Anna, I think in the destroy_all method you need to add the taggable_type as well, otherwise if a user tags objects of two or more models that happen to have the same id, the destroy_all method will destroy the objects from the other model as well.
I’m not sure if this is the best way, but it seems to work: I also added a method to find tags on an object by a specific user (in the InstanceMethods module): (note that returns a comma-separated list..) As an example, I’m going to use the above on my site (http://urbandrinks.com) to show a logged-in user which global tags for a particular place he or she also has tagged the place with. Another small change I’ve found useful is to include two versions of the “find_tagged_with” method. Basically, I keep the original find_tagged_with method and create a second called find_tagged_with_by_user which uses Ben’s method. That way I can still select all the global tags by any user or use the new method to get the tags by a particular user.Bryan
Ben,
Thanks to Bryan for pointing that glaring omission! :) cuz, I am just taggin’ one thing, that’s why ;) BTW, nice site :)
Love, Anna.
Thanks Anna.
One other thing I’ve added is a “tag_list_for_form” method. I may not really need this, but I found that listing the tags in a form text field wouldn’t work with the current method because of the single-quotes. So I modified mine slightly:could’nt get this working on 1.2 keep getting a syntax mysql error
line 1: SELECT * FROM taggings WHERE ( taggable_id = and taggable_type = ‘TransAction’ and client_id = 1)
seems to be missing a taggable id
Phil,
You’re trying to create a tag on object that you haven’t saved, thus you have no primary key on the model.
It all works very well with Rails 1.1.6 until I recently upgraded to Rail 1.2.2.
With 1.2.2, when I submit the item with the tags, it goes through ok on the first time that it’s submitted when the server is turned on… after that it keeps giving me a “User expected, got User” error.
Does anybody else also experienced this strange error?
I get the ‘User expected, got User’ error also. Any idea why require ‘model/user’ fixes this?
If you are only tagging a single model, you can add this method to
acts_as_taggable.rband skip all of the other modifications -def find_by_user_tagged_with(user, list) find_by_sql([ "SELECT #{table_name}.* FROM #{table_name}, tags, taggings " + "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " + "AND #{table_name}.user_id = ? " + "AND taggings.taggable_type = ? " + "AND taggings.tag_id = tags.id AND tags.name IN (?)", user.id, acts_as_taggable_options[:taggable_type], list ]) endIt assumes that you are assigning a
>user_idto the model instances that you are tagging. I haven’t tested it thoroughly, but it seems to work. It would probably be better to just extend the existingfind_tagged_withmethod along the lines of http://www.railsweenie.com/forums/2/topics/699.If you are only tagging a single model, you can add this method to
acts_as_taggable.rband skip all of the other modifications -def find_by_user_tagged_with(user, list) find_by_sql([ "SELECT #{table_name}.* FROM #{table_name}, tags, taggings " + "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " + "AND #{table_name}.user_id = ? " + "AND taggings.taggable_type = ? " + "AND taggings.tag_id = tags.id AND tags.name IN (?)", user.id, acts_as_taggable_options[:taggable_type], list ]) endIt assumes that you are assigning a
>user_idto the model instances that you are tagging. I haven’t tested it thoroughly, but it seems to work. It would probably be better to just extend the existingfind_tagged_withmethod along the lines of http://www.railsweenie.com/forums/2/topics/699.I just wanted to say thanks…before I grabbed all this code and ran off laughing with it!
Anyways, this is exactly what I was looking for. Someone should create acts_as_user_taggable or something from all this.
I’d do it, but my svn repo is private.
I just wanted to say thanks…before I grabbed all this code and ran off laughing with it!
Anyways, this is exactly what I was looking for. Someone should create acts_as_user_taggable or something from all this.
I’d do it, but my svn repo is private.
I am having problems with polymorphic models. I have a class structure with A as the superclass and B as the subclass. I add “acts_as_taggable” to A. Everything works fine, but the Taggings table entries list all taggable_type as “A”, even if it is a “B” instance.
So, this is a problem when it goes to delete all the tags (ie. when someone adds a new set of tags). The original tags are never found, since there is a type mismatch (ie. all tags are typed for the superclass, not the actual instance class).
Anyone else run into this? Any solutions?
Note: I already tried moving “acts_as_taggable” down to B and removing it from A, but this changed nothing.
I fixed the problem. All I had to change was:
Tagging.destroy_all(" taggable_id = #{self.id} and taggable_type = '#{self.class}' and user_id = #{user.id}")should be:Tagging.destroy_all(" taggable_id = #{self.id} and taggable_type = '#{acts_as_taggable_options[:taggable_type]}' and user_id = #{user.id}")The acts_as_taggable plugin automatically uses the base class of a model as the taggable_type. This makes sense because all the objects in the heirarchy will be in the same table, so their IDs will all be different.
I fixed the problem. All I had to change was:
should be:
The acts_as_taggable plugin automatically uses the base class of a model as the taggable_type. This makes sense because all the objects in the heirarchy will be in the same table, so their IDs will all be different.
(Sorry for double-post. I tried to clean up the formatting a little)
I went ahead and summed up my changes to the plugin and created an SVN patch. The patch will take you from the original (by DHH) to acts_as_taggable with user, plus some bug fixes as noted in these comments.
You can get it on my blog: Midnight Oil
NO WARRANTY!
Note, that’s sort of a working title (and location) for my blog, so if it has gone 404 by the time you read this…sorry :(
I was getting this error:
ActiveRecord::AssociationTypeMismatch (User expected, got User): /vendor/plugins/acts_as_taggable/lib/tag.rb:31:in `on’
Changed this line in tag.rb:
def on(taggable, user) tagging = taggings.create :taggable => taggable, :user => user end
to:
def on(taggable, user) tagging = taggings.create :taggable => taggable, :user_id => user.id end
I concur with RS. I had the same problem and it works now with that fix.
How do I find all the instances of the model tagged by the current user?
Update: My blog has moved to a more permanent location. The patch I mentioned can now be found here: http://blog.aisleten.com/2007/02/25/acts_as_taggable-per-user-from-slashdotslash-plus-a-few-fixes/