The following blog post contains material either currently found or soon to be incorporated into my new book, "Easy Active Record for Rails Developers". Among many other topics, you'll learn about model generation, migrations, validations, associations, scopes, joins, includes, forms integration, nested forms, and model testing with RSpec and FactoryGirl. The book is now available, head over to this website's home page to learn more.


Over time, traditional taxonomies have been gradually replaced by metadata-driven approaches to web content categorization on the basis that structured categorization hierarchies are often too inflexible, hindering users’ ability to efficiently find content due to a limited selection vocabulary. A metadata-based approach removes such restrictions because information can be classified under a much larger and more descriptive set of keywords (often referred to as tags) that more holistically define what a user might expect to take away as a result of accessing that content.

To really understand the difference between these two approaches, consider how ArcadeNomad might categorize the arcade games using these two approaches. Using the traditional taxonomical approach the game Asteroids might only be found by navigating through a series of hierarchical categories such as Space and then Shooter whereas using the flat, tag-based approach a much more rich set of classification keywords might give users the ability to immediately locate A steroids using one of many possible descriptors (see the below figure).


Given this tag cloud, the user could click on the tag that makes the most sense, with logical candidates being Classic, Space, and Shooter.

Integrating content tagging into your Rails application is surprisingly easy thanks to a fantastic gem called Acts as Taggable On. Created by Michael Bleigh and maintained by Joost Baaij, Acts as Taggable On is one of the latest incarnations of a line of powerful tagging gems long available to the Rails community. In this blog entry I’ll introduce you to Acts as Taggable On, explaining how I integrated this simple tagging feature into ArcadeNomad.

Installing the acts_as_taggable_on Gem

Install the acts_as_taggable_on gem by adding the following line to your project’s Gemfile:

gem 'acts-as-taggable-on'

Save the Gemfile and complete the installation process by execut

$ bundle
...
Installing acts-as-taggable-on (3.4.2)
...

After acts_as_taggable_on has been installed, you’ll need to generate the tables used to store the tags and associations. This is accomplished using a Rake task bundled with the gem:

$ rake acts_as_taggable_on_engine:install:migrations
 Copied migration 20141021024747_acts_as_taggable_...
 Copied migration 20141021024748_add_missing_unique...

After the migration files have been created, migrate the database:

 $ rake db:migrate

==  ActsAsTaggableOnMigration: migrating ========
-- create_table(:tags)
   -> 0.0319s
-- create_table(:taggings)
   -> 0.0138s
-- add_index(:taggings, :tag_id)
   -> 0.0143s
-- add_index(:taggings, [:taggable_id, :taggable_type, :context])
   -> 0.0116s
==  ActsAsTaggableOnMigration: migrated (0.0719s) ====

==  AddMissingUniqueIndices: migrating =======
-- add_index(:tags, :name, {:unique=>true})
   -> 0.0111s
-- remove_index(:taggings, :tag_id)
   -> 0.0096s
-- remove_index(:taggings, [:taggable_id, :taggable_type, :context])
   -> 0.0062s
-- add_index(:taggings, [:tag_id, :taggable_id, :taggable_type,
    :context, :tagger_id, :tagger_type],
    {:unique=>true, :name=>"taggings_idx"})
   -> 0.0107s
==  AddMissingUniqueIndices: migrated (0.0380s) ==========

At this point two new tables have been created: tags and taggings. The tags table manages the actual tags, such as Space and Classic. The taggings table is a bit more complex. It acts as the bridge between the model you’ve identified as taggable, and the tags table containing the tags associated with the taggable model’s records. Additionally, it defines the context of the tag. This is important because you might optionally wish to tag a model using multiple different contexts. For instance, if we were to extend the Location model to be taggable, the it might be tagged using two different contexts: foods and neighborhood. Therefore the 16-Bit Bar & Arcade in Columbus, Ohio might be tagged as Bar (using the foods tag context) and Downtown (using the neighborhood tag context. This flexible approach gives you the power to create interfaces that allow users to find data using multiple different tag sets. For instance, users might wish to view only a list of arcades offering pizza or popcorn, or restrict the listing to arcades found on the lower east side.

Keep in mind you aren’t required to define taggings in conjunction with a specific context; it’s possible to configure a model as merely taggable and then throw all of your tags into a single pot. I’ll show you how this is done in the next section.

Configuring a Taggable Model

Let’s put the concepts introduced above into action by configuring ArcadeNomad’s Game model to be taggable. We’ll start with the simple use case first, which is to simply configure the model as capable of being associated with tags:

class Game < ActiveRecord::Base

    acts_as_taggable

end

Save the file and then open up the Rails console sandbox to test out the tagging capabilities:

$ rails console --sandbox
>> location = Game.find_by_name('Asteroids')
Game Load (0.6ms)  SELECT  `games`.* FROM `games` 
 WHERE `games`.`name` = 'Asteroids' LIMIT 1
#<Game:0x007ffe10b19840> {
               :id => 7,
             :name => "Asteroids",
      :description => "A vintage arcade game.",
       :created_at => Tue, 21 Oct 2014 16:01:22 UTC +00:00,
       :updated_at => Tue, 21 Oct 2014 16:01:22 UTC +00:00,
     :release_date => 1979,
  :manufacturer_id => 5,
             :slug => "asteroids"
}
>> game.tag_list.add('Space')
ActsAsTaggableOn::Tag Load (1.1ms)  SELECT `tags`.* FROM `tags`
  INNER JOIN `taggings` ON `tags`.`id` = `taggings`.`tag_id`
  WHERE `taggings`.`taggable_id` = 7 AND `taggings`.`taggable_type`
  = 'Game' AND (taggings.context = 'tags' AND taggings.tagger_id IS NULL)
