acts_as_taggable_redux

acts_as_taggable系列のタグ付けプラグインをいろいろチェックしていて、まあ、あるわあるわ。結局どれがいいのかよく分からないが、acts_as_taggable_on_steroidsとacts_as_taggable_reduxのどちらかを採用しようかといろいろソースを見比べている。どっちもチェックする予定だけどまずacts_as_taggable_redux。ただ、最終的にはどっちにしても自分の環境に合わせていじることは必須でしょう。


参考
Rails プラグイン acts_as_taggable_redux でタグクラウドを作ろう
railsで簡単にタグクラウドを作る


追記:
acts_as_taggable_reduxとacts_as_taggable_on_steroidsの比較に関しては以下を書きました。あわせて参考にしてください。
http://d.hatena.ne.jp/yotena/20071220/1198103145

特徴

他のブログで言われているようにtag_cloudが簡単に出来ると言う点と、taggable以外にtaggerというクラスが設定出来るのが一番の特徴といったところでしょうか。
generatorでmigrationが作れますが、これを見ても分かるようにuser_idというカラムがあるって誰(tagger)がタグ付けしたのかが分かるように設定することが前提になってますね。これで、誰(tagger)が何(taggable)に付けたTagというのがモデル化されてます。

class AddActsAsTaggableTables < ActiveRecord::Migration 
  def self.up 
    create_table :tags do |t| 
      t.column :name, :string
      t.column :taggings_count, :integer, :default => 0, :null => false
    end 
    # index関連なので省略 #
    create_table :taggings do |t| 
      t.column :tag_id, :integer 
      t.column :taggable_id, :integer 
      t.column :taggable_type, :string
      t.column :user_id, :integer
    end     
    # index関連なので省略 #
  end 
   
  def self.down 
    drop_table :tags 
    drop_table :taggings 
  end 
end

モデルクラス

上のmigrationによって追加されるテーブルに対応するモデルはTagクラスとTaggingクラス。


Tagクラスには、以下が宣言されて、あとは読めば分かるぐらいの簡単な便利メソッドがあるぐらい。

has_many :taggings

tag(taggable, user_id = nil)でtaggableにタグ付けすることと、taggedでこのタグに関連付けられたtaggingから「このタグを付けられているモノ(taggable)」を探すことが出来る。


Taggingクラスには、以下が宣言されていて、taggable_typeとtaggable_idでポリモーフィック関連があり、userモデルに紐付けされている。

  belongs_to :tag, :counter_cache => true
  belongs_to :taggable, :polymorphic => true
  belongs_to :user

TaggableとTagger

acts_as_taggable宣言をしたモデルはtaggableなクラス(タグ付けされる物)になって、acts_as_tagger宣言されたモデルはtaggerなクラス(タグ付けする人)になると考えるとそのまんま。


Taggerには特にメソッド追加されていないので省略。


Taggableにあるメソッドは以下

  • find_tagged_with(tags, options = {})
  • find_tagged_with_by_user(tags, user, options = {})

あんまり気にしなくて使ってみたが別に問題なさげ。

  • tag_list=(new_tag_list)

これはtaggableなクラスにタグを設定するメソッド。

  • user_id=(new_user_id)

タグ付けした人を特定するためにuser_idを設定するメソッド。
ところでこれ、すでにuser_idというカラムがあるテーブルのモデルクラスにタグ付けするとuser_id=()メソッドを上書きしちゃう気がします。それって誰かが所有しているtaggableオブジェクトにuser_idを設定できなくなっちゃいますね。
例でいうとBookモデルにuser_id(著者だか登録者だか)があって、このモデルにuser(著者だか登録者)をセットするときに

book = Book.new
book.user_id=current_user.id
book.tag_list(params[:tag_list])
book.save

とかしちゃうとbooksテーブルのuser_idではなくて、タグ付けした人のuser_idになってしまうんじゃないかと。なので、とりあえず私は、acts_as_taggable宣言したクラスTaggableBookと宣言してないクラスBookを作りました。acts_as_taggable宣言されたのクラスTaggableBookでは、acts_as_taggableのuser_idメソッドを使って、クラスBookではテーブル自身のuser_idをラップしたメソッドが使われる様にです。たぶんこれで問題なさげです。もっと効率のいい方法おしえて&間違ってたら誰か教えてください。

  • tag_list(user = nil)

user=nilの時に同じタグが大量にリストに入ってしまいますね。用途によるわけですが、たとえばBookモデルがあったとして、「Ruby on Rails」という本があったとして、これにたくさんの人が同じrailsタグとかを付けた場合、tag_listの中身が「rails, rails, rails, rails....RoR, RoR....」とかになった入りするわけです。なので、acts_as_taggable宣言するクラス(上の例のBookに相当)で以下の様にオーバーライドしました。

  def tag_list(user = nil)
    if tags.size>0
      tags.uniq!
    end
    unless user
      tags.collect { |tag| tag.name.include?(" ") ? %("#{tag.name}") : tag.name }.join(" ")
    else
      tags.delete_if { |tag| !user.tags.include?(tag) }.collect 
         { |tag| tag.name.include?(" ") ? %("#{tag.name}") : tag.name }.join(" ")
    end
  end

