Recently I was faced with a somewhat strange problem - I wanted to tag the same model with two different kinds of tags. Each Event needed to have both a regular Tag for categorization, and a PlaceTag to indicate its location.

I was using the acts_as_taggable gem to do the categorization tag. When I started considering how to do the PlaceTag, I quickly realized that the overloads I'd need to add a PlaceTag to the same model were nonexistent. This, coupled with the fact that both the acts_as_taggable gem and the plugin of the same name couldn't do the job (and seemed to be unmaintained lately), got me searching for a more flexible solution.

Bang! The has_many_polymorphs plugin by Evan Weaver to the rescue. He's written a couple of excellent tutorials on how to use his has_many_polymorphs plugin to generate tag clouds, and the bonus for me is that it should be easy to get my PlaceTag set up. The main trouble became tag cloud UI code generation, since the tag cloud code I had been using didn't map exactly to the cloud structures generated by has_many_polymorphs.

My tag clouds were being generated by Tom Fakes' cloud code. Not wanting to cargo cult, I grabbed the Programming Ruby book and looked over the Blocks and Iterators section again (functional programming is amazingly concise but I sometimes don't seem to have the six-dimensional brainiac capacities necessary).

In the end, I got to a solution which seems to work alright. I used the simplest possible Tag and Taggings models from Evan's tutorials, so those models look like this:

/app/models/tag.rb:

CODE:
  1.  
  2. class Tag <ActiveRecord::Base
  3.  
  4. has_many_polymorphs :taggables,
  5. :from => [:events],
  6. :through => :taggings,
  7. :dependent => :destroy
  8.  
  9. def self.cloud(args = {})
  10. find(:all, :select => 'tags.*, count(*) as popularity',
  11. :limit => args[:limit] || 5,
  12. :joins => "JOIN taggings ON taggings.tag_id = tags.id",
  13. :conditions => args[:conditions],
  14. :group => "taggings.tag_id",
  15. :order => "popularity DESC"  )
  16. end
  17.  
  18. end
  19. <em>/app/models/tagging.rb:</em>
  20.  
  21. class Tagging &lt;ActiveRecord::Base
  22. belongs_to :tag
  23. belongs_to :taggable, :polymorphic =&gt; true</code>
  24.  
  25. def before_destroy
  26. # disallow orphaned tags
  27. tag.destroy_without_callbacks if tag.taggings.count &lt;2
  28. end
  29. end
  30.  

The relevant controller code for my index page, which is found in /app/controllers/home_controller.rb:

CODE:
  1. class HomeController &lt;ApplicationController
  2.  
  3. def index
  4. @cloud = Tag.cloud
  5. ...
  6. end
  7.  
  8. end
  9.  

In my case, the tag cloud is displayed in the home layout view, /layouts/home.rhtml, but the tag cloud itself is a shared partial so that it can be re-used throughout the app. The relevant tag cloud code for inclusion in home.rhtml is simply:

CODE:
  1.  
  2. &lt;%= render(:partial =&gt; "shared/tag_cloud", :object=&gt; @cloud) %&gt;
  3.  

The tag cloud partial itself is a slightly modified version of the same thing originally written by Tom Fakes, like so:

/app/views/shared/_tag_cloud.rhtml:

CODE:
  1.  
  2. &lt;% if tag_cloud.length&gt; 0 %&gt;
  3. &lt;% build_tag_cloud(tag_cloud, %w(cloud1 cloud2 cloud3 cloud4 cloud5 cloud6 cloud7 cloud8 cloud9)) do |tag, cloud_class| %&gt;
  4. &lt;%= link_to(h("#{tag}"), tag_item_url(tag), { :class =&gt; "#{cloud_class} ", :style =&gt; "margin: .15em" } ) %&gt;
  5. &lt;% end %&gt;
  6. &lt;% else %&gt; No tags found.
  7.  
  8. &lt;% end %&gt;
  9.  

Finally, I put the code that actually figures out the style for each tag in /app/helpers/application_helper.rb so that it's accessible from anywhere in the application. This was the part that drove me to re-read Blocks and Iterators:

CODE:
  1.  
  2. module ApplicationHelper</code>
  3.  
  4. def build_tag_cloud(tag_cloud, style_list)
  5. max, min = 0, 0
  6. tag_cloud.each do |tag|
  7. max = tag.popularity.to_i if tag.popularity.to_i&gt; max
  8. min = tag.popularity.to_i if tag.popularity.to_i &lt;min
  9. end
  10.  
  11. divisor = ((max - min) / style_list.size) + 1
  12.  
  13. tag_cloud.each do |tag|
  14. yield tag.name, style_list[(tag.popularity.to_i - min) / divisor]
  15. end
  16. end
  17.  
  18. # Create a link to a tag's view page.
  19. # this could easily be different in your application, depending on how you structure your tag searches,
  20. # but it seems smart to include it here as my tag cloud code depends on it. Change the tag_item_url in
  21. def tag_item_url(name)
  22. "/search/by_tag/#{name}"
  23. end
  24. end

It seems to do the trick. Thanks to Evan and Tom for doing 90% of the work!


3 Responses to “Tag Clouds in Ruby on Rails using has_many_polymorphs”  

  1. 1 ARIANNA

    IM A HRYCYSZYN TOOOOOOOO…RANDOM MOMENT SORRY BUT STILL TRUE

  2. 2 senthil

    Hi
    Pls suggest a way to use the plugin for adding two sets of tags to a model
    thanks

  3. 3 cvibha

    Hi

    Nice post..

    I wanted to know if we can sort the tags alphabetically

    thanks

Leave a Reply