基礎Ruby on Rails Chapter8 アクション・コールバック／マイアカウントページの作成

前 [基礎Ruby on Rails Chapter8 単数リソース](https://qiita.com/tseno/items/9633572a7e0f38d1ca56)
次 [基礎Ruby on Rails Chapter9 ActiveRecoedの活用／コールバック](https://qiita.com/tseno/items/3b068cfbb16b8ba19528)

# アクション・コールバックを利用したアクセス制限

## アクション・コールバックとは

- コントローラ内で、いずれかのアクションが呼ばれたときに行う前処理。アクセス制限などに使う。2種類の書き方がある。
- 1つ目の書き方。ブロックを使用する。全てのアクション実行前に、コードが実行される。

```rb
class ExampleController < ApplicationController
  before_action do
    # アクション実行前に行う処理
  end
end
```

- 1つ目の書き方の特定アクションバージョン。`only`で指定したもののみ。`except`で除外。

```rb
class ExampleController < ApplicationController
  before_action only: [:index, :show] do
    # アクション実行前に行う処理
  end
end
```

- 2つ目の書き方は、プライベートメソッドを定義して、そのメソッドを定義する方法。`only`、`except`は上記と同じ

```rb
class ExampleController < ApplicationController
  before_action :do_something, only: [:index, :show]
  before_action :do_something, except: [:index, :search]

  private def :do_something
    # アクション実行前に行う処理
  end
end
```

## 会員限定のコンテンツ

- `LoginRequired`、`Forbidden`を一般的なエラーを表すStandardErrorから継承して作成する。
- loginRequiredメソッドは、ログインしていないとエラーが発生する。before_actionのために作成。 

```rb:app/controllers/application_controller.rb（一部）
# 以下を追加
  class LoginRequired < StandardError; end
  class Forbidden < StandardError; end

  private def loginRequired
    raise LoginRequired unless current_member
  end
end
```

- before_actionにlogin_requiredメソッドをアクション・コールバックに指定する。これによりログインしていない状態で、membersのアクションを呼ぶとエラーが発生する。

```rb:app/controllers/members_controller.rb（一部）
class MembersController < ApplicationController
  before_action :login_required
```

- ヘッダのメニューに、`if current_member`を入れて、会員名簿と管理ページをログインしていない状態では見えなくする。

```erb:app/views/shared/_header.html.erb（一部）
<nav class="menubar">
  <ul>
    <%= menu_link_to "TOP", :root %>
    <%= menu_link_to "ニュース", "#" %>
    <%= menu_link_to "ブログ", "#" %>
    <% if current_member %>
      <%= menu_link_to "会員名簿", :members %>
      <%= menu_link_to "管理ページ", "#" %>
    <% end %>
  </ul>
</nav>
```

- ログインしていない状態でmembersにアクセスすると、エラーが発生する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/461ac53c-4f46-8f57-36b2-3c280ab95167.png)

# マイアカウントページの作成

## ルーティングの設定

- ルーティングに、AccountControllerにshow、edit、updateのアクションを追加する。

```rb:config/routes.rb（一部）
  resource :account, only: [:show, :edit, :update]
end
```

|アクション|パス|HTTPメソッド|パスを示すシンボル|パスを返すメソッド|
|:--|:--|:--|:--|:--|
|show|/account|GET|:account|account_path|
|edit|/account/edit|GET|:edit_account|edit_account_path|
|update|/account|PATCH|:account|account_path|

- `bin/rails routes -c コントローラ名`で、routes.rbで設定されたパスとHTTPメソッドの一覧を表示できる。

```sh
$ bin/rails routes -c account
      Prefix Verb  URI Pattern             Controller#Action
edit_account GET   /account/edit(.:format) accounts#edit
     account GET   /account(.:format)      accounts#show
             PATCH /account(.:format)      accounts#update
             PUT   /account(.:format)      accounts#update
```

## AccountsControllerの作成

### アカウント情報の表示

- AccountControllerを作成する。show、editも加えて、テンプレートファイルも作成する。

```sh
$ bin/rails g controller accounts show edit
      create  app/controllers/accounts_controller.rb
      invoke  erb
      create    app/views/accounts
      create    app/views/accounts/show.html.erb
      create    app/views/accounts/edit.html.erb
```

- sessionコントローラはログインしたメンバーのみアクセスできるように、`before_action :login_required`を付ける。
- showメソッドでは、自分自身のオブジェクトをインスタンス変数@memberにセットする。

```rb:app/controllers/accounts_controller.rb
class AccountsController < ApplicationController
  before_action :login_required

  def show
    @member = current_member
  end
（省略）
```

- 部分テンプレート_body.html.erbを新規作成する。ほぼ同じなので、app/views/members/show.html.erbから、tableタグの部分をコピペして作成する。
 
```erb:app/views/members/_body.html.erb
<table class="attr">
  <tr>
    <th width="150">背番号</th>
    <td><%= @member.number %></td>
  </tr>
  <tr>
    <th>ユーザー名</th>
    <td><%= @member.name %></td>
  </tr>
  <tr>
    <th>氏名</th>
    <td><%= @member.full_name %></td>
  </tr>
  <tr>
    <th>性別</th>
    <td><%= @member.sex == 1 ? "男" : "女" %></td>
  </tr>
  <tr>
    <th>誕生日</th>
    <td><%= @member.birthday&.strftime("%Y年%m月%d日") %></td>
  </tr>
  <tr>
    <th>メールアドレス</th>
    <td><%= @member.email %></td>
  </tr>
  <tr>
    <th>管理者</th>
    <td><%= @member.administrator? ? "○" : "－" %></td>
  </tr>
</table>
```

- app/views/members/show.html.erbは、上記で作成した_body.html.erbで置き換える。

```erb:app/views/members/show.html.erb（一部）
（省略）
<div class="toolbar"><%= link_to "編集", [:edit, @member] %></div>
<%= render "body" %>
```

- app/views/account/show.html.erbは、上記で作成した_body.html.erbを使用する。

```erb:app/views/account/show.html.erb
<% @page_title = "マイアカウント" %>
<h1><%= @page_title %></h1>
<ul class="toolbar">
  <%= menu_link_to "アカウント情報の編集", :edit_account %>
</ul>
<%= render "members/body" %>
```

- サイトのヘッダーにある「〇〇さん」を、自分自身の情報を見るshowアクションのリンクにする。

```erb:app/views/shared/_header.html.erb（一部）
    <%= menu_link_to current_member.name + "さん", :account %>
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/85c54ab2-7b69-f083-34f9-4f9a2cbbfaae.png)

### アカウント情報の編集

- `_form.html.erb`を`_member_form.html.erb`と名称変更して、sharedディレクトリに移動する。

```sh
$ mv app/views/members/_form.html.erb app/views/shared/_member_form.html.erb
```

- new.html.erbも、`member_form` → `shared/member_form`に変更する。

```erb:app/views/members/new.html.erb（一部）
  <%= render "shared/member_formber_form", form: form %>
```

- edit.html.erbも、`member_form` → `shared/member_form`に変更する。

```erb:app/views/members/edit.html.erb（一部）
  <%= render "shared/member_form", form: form %>
```

- アカウント情報の変更画面を作成する。

```erb:app/views/accounts/edit.html.erb
<% @page_title = "アカウント情報の変更" %>

<h1><%= @page_title %></h1>

<div class="toolbar"><%= link_to "マイアカウントに戻る" %></div>

<%= form_for @member, as: "account", url: :account do |form| %>
  <%= render "shared/member_form", form: form %>
  <div><%= form.submit %></div>
<% end %>
```

- 管理者欄は、Memberコントローラの場合のみ表示させる。Accountコントローラでは表示しない。
- human_attribute_nameは、属性名をロケールテキスト（ja.yml）から取得する。

```erb:app/views/shared/_member_form.html.erb（一部）
（省略）
  <% if controller.kind_of?(MembersController) %>
  <tr>
    <th><%= Member.human_attribute_name(:administrator) %></th>
    <td>
      <%= form.check_box :administrator, disabled: !current_member.administrator? %>
      <%= form.label :administrator %>
    </td>
  </tr>
  <% end %>
</table>
```

- editアクションと、updateアクションを実装する。

```rb:app/controllers/accounts_controller.rb（一部）
（省略）
  def edit
    @member = current_member
  end

  def update
    @member = current_member
    @member.assign_attributes(params[:account])
    if @member.save
      redirect_to :account, notice: "アカウント情報を更新しました。"
    else
      render "edit"
    end
  end
end
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/1aff339e-3516-29f2-4e84-aa09f35a0dc7.png)


