20
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Ruby on Rails チュートリアル 機能拡張4(返信機能)

Last updated at Posted at 2018-01-19

#Ruby on Rails チュートリアルについて
Ruby on Railsを勉強したいというと、まず紹介される有名なRailsのチュートリアル。
内容はハードですが、無料でRailsによるWebアプリケーション開発を楽しく学べます。

Ruby on Rails チュートリアル
https://railstutorial.jp/

#Sample Appの拡張
チュートリアルの最後には、作成したSampleAppの拡張機能についていくつかのヒントが記載されています。
その中の以下の機能を順に実装していきます(途中で挫折するかも。。。)。

  1. ユーザー検索
  2. マイクロポスト検索
  3. フォロワーの通知
  4. 返信機能
  5. メッセージ機能

#返信機能
今回は、4つ目の返信機能の実装を行います。
返信機能は下記の仕様で作っていきます。

  • 返信先は、@ID-usernameで本文の中で指定する

    例: IDが1、User名が「Example User」の場合、@1-Example-Userとなる
  • 返信は、投稿者と返信先ユーザーのフィードにのみ表示される
  • 自分への返信はできない
  • 指定したIDが存在しない場合、マイクロポスト投稿時にエラーとする
  • 指定したIDとそのIDのユーザー名が一致しない場合、マイクロポスト投稿時にエラーとする

前回(フォロワーの通知)はこちら

##環境と準備
今回は、特に新たにインストールする必要のあるモジュールはありません。

モジュール バージョン
Rails 5.1.2
Ruby 2.3.1

##実装
まずRailsチュートリアルに記載してあるように、micropostsにin_reply_to属性を追加します。
in_reply_toには返信先のユーザーのIDを格納するためIntegerとし、デフォルトを0とします。
そのため、返信先が指定されていない場合、in_reply_toは0になります。
さらにindexを追加し、DBにマイグレートします。

そして、次もチュートリアルに記載のあるようにMicropostクラスにscope(including_replies)を追加します。
scopeを記述する前に、追加したscopeを利用した新しいfeedメソッドの処理を記載します。

