Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #11 プロフィール編集編

# こんな人におすすめ
* プログラミング初心者でポートフォリオの作り方が分からない
* Rails Tutorialをやってみたが理解することが難しい

前回：[#10.5 RSpecでTutorialのテストを書き直す](https://qiita.com/aokyo17/items/fcd22c7d9428c3ea8a3a)
次回：[#12 ActionMailer, アクティベーション編](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)

# こんなことが分かる
* プロフィール編集画面の例
* Bootstrap4のCardの使用例
* FactoryBotで複数ユーザを生成する方法
* before_actionによる強制リダイレクトする方法
* 直前のリンクを記憶しリダイレクトする方法

一緒に勉強していきまっしょ:bow:

# 今回の流れ
1. プロフィール編集のフォームをつくる
* プロフィール編集失敗/成功時を考える
* ユーザだけがプロフィール編集できるようにする
* ログイン後は編集ページにリダイレクトさせる

本ポートフォリオは[Tutorial10章](https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#cha-updating_showing_and_deleting_users)のユーザ一覧機能、フォームのパーシャルは作成しません。
代わりに編集ページに記録した時間をリセットするボタンを付与します。
（[ポートフォリオ#1](https://qiita.com/aokyo17/items/4df4cfd97891addc0452)を参照）

# プロフィール編集のフォームをつくる
[Tutorial 10.1.1 編集フォーム](https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-edit_form)を参考にフォーム画面をつくる。
Tutorialと異なるのは、記録時間をリセットするボタンを設けること。
それに伴ってセクションが2つ欲しい。
そこで新たなBootstrap4のコンポーネント、Cardを使用する。

```erb:app/views/users/edit.html.erb
<% provide(:title, 'プロフィール編集') %>
<div class="container edit-container">
  <div class="edit-title">
    <%= image_tag 'profile.png', class: 'edit-img' %>
    <h1 class="form-title form-title-edit">プロフィール編集</h1>
  </div>
  <%= form_with(model: @user, url: user_path(@user), local: true) do |form| %>
  <div class="card-deck">
    <div class="col-md-6">
      <section class="card">
        <h2 class="card-header form-title">アカウント情報変更</h2>
        <div class="card-body">
          <%= render 'shared/error_messages' %>
          
          <div class="form-group">
            <%= form.text_field :name, class: 'form-control', placeholder: "名前" %>
          </div>
          <div class="form-group">
            <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
          </div>
          <div class="form-group">
            <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
          </div>
          <div class="form-group">
            <%= form.password_field :password_confirmation, class: 'form-control', placeholder: "パスワード（再入力）" %>
          </div>
          <div class="form-group">
            <%= form.submit "変更", class: 'btn btn-info btn-lg form-submit' %>
          </div>
        </div>
      </section>
    </div>

    <div class="col-md-6">
      <section class="card">
        <h2 class="card-header form-title">記録時間リセット</h2>
        <div class="card-body">
          <div class="form-group form-group-reset_time">
            <%= form.check_box :reset_time, class: 'form-check-input' %>
            <%= form.label :reset_time, '記録時間をリセットする', class: 'form-check-label' %>
          </div>
          <div class="form-group">
            <%= form.submit "変更", class: 'btn btn-info btn-lg form-submit' %>
          </div>
        </div>
      </section>
    </div>
  </div>
  <% end %>
</div>
```

ビューができたのでここへのリンク先も修正しよう。
（変更はヘッダーのほんの一部なので大幅省略）

```erb:app/views/layouts/_header.html.erb
<!-- 中略 -->
<% if logged_in? %>
  <li class="nav-item nav-item-extend"><%= link_to "記録画面へ", user_path(current_user), class: "btn btn-info btn-md btn-extend" %></li>
  <li class="nav-item nav-item-extend"><%= link_to "プロフィール", edit_user_path(current_user), class: "btn btn-secondary btn-md btn-extend" %></li>
  <li class="nav-item nav-item-extend"><%= link_to "ログアウト", logout_path, method: :delete, class: "btn btn-danger btn-md btn-extend btn-logout-extend" %></li>
<!-- 中略 -->
```

（scssは省略）
![lantern_lantern_profile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/a573f696-3454-45b9-c0e8-8a15a2235771.png)

# プロフィール編集失敗時を考えよう
編集失敗したらエラーを出して同じ画面に戻るようにする。
じゃあまずはテスト、その後コントローラを整えよう。

## 失敗時のテストを書く
テストを行う前に準備を済ませる。
これは後述する、ログインユーザのみが編集できるテストの準備も加味している。
具体的にはログインしたユーザを用意する。

そこで少し戻るけど、[Tutorial 9.3.1 [Remember me] ボックスをテストする](https://railstutorial.jp/chapters/advanced_login?version=5.1#sec-testing_the_remember_me_box)を参考に、テスト用ログインメソッドlog_in_asを用意する。

```rb:spec/support/application_helper.rb
module ApplicationHelpers
  
  def is_logged_in?
    !session[:user_id].nil?
  end
  
  def log_in_as(user, remember_me = 0)
    get login_path
    post login_path, params: {
      session: {
        email: user.email,
        password: user.password,
        remember_me: remember_me
      }
    }
    follow_redirect!
  end
end
```

おっけ。じゃあテストといこう。

```bash:bash
$ rails g rspec:request users_edit
```

```rb:spec/requests/users_edits_spec.rb
require 'rails_helper'

RSpec.describe "UsersEdits", type: :request do
  
  let(:user) { create(:user) }
  
  def patch_invalid_information
    patch user_path(user), params: {
      user: {
        name: "",
        email: "foo@invalid",
        password: "foo",
        password_confirmation: "bar"
      }
    }
  end

  describe "GET /users/:id/edit" do
    context "invalid" do
      it "is invalid edit informaiton" do
        log_in_as(user)
        expect(is_logged_in?).to be_truthy
        get edit_user_path(user)
        expect(request.fullpath).to eq '/users/1/edit'
        patch_invalid_information
        expect(flash[:danger]).to be_truthy
        expect(request.fullpath).to eq '/users/1'
      end
    end
  end
end
```

## 失敗時のコントローラを整える
テストが書けたらコントローラの振る舞いを書こう。
成功時の処理は後にする。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  # 中略
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # 成功した時の処理（後述）
    else
      flash.now[:danger] = 'プロフィールの編集に失敗しました'
      render 'edit'
    end
  end
  
  private
    
    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end
end
```

これで失敗時の動作は終了。

## 備考：エラーを捕えるテストの課題
少々課題が残った。
本当はUserモデルが持つエラーを捕えるためにこういうテストを行いたいが、

```rb:requests/users_edits_spec.rb
require 'rails_helper'

RSpec.describe "UsersEdits", type: :request do
  # 中略
  describe "GET /users/:id/edit" do
    context "invalid" do
      it "is invalid edit informaiton" do
        log_in_as(user)
        expect(is_logged_in?).to be_truthy
        get edit_user_path(user)
        expect(request.fullpath).to eq '/users/1/edit'
        patch_invalid_information
        expect(user.errors.any?).to be_truthy
        expect(request.fullpath).to eq '/users/1'
      end
    end
  end
end
```

```bash:bash
Failures:

  1) UsersEdits GET /users/:id/edit invalid is invalid edit informaiton
     Failure/Error: expect(user.errors.any?).to be_truthy
     
       expected: truthy value
            got: false
```

失敗する。
他にexpect(user.errors.full_messages)や評価前のuser.save!なども試したが、そもそも値が入っていなかった。

今回はuserコントローラにflash[:danger]の値を与えた場合のフラッシュを捕えるテストに成功したので、一応ここでのテストは締めくくる（要勉強）。

---

# プロフィール編集失敗時を考えよう
さっきと同じようにテスト、コントローラの順で書く。
[Tutorial 10.1.4 TDDで編集を成功させる](https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-successful_edits)とは異なり、リダイレクト先は再びプロフィール編集画面にする。
加えてパスワードを入力しないと変更不可にするので、バリデーションはいじらない。

ただこのままだと利用者が、
「新しいパスワード入力できるの？今までのパスワード入力したらいいの？」
となってしまうので、この問題は別の回で紹介する。

とりあえずテストとコントローラをつくる。

## 成功時のテストを書く

```rb:spec/requests/users_edits_spec.rb
# 中略
context "valid" do
  it "is valid edit information" do
    log_in_as(user)
    get edit_user_path(user)
    patch_valid_information
    expect(flash[:success]).to be_truthy
    follow_redirect!
    expect(request.fullpath).to eq '/users/1/edit'
  end
end
# 中略
```

## 成功時のコントローラを整える

```rb:app/controllers/users_controller.rb
# 中略
def update
  @user = User.find(params[:id])
  if @user.update_attributes(user_params)
    flash[:success] = 'プロフィールの更新に成功しました'
    redirect_to @user
  else
    flash.now[:danger] = 'プロフィールの編集に失敗しました'
    redirect_to edit_user_path(@user)
  end
end
# 中略
```

成功時↓
![lantern_lantern_edit_success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/937fcd65-161f-35a6-e8cb-706d14de515b.png)
失敗時↓
![lantern_lantern_edit_failure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/d35bd286-e566-dc77-60a3-01affc4d395f.png)

ちなみにflashとflash.nowの使い分けはこう。

* リダイレクトあり → flash
* リダイレクトなし → flash.now

参考にさせていただきました↓
[[WIP/初学者]flashとflash.nowの使い分け](https://qiita.com/shi-ma-da/items/ea433c337d2a691ff1bc)

# ログインユーザだけがプロフィール編集できるようにする
## 非ログイン時にログインを促す
[Tutorial 10.2 認可](https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-authorization)からプロフィール編集前にログインを要求することができる。
Tutorialでは/user/:idのページに誰でもアクセスできるが、ポートフォリオではここもログインを要求したい。

テスト→コントローラの順で（以下略

### ログインを促すテスト
テストを追加、編集する。

```bash:bash
$ rails g rspec:request users
```

```rb:spec/requests/users_spec.rb
require 'rails_helper'

RSpec.describe "Users", type: :request do
  
  let(:user) { create(:user) }
  
  describe "GET /users/:id" do
    it "does not go to users/1 because of having not log in" do
      get user_path(user)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end
  end
end
```

```rb:spec/requests/users_edits_spec.rb
# 中略
describe "GET /users/:id/edit" do
  context "invalid" do
    it "is invalid because of having not log in" do
      get edit_user_path(user)
      follow_redirect!
      expect(request.fullpath).to eq '/login'
    end
    # 中略
  end
end
```

### ログインを促すコントローラ
あとはTutorialを参考につくればよい。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:show, :edit, :update]

  # 中略
  private
    
  # 中略
    def logged_in_user
      unless logged_in?
        flash[:warning] = 'ログインしてください'
        redirect_to login_url
      end
    end
end
```

## 別ユーザでのログインでは編集できないようにする
ログインしているからといって別ユーザでは意味がない。
ホーム画面にリダイレクトすることにしよう。

### 別ユーザをホーム画面にリダイレクトするテスト
テスト環境には複数のユーザが必要だ。
なのでFactoryBotで新たなユーザを生成する。

```rb:spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Michael Example" }
    email { "michael@example.com" }
    password { "password" }
    password_confirmation { "password" }
  end
  
  factory :other_user, class: User do
    name { "Sterling Archer" }
    email { "duchess@example.gov" }
    password { "foobar" }
    password_confirmation { "foobar" }
  end
end
```

これに注目。

```rb:
factory :other_user, class: User do
```

本来FactoryBotのデータはクラスを明示的に示す必要がある。
だから複数のユーザを作成する際はクラス名を明記する。

参考にさせていただきました↓
[FactoryBotでテストデータ作成する方法](https://qiita.com/YUKchannel/items/a83fbeee48ab7bf3508e)

それではテストを書く。

```rb:spec/requests/users_edits_spec.rb
require 'rails_helper'

RSpec.describe "UsersEdits", type: :request do
  
  let(:user) { create(:user) }
  let(:other_user) { create(:other_user) }

  # 中略
  describe "GET /users/:id/edit" do
    context "invalid" do
      it "is invalid because of having not log in" do
      # 中略
      end

      it "is invalid because of having log in as wrong user" do
        log_in_as(other_user)
        get edit_user_path(user)
        follow_redirect!
        expect(request.fullpath).to eq '/'
      end
      # 中略
      it "is invalid edit informaiton" do
      # 中略
      end
      
      it "does not redirect update because of having log in as wrong user" do
        log_in_as(other_user)
        get edit_user_path(user)
        patch_valid_information
        follow_redirect!
        expect(request.fullpath).to eq '/'
      end
    # 中略
```

### 別ユーザをホーム画面にリダイレクトするコントローラ
書くことはさっきとほぼ一緒。
ところでこういう記述をしようと思うけれど、

```rb:
@user == current_user
```

この箇所はリファクタリングしておく。

```rb:app/helpers/sessions_helper.rb
module SessionsHelper
  # 中略
  def current_user?(user)
    user == current_user
  end
```

よってこれを加味すると以下のコントローラが出来上がる。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:show, :edit, :update]
  before_action :correct_user, only: [:show, :edit, :update]
  # 中略
  private
    # 中略
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless current_user?(@user)
    end
end
```

# ログイン後は編集ページにリダイレクトしてあげる
編集画面でログインを勧められたのに、ログイン後に別の画面だとユーザが悲しい。
というわけで元いた画面にリダイレクトしよう。

## 編集ページにリダイレクトするテスト

```rb:spec/requests/users_edits_spec.rb
# 中略
context "valid" do
  # 中略
  it "goes to previous link because they had logged in as right user" do
    get edit_user_path(user)
    follow_redirect!
    expect(request.fullpath).to eq '/login'
    log_in_as(user)
    expect(request.fullpath).to eq '/users/1/edit'
  end
end
# 中略
```

## 編集ページにリダイレクトする動作
では実際の動作を記述する。
手順はこう。

* 直前のリンクを記憶する
* ログイン後、記憶したリンクがあるならそこにリダイレクトする

そのためにsessionsヘルパーに2つのメソッドを用意しよう。

```rb:app/helpers/sessions_helper.rb
module SessionsHelper
  # 中略
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
  
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end
end
```

* store_location → 直前のリンクを記憶する
* redirect_back_or → 記憶したリンクか任意のリンクにリダイレクトする

これで準備はできた。
あとはコントローラを編集しよう。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  # 中略
  def logged_in_user
    unless logged_in?
      store_location
      flash[:warning] = 'ログインしてください'
      redirect_to login_url
    end
  end
```

```rb:app/controllers/sessions_controller.rb
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] = 'メールアドレスかパスワードが正しくありません'
      render 'new'
    end
  end
  # 中略
```

よおし。

---

繰り返しになるが、本ポートフォリオはユーザ一覧画面が存在しない。
よって[Tutorial 10.3 すべてのユーザーを表示する](https://railstutorial.jp/chapters/updating_and_deleting_users?version=5.1#sec-showing_all_users)以後のTutorial10章は使用しない。
したがって今回はここまで。

前回：[#10.5 RSpecでTutorialのテストを書き直す](https://qiita.com/aokyo17/items/fcd22c7d9428c3ea8a3a)
次回：[#12 ActionMailer, アクティベーション編](https://qiita.com/aokyo17/items/f916ba608319b3323b7a)