ユーザーを更新する
ユーザーの編集ビュー
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
</div>
</div>
上記のHTMLソースを見てみると
<form accept-charset="UTF-8" action="/users/1" class="edit_user"
id="edit_user_1" method="post">
<input name="_method" type="hidden" value="patch" />
.
.
.
</form>
WebブラウザはネイティブではPATCHリクエストを送信できないので、RailsではPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを「偽造」している。
また、ユーザー編集(edit)のHTMLはユーザー新規登録(new)のコードと完全に一緒です。Rails では、ユーザーが新規なのか、それとも既存のユーザーであるのかをActive Recordのnew_record?メソッドを使って区別しています。
$ rails console
>> User.new.new_record?
=> true
>> User.first.new_record?
=> false
form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOST、falseのときはPATCHを使っています。
noopener
target="_blank"の脆弱性対策として、一緒にrel="noopener"もつける。
編集時のテスト
編集の成功と失敗時のテスト
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "unsuccessful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
patch user_path(@user), params: { user: { name: "",
email: "foo@invalid",
password: "foo",
password_confirmation: "bar" } }
assert_template 'users/edit'
assert_select "div.alert", "The form contains 4 errors."
end
test "successful edit" do
get edit_user_path(@user)
assert_template 'users/edit'
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
また、編集時にパスワードを空にしておくと、パスワードのバリデーションに引っかかってしまうので、
allow_nil: trueというオプションをvalidatesに追加する。
class User < ApplicationRecord
attr_accessor :remember_token
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
.
.
.
end
上記のようにすると、新規ユーザー登録時に空のパスワードが有効になってしまうかと思われるかもしれないが、has_secure_passwordでは(追加したバリデーションとは別に)オブジェクト生成時に存在性を検証するようになっているため、空のパスワードが新規ユーザー登録時に有効なることはない。
フレンドリーフォワーディング
フレンドリーフォワーディングとは、ユーザーがログインした後、ログイン直前に閲覧していたページへリダイレクトさせる機能。
フレンドリーフォワーディングのテスト
require 'test_helper'
class UsersEditTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
.
.
.
test "successful edit with friendly forwarding" do
get edit_user_path(@user) # ユーザー編集ページにアクセス
log_in_as(@user) # ログイン
assert_redirected_to edit_user_url(@user) # ログイン直前にアクセスしたページにリダイレクトされているか確認
name = "Foo Bar"
email = "foo@bar.com"
patch user_path(@user), params: { user: { name: name,
email: email,
password: "",
password_confirmation: "" } }
assert_not flash.empty?
assert_redirected_to @user
@user.reload
assert_equal name, @user.name
assert_equal email, @user.email
end
end
フレンドリーフォワーディングの実装
module SessionsHelper
.
.
.
# 記憶したURL (もしくはデフォルト値) にリダイレクト
def redirect_back_or(default)
redirect_to(session[:forwarding_url] || default)
session.delete(:forwarding_url)
end
# アクセスしようとしたURLを覚えておく
def store_location
session[:forwarding_url] = request.original_url if request.get?
end
end
store_locationメソッド
リクエストが送られたURLをsession変数の:forwarding_urlキーに格納する。
ただし、GETリクエストの場合のみ。
redirect_back_orメソッド
session変数に格納されているURLがnilでなければ、session[:forwarding_url]を使い、そうでなければデフォルトのURLを使う。
この場合、redirect文を実行しても、次のセッション削除もされます。明示的にreturn文やメソッド内の最終行が呼び出されない限り、リダイレクトは発生しない。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
def edit
end
.
.
.
private
def user_params
params.require(:user).permit(:name, :email, :password,
:password_confirmation)
end
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
store_location ⇦追加 GETリクエストが送られたURLを格納
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
redirect_to(root_url) unless current_user?(@user)
end
end
class SessionsController < ApplicationController
.
.
.
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user ⇦追加
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
サンプルユーザー作成
Faker gemを使い、テストユーザーを一気に作成する。
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
gem 'faker', '1.7.3'
.
.
.
$ bundle install
サンプルユーザーを生成するRubyスクリプトを追加する。
Rails ではdb/seed.rbというファイルを標準として使う。
User.create!(name: "Example User",
email: "example@railstutorial.org",
password: "foobar",
password_confirmation: "foobar")
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)
end
create!は基本的にはcreateメソッドと同じですが、ユーザーが無効な場合に falseを返すのではなく、
例外を発生させます。こうしておくことで、見過ごしやすいエラーを回避できます。
Railsタスクを実行(db:seed)
$ rails db:migrate:reset
$ rails db:seed
ページネーション
Gemfileにwill_paginate gemとbootstrap-will_paginate gemを追加する。
source 'https://rubygems.org'
gem 'rails', '5.1.6'
gem 'bcrypt', '3.1.12'
gem 'faker', '1.7.3'
gem 'will_paginate', '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
.
.
$ bundle install
ビューにwill_paginateメソッドを追加する。
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
will_paginateメソッドの特性として、usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成します。
paginateを使うことで、ユーザー一覧のページネーションを行えるようになります。
paginateメソッドの引数にpageパラメータにはparams[:page]が使われているが、これはwill_paginateによって自動的に生成されている。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:index, :edit, :update]
.
.
def index
@users = User.paginate(page: params[:page])
end
.
.
end
ユーザー一覧のテスト
ページネーションを確認するのに多くのユーザーが必要なので、fixtureにユーザーを追加する。
michael:
name: Michael Example
email: michael@example.com
password_digest: <%= User.digest('password') %>
archer:
name: Sterling Archer
email: duchess@example.gov
password_digest: <%= User.digest('password') %>
lana:
name: Lana Kane
email: hands@example.gov
password_digest: <%= User.digest('password') %>
malory:
name: Malory Archer
email: boss@example.gov
password_digest: <%= User.digest('password') %>
<% 30.times do |n| %>
user_<%= n %>:
name: <%= "User #{n}" %>
email: <%= "user-#{n}@example.com" %>
password_digest: <%= User.digest('password') %>
<% end %>
ページネーションを含めた一覧ページのテスト
require 'test_helper'
class UsersIndexTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
end
test "index including pagination" do
log_in_as(@user)
get users_path
assert_template 'users/index'
assert_select 'div.pagination', count: 2
User.paginate(page: 1).each do |user|
assert_select 'a[href=?]', user_path(user), text: user.name
end
end
end
一覧ページのリファクタリング
元のテンプレート
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
<% end %>
</ul>
<%= will_paginate %>
ユーザーのliをrender呼び出しに置き換える
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<% @users.each do |user| %>
<%= render user %>
<% end %>
</ul>
<%= will_paginate %>
ここでは、renderをパーシャル(ファイル名の文字列)に対してではなく、Userクラスのuser変数に対して実行しています。
この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しに行くので、下記のパーシャルを作成する必要があります。
<li>
<%= gravatar_for user, size: 50 %>
<%= link_to user.name, user %>
</li>
さらに、renderを@users変数に対して直接実行します。
<% provide(:title, 'All users') %>
<h1>All users</h1>
<%= will_paginate %>
<ul class="users">
<%= render @users %>
</ul>
<%= will_paginate %>
Railsは@usersをUserオブジェクトのリストであると推測します。
さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力します。