Rails Tutorial 第二版(Rails 4.0)の機能を拡張する1(返信機能)


Ruby on Rails Tutorial の第二版11.4.1にある、「サンプルアプリケーションの機能を拡張する」を実践しました。今回は返信機能の拡張です。最新の第三版ではなく第二版なのは、自分の会社がRspecを用いているからです。

[Ruby on Rails チュートリアル：11.4.1](http://railstutorial.jp/chapters/following-users?version=4.0#sec-replies)

# 仕様

あくまでも演習ということで、 あまり現実的とは言えませんが、 以下のような仕様としました。

1. マイクロポスト入力中に@記号に続けてidとユーザー名を組み合わせたユニークなフレーズを入力すると、 そのマイクロポストは返信となる。例えば、idが1でユーザー名が"Example User"なら、@1-Example-Userとする(スペースはハイフンで置換)。
2. 返信扱いのマイクロポストは、 マイクロポストの送信者(自分)と受信者(@1-Example-Userのように指定したユーザー)のフィードにのみ表示される。
3. 返信先として指定できるのは、 一つのマイクロポストにつき一人のみ(@1-Example-Userのようなフレーズを複数入力しても、一つ目のユーザーにしか返信されない)。


# 実装

## Micropostsへのカラムの追加

各マイクロポストに、 返信先のユーザーを指定するためのカラムが必要なので、追加する。"rails g migration AddInReplyToToMicropost"を実行。マイグレーションスクリプトを以下のように変更し、 "rake db:migrate"。

```rb
class AddInReplyToToMicroposts < ActiveRecord::Migration
  def change
  	add_column :microposts, :in_reply_to, :string, default: ""
  end
end
```

カラム名は"in_reply_to"、defaultは""とした。

## in_reply_toカラムへの保存

Micropostモデルにbefore_saveを設定し、 Micropostのcontentから返信先ユーザーを表すフレーズを抽出し、 in_reply_toカラムに入力する。

```rb:app/model/micropost.rb
class Micropost < ActiveRecord::Base
  ・
  ・
  before_save { in_reply_to = reply_user.to_s }
  ・
  ・
  ・
  def reply_user
    if user_unique_name = content.match(/(@[^\s]+)\s.*/)
      user_unique_name[1]
    end
  end
end
```

## scopeの追加

任意のマイクロポストの"in_reply_to"カラムに返信先ユーザーを表すフレーズ(@1-Example-Userの形式)が入っている場合、 そのマイクロポストは返信先ユーザーおよび送信者(自分)からしか見えないようにする必要がある。例えば、@1-Example-Userというフレーズがcontentに入っていたら、 そのマイクロポストはid: 1、 name: "Example User"の属性を持つユーザーに対する返信となり、 自分と返信先ユーザーのフィードにのみ表示される。この機能を実現するため、 Micropostモデルに以下のようなscopeを追加する。

```rb:app/model/micropost.rb
class Micropost < ActiveRecord::Base
  ・
  ・
  scope :including_replies, ->(user){where("in_reply_to = ? OR in_reply_to = ? OR user_id = ?", "", "@#{user.id}\-#{user.name.sub(/\s/,'-')}", user.id)}

  ・
  ・
end
```

## scopeによるフィードの絞り込み

上で実装したscopeをstatic_pages_controllerで使用する。それに伴って、 User.feedメソッドも以下のように変更。

```rb:app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
  def home
  	if signed_in?
	  	@micropost = current_user.microposts.build
			
      # 元のコード
      # @feed_items = current_user.feed.paginate(page: params[:page])
      
      # 変更後のコード
      @feed_items = current_user.feed(current_user).paginate(page: params[:page])
	  end
  end
  ・
  ・
  ・
  ・
end
```

```rb:app/model/user.rb

class User < ActiveRecord::Base
  ・
  ・
  ・
  ・
  ・
  def feed(user)
    Micropost.including_replies(user).from_users_followed_by(self)
  end
  ・
  ・
  ・
end
```

これで、簡易的な返信機能が実装できました。

# おまけ

以下、 RSpecでの簡単なテストです。

```rb:spec/requests/static_pages_spec.rb

describe "Static pages" do

  subject { page }
  describe "Home page" do
  ・
  ・
  ・
    describe 'reply micropost' do
      # 送信者としてのユーザーを作成
      let(:user){FactoryGirl.create(:user)}
      # 返信先のユーザーを作成
      let(:reply_user){FactoryGirl.create(:user)}
      # 第三者のユーザーを作成
      let(:other_user){FactoryGirl.create(:user)}
      before do
        # 送信者から、返信先ユーザーに向けたマイクロポストを投稿
        FactoryGirl.create(:micropost, user: user, content: "@#{reply_user.id}\-#{reply_user.name.strip.sub(/\s/,'-')}\nhello world")
        reply_user.follow!(user)
      end
      #送信者からはマイクロポストがフィードに表示される
      describe 'at user' do
        before do
          sign_in user
          visit root_path
        end
        it { should have_content("hello world") }
      end
      #送信先ユーザーにもマイクロポストがフィードに表示される
      describe 'at reply_user' do
        before do
          sign_in reply_user
          visit root_path
        end
        it { should have_content("hello world") }
      end
      #第三者ユーザーにはマイクロポストがフィードに表示されない 
      describe 'at other_user' do
        before do
          sign_in other_user
          visit root_path
        end
        it { should_not have_content("hello world") }
      end
    end
    ・
    ・
    ・
```