【Rails】ユーザー情報の編集【Rails Tutorial 10章まとめ】

##Usersコントローラの編集
Usersコントローラにアクションを追加し、ユーザー情報の編集やユーザー一覧の表示、ユーザーの削除を行えるようにする。

##ユーザー情報の編集
###editアクションと編集フォーム
Usersコントローラにeditアクションを追加する。
editアクションに対応するURLはusers/:id/edit(名前付きルートはedit_user_path(user))なので、params[:id]を使えば編集したいユーザーを取得できる。

```app/controllers/users_controller.rb
  def edit
    @user = User.find(params[:id])
  end
```

editアクションに対応するeditビューは以下のようになる。

```erb:app/views/users/edit.html.erb
<% 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に変換される。

```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アクションとビューができたので、レイアウトのヘッダー部分にリンクを貼っておく。

```erb:app/views/layouts/_header.html.erb
<% 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のリンクしか違いがないので、パーシャルにまとめる。

```erb:app/views/users/_form.html.erb
<%= 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ビュー

```erb:app/views/users/new.html.erb
<% 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ビュー

```erb:app/views/users/edit.html.erb
<% 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>
```

ここでは、２つのprovideメソッドとyieldを使っている。
一つはform_forの送信先である。
新規登録フォームではcreateアクションに送信するので、signup_pathとする。
編集フォームではupdateアクションに送信するので、user_pathとする（引数(user)は不要）。

もう一つはフォーム送信ボタンの文字である。

##編集の失敗
###編集失敗時の処理
更新フォームの送信先であるupdateアクションを書く。

```app/controllers/users_controller.rb
  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でその属性を更新する。
更新に失敗した場合は編集画面に戻る。

###編集失敗時のテスト
ユーザー編集用の統合テストを作成する。

```console
$ rails generate integration_test users_edit
```
ユーザー情報の更新に失敗した場合のテストを書く。

```test/integration/users_edit_test.rb
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アクションと同様に、フラッシュメッセージを表示してユーザー表示ページに移動する。

```app/controllers/users_controller.rb
  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/integration/users_edit_test.rb
  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を追加して、この例外に対応する。

```app/models/user.rb
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
```

テストがGREENとなることを確認する。

##ユーザーの認可
###認可
ユーザーの編集機能が完成したが、この状態では誰もが任意のユーザー情報を変更できてしまう。
そこで、認可(authorization)機能を追加して、特定のページへのアクセスを制限する。

###before_actionとlogged_in_userメソッド
before_acitonメソッドを使うことで、各アクションの実行前に特定の操作を行うことができる。

ログインしていないとユーザー情報を編集できないようにする。
editアクションとupdateアクションの前にログインしているかを判定して、ログインしていなければログインページにリダイレクトするlogged_in_userメソッドを定義する。

```app/controllers/users_controller.rb
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/integration/users_edit_test.rb
  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/controllers/users_controller_test.rb
  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アクションに設定する。

```app/controllers/users_controller.rb
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?というヘルパーメソッドにしておく。

```app/helpers/sessions_helper.rb
  # 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end
```

```app/controllers/users_controller.rb
    # 正しいユーザーかどうか確認
    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ファイルに別のユーザーを作成する。

```test/fixtures/users.yml
  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') %>
```

テストを書く。

```test/integration/users_edit_test.rb
 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にリダイレクトすること以外は、非ログイン時のテストと同じである。