2008-11-29 Railsで作るTwitterもどき (4)
■[Rails] Railsで作るTwitterもどき (コントローラ編)
これまでの流れ:
Railsで作るTwitterもどき (1) モデル編他
Railsで作るTwitterもどき (3) 下準備 2
とりあえず、ここで作る機能としては以下のものを考えておく。
- 自分のホームを表示 (発言用フォーム、自分の発言と、自分がフォローしている人の発言の表示)
- 他人のページを表示 (その人の発言のみ表示)
- 発言
- 発言削除
- フォロー、フォロー解除
- 「あなたがフォロー」を表示
- 「あなたをフォロー」を表示
- 公開つぶやき表示
- 個別発言表示
コントローラ編と言いつつ、まずはモデルの修正から。
つぶやきを並べて表示する際には、最新のものを上に表示することになる。これは自分のページでも他人のページでも同じ。has_manyの関連にcreated_at descを入れても良いけど、ここではRails2.1からの機能であるnamed_scopeを使ってみる。
# app/model/tweet.rb class Tweet < ActiveRecord::Base belongs_to :user has_many :favoriteships, :dependent => :destroy has_many :favorite_users, :through => :favoriteships, :source => :user named_scope :order, :order => "tweets.created_at desc" # 追加 validates_length_of :body, :within => 1..200 # 追加 end
JOINされることを見越して、named_scopeにはテーブル名も入れておいた方が良い。これでfindする際に :order => "created_at desc" といちいち書かなくて良くなる。
ついでにvalidationも追加しておいた。
URL設計編で決めたroutesをもとに、ざっくりコントローラをgenerateしてみる。
$ ./script/generate controller Members home show friends followers $ ./script/generate controller Tweets show list
作成するコントローラは2つ。ユーザ関連のMembersと、つぶやき関連のTweets。アクションのひな形もgenerateの引数で作成。
まずはフィルタ関連から設定。
Membersコントローラは基本的にログイン必須。例外として /ユーザ名 とした時には、ログインしてなくてもその人の発言が見えるようにする(このアクション名はshow)。
また、パスにユーザ名を含むようなアクションは、下準備編2で作った set_user をbefore_filterで呼んで、@user を作るようにする。
# app/controllers/members_controller.rb class MembersController < ApplicationController before_filter :login_required, :except => [:show] before_filter :set_user, :only => [:show, :friends, :followers] # 略 end
発言の削除をするアクション(destroy)があるので、ここはログイン必須にしておく。また他人の個別発言を見せるページ(show)はパスにユーザ名を含むので(/ユーザID/tweets/ID)、これも set_userを呼んでおく。
# app/controllers/tweets_controller.rb class TweetsController < ApplicationController before_filter :login_required, :only => [:destroy] before_filter :set_user, :only => [:show] # 略 end
Tweetsコントローラの方が簡単なので、そちらから説明。
Tweetsコントローラでは、
- 個別発言の表示
- 発言削除
- 公開つぶやきの表示
を行うことにする。
個別発言表示。
/ユーザID/tweets/発言ID
# 個別発言表示 def show unless @tweet = @user.tweets.find(params[:id]) rescue nil render :text => "Not Found" and return end end
表示するつぶやきのIDはroutesの設定により、params[:id] で取り出される。もし該当するつぶやきが見つからなかったら Not Foundと表示するようにしてみた。
@user は set_user によるもの。Userモデルのインスタンスだ。@user.tweetsでその人の発言が取り出され、そこにidでfindすることにより、そのユーザの発言であることを保証する(これはGETメソッドなので、ユーザIDもつぶやきのIDもURL直打ちできてしまうための対策)。
@user.tweets # @user の発言全部 @user.tweets.find(params[:id]) # @user の発言のうち、idがparams[:id] であるもの。 @user.tweets.find(params[:id]) rescue nil # ActiveRecord::NotFound 例外を補足してnilを返す
発言削除。
/tweets/destroy/発言ID
# 発言削除 def destroy if tweet = current_user.tweets.find(params[:id]) rescue nil tweet.destroy end redirect_to home_path end
先ほどと同様に、発言をfindして単純にdestroy。見つからなければ何もしない。
この操作は、自分のつぶやきに対してしか行えないので、@userではなくて current_user.tweets とする。
current_user と書いてあるが、@current_user としても良い(@current_userは、destroyの before_filterに設定した login_requiredを呼ぶと作られるので)。
発言を削除できるのは、自分のホームだけにしておこうと思うので、削除し終わったら自分のホームにリダイレクトするようにしてみた。リダイレクト先が home_pathとできるのは、config/routes.rb にそのように書いてあるため。
公開つぶやき。
/public_timeline
PER_PAGE = 30 # 公開つぶやき一覧 def list @tweets = Tweet.order.paginate(:page => params[:page], :per_page => PER_PAGE, :include => :user) end
ここで named_scope 登場。Tweet.order となっているのがそれ。SELECT文に "ORDER BY tweets.created_at desc" が入るようになる。
返り値はそのままメソッドチェインで、will_paginateプラグインのメソッドである paginateに渡す。
1ページにいくつつぶやきを表示するかは、PER_PAGEとして定数にしておいた。
また、公開つぶやきでは、発言しているユーザ名なども併せて表示するため、:user を include (JOIN) している。
Tweetsコントローラの全貌はこんなかんじ。
# app/controllers/tweets_controller.rb class TweetsController < ApplicationController before_filter :login_required, :only => [:destroy] before_filter :set_user, :only => [:show] PER_PAGE = 30 # 個別発言表示 def show unless @tweet = @user.tweets.find(params[:id]) rescue nil render :text => "Not Found" and return end end # 公開つぶやき一覧 def list @tweets = Tweet.order.paginate(:page => params[:page], :per_page => PER_PAGE, :include => :user) end # 発言削除 def destroy if tweet = current_user.tweets.find(params[:id]) rescue nil tweet.destroy end redirect_to home_path end end
次に、Membersコントローラの実装に入る。
Membersコントローラでは、
- 自分のホームの表示
- 他人のホームの表示
- 発言
- フォロー、フォロー解除
- 「あなたがフォロー」を表示
- 「あなたをフォロー」を表示
これも簡単な所から実装していく。
発言。
/members/update
# 発言 def update @tweet = Tweet.new(params[:tweet]) current_user.tweets << @tweet if @tweet.valid? redirect_to home_path end
Ajaxで実装しても良かったが、ここでは普通にPOSTしておく。
params[:tweet] でつぶやき本文が送られて来ると仮定。
current_user.tweets << @tweet
で、自分の発言としてsaveされる。別に@tweet.valid? によるバリデーションは付けなくても良いのだが、念のため。
発言したら、ホームにリダイレクト。
「あなたがフォロー」
/friends (自分がフォローしている人々)
/ユーザID/friends (他人がフォローしている人々)
「あなたをフォロー」
/followers (自分のフォロワー)
/ユーザID/followers (他人のフォロワー)
これは、それぞれユーザの一覧が出て来るページ。
PER_PAGE = 30 # あなたがフォロー def friends @friends = @user.friends.paginate(:page => params[:page], :per_page => PER_PAGE) end # あなたをフォロー def followers @followers = @user.followers.paginate(:page => params[:page], :per_page => PER_PAGE) end
これもpaginateしている。
current_userではなくて、@userとしておくことで、自分のフォロワーでも他人のフォロワーでも使い回しが効く (自分の場合は @current_user == @user となるので)。
他人のページ。
/ユーザID
他人のつぶやき一覧が読めるページ。
# 他人のページ def show @tweets = @user.tweets.order.paginate(:page => params[:page], :per_page => PER_PAGE, :include => :user) end
named_scopeはTweetモデルに付いているので、@user.tweets.order という事が可能。
自分のページ。
/home
ここには、自分のつぶやきと、自分がフォローしているユーザのつぶやきが表示される。
# 自分のホーム def home @tweets = Tweet.order.paginate(:page => params[:page], :conditions => { :user_id => @current_user.friends.map(&:id) << @current_user.id }, :include => :user, :per_page => PER_PAGE) end
いままでのものより、ちょっと難しい。paginateに渡している :conditions に注目。
@current_user.friends # 自分がフォローしているユーザ @current_user.friends.map(&:id) # map(&:id) で id 列だけ切り出す。idが配列で返る。 @current_user.friends.map(&:id) << @current_user.id # 自分のidも足す # :conditions => { :user_id => [上記のidの配列] } とすることで、 # "user_id IN (1, 3, 25, 40, 2)" みたいなSQLが発行される。
つまり、自分がフォローしているユーザのIDと、自分のIDを同じ配列に入れてconditionsに渡している。
これにより、自分のつぶやきと、自分がフォローしているユーザのつぶやきをfindできる。
最後に、フォロー、フォロー解除。
これはAjaxで実装することにする。
図のようなリンクを用意して、クリックするたびにフォローしたり、フォロー解除したりする。
htmlですごくおおざっぱに書くと
<a href="#" id="follow">フォローする</a>
みたいな感じ(と言われても困ると思うが、Ajax用にidを振ったということが重要)。
では実装。Ajaxから呼び出されるメソッドには、接頭辞としてajax_みたいなものを付けておくと後から分かりやすい。
RailsのAjaxだとRJSテンプレートを使う方法もあるが、ほとんどの場合は、以下のようにコントローラにRJSを書いて事足りる。
# フォローする、フォロー解除 def ajax_toggle_follow if f = Friendship.find(:first, :conditions => { :user_id => @current_user.id, :friend_id => params[:id]}) # フォロー解除する f.destroy str = "フォローする" else # フォローする Friendship.create(:user_id => @current_user.id, :friend_id => params[:id]) str = "フォロー解除" end # RJS部分 render :update do |html| html.<< "$('follow').innerHTML = '#{str}'" end end
フォローするのと、フォロー解除するのが一緒のメソッド。
関連テーブルである friendships テーブルを見て、見つかったらフォローしてるということだから、destroyでフォロー解除。
見つからなかったら Freindship.create で関連テーブルのレコードを直接作る事によりフォローする。
最後に、インラインRJSで先ほどidを振ったリンクの、ラベル部分だけをJavaScriptで変更。
ということで、Membersコントローラは以下のようになる。
# app/controllers/members_controller.rb class MembersController < ApplicationController before_filter :login_required, :except => [:show] before_filter :set_user, :only => [:show, :friends, :followers] PER_PAGE = 30 # 自分のホーム def home @tweets = Tweet.order.paginate(:page => params[:page], :conditions => { :user_id => @current_user.friends.map(&:id) << @current_user.id }, :include => :user, :per_page => PER_PAGE) end # 他人のページ def show @tweets = @user.tweets.order.paginate(:page => params[:page], :per_page => PER_PAGE, :include => :user) end # あなたがフォロー def friends @friends = @user.friends.paginate(:page => params[:page], :per_page => PER_PAGE) end # あなたをフォロー def followers @followers = @user.followers.paginate(:page => params[:page], :per_page => PER_PAGE) end # 発言 def update @tweet = Tweet.new(params[:tweet]) current_user.tweets << @tweet if @tweet.valid? redirect_to home_path end # フォローする、フォロー解除 def ajax_toggle_follow if f = Friendship.find(:first, :conditions => { :user_id => @current_user.id, :friend_id => params[:id]}) f.destroy str = "フォローする" else Friendship.create(:user_id => @current_user.id, :friend_id => params[:id]) str = "フォロー解除" end render :update do |html| html.<< "$('follow').innerHTML = '#{str}'" end end end
コントローラの実装も無事終了。
どれも1行から数行のコードでアクションができている。
ORマッパである ActiveRecordを使っているので、コードにほとんどSQLが現れないことにも注目してほしい。
同じ引数をとる paginateが何回も出てきて、DRYじゃないのだが、これを簡単にしようとすると少し厄介なので今回はパス。
さて、あとはビューの実装を残すのみだ。