Usersコントローラの編集
Usersコントローラにアクションを追加し、ユーザー情報の編集やユーザー一覧の表示、ユーザーの削除を行えるようにする。
ユーザー情報の編集
editアクションと編集フォーム
Usersコントローラにeditアクションを追加する。
editアクションに対応するURLはusers/:id/edit(名前付きルートはedit_user_path(user))なので、params[:id]を使えば編集したいユーザーを取得できる。
def edit
@user = User.find(params[:id])
end
editアクションに対応するeditビューは以下のようになる。
<% 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>
これはユーザー新規登録フォームとほぼ同じである。
編集フォームのform_forは以下のような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>
form_forのコードは新規登録フォームと同じなのに、各属性が更新用になっているのは、Railsが新規ユーザーであるか既存ユーザーの編集であるかを自動で判別してくれるからである。
なお、gravatar用のaタグについているtarget="_blank"とrel="noopener"は、リンク先を別のタブで開くためのものである(rel属性はセキュリティ上の問題を解決するためのもの)。
editアクションとビューができたので、レイアウトのヘッダー部分にリンクを貼っておく。
<% if logged_in? %>
<li><%= link_to "Users", '#' %></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li>
<li><%= link_to "Log out", logout_path, method: :delete %></li>
</ul>
</li>
<% else %>
editページとsignupページのパーシャル
ユーザー編集フォームと新規登録フォームは、フォームの送信ボタンの文字と、gravatarのリンクしか違いがないので、パーシャルにまとめる。
<%= form_for(@user, url: yield(:url)) do |f| %>
<%= render 'shared/error_messages', object: @user %>
<%= 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 %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>
signupビュー
<% provide(:title, 'Sign up') %>
<% provide(:url, signup_path) %>
<% provide(:button_text, 'Create my account') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
</div>
</div>
editビュー
<% provide(:title, 'Edit user') %>
<% provide(:url, user_path) %>
<% provide(:button_text, 'Save changes') %>
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= render 'form' %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="http://gravatar.com/emails" target="_blank">Change</a>
</div>
</div>
</div>
ここでは、2つのprovideメソッドとyieldを使っている。
一つはform_forの送信先である。
新規登録フォームではcreateアクションに送信するので、signup_pathとする。
編集フォームではupdateアクションに送信するので、user_pathとする(引数(user)は不要)。
もう一つはフォーム送信ボタンの文字である。
編集の失敗
編集失敗時の処理
更新フォームの送信先であるupdateアクションを書く。
def edit
@user = User.find(params[:id])
end
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
# 更新に成功した場合を扱う。
else
render 'edit'
end
end
ユーザーを取得した後update_attributesでその属性を更新する。
更新に失敗した場合は編集画面に戻る。
編集失敗時のテスト
ユーザー編集用の統合テストを作成する。
$ rails generate integration_test users_edit
ユーザー情報の更新に失敗した場合のテストを書く。
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'
end
end
updateアクションにはPATCHリクエストを送信する点に注意する。
編集の成功
編集成功時の処理
編集に成功した場合は、新規登録のcreateアクションと同様に、フラッシュメッセージを表示してユーザー表示ページに移動する。
def update
@user = User.find(params[:id])
if @user.update_attributes(user_params)
flash[:success] = "Profile updated"
redirect_to @user
else
render 'edit'
end
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
変更したいnameやemailを変数に入れているのは、編集完了後のUserオブジェクトの各属性と比較するためである。
assert_equalでエラーが出れば、編集に成功したはずなのにnameやemailが変わっていないことになる。
ここでテストはREDになるのだが、それはパスワードが有効な値でないためである。
パスワードの変更は必須ではないため、Userモデルのパスワードのバリデーションにallow_nil: trueを追加して、この例外に対応する。
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
テストがGREENとなることを確認する。
ユーザーの認可
認可
ユーザーの編集機能が完成したが、この状態では誰もが任意のユーザー情報を変更できてしまう。
そこで、認可(authorization)機能を追加して、特定のページへのアクセスを制限する。
before_actionとlogged_in_userメソッド
before_acitonメソッドを使うことで、各アクションの実行前に特定の操作を行うことができる。
ログインしていないとユーザー情報を編集できないようにする。
editアクションとupdateアクションの前にログインしているかを判定して、ログインしていなければログインページにリダイレクトするlogged_in_userメソッドを定義する。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
.
.
.
private
.
.
.
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
end
before_actionでは、アクション前に実行するメソッドをハッシュの形(:logged_in_user)で指定する。
これで、ログインしていないユーザーだとeditページにアクセスできなくなる。
また、これが原因でテストがREDになるので修正する。
各テストの前にlog_in_asヘルパーメソッドを使ってログインしておく。
test "unsuccessful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
test "successful edit" do
log_in_as(@user)
get edit_user_path(@user)
.
.
.
end
logged_in_userのテスト
logged_in_userのテストを書く。
test "should redirect edit when not logged in" do
get edit_user_path(@user)
assert_not flash.empty?
assert_redirected_to login_url
end
test "should redirect update when not logged in" do
patch user_path(@user), params: { user: { name: @user.name,
email: @user.email } }
assert_not flash.empty?
assert_redirected_to login_url
end
updateアクションでは、PATCHリクエストでユーザー情報を送信する。
テストの前後でbefore_actionをコメントアウトして、正しくテストできているかを確認しておく。
正しいユーザーを要求する
ユーザーが他のユーザー情報を編集できないようにするために、correct_userメソッドを作成し、before_actionでedit、updateアクションに設定する。
class UsersController < ApplicationController
before_action :logged_in_user, only: [:edit, :update]
before_action :correct_user, only: [:edit, :update]
.
.
.
# beforeアクション
# ログイン済みユーザーかどうか確認
def logged_in_user
unless logged_in?
flash[:danger] = "Please log in."
redirect_to login_url
end
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
unless @user == current_user
flash[:danger] = "You have no authority for the page"
redirect_to(root_url)
end
end
end
このメソッドに該当するのはログイン済みのユーザーなので、ログインページではなくルートURLにリダイレクトする。
(ちなみに、チュートリアルではここでフラッシュメッセージを入れてないせいで後のテストがREDになるので勝手に入れてます。)
慣習として、unless @user == current_userの部分をcurrent_user?というヘルパーメソッドにしておく。
# 渡されたユーザーがログイン済みユーザーであればtrueを返す
def current_user?(user)
user == current_user
end
# 正しいユーザーかどうか確認
def correct_user
@user = User.find(params[:id])
unless current_user?(@user)
flash[:danger] = "You have no authority for the page"
redirect_to(root_url)
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') %>
テストを書く。
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect edit when logged in as wrong user" do
log_in_as(@other_user)
get edit_user_path(@user)
assert flash.empty?
assert_redirected_to root_url
end
test "should redirect update when logged in as wrong user" do
log_in_as(@other_user)
patch user_path(@user), params: { user: { name: @user.name,
email: @user.email } }
assert flash.empty?
assert_redirected_to root_url
end
log_in_asメソッドを使って別のユーザーでログインすることと、ルートURLにリダイレクトすること以外は、非ログイン時のテストと同じである。