概要
この記事は、Rails学習のためにチュートリアル14.4.4の機能拡張にて、返信機能を追加した際の記録です。
実装仕様について
- マイクロポスト中に@記号に続けてユーザ名を入力するとユーザに返信できる。
- 返信は受信者と送信者のフィードにのみ表示される。
- ユーザ名の重なりはユーザ登録の項目に一意のユーザ名(ニックネーム)を追加することで対応する。
- ニックネームは半角英数字と’_’のみの入力を許可する(大文字小文字は区別しない)
- ニックネームがわかるように、フィードのユーザ名の表示はユーザ名+@ニックネームとする
- 複数リプライは考慮せず、1つ目の@記号のみ対応する。
実装
ニックネームの追加
仕様との対応は以下のとおり。
- ユーザ名の重なりはユーザ登録の項目に一意のユーザ名(ニックネーム)を追加することで対応する。
- ニックネームは半角英数字と’_’のみの入力を許可する(大文字小文字は区別しない)
###ユーザモデルにニックネームを追加
マイグレーションを作成した。
rails generate migration add_nickname_to_users nick_name:string
メモ:クラス名をadd_***to@@にすることで自動でカラム追加メソッドがスクリプトに記入される。
マイグレーションファイルにchange_column_null
(NOT NULL制約)とadd_index
(インデックスの追加と一意制約)を追加した。
class AddNicknameToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :nick_name, :string
change_column_null :users, :nick_name, false
add_index :users, :nick_name, unique: true
end
end
###ユーザモデルにニックネームを追加
チュートリアル6章を参考に以下のとおり追加した。
ニックネームは正規表現によるチェックをする。
#ニックネームは保存前に小文字にする
before_save :downcase_nick_name
#ユーザモデルにニックネームを追加
VALID_NICKNAME_REGEX = /\A[\w]+\z/i
validates :nick_name, presence:true, length:{maximum:15},
format: {with: VALID_NICKNAME_REGEX },
uniqueness: { case_sensitive: false }
・
・
・
# ニックネームを全て小文字にする
def downcase_nick_name
self.nick_name.downcase!
end
###単体テストの作成
テストモデルの各ユーザーにニックネーム属性を追加。
以下は例。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
admin: true
activated: true
activated_at: <%= Time.zone.now %>
nick_name: Michael
バリデーションテストにニックネームを追加した。
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar",
nick_name: “example" )
end
#ニックネームのバリデーションチェック
test "nick_name should be present" do
@user.nick_name = " "
assert_not @user.valid?
end
#ニックネームの長さチェック
test "nick_name should not be too long" do
@user.nick_name = "a" * 16
assert_not @user.valid?
end
#ニックネームのフォーマットチェック
test "nick_name validation should accept valid name" do
valid_nick_names = %w[user User1 U_SER U3 1 _U2 _ u]
valid_nick_names.each do |valid_nick_name|
@user.nick_name = valid_nick_name
assert @user.valid?, "#{valid_nick_name.inspect} should be valid"
end
end
test "nick_name validation should reject invalid names" do
invalid_nick_names = %w[,com .org user.name@
ユーザ obar+baz.com foobar..com]
invalid_nick_names.each do |invalid_nick_name|
@user.nick_name = invalid_nick_name
assert_not @user.valid?, "#{invalid_nick_name.inspect} should be invalid"
end
end
#ニックネーム一意性のチェック
test "nick_names should be unique" do
duplicate_user = @user.dup
duplicate_user.nick_name = @user.nick_name.upcase
@user.save
assert_not duplicate_user.valid?
end
#ニックネームが小文字で保存されることのチェック
test "nick_names should be saved as lower-case" do
mixed_case_nick_name = "FooExAMPle_CoM1"
@user.nick_name = mixed_case_nick_name
@user.save
assert_equal mixed_case_nick_name.downcase, @user.reload.nick_name
end
以下でモデルテストを実施した。
rails test:models
##ニックネーム登録画面
ニックネームはユーザ登録と同時に行うのでユーザ登録画面に入力項目を追加した。
###テストの作成
サインアップ時のニックネームのテストを作成した。
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post signup_path, params: { user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar",
nick_name: "" } } #ニックネーム追加
end
test "valid signup information with account activation" do
get signup_path
assert_difference 'User.count', 1 do
post users_path, params: { user: { name: "Example User",
email: "user@example.com",
password: "password",
password_confirmation: "password",
nick_name: “example" } } #ニックネーム追加
プロフィールの表示名のテストを作成した。
test "profile display" do
get user_path(@user)
assert_template 'users/show'
assert_select 'title', full_title(@user.name)
assert_select 'h1', text: "#{@user.name} @#{@user.nick_name}"
セッティング画面でニックネーム変更のテストを作成した。
test "unsuccessful edit" do
log_in_as(@user)
#ページにアクセス
get edit_user_path(@user)
#目的のページかどうか判定
assert_template 'users/edit'
#変更を送信(PATCHメソッドを使う)
patch user_path(@user), params: { user: { name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar",
nick_name: "" } }
#失敗時のページが目的のページかどうか判定
assert_template 'users/edit'
#エラーメッセージが表示されている
assert_select 'div.alert', 'The form contains 6 errors.'
test "successful edit" do
log_in_as(@user)
#ページにアクセス
get edit_user_path(@user)
assert_template 'users/edit'
#name and emailを変更
name = "Foo Bar"
email = "foo@bar.com"
nick_name = "foobar"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "",
nick_name: nick_name } }
#フラッシュメッセージがあることを確認
assert_not flash.empty?
#最新のユーザ情報を読み出す
assert_redirected_to @user
@user.reload
#変更結果が反映されているか確認
assert_equal name, @user.name
assert_equal email, @user.email
assert_equal nick_name, @user.nick_name
test "successful edit with friendly forwarding" do
get edit_user_path(@user)
assert_equal session[:forwarding_url], request.protocol + request.host + edit_user_path(@user)
log_in_as(@user)
assert_redirected_to edit_user_url(@user)
assert_nil session[:forwarding_url]
name = "Foo Bar"
email = "foo@bar.com"
nick_name = "foobar"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "",
nick_name: nick_name } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
assert_equal nick_name, @user.nick_name
インデックスページのニックネーム表示テストを作成した。
test "user nick_name test in index" do
log_in_as(@user)
get root_path
assert_select 'h1', text: "#{@user.name}@#{@user.nick_name}"
end
マイクロポストのニックネーム表示テストを作成した。
test "micropost interface" do
log_in_as(@user)
get root_path
・
・
・
# マイクロポストに@nick_nameがあるかチェック
assert_select 'h1', text: "#{@user.name} @#{@user.nick_name}"
###サインアップ画面の変更
サインアップにニックネームの入力項目を追加した。
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :nick_name %>
<%= f.text_field :nick_name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
user_params
メソッドでnick_name
の取得を許可した。
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation, :nick_name)
end
既存ユーザにニックネームを付与した。
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar",
admin: true,
activated: true,
activated_at: Time.zone.now,
nick_name: "example")
99.times do |n|
name = Faker::Name.name
email = "example-#{n+1}@railstutorial.org"
password = "password"
User.create!(name: name,
email: email,
password: password,
password_confirmation: password,
activated: true,
activated_at: Time.zone.now,
nick_name: "example_#{n+1}")
end
DBをリセットしてseedsを反映した。
rails db:migrate:reset
rails db:seeds
##ユーザ名の後ろに@nick_nameの表示を追加
仕様との対応は以下のとおり。
- ニックネームがわかるように、フィードのユーザ名の表示はユーザ名+@ニックネームとする
###ビューの変更
ユーザプロフィールビューに@nick_nameを追加した。
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %> @<%= @user.nick_name %>
ユーザ情報にも@nick_nameを追加する。
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %> @<%= current_user.nick_name %></h1>
マイクロポストにも追加する。
<li id="micropost-<%= micropost.id %>">
<%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
<span class="user"><%= link_to micropost.user.name, micropost.user %> @<%= micropost.user.nick_name %></span>
##マイクロポストモデルの変更
仕様との対応は下記のとおり。
- マイクロポスト中に@記号に続けてユーザ名を入力するとユーザに返信できる。
- 複数リプライは考慮せず、1つ目の@記号のみ対応する。
###返信先/送信元を追加
マイクロポストモデルに返信先・送信元を追加した。
rails generate migration add_reply_to_from_to_microposts reply_to:integer reply_from:integer
rails db:migrate
private
def micropost_params
params.require(:micropost).permit(:content, :picture, :reply_to, :reply_from)
end
###テストの作成
モデルのテストを作成した。
test "input reply_to id" do
#reply_toに宛先を入れる
reply_to = @user.microposts.build(content: "This is Reply Test", reply_to: @replyto.id, reply_from: @user.id)
#宛先が入っているか確認
assert_equal reply_to.reply_to, @replyto.id
assert_equal reply_to.reply_from, @user.id
end
##返信宛先の検出
仕様との対応は下記のとおり。
- マイクロポスト中に@記号に続けてユーザ名を入力するとユーザに返信できる。
- 複数リプライは考慮せず、1つ目の@記号のみ対応する。
###テストの作成
受信者と送信者が入力され、送信者のフィードに表示されるかを確認するテストを作成した。
def setup
@user = users(:michael)
@replyto = users(:archer)
@replyto2 = users(:lana)
end
test "micropost reply" do
log_in_as(@user)
get root_path
# 有効な送信
content = "@#{@replyto.nick_name} @#{@replyto2.nick_name} This micropost is test for reply."
post microposts_path, params: { micropost: { content: content } }
get root_path
#送信者に表示されているかチェック
assert_match @user.microposts.first.reply_to.to_s, @replyto.id.to_s
assert_match @user.microposts.first.reply_from.to_s, @user.id.to_s
assert_match content, response.body
end
###マイクロポストから宛先を検出
マイクロポスト作成時にニックネームを探し、受信者と送信者のid
を入力するようにコントローラを変更した。
def create
@micropost = current_user.microposts.build(micropost_params)
#正規表現のフォーマット作成
nickname_regex = /@([\w]{1,15})/i
#正規表現にマッチするものを探す
@micropost.content.match(nickname_regex)
#あったらニックネームからユーザを探す、なければ$1がnilになる
if $1
reply_user = User.find_by(nick_name: $1.downcase)
#ユーザがいたらカラムにセット
@micropost.reply_to = reply_user.id if reply_user
@micropost.reply_from = current_user.id
end
以下の記事を参考にさせて頂きました。
##リプライマイクロポストの表示制限
仕様との対応は以下のとおり。
- 返信は受信者と送信者のフィードにのみ表示される。
###テストの作成
送信者と受信者にのみ表示されていることのテストを作成した。
# 受信者に表示されているかチェック
log_in_as(@replyto)
get root_path
assert_match content, response.body
# 送信者のフォロワー/受信者のフォロワーに表示されていないかチェック
log_in_as(@replyto2)
get root_path
assert_no_match content, response.body
five:
follower: archer
followed: lana
###ユーザへの表示制限
マイクロポストの抽出条件を変更した。
def feed
#スケールしたステータスフィードを返す
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"
Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id",user_id: id)
.where(reply_to: [nil,""])
.or(Micropost.where(reply_to: id).or(Micropost.where(reply_from: id)))
end
##テスト
テストを実施してGREENになることを確認した。
rails test
#最後に
チュートリアルにはスコープを使うと書いてあったが理解できなかったので、実装を優先した。
この後、スコープを使った方法に修正したい。