(追記)さらにもろもろ問題があったので、以下のように修正。
http://d.hatena.ne.jp/yotena/20080507/1210112159

def tag_list(user = nil)
  unless user
    tags.uniq.collect { |tag| tag.name.include?(" ") ? %("#{tag.name}") : tag.name }.join(" ")
  else
    taggings.find(:all, :conditions=>['user_id=?', user.id]).map { |tagging| tagging.tag }.uniq.collect { |tag| tag.name.include?(" ") ? %("#{tag.name}") : tag.name }.join(" ")
  end
end
  • update_tags

ソースは以下のとおりなんですが、
ここは、

        def update_tags
          if @new_tag_list
            Tag.transaction do
              unless @new_user_id
                taggings.destroy_all
              else
                taggings.find(:all, :conditions => "user_id = #{@new_user_id}").each do |tagging|
                  tagging.destroy
                end
              end

              Tag.parse(@new_tag_list).each do |name|
                Tag.find_or_create_by_name(name).tag(self, @new_user_id)
              end

              tags.reset
              taggings.reset
              @new_tag_list = nil
            end
          end
        end

これのうち、

              unless @new_user_id
                taggings.destroy_all
              else

の部分、@new_user_idがなかったらBookモデルのすべてのタグを削除してる気がします。

        def update_tags
          if @new_tag_list
            Tag.transaction do
#              unless @new_user_id
#                taggings.destroy_all
#              else
              if @new_user_id > 0
                taggings.find(:all,
                    :conditions => "user_id = #{@new_user_id}").each do |tagging|
                  tagging.destroy
                end
              end

              Tag.parse(@new_tag_list).each do |name|
                Tag.find_or_create_by_name(name).tag(self, @new_user_id)
              end

              tags.reset
              taggings.reset
              @new_tag_list = nil
            end
          end
        end

にしたほうがいいのかなー。あってる?

helper

  • tag_cloud

acts_as_taggable_reduxの目玉機能なんですが、

      tags = Tag.find(:all, :limit => 100, :order => 'taggings_count DESC').sort_by(&:name)

の部分。だれもタグ付けしてないタグ(だれかが一度付けたタグを消してしまうとtaggings_countが0になる)も出しちゃいそう(実際出てた。)なので、以下のように修正してみました。参考までに。

tags = Tag.find(:all,
  :limit => 100,
  :conditions =>['taggings_count>0'],
  :order => 'taggings_count DESC').sort_by(&:name)

この部分はバグというよりは、TODOコメントにあるように、使う人が自分で修正することが前提なんでしょう。


これ以外に、tag_cloud_for_taggerとかtag_cloud_for_taggableとかtag_cloud_for_taggable_and_taggerとか作ってみたらかなり便利なプラグインになりました。(あまり汎用的に作ってないのと、昨日サーフィンで疲れて眠いので、サンプルソースは要望があればいつかちょっと修正してから載せます)


以下、user_id=()メソッドに関する補足です。
ただし、補足2、3は試してないので検証してから使ってね。

補足1

わたしがとった手はこんな感じです。

# books tableはこんな感じ
# id --> integerr
# title --> string
# user_id -->integer

#メソッドuser_id(uid)はbooksテーブルのuser_idを設定
class Book < ActiveRecord::Base
  attr_accessible :id, :title, :user_id
end

#メソッドuser_id(uid)はacts_as_taggableのuser_idなので
#taggingsテーブルのuser_idに設定する。
class TaggableBook < ActiveRecord::Base
  acts_as_taggable
  attr_accessible :tag_list, :user_id
  self.table_name = 'books'
end

BookモデルもTaggableBookモデルは、どちらもbooksテーブルを扱うモデル。
ただし、
booksテーブルに行を追加したい時は、Bookモデルを使って、
タグを付けたいときにはTaggableBookモデルを使う。

補足2

user_idだけどbooksテーブルに付いてるuserはownerだ!と割り切れば、owner_idというアクセサメソッド作っちゃうのもいいかも。

class Book < ActiveRecord::Base
  def owner_id=(uid)
    write_attribute 'user_id', uid
  end
  def owner_id
    read_attribute 'user_id'
  end
end

この方法だと無駄にTaggableBookなんてモデルを作らなくてもよくなる。

補足3

もうひとつ手としては、vendor配下のacts_as_taggable.rbファイル中のuser_id=()メソッドは以下の通りですが

def user_id=(new_user_id)
   @new_user_id = User.find(new_user_id).id
end

これをいじってしまうという手になりますかね。ただし、こっちをいじっちゃうと、プラグインがバージョンアップしても自分でメンテナンスしないといけないです。

def user_id=(new_user_id)
   @new_user_id = User.find(new_user_id).id
  write_attribute 'user_id', new_user_id
end

この方法でも無駄にTaggableBookなんてモデルを作らなくてもよくなる。