Tag Clouds in Ruby on Rails using has_many_polymorphs
Posted by Dave Hrycyszyn 31st March 2007 in Ruby on Rails, TechnicalRecently 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:
class Tag < ActiveRecord::Base
has_many_polymorphs :taggables,
:from => [:events],
:through => :taggings,
:dependent => :destroy
def self.cloud(args = {})
find(:all, :select => ‘tags.*, count(*) as popularity’,
:limit => args[:limit] || 5,
:joins => “JOIN taggings ON taggings.tag_id = tags.id”,
:conditions => args[:conditions],
:group => “taggings.tag_id”,
:order => “popularity DESC” )
end
end
/app/models/tagging.rb:
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
def before_destroy
# disallow orphaned tags
tag.destroy_without_callbacks if tag.taggings.count < 2
end
end
The relevant controller code for my index page, which is found in /app/controllers/home_controller.rb:
class HomeController < ApplicationController
def index
@cloud = Tag.cloud
…
end
end
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:
<%= render(:partial => "shared/tag_cloud", :object=> @cloud) %>
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:
<% if tag_cloud.length > 0 %>
<% build_tag_cloud(tag_cloud, %w(cloud1 cloud2 cloud3 cloud4 cloud5 cloud6 cloud7 cloud8 cloud9)) do |tag, cloud_class| %>
<%= link_to(h(”#{tag}”), tag_item_url(tag), { :class => “#{cloud_class} “, :style => “margin: .15em” } ) %>
<% end %>
<% else %>No tags found.
<% end %>
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:
module ApplicationHelper
def build_tag_cloud(tag_cloud, style_list)
max, min = 0, 0
tag_cloud.each do |tag|
max = tag.popularity.to_i if tag.popularity.to_i > max
min = tag.popularity.to_i if tag.popularity.to_i < min
end
divisor = ((max - min) / style_list.size) + 1
tag_cloud.each do |tag|
yield tag.name, style_list[(tag.popularity.to_i - min) / divisor]
end
end
# Create a link to a tag’s view page.
# this could easily be different in your application, depending on how you structure your tag searches,
# but it seems smart to include it here as my tag cloud code depends on it. Change the tag_item_url in
def tag_item_url(name)
“/search/by_tag/#{name}”
end
end
It seems to do the trick. Thanks to Evan and Tom for doing 90% of the work!
Search
Categories
- News
(40)
- Victoria’s Secret
(7)
- Stu's View
(3)
- Design
(13)
- Technical
(11)
- Actionscript
(3)
- GNU / Linux
(4)
- FLOSS
(7)
- C# / .NET
(2)
- XHTML / CSS
(1)
- Agile Development
(2)
- Joeys Corner
(4)
- Sleeping Stu
(2)
- The Hoseclamp's Notebook
(13)
- Ruby on Rails
(3)
Monthly Archives
- August 2008 (1)
- July 2008 (1)
- June 2008 (1)
- May 2008 (2)
- March 2008 (1)
- February 2008 (1)
- January 2008 (1)
- November 2007 (2)
- August 2007 (1)
- May 2007 (2)
- April 2007 (5)
- March 2007 (12)
- February 2007 (10)
- January 2007 (4)
- November 2006 (2)
- September 2006 (4)
- August 2006 (1)
- July 2006 (1)
- June 2006 (6)
- May 2006 (2)
- April 2006 (4)
- March 2006 (14)
- February 2006 (12)
- January 2006 (1)
- December 2005 (1)
- November 2005 (1)
IM A HRYCYSZYN TOOOOOOOO…RANDOM MOMENT SORRY BUT STILL TRUE
Hi
Pls suggest a way to use the plugin for adding two sets of tags to a model
thanks