#Ruby on Rails チュートリアルについて
Ruby on Railsを勉強したいというと、まず紹介される有名なRailsのチュートリアル。
内容はハードですが、無料でRailsによるWebアプリケーション開発を楽しく学べます。
Ruby on Rails チュートリアル
https://railstutorial.jp/
#Sample Appの拡張
チュートリアルの最後には、作成したSampleAppの拡張機能についていくつかのヒントが記載されています。
その中の以下の機能を順に実装していきます(途中で挫折するかも。。。)。
- ユーザー検索
- マイクロポスト検索
- フォロワーの通知
- 返信機能
- メッセージ機能
#返信機能
今回は、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メソッドの処理を記載します。
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ではなくクラスメソッドで定義するようにします。
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のコールバックで実施することにします。
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
でチェックを行います。
あと、:presence
をfalse
に設定しています。
まず、set_in_reply_to
を以下のように実装します。
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?
の実装は以下のとおりです。
def is_i?(s)
Integer(s) != nil rescue false
end
返信先のユーザーのIDの取得ができたので、次はバリデーションを実装します。
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
少し複雑ですが、以下の順番でチェックを行なっています。
- 返信先が指定されていない場合、チェックしない
- 指定したIDのユーザーが見つからない場合、エラーとする
- 自分自身に返信を行なった場合、エラーとする
- 指定したIDのユーザー名が間違っていた場合、エラーとする
エラーメッセージ追加時に指定している:base
は、エラーメッセージを表示する際に、属性名を表示しないようにできます。
reply_to_user_name_correct?
は、指定されたIDのユーザー名が正しいかをチェックするメソッドです。
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 "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 "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