# パスワード変更機能

## 独立したパスワード変更フォームを作る理由

- パスワード変更は上記で作成したアカウント情報変更フォームにパスワード変更欄を置いておくと、パスワードが空の場合はパスワードを変更しないのか、空にするのかよくわからない。
- パスワードを変更するのに、変更前と、変更後を２回入力されるなど、わかりやすいユーザーインタフェースを提供するため。

## ルーティングの設定

- 自分のパスワードは、自分のアカウント情報と同様に単数リソースとして扱える。
- passwordコントローラに、`show`、`edit`、`update`のアクションを用意する。

```rb:config/routes.rb（一部）
（省略）
  resource :password, only: [:show, :edit, :update]
end
```

|アクション|パス|HTTPメソッド|パスを示すシンボル|パスを返すメソッド|
|:--|:--|:--|:--|:--|
|show|/password|GET|:password|password_path|
|edit|/password/edit|GET|:edit_password|edit_password_path|
|update|/password|PATCH|:password|password_path|

## Memberモデルの変更

- memberモデルにcurrent_passwordを読み書き可能な属性に追加する。
- 元々password属性には、空文字禁止のバリデーションが設定されているが、これは新規作成の時しか働かない。
- current_passwordに値がセットされている場合は、常に空文字でもnilでもないことを確認するようにする。

