Hatena::Diary

篳篥日記 このページをアンテナに追加 RSSフィード

2008-11-29 Railsで作るTwitterもどき (4)

[] Railsで作るTwitterもどき (コントローラ編) 01:25  Railsで作るTwitterもどき (コントローラ編) - 篳篥日記 のブックマークコメント

これまでの流れ:

Railsで作るTwitterもどき (1) モデル編他

Railsで作るTwitterもどき (2) URL設計

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

Tweetsコントローラにもフィルタ設定。

発言の削除をするアクション(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で実装することにする。

図のようなリンクを用意して、クリックするたびにフォローしたり、フォロー解除したりする。

f:id:hichiriki:20081130004910p:image

htmlですごくおおざっぱに書くと

<a href="#" id="follow">フォローする</a>

みたいな感じ(と言われても困ると思うが、Ajax用にidを振ったということが重要)。


では実装。Ajaxから呼び出されるメソッドには、接頭辞としてajax_みたいなものを付けておくと後から分かりやすい。

RailsAjaxだと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じゃないのだが、これを簡単にしようとすると少し厄介なので今回はパス。


さて、あとはビューの実装を残すのみだ。

Railsで作るTwitterもどき (5)に続く。