#382 Tagging
- Download:
- source codeProject Files in Zip (94.1 KB)
- mp4Full Size H.264 Video (27.5 MB)
- m4vSmaller H.264 Video (13.8 MB)
- webmFull Size VP8 Video (15.6 MB)
- ogvFull Size Theora Video (32.7 MB)
下のスクリーンショットは、記事の一覧を表示しているブログアプリケーションのページです。タグ機能を追加して、各記事に複数のタグをつけられるようにします。今回のエピソードでは、Railsアプリケーションにタグ機能を付加する2つの方法を紹介します。
Acts-as-Taggable-onを使ってタグ機能を付加する
タグ付け用のライブラリはいくつもありますが、飛び抜けて人気があるのがacts-as-taggable-onです。このgemは比較的古いですが、長い間メンテナンスされている数少ないgemの一つです。Railsアプリケーションで利用するには、gemfileに追加してbundleコマンドを実行してインストールします。
gem 'acts-as-taggable-on'
次にこのgemが提供するジェネレータを実行してmigrationファイルを作成し、データベースのマイグレーションを行なってacts-as-taggable-on
が使用するフィールドを追加します。
$ rails g acts_as_taggable_on:migration $ rake db:migrate
このマイグレーションによってtags
とtaggings
の2つのテーブルが追加されます。これらのモデルはgemに含まれているので改めて作成する必要はありません。では早速アプリケーションにタグ機能を付加する作業に入りましょう。記事の編集ページに、その記事のタグを入力するためのフィールドを追加することにします。そのためにArticle
モデルに、gemが提供するacts_as_taggable
メソッドの呼び出しを追加し、さらにattr_accessible
のリストにtag_list
フィールドを追加します。
class Article < ActiveRecord::Base attr_accessible :content, :name, :tag_list acts_as_taggable end
次に記事の入力フォームに、タグのリストを編集するための新規のフィールドを追加します。
<div class="field"> <%= f.label :tag_list, "Tags (separated by commas)" %><br /> <%= f.text_field :tag_list %> </div>
ではこれを試してみましょう。記事を編集して新しいフィールドにいくつかのタグをカンマ区切りで入力すると、後で再度同じ記事を編集するときにはこれらのタグがリスト表示されているので、ここまでのところはうまく動いているようです。
タグ編集用に自動補完機能を付加したい場合はエピソード102を参照してください。その詳細はここでは省略します。次に各記事のタグをそれぞれの本文の下に追加します。これを行なうもっとも簡単な方法は、各記事ごとのtag_list
を表示する方法です。
<h1>Articles</h1> <div id="articles"> <% @articles.each do |article| %> <h2><%= link_to article.name, article %></h2> <%= simple_format article.content %> <p>Tags: <%= article.tag_list %></p> <p><%= link_to "Edit Article", edit_article_path(article) %></p> <% end %> </div> <p><%= link_to "New Article", new_article_path %></p>
各記事のタグがindex
ページに表示されました。
タグがリンクになっていてクリックするとそのタグを持つ記事だけが表示されるようになっていたら便利でしょう。タグリストを設定したときにはタグをカンマで区切った単独の文字列として設定したのですが、実際にはtag_list
属性は文字列の配列を返します。このタグの配列を、次のようなリンクのセットに変換できます。
<p>Tags: <%= article.tag_list.map { |t| link_to t, tag_path(t) }.join(', ') %></p>
タグのためのルートがないので設定します。すべてのタグのresourceを設定することもできますが、シンプルに保つために単一のルートを設定します。これは:tag
という引数をとって、記事のindexページにマッピングします。
Blog::Application.routes.draw do get 'tags/:tag', to: 'articles#index', as: :tag resources :articles root to: 'articles#index' end
ページをリロードすると、タグ部分のHTMLがエスケープされているので、うまく動作しません。これを修復するには、このコードをエスケープされないようにraw
メソッドを介して渡します。このメソッドを使用しているときは、ビュー層で少し複雑なことをしているというサインなので、ヘルパーメソッドへ移すことを検討したほうがいいでしょう。ここではそれは行ないませんが、頭の片隅には入れておいてください。
<p>Tags: <%= raw article.tag_list.map { |t| link_to t, tag_path(t) }.join(', ') %></p>
ページをリロードすると、各タグのリンクができていてそれぞれが正しいパスを指定しています。しかし、タグをクリックしてもまだ記事リストにフィルタがかかりません。
これはArticlesController
のindexアクションで簡単に行なうことができます。:tag
パラメータをチェックして、見つかったらそのタグでリストをフィルタリングします。gemにはtagged_with
というメソッドがあり、ある特定のタグが付いた記事を検索することができます。
def index if params[:tag] @articles = Article.tagged_with(params[:tag]) else @articles = Article.all end end
ページをリロードすると、記事は選択したタグでフィルタがかけられています。
タグクラウドを追加する
タグリストが動作するようになりましたが、すべてのタグを見たい場合にはどうすればいいでしょうか? よく利用されるのがタグクラウドを使用する方法で、すべてのタグを人気度によってサイズを変えて表示させます。この機能をindexテンプレートの最上部の近くに追加します。gemが提供するtag_cloud
メソッドがあるのでこの作業はとても簡単です。
<div id="tag_cloud"> <% tag_cloud Article.tag_counts, %w{s m l} do |tag, css_class| %> <%= link_to tag.name, tag_path(tag.name), class: css_class %> <% end %> </div>
このメソッドは2つの引数をとります。1つ目は表示させたいタグのセットです。これを簡単に取得するには、Article.tag_counts
を呼び出すと、タグのリストを表示し、それぞれが使用された回数を表示します。この情報は各タグの大きさを決めるのに必要です。二つ目に渡す引数は、複数種のサイズとして使用するためのCSSクラスの配列です。またこのメソッドはブロックをとり、そこにtag
オブジェクトとそれと一致するCSSクラスが渡されます。このブロックの中でタグを描画して、tagオブジェクトの名前へのlink_to
、 tag_path
(URLでタグ名を使いたいので、これにもtag名を渡します)、CSSクラスを渡します。タグクラウド用のスタイルをまだ定義していなかったので、ここで追加します。
#tag_cloud { width: 400px; line-height: 1.6em; .s { font-size: 0.8em; } .m { font-size: 1.2em; } .l { font-size: 1.8em; } }
記事のページをリロードすると、追加した2つのタグを含むタグクラウドが表示されます。記事にいくつかタグを追加してページをリロードすると、見た目がきれいになったタグクラウドが表示され、そこにはタグがついた記事の数によってサイズが異なるいくつかのタグが含まれています。タグをクリックすると、そのタグが付いた記事だけを表示するようフィルタがかかります。
その他にも、いろいろなことができます。現在のタグをハイライト表示したり、タグを名前順にソートしたりできますが、ここではそれは行ないません。
ゼロからタグ機能を実装する
acts-as-taggable-on gem
を利用していろいろなことができましたが、同じようなものをゼロから作った場合はどれくらいの作業が必要になるでしょうか? これを行なう前に、追加した機能のいくつかを元に戻します。まずデータベースに対しておこなった変更を取り消し、マイグレーションを削除します。
$ rake db:rollback $ rails d acts_as_taggable_on:migration
次にGemfileからgemを削除して、bundleコマンドを再度実行します。ようやくこれで、タグ機能をゼロから作り直す準備ができました。まず最初にTag
モデルを作成し、name
のカラムを付加します。
$ rails g model tag name
次にTagging
モデルを作成し、これをTag
とArticle
の両方に属する(belongs_to)ように設定します。
$ rails g model tagging tag:belongs_to article:belongs_to
gemが内部でおこなっているのと同じようにポリモーフィック関連を設定することもできますが、簡単に設定するために、ここではArticle
モデルに直接リンクします。そしてrake db:migrate
を実行して新規テーブルを追加します。次にモデルに関連を設定していきます。まず最初はTag
です。
class Tag < ActiveRecord::Base attr_accessible :name has_many :taggings has_many :articles, through: :taggings end
acts_as_taggable
の機能を置き換えるためには、Article
モデルをもう少し複雑にする必要があります。タグを管理するための関連といくつかのメソッドを追加します。
class Article < ActiveRecord::Base attr_accessible :content, :name, :tag_list has_many :taggings has_many :tags, through: :taggings def self.tagged_with(name) Tag.find_by_name!(name).articles end def self.tag_counts Tag.select("tags.*, count(taggings.tag_id) as count"). joins(:taggings).group("taggings.tag_id") end def tag_list tags.map(&:name).join(", ") end def tag_list=(names) self.tags = names.split(",").map do |n| Tag.where(name: n.strip).first_or_create! end end end
まずtaggingsとの関連とtaggingsを介したtagsとの関連を設定し、Article
モデルに対してアプリケーションの他の場所から呼び出すための4つのメソッドを実装します。tagged_with
メソッドは名前が一致したタグを持つ記事を返し、tag_counts
メソッドはタグとそのカウント数を返します。(このメソッドはTag
モデルに置いた方がいいかも知れません。) またtag_list
のgetterとsetterのメソッドも作りますが、getterが配列ではなく文字列を返すという違いがあります。このため、現状index
テンプレートがtag_list
を配列として処理してしまうのでこれを修正します。article.tags
を呼び出してname
属性をマップさせることで、gemと似た振る舞いになるようにします。
<%= raw article.tags.map(&:name).map { |t| link_to t, tag_path(t) }.join(', ') %>
最後に書くのがtag_cloud
メソッドです。これをApplicationHelper
モジュールに書きます。
module ApplicationHelper def tag_cloud(tags, classes) max = tags.sort_by(&:count).last tags.each do |tag| index = tag.count.to_f / max.count * (classes.size - 1) yield(tag, classes[index.round]) end end end
このメソッドはとてもシンプルで、gemのメソッドと同じ引数を取ります。ここでもっとも件数が多いタグを探して、他のタグの相対的な出現頻度を決めて、それぞれで使用するクラスを決定します。これでおおよそ同じ機能をゼロから作ることができました。記事にいくつかのタグを追加してから一覧に戻ると、記事の横にタグが表示され、クラウドにも追加されているのがわかります。
今回の方法では、コードの大部分はArticle
モデルにあります。この機能を実装するのにそれほど多くのコードを書かずに済み、アプリケーションに依存しすぎていることもありません。gemの方には、スクラッチで開発したバージョンには加えることができなかった機能がいくつかあります。例えばモデルにタグを作成するときに違うコンテキストを与えることができる点などです。gem版では所有者を記録することができるので、例えばUserをタグを付けた人としてモデルの記録に残すこともできます。
シンプルなタグ付けのソリューションが欲しければスクラッチで開発するのも一つの案です。一方フル機能のソリューションを求めるのであれば、gemを利用する方がいいでしょう。