app/models/user.rb
def feed
  following_ids = "SELECT followed_id FROM relationships
                   WHERE follower_id = :user_id"
  Micropost.including_replies(id)
           .where("user_id IN (#{following_ids})
                   OR user_id = :user_id", user_id: id)
end

上記のように、scopeの後にwhereで今までのfeedの処理に繋げられるようにscopeを定義します。
上で設定した仕様を満たすためには、

  • in_reply_toが0または自分のIDである(返信先の指定がないor返信先が自分)
  • user_idが自分のIDである(自分が投稿者)

のどちらかでなけらばならないとわかります。

あとはこれを実装すればいいね!
ってなるんですが、実はこちらにあるように、引数を取るscopeはクラスメソッドで定義することが推奨されています。

そのため、scopeではなくクラスメソッドで定義するようにします。

app/models/micropost.rb
def Micropost.including_replies(id)
    where(in_reply_to: [id, 0]).or(Micropost.where(user_id: id))
end

feedメソッドと合わせて2回もuser_idが自分のIDであることを確認していて凄く気持ち悪いですが、私の浅はかな知識ではこれ以外うまくいきませんでした。。。

さて、ユーザーのフィードに出す準備は整ったので、次に本文から返信先を取得する処理を実装します。

まずはどのタイミングで返信先を取得し、どのタイミングで返信先の内容をチェックするかを決めます。
チェックは通常のvalidateで指定するとして、返信先の取得はvalidationが走る前にしなければならないので、before_validationのコールバックで実施することにします。

app/models/micropost.rb
before_validation :set_in_reply_to # ここ

validates :user_id, presence: true
validates :content, presence: true, length: {maximum: 140}
validates :in_reply_to, presence: false
validate :picture_size, :reply_to_user # ここ

名前はよくないですが、set_in_reply_toで本文から返信先を抜き出し、reply_to_userでチェックを行います。
あと、:presencefalseに設定しています。

まず、set_in_reply_toを以下のように実装します。

app/models/micropost.rb
def set_in_reply_to
  if @index = content.index("@")
    reply_id = []
    while is_i?(content[@index+1])
      @index += 1
      reply_id << content[@index]
    end
    self.in_reply_to = reply_id.join.to_i
  else
    self.in_reply_to = 0
  end
end

処理は非常に単純で、本文中の@を探し、その位置から数字が続くだけ文字を取得し、最後に連結&Integerにキャストしています。
indexを@indexとインスタンス変数にしているのは、バリデーションの際にもこのindexを利用するためです。
また、数値のチェックに利用しているis_i?の実装は以下のとおりです。

app/models/micropost.rb
def is_i?(s)
  Integer(s) != nil rescue false
end

返信先のユーザーのIDの取得ができたので、次はバリデーションを実装します。

app/models/micropost.rb
def reply_to_user
  return if self.in_reply_to == 0 # 1
  unless user = User.find_by(id: self.in_reply_to) # 2
    errors.add(:base, "User ID you specified doesn't exist.")
  else
    if user_id == self.in_reply_to # 3
      errors.add(:base, "You can't reply to yourself.")
    else
      unless reply_to_user_name_correct?(user) # 4
        errors.add(:base, "User ID doesn't match its name.")
      end
    end
  end
end

少し複雑ですが、以下の順番でチェックを行なっています。

  1. 返信先が指定されていない場合、チェックしない
  2. 指定したIDのユーザーが見つからない場合、エラーとする
  3. 自分自身に返信を行なった場合、エラーとする
  4. 指定したIDのユーザー名が間違っていた場合、エラーとする

エラーメッセージ追加時に指定している:baseは、エラーメッセージを表示する際に、属性名を表示しないようにできます。
reply_to_user_name_correct?は、指定されたIDのユーザー名が正しいかをチェックするメソッドです。

app/models/micropost.rb
def reply_to_user_name_correct?(user)
  user_name = user.name.gsub(" ", "-")
  content[@index+2, user_name.length] == user_name
end

まず指定されたIDのユーザー名を取得し、空白を「-」で埋めています。
そして、先ほど取得していた@indexを2つ進め、実際のユーザー名の長さだけ本文を抜き出し、比較しています。
@indexを2つ進めるのは、@12-user-nameを指していたのを@12-user-nameのように進めるためです。

これで実装は終わりです。
短いような気がしますが、viewとcontrollerの変更が必要なかったためですね。

##テスト
最後にテストについて記載します。
今回は、

  • 返信投稿時の返信先の指定
  • 返信の表示範囲

をテストします。
テストは、チュートリアル内で作成した、microposts_inteface_test.rbに追記します。

まず、返信先の指定のテストです。

test/integration/microposts_inteface_test_test.rb
~
省略
~
test "reply to other user" do
    log_in_as(@user)
    get root_path

    # invalid post(ID doesn't exist)
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: {micropost: {content: "@1000000000000000000"}}
    end
    assert_select 'div#error_explanation'
    # invalid post(Reply to yourself)
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: {micropost: {content: "@#{@user.id}-Hoge-Hoge"}}
    end
    assert_select 'div#error_explanation'
    # invalid post(ID doesn't match its user name)
    other_user = users(:fuga)
    assert_no_difference 'Micropost.count' do
      post microposts_path, params: {micropost: {content: "@#{other_user.id}-Hogera-Hogera"}}
    end
    assert_select 'div#error_explanation'

    # valid post
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: {micropost: {content: "@#{other_user.id}-Fuga-Fuga"}}
    end
  end
~
省略
~

存在しないIDを指定した場合、自分自身への返信した場合、IDとユーザー名が一致しない場合、正しく返信先を指定した場合の投稿数の変化を確認しています。

次に、返信が投稿者と返信先のフィードのみに表示され、それ以外のユーザーのフィードには表示されないことをチェックします。

test/integration/microposts_inteface_test_test.rb
~
省略
~
test "reply post visibility" do
    log_in_as(@user)
    get root_path
    reply_to_user = users(:fuga)
    content = "@#{reply_to_user.id}-Fuga-Fuga"
    post microposts_path, params: {micropost: {content: content}}
    follow_redirect!
    assert_match content, response.body

    # should be visible
    log_in_as(reply_to_user)
    get root_path
    assert_match content, response.body

    # shouldn't be visible
    other_user = users(:piyo)
    log_in_as(other_user)
    get root_path
    assert_no_match content, response.body
  end

投稿者、返信先、他ユーザーの順番にフィードを確認しています。

両方のテストが通ったことを確認して終了です。

#最後に
私はまだRails歴2週間ほどであり、以上の実装/テストも私の環境で動いたということに過ぎません。
修正点や指摘等ございましたら、ぜひコメントお願いいたします。

#参考記事
返信機能では、下記のページを参考にさせていただきました。

Active Record クエリインターフェイス
https://railsguides.jp/active_record_querying.html#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97

Activeレコードのコールバック
http://ruby.studio-kingdom.com/rails/guides/active_record_callbacks

Validation error messages without attribute
https://stackoverflow.com/questions/6863307/validation-error-messages-without-the-attribute

20
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?