##返信機能仕様
・返信先は@ID-usernameで本文のなかで指定する。(例、IDが1、User名が『suzu』だった場合、@1-suzuとなる。)
・返信は投稿者と返信先のユーザーのフィードのみ表示される。
・自分に対しての返信はできない。
・指定したIDが存在しない場合、投稿時にエラーが出る。
・指定したIDとそのIDのユーザー名が一致しない場合、投稿時にエラーとする。
##実装
・micropostsへのカラムを追加する。
・各投稿に返信先のユーザーを指定するためのカラムが必要になるので追加する。
#実行
rails g migration AddInReplyToToMicropost
・以下の変更を加える。
class AddInReplyToToMicropost < ActiveRecord::Migration[6.1]
def change
add_column :microposts, :in_reply_to, :string, default: ""
end
add_index :microposts,[:user_id, :created_at]
end
・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ではなくクラスメソッドで定義するようにします。
def Micropost.including_replies(id)
where(in_reply_to: [id,0]).or(Micropost.where(user_id:id))
end
・ユーザーのフィードに出す準備はできたので、本文から返信先を取得する処理を実装します。
before_validation :set_in_reply_to #ここを追加
belongs_to :user
default_scope -> { order(created_at: :desc) }
validates :user_id,presence: true
validates :content,presence: true,length:{ maximum: 300}
validates :in_reply_to,presence: false #ここを追加
validate :picture_size, :reply_to_user #ここを追加
・どのタイミングで返信先を取得しどのタイミングで返信先の内容をチェックするかを決めます。
・チェックは通常のvalidateで指定し、返信先の取得はvalidationが走る前にbefore_validationのコールバックで実施することにしました。
・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
#指定したIDユーザーが見つからなかった場合エラーにする。
unless user = User.find_by(id: self.in_reply_to)
errors.add(:base,"指定したユーザーIDが存在しません。")
else
#自分自身に返信を行った場合エラーにする
if user_id == self.in_reply_to
errors.add(:base,"自分に返信する事はできません。")
else
#指定したIDのユーザー名が間違っていた場合エラーにする
unless reply_to_user_name_correct?(user)
errors.add(:base,"ユーザーIDその名前と一致しません。")
end
end
end
end
・エラーメッセージ追加時に指定している:baseは、エラーメッセージを表示する際に属性名を表示しないようにできています。
def reply_to_user_name_correct?(user)
user_name = user.name.gusb(" ","-")
content[@index+2,user_name.length] == user_name
end
・指定されたIDのユーザー名を取得して、空白を『-」で埋めています。
・先ほど取得した@indexを2つ進めて、実際のユーザー名の長さだけ本文を抜き出し、比較しています。
・以上です。