acts_as_taggable libraries and the big case problem.
Over the years I've had a chance to use the three tagging libraries available for Ruby on Rails:
I think the original acts as taggable is now defunct. The other libraries are derivatives of that library. In using tags on various sites the problem I always seem to run across is how to deal with tag case. For example, to some blue is the same as Blue. However, is god the same as God? It depends on who you ask. It seems that acts-as-taggable-on handles the case problem properly. I noticed that if I add the tag 'blue' to an object I cannot add another tag called 'Blue'. However, if I delete 'blue' and then add the tag 'Blue' it works as expected and the upper case tag becomes associated with the object.
acts_as_taggable_on_steroids doesn't handle the case problem especially well and I frequently run across this error:
ActiveRecord::RecordInvalid (Validation failed: Tag has already been taken):
It turns out that that the key difference between the two libraries is in how they setup the tag relationship.
acts-as-taggable-on does this:
def save_tags (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type| next unless instance_variable_get("@#{tag_type.singularize}_list") owner = instance_variable_get("@#{tag_type.singularize}_list").owner new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name) old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) } transaction do base_tags.delete(*old_tags) if old_tags.any? new_tag_names.each do |new_tag_name| new_tag = Tag.find_or_create_with_like_by_name(new_tag_name) Tagging.create(:tag_id => new_tag.id, :context => tag_type, :taggable => self, :tagger => owner) end end end true end
while acts_as_taggable_on_steroids does it this way:
def save_tags
return unless @tag_list
new_tag_names = @tag_list - tags.map(&:name)
old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
self.class.transaction do
if old_tags.any?
taggings.find(:all, :conditions => ["tag_id IN (?)", old_tags.map(&:id)]).each(&:destroy)
taggings.reset
end
new_tag_names.each do |new_tag_name|
tags << Tag.find_or_create_with_like_by_name(new_tag_name)
end
end
true
end
The key difference is in this:
Tagging.create(:tag_id => new_tag.id, :context => tag_type, :taggable => self, :tagger => owner)
versus:
tags << Tag.find_or_create_with_like_by_name(new_tag_name)
The first will return false and on go on it's way. The second throws an exception. Which is the right way of dealing with the problem? I guess it depends. I don't feel like either is a great solution. Both libraries assume that 'blue' == 'Blue'. If that assumption is correct then a different bit of code should change in each library. Tag.rb should lower case the names in the comparison:
def ==(object) super || (object.is_a?(Tag) && name == object.name) end
changes to:
def ==(object) super || (object.is_a?(Tag) && name.downcase == object.name.downcase) end
However, if you want to leave each tag as the user specified rather than change the case then a different line needs to be changed in tag.rb
# LIKE is used for cross-database case-insensitivity def self.find_or_create_with_like_by_name(name) find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name) end
will need to change to
# = is used for to ensure tags are case sensitive def self.find_or_create_with_like_by_name(name) find(:first, :conditions => ["name =", name]) || create(:name => name) end
Of course the second change will result in the duplication of tags in your site - you will end up with tags 'Blue' and 'blue', but that is the intent. Your searches might need to be adjusted accordingly.
Justin Ball is a software consultant and entrepreneur with a passion for Ruby. He evolved from a C++ and .Net monkey into a python programmer and finally found Ruby. In the rare moments when he isn't writing code, talking about code or measuring his code productivity in profanity per hour, you can find him on his bike in the mountains or on the roads surrounding Cache Valley. 









