Tagtical

<img src=“https://secure.travis-ci.org/Mixbook/tagtical.png” />

This plugin was originally based on acts_as_taggable_on by Michael Bleigh (mbleigh.com/). That plugin was based on acts_as_taggable_on_steroids by Jonathan Viney.

While a lot concepts are the same (taggings + tags tables using polymorphism), this adaption introduces the concept of “relevance” for a tag and allows for the creation of subclasses on Tag.

For instance, if you want to tag a photo with “Mood Tags”. You would simply subclass Tag with Tag::Mood and you could add functionality specific to that model. This involves moving the “context” off of Tagging and moving it onto Tag as a “type” column. It acts not only as a “context”, but also as a designator for the STI class. Subsequently, you could also add a “relevance” for how applicable that mood is.

Tagtical allows for an arbitrary number of Tag subclasses, each of which can be extended to the needs of the application. Note: Tag subclasses are required! You cannot do custom “contexts” as you could in acts_as_taggable_on/

Here are the main differences between tagtical and acts_as_taggable_on:

  1. Add “relevance” to the Tagging class so you can weight the tags to the object.

  2. Tagtical removes “context” off the taggings table and adds “type” onto the tags table.

  3. The traditional functionality of tags is preserved while laying the foundation for STI on the Tag class.

You can choose to extend the Tag class at a later time.

  1. Tag “name” now becomes tag “value”. The difference is small, but significant. If you had

GeoTag’s, for example, you wouldn’t refer to its “name”, you would refer to its “value”. The value could be a serialized field of long and lat if you wanted.

  1. Support a config/tagtical.yml to further configure the application. For example, since most people

usually have one User class for their application, there is no reason to do polymorphic on “tagger”, so I give the user the option to specify the class_name specifically for tagger.

  1. :parse option to tag_list is inverted. Specify :parse => false if you don’t want your tag values

parsed.

Example:
  # would give you a tag of "red, blue" instead of "red" and "blue".
  tag_list.add("red, blue", :parse => false)
  1. Custom contexts are not supported since we use STI. That is, you must define a set of tag types

ahead of time.

  1. Functionality for joining across tags (seeing which tags two different models have in common)

with both superset and subset matching.

Additions include:

  1. Scopes are created on Tag so you can do photo.tags.color and grab all the tags of type Tag::Color, for example.

  2. Scopes are also created on the Taggable model so you could do Model.with_colors(“red”, “blue”) and it would return everything tagged with those colors.

Installation

Rails & Ruby Versions

Tagtical was developed on Rails 3.0.5 and Ruby 1.9.2

It can probably work with older versions, but would take a few tweaks.

Plugin

Tagtical is available both as a gem and as a traditional plugin. For the traditional plugin you can install like so:

script/plugin install git://github.com/Mixbook/tagtical.git

Gem

gem install tagtical

Post Installation

  1. script/generate tagtical_migration

  2. rake db:migrate

Rails 3.0

To use it, add it to your Gemfile:

gem 'tagtical'

Post Installation

  1. rails generate tagtical:migration

  2. rake db:migrate

Testing

Tagtical uses RSpec for its test coverage. Inside the plugin directory, you can run the specs for RoR 3.0.5 with:

rake spec

Rails 2.3 is not supported, however I left the stub code from acts_as_taggable_on in there in case someone wants to try to get it working:

rake rails2.3:spec

If you already have RSpec on your application, the specs will run while using:

rake spec:plugins

Usage

class User < ActiveRecord::Base
  # Alias for <tt>tagtical :tags</tt>:
  acts_as_taggable :activities, :interests, :sports # top level, generic :tags is already included.
end
module Tag
  class Activity < Tagtical::Tag
  end
  class Sport < Activity

    # You can also set the possible values for a specific tag type
    self.possible_values = %w{boxing basketball tennis hockey footabll soccer}

    def ball?
      value=~/ball$/i
    end
  end
end

# Basic TagList Functionality

@user = User.new(:name => "Bobby")
@user.tag_list = "awesome, slick, hefty" # this should be familiar
@user.activity_list = "joking, clowning, boxing" 
@user.activity_list        # => ["joking","clowning","boxing"] as TagList
@user.activity_list.to_s   # => "joking, clowning, boxing"
@user.save

