今日やったこと9
第10章 ユーザーの更新・表示・削除
10.1 ユーザーを更新する
ユーザーを編集するためのeditアクションを作成する。
def edit @user = User.find(params[:id]) end
editアクションに対応するeditビューを実装
app/views/users/edit.html.erb
WebブラウザはネイティブではPATCHリクエストを送信できないので、RailsはPOSTリクエストとinputフィールドを利用してPATCHリクエストを「偽造」している。
<input name="_method" type="hidden" value="patch" />
Railsは、form_for(@user)を使ってフォームを構成すると、@user.new_record?がtrueのときにはPOSTを、falseのときにはPATCHを使う。
$ rails console >> User.new.new_record? => true >> User.first.new_record? => false
ヘッダーに”Setting”リンクを追加する。
editビューへのパスとcurrent_userのヘルパーメソッドを使う。
<%= link_to "Settings", edit_user_path(current_user) %>
演習
・10.1.1.1
target="_blank"で新しいページを開くときには、セキュリティのためrel属性にnoopenerを設定する。
<a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
演習
・10.1.3.1
assert_selectを使ってalertクラスのdivタグを探しだし、「The form contains 4 errors.」というエラーメッセージが表示されているかテストする行を追加する。
assert_select "div.alert", "The form contains 4 errors."
Updateアクションを書くまえに編集の成功に対するテストを書く。
テストを先に書くことでユーザーが実際に使うイメージや追加するコードがわかりやすくなる。
class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end ・ ・ ・ # 編集の成功に対するテスト test "successful edit" do get edit_user_path(@user) assert_template 'user/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_redirect_to @user @user.reload assert_equal name, @user.name assert_equal email, @user.email end end
ユーザー名やメールアドレスだけを編集したいときはパスワード欄を入力する必要はないが空のままだとバリテーションに引っかかってしまうので空のままでも更新できるようにvalidatesにallow_nil: trueというオプションを追加する。
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
updateアクションを作成しテストする。
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
10.2 認可
今のままでは誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまうのでユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないようにする。
ログインしていないユーザーが保護されたページにアクセスしようとしたときはログインページに転送して、メッセージも表示するようにする。
許可されていないページに対してアクセスするログイン済みのユーザーがいたら 、ルートURLにリダイレクトさせるようにする。
転送させる仕組みを実装したいときは、Usersコントローラの中でbeforeフィルターを使う。beforeフィルターは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みである。
ユーザーにログインを要求するために、logged_in_userメソッドを定義してbefore_action :logged_in_userという形式で使用する。
デフォルトでは、beforeフィルターはコントローラ内のすべてのアクションに適用されるので、ここでは適切な:onlyオプション (ハッシュ) を渡すことで、:editと:updateアクションだけにこのフィルタが適用されるように制限をかけている。
app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # beforeアクション # ログイン済みユーザーかどうか確認 def logged_in_user unless logged_in? flash[:danger] = "Please log in." redirect_to login_url end end end
editアクションやupdateアクションをテストする前にログインしておく必要があるので、log_in_asヘルパーを使ってテストを修正する。
test/integration/users_edit_test.rb require 'test_helper' class UsersEditTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end 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 end
beforeフィルターが機能しているかどうかのテストを追加する。
test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end ・ ・ ・ # editアクションの保護に対するテスト test "should redirect edit when not logged in" do get edit_user_path(@user) assert_not flash.empty? assert_redirected_to login_url end # updateアクションの保護に対するテスト 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 end
ユーザーが自分の情報だけを編集できるようにする必要があるためのテストを追加する。
ユーザーの情報が互いに編集できないことを確認するために、ユーザー用のfixtureファイルに2人目のユーザーを追加する。
test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> # 2人目のユーザーを追加する archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %>
次に、 log_in_asメソッドを使って、editアクションとupdateアクションをテストする。
test/controllers/users_controller_test.rb require 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) #2人目のユーザー 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 #ルート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 end
別のユーザーのプロフィールを編集しようとしたらリダイレクトするようにさせるためにcorrect_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出すようにする。
app/controllers/users_controller.rb class UsersController < ApplicationController before_action :logged_in_user, only: [:edit, :update] #editとupdateの各アクションでcorrect_userを呼び出す before_action :correct_user, only: [:edit, :update] . . . def edit end def update if @user.update_attributes(user_params) flash[:success] = "Profile updated" redirect_to @user else render 'edit' end end . . . private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end # 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]) redirect_to(root_url) unless @user == current_user end end