Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #6 ユーザー登録画面, エラー日本語化編

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

前回：[#5 Userモデル編](https://qiita.com/aokyo17/items/48137ccb982f936d035f)
次回：[#7 ログイン機能の下準備編](https://qiita.com/aokyo17/items/2f71cc0feb795d640c4a)

# 今回の流れ
1. 今回のゴールを把握する
2. ユーザー登録画面のビューを作る
3. フォームのエラーを日本語化する
4. flashを作る
5. Usersコントローラーを作る
6. ユーザー登録画面のテストを作る
7. 本番環境を整える

※ この記事は、ポートフォリオを作る理由を**web系自社開発企業に転職するため**とします。
※ 2020年4月1日、記事を大幅に更新しました。

# 今回のゴールを把握する
ゴールは、ユーザー登録画面を完成させることです。
対応するURLとUsersコントローラーのアクション、役割は以下の通りです。

| URL | アクション | 役割 |
| --- | --- | --- |
| /signup | new | フォーム画面を表示する |
| /users | create | フォーム情報を送信する |
| /users/:id | show | ユーザーページを表示する |

まずは、ユーザー登録画面のビューを作ります。
その際、フォームに不備がある場合のエラーも作ります。
続いて、フォームのエラーを日本語化します。
続いて、初めてのユーザーに登録完了を伝えるflashを作ります。
続いて、Usersコントローラーを作ります。
続いて、ユーザー登録失敗/成功時のテストを作ります。
最後に、デプロイを行うために本番環境を整えます。

以上です。

# ユーザー登録画面のビューを作る
ユーザー登録画面のビューを作ります。 
フォームに不備がある場合のエラーも作ります。
[Rails Tutorial 7.2 ユーザー登録フォーム](https://railstutorial.jp/chapters/sign_up?version=5.1#sec-signup_form)との変更点は以下の2つです。

* labelではなくplaceholderを使う
* form_forではなくform_withを使う

フォームを作成するform_forは非推奨になりました。
代わりにform_withを使います。

```erb:app/views/users/new.html.erb
<% provide(:title, '新規ユーザ作成') %>
<div class="container signup-container">
  <div class="row">
    <div class="col">
      <div class="signup-logo-img">
        <%= link_to image_tag('lantern_lantern_logo.png', width: 100), root_path, class: "logo-img" %>
      </div>
      <h1 class="signup-title">新規ユーザ作成</h1>
      <%= form_with(model: @user, url: signup_path, local: true) do |form| %>
        <%= 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>
      <% end %>
    </div>
  </div>
</div>
```

```erb:app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger alert-form-extend" role="alert">
      <%= @user.errors.count %>個のエラーがあります
    </div>
    <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>
```

```scss:app/assets/stylesheets/application.scss
// 中略
// signup

.signup-container {
  width: 100%;
  max-width: 330px;
  margin: auto;
  padding-top: 1rem !important;
}

.signup-logo-img {
  text-align: center;
}

.signup-title {
  text-align: center;
  font-weight: normal;
  font-size: 1.5rem;
  margin-bottom: 2rem;
}

.form-control {
  margin-bottom: 0;
}

.form-submit {
  width: 100%;
  margin-top: 1rem;
}

#error_explanation {
  color: red;
  ul {
    color: red;
    padding-bottom: 0;
  }
}

.field_with_errors .form-control {
  border-color: red;
}

.alert-form-extend {
  margin: 1.5rem 0 1rem;
}
```

# フォームのエラーを日本語化する
フォームのエラーを日本語化します。
日本語化には、rails-i18nを使います。

```diff:Gemfile
+ gem 'rails-i18n'
```

```shell:shell
$ bundle install
```

続いて、設定を加えます。
特別に、理解する必要はありません。

```rb:config/application.rb
# 中略
module LanternApp
  class Application < Rails::Application
# 中略
# 以下2行を追加
    config.i18n.default_locale = :ja
    config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
  end
end
```

```shell:shell
$ mkdir config/locales/models
$ touch config/locales/models/ja.yml
```

```yml:locales/models/ja.yml
ja:
  activerecord:
    models:
      user: ユーザ
    attributes:
      user:
        name: 名前
        email: メールアドレス
        password: パスワード
        password_confirmation: パスワード（再入力）
```

モデル名と属性名に、それぞれ対応する日本語を与えます。
これにより、日本語化が可能になります。

最後に、サーバーを再起動します。
メッセージが日本語になりました。
![lantern_lantern_signup_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/18acb559-9606-15fe-483b-685c0d79f344.png)
（唐突なスマホ画面をお許しください。）

参考になりました↓
[[初学者]Railsのi18nによる日本語化対応](https://qiita.com/shi-ma-da/items/7e5c3d75c9a9f51abdd5)
[Railsのバリデーションエラーのメッセージの日本語化](https://qiita.com/Ushinji/items/242bfba84df7a5a67d5b)

# flashを作る
初めてのユーザーに登録完了を伝えるflashを作ります。
Rails Tutorialではflashの実装にcontent_tagを使っています。
しかし[Rails 5.1からはtag(:br)よりtag.brを推奨](https://qiita.com/akicho8/items/bd3fb6b78b1035053583)するので、こちらを使います。

```erb:app/views/layouts/application.html.erb
<!-- 中略 -->
<body>
  <%= render 'layouts/header' %>
  <% flash.each do |message_type, message| %>
    <%= tag.div message, class: "alert alert-#{message_type}" %>
  <% end %>
  <%= yield %>
  <%= render 'layouts/footer' %>
  <%= debug(params) if Rails.env.development? %>
</body>
```

# Usersコントローラーを作る
Usersコントローラーを作ります。
newとcreateアクションを編集します。

```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end
  
  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Lantern Lanternの世界へようこそ"
      redirect_to @user
    else
      render 'new'
    end
  end
  
  def show
    @user = User.find(params[:id])
  end
  
  private
    
    def user_params
      params.require(:user).permit(:name, :email, :password, :password_confirmation)
    end
end
```

各アクションで各インスタンス変数@userを用意します。
各@userの役割は、以下の通りです。

* new → Userモデルを作る受け皿を作る
* create → フォームの情報からUserモデルを作る
* show → フォームから送信されたパラメーターからユーザーを特定する

セキュリティ上、フォームに余計な情報が送信される可能性があります。
そこで、user_paramsメソッドを作ります。
このメソッドは、フォームから受け取る情報をUserモデルの属性に限定します。
それにより、セキュリティ面を向上させます。

ユーザー登録が成功するとこのような画面になります。
![lantern_lantern_user_flash.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/454653/127251b0-9059-547f-d4e1-7e27a698e2d5.png)

# 本番環境を整える
デプロイを行うために本番環境を整えます。
ここは[Tutorial 7.5 プロのデプロイ](https://railstutorial.jp/chapters/sign_up?version=5.1#sec-professional_grade_deployment)通りに進めます。

具体的にはアプリケーションサーバーにPumaを使うよう、設定しています。
サーバーに関しては[#18](https://qiita.com/aokyo17/items/281b5a1658ea58bc7b24)で学習するので、現状理解しなくて大丈夫です。

まず、SSLを有効にします。

```rb:config/environments/production.rb
# 中略
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
# 中略
```

次に、Pumaを設定します。

```rb:config/puma.rb
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count

preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/
  # deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  ActiveRecord::Base.establish_connection
end
```

最後に、Herokuで使うアプリケーションサーバーをPumaに設定します。
その設定に、Procfileを使います。

```shell:shell
$ touch ./procfile
```

```:./Procfile
web: bundle exec puma -C config/puma.rb
```

それを終えたら、commitとpushを行います。

```shell:shell
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
$ git push
$ git push heroku
$ heroku run rails db:migrate
```

無事に本番環境が動いていたら成功です。

参考になりました↓
[Herokuに必要なProcfileの書き方についてまとめておく](https://creepfablic.site/2019/06/15/heroku-procfile-matome/)

# ユーザー登録画面のテストを作る
ユーザー登録失敗/成功時のテストを作ります。
[Rails Tutorial 7.3.4 失敗時のテスト](https://railstutorial.jp/chapters/sign_up?version=5.1#sec-a_test_for_invalid_submission)を参考に、Request / System specで書きます。

```shell:shell
$ rails g rspec:request users_signup
$ touch spec/systems/users_signup_spec.rb
```

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

RSpec.describe "UsersSignups", type: :request do
  describe "GET /signup" do
    it "is invalid signup information" do
      get signup_path
      expect {
        post signup_path, params: {
          user: {
            name: "",
            email: "user@invalid",
            password: "foo",
            password_confirmation: "bar"
          }
        }
      }.not_to change(User, :count)
    end
    
    it "is valid signup information" do
      get signup_path
      expect {
        post signup_path, params: {
          user: {
            name: "Example User",
            email: "user@example.com",
            password: "password",
            password_confirmation: "password"
          }
        }
      }.to change(User, :count).by(1)
    end
  end
end
```

```rb:spec/systems/users_signup_spec.rb
require 'rails_helper'

RSpec.describe "UsersSignups", type: :system do
  
  it "is invalid because it has no name" do
    visit signup_path
    fill_in '名前', with: ''
    fill_in 'メールアドレス', with: 'user@invalid'
    fill_in 'パスワード', with: 'foo'
    fill_in 'パスワード（再入力）', with: 'bar'
    click_on '新規ユーザ作成'
    expect(current_path).to eq signup_path
    expect(page).to have_selector '#error_explanation'
    # expect(page).to have_selector 'li', text: '名前を入力してください'
  end
  
  it "is valid because it fulfils condition of input" do
    visit signup_path
    fill_in '名前', with: 'Example User'
    fill_in 'メールアドレス', with: 'user@example.com'
    fill_in 'パスワード', with: 'password'
    fill_in 'パスワード（再入力）', with: 'password'
    click_on '新規ユーザ作成'
    # follow_redirect!
    expect(current_path).to eq user_path(1)
    expect(page).not_to have_selector '#error_explanation'
  end
end
```

Request specでは、もっぱらuserが追加されたかを確認します。
System specでは、画面が正しく遷移するか確認します。

assert_templateは現在使用できないので、リダイレクト先のdivを検証します。
マッチャにchangeを使用する際、expectの中身はblockである必要があります。
よって、expect()ではなく、expect{}で書きます。

コメントアウトを載せている理由は、以下の通りです。

* 1つ目：テキストに対するテストは壊れやすい
テキストはよく変更することがあるので、テストは壊れやすいです。この場合、flashが空でないかをテストするだけに留めます。

* 2つ目：follow_redirect!をするとエラーが発生する
直接リダイレクトする操作はブラウザ上で期待できないために、エラーが発生します（詳しくは[#8](https://qiita.com/aokyo17/items/9d464e5ee8838557d4ad)で解説します）。

参考になりました↓
[rspecでのsubjectの使い方について](https://teratail.com/questions/11352)
[使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」](https://qiita.com/jnchito/items/2e79a1abe7cd8214caa5)

今回は以上です。

---
前回：[#5 Userモデル編](https://qiita.com/aokyo17/items/48137ccb982f936d035f)
次回：[#7 ログイン機能の下準備編](https://qiita.com/aokyo17/items/2f71cc0feb795d640c4a)