# Cascade tag_list setters =)
# It will look at the "possible_values" if provided, and stuff the tags down at that level.

@user.set_activity_list(["clowning", "boxing"], :cascade => true)
@user.save!
@user.sport_list  # => ["boxing"]

@user.tags # => [<Tag value:"awesome">,<Tag value:"slick">,<Tag value:"hefty">]
@user.activities # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">,<Tag::Activity value:"boxing">]

@frankie = User.create(:name => "Frankie", :activity_list => "joking, flying, eating")
User.activity_counts # => [<Tag::Activity value="joking" count=2>,<Tag::Activity value="clowning" count=1>...]
@frankie.activity_counts

@user.sport_list = {"boxing" => 4.5  # Since Sport's parent is Activity, it will move "boxing" down
                                     # from Activity to Sport and give it a relevance of 4.5.
@user.save

# Possible Values Checking

@user.sport_list = ["chess"]
@user.save! # <=== will throw an error, chess is not in possible_values from Tag::Sport

# Tagging Scopes

# Tagtical allows for tagging scopes built with the paradigm of "parents", "current", and "children".
# This way you can get any level of the inheritance chain and in any direction!
@user.activities # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">,<Tag::Sport value:"boxing">]
@user.activities(:scope => :children) # => [<Tag::Sport value:"boxing">] - look at only the STI subclasses
@user.activities(:children)           # => shorthand for above
@user.tags(:scope => :children, :except => :sports) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">]
                                                    #  - filter list by excluding
@user.tags(:scope => :children, :only => :sports)   # => [<Tag::Sport value:"boxing">]
                                                    #  - filter list by including

@user.activities(:scope => :parents) # Gets tags above activities (current just top-level tags)
@user.activities(:scope => :current) # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class
@user.activities(:scope => :==)      # => [<Tag::Activity value:"joking">,<Tag::Activity value:"clowning">] - look at only the current STI class

# Questioner Methods

@user.activities.first.athletic? # => false
@user.sports.all(&:ball?) # => true

# Database Access Optimizations

** Sequence 1
@user.tags.to_a # load in the tags
@user.sports.to_a     # will not trigger a database hit, instead will get the sports tags off tags
@user.activities.to_a # same as above

** Sequence 2
@user.activities.to_a # load in the tags
@user.sports.to_a     # will not trigger a database hit, instead will get the sports tags off activities, since it inherits from activities
@user.sports(:conditions => "value='Soccer'") # will actually hit the database if arguments are passed in.

— Defining Subclasses

There is a lot of flexibility when it comes to naming subclasses. Lets say the type column had a value of “color”. You could define the subclass any of these ways:

module Tagtical
  module Tag
    class Color < Tagtical::Tag
    end
  end
end
module Tag
  class Color < Tagtical::Tag
  end
end
class Color < Tagtical::Tag
end
module Tagtical
  module Tag
    class ColorTag < Tagtical::Tag
    end
  end
end
module Tag
  class ColorTag < Tagtical::Tag
  end
end
class ColorTag < Tagtical::Tag
end

This allows for a wide range of folder structures. You could nest files (with corresponding models) like this: app/models/tagtical/tag/color.rb app/models/tag/color.rb app/models/color.rb app/models/tagtical/tag/color_tag.rb app/models/tag/color_tag.rb app/models/color_tag.rb

Finding Tagged Objects

Tagtical utilizes named_scopes to create an association for tags. This way you can mix and match to filter down your results, and it also improves compatibility with the will_paginate gem:

class User < ActiveRecord::Base
  acts_as_taggable
  named_scope :by_join_date, :order => "created_at DESC"
end

User.tagged_with("awesome").by_join_date
User.tagged_with("awesome").by_join_date.paginate(:page => params[:page], :per_page => 20)

# Find a user with matching all tags, not just one
User.tagged_with(["awesome", "cool"], :match_all => :true)

# Find a user with any of the tags:
User.tagged_with(["awesome", "cool"], :any => true)

Relationships

You can find objects of the same type based on similar tags on certain contexts. Also, objects will be returned in descending order based on the total number of matched tags.

@bobby = User.find_by_name("Bobby")
@bobby.activity_list # => ["jogging", "diving"]

@frankie = User.find_by_name("Frankie")
@frankie.activity_list # => ["hacking"]

@tom = User.find_by_name("Tom")
@tom.activity_list # => ["hacking", "jogging", "diving"]

@tom.find_related_activities # => [<User name="Bobby">,<User name="Frankie">]
@bobby.find_related_activities # => [<User name="Tom">] 
@frankie.find_related_activities # => [<User name="Tom">]

Dynamic Tag Contexts

In addition to the generated tag contexts in the definition, it is also possible to allow for dynamic tag contexts (this could be user generated tag contexts!)

@user = User.new(:name => "Bobby")
@user.set_tag_list_on(:customs, "same, as, tag, list")
@user.tag_list_on(:customs) # => ["same","as","tag","list"]
@user.save
@user.tags_on(:customs) # => [<Tag value='same'>,...]
@user.tag_counts_on(:customs)
User.tagged_with("same", :on => :customs) # => [@user]

In the future, lets say you wanted to add additional methods for these specific tags. You would simply just define the subclass and the code will automatically instantiate it as that class. Just do:

class CustomTag < Tagtical::Tag
  def some_custom_function
  end
end

Now moving forward, these classes will be instantiated with this model. Wow cool!

Tag Groups

This functionality allows to join models across tags (seeing which tags two different models have in common) with both superset and subset matching.

You can do it simply by providing :has_many_through_tags call in taggable model, like:

class User < ActiveRecord::Base
  acts_as_taggable :skills
  has_many_through_tags :schools, :superset
end

class School < ActiveRecord::Base
  acts_as_taggable :skills
  has_many_through_tags :users # :subset is default
end

So, users can study in a school only if they have all the required skills. We want to get list of all schools of a user and all users of a school.

school1.skill_list = "math, biology"
school2.skill_list = "programming, chemistry"
user.skill_list = "chemistry, math, biology"

user.schools # => [school1]
school1.users # => [user]
school2.users # => []

You also may specify different class name, if you want to name your association differently:

class User < ActiveRecord::Base
  acts_as_taggable :skills
  has_many_through_tags :possible_schools, :superset, class_name: "School"
end

Tag Ownership

Tags can have owners:

class User < ActiveRecord::Base
  acts_as_tagger
end

class Photo < ActiveRecord::Base
  acts_as_taggable :locations
end

@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
@some_user.owned_taggings
@some_user.owned_tags
@some_photo.locations_from(@some_user)

Tag cloud calculations

To construct tag clouds, the frequency of each tag needs to be calculated. Because we specified tagtical on the User class, we can get a calculation of all the tag counts by using User.tag_counts_on(:customs). But what if we wanted a tag count for an single user’s posts? To achieve this we call tag_counts on the association:

User.find(:first).posts.tag_counts_on(:tags)

A helper is included to assist with generating tag clouds.

Here is an example that generates a tag cloud.

Helper:

module PostsHelper
  include Tagtical::TagsHelper
end

Controller:

class PostController < ApplicationController
  def tag_cloud
    @tags = Post.tag_counts_on(:tags)
  end
end

View:

<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
  <%= link_to tag.value, { :action => :tag, :id => tag.value }, :class => css_class %>
<% end %>

CSS:

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

Contributors

  • Aryk Grosz - Original Author

  • Jonathan Nevelson - Taggable Scopes

Contributors (from the acts_as_taggable_on project)

  • TomEric (i76) - Maintainer

  • Michael Bleigh - Original Author of acts_as_taggable_on

  • Szymon Nowak - Rails 3.0 compatibility

  • Jelle Vandebeeck - Rails 3.0 compatibility

  • Brendan Lim - Related Objects

  • Pradeep Elankumaran - Taggers

  • Sinclair Bain - Patch King

Patch Contributors (from the acts_as_taggable_on project)

  • tristanzdunn - Related objects of other classes

  • azabaj - Fixed migrate down

  • Peter Cooper - named_scope fix

  • slainer68 - STI fix

  • harrylove - migration instructions and fix-ups

  • lawrencepit - cached tag work

  • sobrinho - fixed tag_cloud helper

Copyright © 2011 Aryk Grosz (mixbook.com/) released under the MIT license