=> ["Space"]
>> game.tag_list.count
=> 1
>> game.tag_list.add('Classic')
...
>> game.tag_list
[
[0] "Space",
[1] "Classic"
]

Obviously you’ll want to use other means for adding tags to your application content, such as a form or seeder file. However the general syntax remains the same; just use the tag_list collection’s add method to add new tags, and use the remove method to remove tags (not demonstrated here but discussed in the project README).

Creating a Tag Cloud

Users find tag clouds appealing because of the immediate visual payoff; at a glance it is not only apparent what topics are covered on your site, but also thanks to the varied font sizes and weightings, which tags are used with more frequency than others. At the beginning of this section I presented an example of a tag cloud used in the ArcadeNomad application. In this section I’ll show you how that cloud was created. Begin by adding the following code to the view helper file associated with the view in which you’d like the tag cloud to appear (found in app/helpers):

module GamesHelper
  include ActsAsTaggableOn::TagsHelper
end

The TagsHelper gives you access to the Acts As Taggable On tag_cloud method, which we’ll use in the view to create the tag cloud. Next, in the desired controller action you’ll execute the tag_counts_on method in conjunction with the desired model within action associated with the view where you’d like the tag cloud to appear. In this example I’d like to create a tag cloud based on the tags associated with the Game model:

@tags = Game.tag_counts_on(:tags)

Next, open the view in which you’d like the tag cloud to appear.

<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
  <%= link_to tag.name, tag_path(tag), :class => css_class %>
<% end %>

Of course, we haven’t yet created the Tag controller yet therefore if you try to load the view containing this code you’ll receive an error. Let’s fix this problem next.

Retrieving Games According to Tag

When the tag cloud renders, each tag will be linked to a URL that looks like this: http://arcadenomad.com/tag/5. Therefore to retrieve a list of games associated with a given tag you’ll need to create a Tag controller (obviously you can change the controller to whatever you please):

$ rails g controller Tag show

This will generate a controller named Tag containing just a single action named show. Although only a single action is used I’ll go ahead and make this controller RESTful by adding the following line to your config/routes.rb file:

resources :tag

If you do make the controller RESTful you’ll want to remove the line get tag/show from routes.rb, which was added when the controller was created.

Next, open the Tag controller (app/controllers/tag_controller.rb) and update the show action to look like this:

def show
  @tag = Tag.find(params[:id])
  @games = Game.tagged_with(@tag.name)
end

This code looks pretty standard, however it’s not going to work as expected just yet for one simple reason: the Tag model doesn’t exist! A tags table was generated by Acts As Taggable On, however a corresponding model was not. To create it, you’ll invoke the model generator per usual, passing along --migration false because we don’t want to create a migration since the table already exists:

$ rails g model Tag --migration false
   invoke  active_record
   create    app/models/tag.rb
   invoke    rspec
   create      spec/models/tag_spec.rb
   invoke      factory_girl
   create        spec/factories/tags.rb

With the model created, we can wrap up the feature by adding the following code to the Tag controller’s show view:

<h1>Games Tagged as <%= @tag.name %></h1>

<% if @games.count > 0 %>

  <ul>

  <% @games.each do |game| %>

    <li><%= link_to game.name, game_path(game) %></li>

  <% end %>

  </ul>

<% else %>

  <p>This tag is not associated with any games.</p>

<% end %>

Styling the Tag Cloud CSS

If you load the view after having created a few tags and tag mappings, you’ll see no differentiation between the styling of each tag, regardless of their respective usage frequency. This is because you need to stylize the css1, css2, css3, and css4 tags injected into the link_to method. Add the following four styles to your project’s CSS:

.css1 { font-size: 1.0em; }
.css2 { font-size: 1.2em; }
.css3 { font-size: 1.4em; }
.css4 { font-size: 1.6em; }

Summary

As this entry hopefully demonstrated, it’s incredibly easy to integrate tagging into your Rails application using the powerful Acts As Taggable On gem. As always, if you get stuck or have any other questions, feel free to e-mail me at wj AT wjgilmore.com.

Comments