```rb:app/models/member.rb（一部）
（省略）
  attr_accessor :current_password
  validates :password, presence: { if: :current_password }
（省略）
```

## パスワード変更フォームの作成

- PasswordsControllerを作成する。editも加えて、テンプレートファイルも作成する。

```sh
$ bin/rails g controller passwords edit
      create  app/controllers/passwords_controller.rb
      invoke  erb
      create    app/views/passwords
      create    app/views/passwords/edit.html.erb
```

- PasswordsControllerに、show、editアクションを追加する。

```rb:app/controllers/passwords_controller.rb
class PasswordsController < ApplicationController
  before_action :login_required

  def show
    redirect_to :account
  end

  def edit
    @member = current_member
  end
end
```

- パスワードを変更フォームのテンプレートを追加する。

```erb:app/views/passwords/edit.html.erb
<% @page_title = "パスワードの変更" %>
<h1><%= @page_title %></h1>

<div class="toolbar"><% link_to "マイアカウントに戻る", :account %></div>

<%= form_for @member, as: "account", url: :password do |from| %>
  <%= render "shared/errors", obj: @member %>

  <table class="attr">
    <tr>
      <th><%= form.label :current_password %></th>
      <td><%= form.password_field :current_password %></td>
    </tr>
    <tr>
      <th><%= form.label :password %></th>
      <td><%= form.password_field :password %></td>
    </tr>
    <tr>
      <th><%= form.label :password_confirmation %></th>
      <td><%= form.password_field :password_confirmation %></td>
    </tr>
  </table>

  <div><%= form.submit "変更" %></div>
<% end %>
```

- パスワード変更フォームで使用するラベルテキストをロケールファイルに追加する。

```yml:config/locales/ja.yml（一部）
#（省略）
        current_password: 現在のパスワード
        password: 新しいパスワード
        password_confirmation: 新しいパスワードの確認
#（省略）
```

- 入力フォームのテーブルの上に、`パスワードの変更`リンクを追加する。

```erb:app/views/accounts/show.html.erb（一部）
（省略）
<ul class="toolbar">
  <%= menu_link_to "アカウント情報の編集", :edit_account %>
  <%= menu_link_to "パスワードの変更", :edit_password %>
</ul>
<%= render "members/body" %>
```

- パスワード変更フォームが表示される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/2da555d0-a90f-c977-ca98-e91beb415a29.png)

## 新しいパスワードの保存

- パスワードを変更する、updateアクションを追加する。
- 現在のパスワードを画面から取得し、authenticateで認証する。
- 正しければ、assign_attributesで、画面の値（新パスワード）を@memberにセットする。
- 新しいパスワードが空だったり、確認と異なっていたりするとバリデーションエラーとなる。
- 保存に成功したら、マイアカウントに戻る。

```rb:app/controllers/passwords_controller.rb（一部）
（省略）
  def update
    @member = current_member
    current_password = params[:account][:current_password]

    if current_password.present?
      if @member.authenticate(current_password)
        @member.assign_attributes(params[:account])
        if @member.save
          redirect_to :account, notice: "パスワードを変更しました。"
        else
          render "edit"
        end
      else
        @member.errors.add(:current_password, :wrong)
        render "edit"
      end
    else
      @member.errors.add(:current_password, :empty)
      render "edit"
    end
  end
end
```

- エラーメッセージ`wrong`を追加する。「現在のパスワードが正しくありません。」に使う。

```yml:config/locales/ja.yml

    errors:
      messages:
        invalid_member_name: は半角英数字で入力してください。
        wrong: が正しくありません。
```

## メンバー追加フォームの修正

- メンバーを追加するには初期パスワードを入力する必要があるので、新規作成の場合、emailの下にパスワード欄を表示する。
- new_record?はモデルオブジェクトがデータベースに保存されていないときにtrueを返す。

```erb:app/views/shared/_member_form.html.erb（一部）
（省略）
  <% if @member.new_record? %>
    <tr>
      <th><%= form.label :password, "パスワード" %></th>
      <td><%= form.text_field :password %></td>
    </tr>
  <% end %>
```

- 新規登録に、パスワード入力欄が表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/39e3e41a-3af0-e7e3-72ec-a59fe2eaecdb.png)


参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)