Head London

Tag Clouds in Ruby on Rails using has_many_polymorphs

Saturday 31st March 2007 at 5:13 pm

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]
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",
:o rder => "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
[/code]

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

[code]class HomeController < ApplicationController

def index
@cloud = Tag.cloud
...
end

end
[/code]

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]
<%= render(:partial => "shared/tag_cloud", :o bject=> @cloud) %>
[/code]

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]
<% 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 %>
[/code]

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]
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
[/code]

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

4 comments so far.

  1. ARIANNA says:

    IM A HRYCYSZYN TOOOOOOOO…RANDOM MOMENT SORRY BUT STILL TRUE

  2. senthil says:

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

  3. cvibha says:

    Hi

    Nice post..

    I wanted to know if we can sort the tags alphabetically

    thanks

  4. Ian Fleeton says:

    To sort by name, in tag.rb change
    :o rder => “popularity DESC”
    to
    :o rder => “name ASC”

Leave a comment