【Rails】プロフィール編集機能の実装

Gem carrierwave(Webアプリケーションにファイルアップロード機能を提供するGem)を使用する。


## アップローダーとは
・アップローダーとはクラスオブジェクトである。
・アップローダーから生成されたオブジェクトを使って、ファイル名やファイルの保存先を取得できる。
・アップローダーにはファイルアップロードに関する設定を書ける

## 実装の流れ
1. アップローダーを作成
2. アップローダーをモデルで使用するように宣言
3. アップロードの設定を各ファイルで行う

## 達成したいこと
▪️ユーザーのプロフィール編集機能を実装する
▪️編集画面からユーザーのアバターを登録出来る機能を実装する
▪️プロフィール詳細画面ではメールアドレス、氏名、アバター画像が表示されること
▪️プロフィール詳細画面に、編集画面へ遷移する「編集」ボタンが存在すること
▪️編集が完了してプロフィール画面に遷移した際に以下のメッセージが表示されること
　　・成功時・・・「ユーザーを更新しました」
　　・失敗時・・・「ユーザーを更新出来ませんでした」
▪️ヘッダーのアバター画像をクリックすると表示される「プロフィール」からプロフィール詳細画面に遷移できること
▪️編集が完了するとプロフィール詳細画面にリダイレクトされること
アバターと表示されたラベルをクリックすることで、画像ファイルの選択ができること

## 手順
1. Gem carrierwaveのインストール
1. rails g uploader コマンドでavatarアップローダーの作成
1. アップローダーファイルapp/uploaders/avatar_uploader.rbに必要な記載（デフォルト画像、許可する画像形式）を追加
1. マイグレーションを行い画像用のカラムをDBに追加
1. アップローダーをカラムと関連付ける
1. usersコントローラにストロングパラメータ、キャッシュの記載を追記
1. ルーティングを追記（単一resource）
1. プロフィールフォームのビューファイルに画像投稿用のフィールド＆キャッシュ用のフィールドを追加する
1. 画像投稿用カラムの項目を日本語表記にしたいので、ja.ymlファイルに翻訳を追記。
1. 掲示板の部分テンプレートに、アップロードした画像のURLを指定

gemファイルに以下の記載を追加。
```php:Gemfile
gem 'carrierwave', '~> 2.2.2'
```

ターミナルで以下を実行
```php:ターミナル
docker compose run web bundle install
```
gemを読み込むためにサーバーを再起動するのを忘れずに！

掲示板の画像用のアップローダークラスを作成するので、board_imageを指定してアップローダークラスを作成する。

```php:ターミナル
rails generate uploader Avatar
```
app/uploaders/avatar_uploader.rbが作成される。

作成したアップローダークラス（BoardImageUploader）では、アップロードする画像の拡張子やサイズ、保存するパスを指定することができる。

### app/uploaders/avatar_uploader.rbを下記のように編集
```php:app/uploaders/board_image_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base

  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

# 登録時にファイル選択をしなかった場合、デフォルト画像をimageとして渡す
  def default_url
      'sample.jpg'
　　　　end

# 許可する画像形式を指定
　　　　　def extension_allowlist
    %w[jpg jpeg png gif]
　　　　end
end
```
デフォルトで「storage :file」が指定されているので、アップロードした画像はpublic/配下に保存される。
保存されるディレクトリは「store_dir」で設定される。
（保存されるのは画像ファイルそのものではなく参照データ）
（アップロードされた画像ファイルが"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"の形式で保存されることを示している）

(デフォルト画像はassets/images配下に設置すること)

### アップロードされた画像ファイルをGithubで管理したくないので、.gitignoreファイルに下記の記載をする
```php:.gitignore
# Ignore uploaded files in development
 /public/uploads
```

### アップローダーをモデル内のカラムに取り付けたいので、まずは画像保存用のカラムをDB内に用意する。
マイグレーションファイルを作成して、マイグレーションを実行。
(カラム名はboard_image。型はString)
```
rails g migration add_avatar_to_users avatar:string
rails db:migrate
```

### アップローダーをカラムと関連付ける
下記のように書くことで、レコードの保存時に画像が自動的にpublid/uploads配下に保存され、DBのavatarカラムには画像のファイル名のみが保存される。
```PHP:app/models/board.rb
class User < ApplicationRecord
  mount_uploader :avatar, AvatarUploader #追記
```


### 単一ルーティングを記載
プロフィールページは常にログイン中のユーザーのプロフィールを参照し、他のidのユーザーのページを参照することはない。
このような場合は単数形リソースを使用する。
```PHP:config/routes.rb
resource :profile, only: %i[show edit update]
```
```
edit_profile GET    /profile/edit(.:format)             profiles#edit
     profile GET    /profile(.:format)                  profiles#show
             PATCH  /profile(.:format)                  profiles#update
             PUT    /profile(.:format)                  profiles#update
```

### プロフィールコントローラを作成する
```
$ bundle exec rails g controller profiles
```
### profilesコントローラに以下のように記載。
```PHP:app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
  before_action :set_user, only: %i[edit update]

  def edit; end

  def update
    if @user.update(user_params)
      redirect_to profile_path, success: t('defaults.flash_message.updated', item: User.model_name.human)
    else
      flash.now['danger'] = t('defaults.flash_message.not_updated', item: User.model_name.human)
      render :edit, status: :unprocessable_entity
    end
  end

  def show; end

  private

  def set_user
    @user = User.find(current_user.id)
  end

  def user_params
    params.require(:user).permit(:email, :last_name, :first_name, :avatar, :avatar_cache)
  end
end
```

ストロングパラメータにavatarカラムを記載する、「avatar_cache」の記述を記載する
```
  def user_params
    params.require(:user).permit(:email, :first_name, :last_name, :avatar, :avatar_cache)
  end
```
```
def update
    if @user.update(user_params)
      redirect_to profile_path, success: t('defaults.flash_message.updated', item: User.model_name.human)
    else
      flash.now['danger'] = t('defaults.flash_message.not_updated', item: User.model_name.human)
      render :edit, status: :unprocessable_entity
    end
  end
```

status: :unprocessable_entityオプションをrenderメソッドに追加することで、エラーメッセージと共に「422 Unprocessable Entity」ステータスコードをクライアントに返す。これにより、クライアントはエラーが発生したとともに、そのエラー原因を理解できる。
```
# BAD
def set_user
  @user = current_user
end

# GOOD
def set_user
  @user = User.find(current_user.id)
end
```
上記のBAD例で実装すると、プロフィール編集の失敗時にcurrent_userがupdateの影響を受けてしまい、画面上でプロフィールとして表示される名前が変わってしまう。

### プロフィール編集画面「app/views/profiles/edit.html.erb」を他のファイルを参考にしながら作成
画像投稿用のフィールド＆「(カラム名)_cache」という名前のhidden_fieldを作成するのを忘れずに。
```app/views/profiles/edit.html.erb
<% content_for(:title, t('.title')) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 col-lg-8 mx-auto">
      <h1><%= t('.title') %></h1>
      <%= form_with model: @user, url: profile_path, method: :put do |f| %>
      <%= render 'shared/error_messages', object: f.object %>
        <div class="mb-3">
          <%= f.label :email, class: "form-label" %>
          <%= f.email_field :email, class: "form-control" %>
        </div>
        <div class="mb-3">
          <%= f.label :last_name, class: "form-label" %>
          <%= f.text_field :last_name, class: "form-control" %>
        </div>
        <div class="mb-3">
          <%= f.label :first_name, class: "form-label" %>
          <%= f.text_field :first_name, class: "form-control" %>
        </div>
        <div class="mb-3">
          <%= f.label :avatar %>
          <%= f.file_field :avatar, class: 'form-control mb-3', accept: 'image/*' %>
          <%= f.hidden_field :avatar_cache %>
          <div class='mt-3 mb-3'>
            <%= image_tag @user.avatar.url,
                          class: 'rounded-circle',
                          id: 'preview',
                          size: '100x100' %>
          </div>
        </div>
        <%= f.submit class: "btn btn-primary" %>
      <% end %>
      <div class='text-center'>
        <%= link_to t('.to_login_page'), login_path %>
      </div>
    </div>
  </div>
</div>
```
accept: 'image/*は、画像ファイル全般を指定している。例えば、video/*は動画ファイル全般を指す。

### プロフィールページのビューファイルを作成。

```
<div class="container pt-3">
  <div class="row">
    <div class="col-md-10 offset-md-1">
      <h1 class="float-left mb-5"><%= t('.title') %></h1>
      <%= link_to t('defaults.edit'), edit_profile_path, class: 'btn btn-success float-right' %>
      <table class="table">
        <tr>
          <th scope="row"><%= User.human_attribute_name(:email) %></th>
          <td><%= current_user.email %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:full_name) %></th>
          <td><%= current_user.decorate.full_name %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:avatar) %></th>
          <td><%= image_tag current_user.avatar_url, class: 'rounded-circle', size: '50x50' %></td>
        </tr>
      </table>
    </div>
  </div>
</div>

```


### 該当項目に対し、ja.ymlファイルに翻訳を入力する。



### アップロードしたアバターがヘッダーに表示されるよう、app/views/shared/_header.html.erbのテンプレートに、アップロードした画像のURLを指定、リンクを入力
```
<li class='nav-item dropdown dropdown-slide'>
          <%= link_to '#', class: 'nav-link', data: { bs_toggle: 'dropdown' }, aria: { haspopup: 'true', expanded: 'false' }, id: 'header-profile' do %>
            <%= image_tag current_user.avatar_url, class: 'rounded-circle mr15', width: '40', height: '40' %>
          <% end %>
          <div class='dropdown-menu dropdown-menu-end'>
            <div class='dropdown-item'><%= current_user.decorate.full_name %></div>
            <div class='dropdown-divider'></div>
            <%= link_to t('header.profile'), profile_path, class: 'dropdown-item' %>
            <%= link_to t('header.logout'), logout_path, class: 'dropdown-item', data: { turbo_method: :delete } %>
          </div>
        </li>
```
↓
以下の「current_user.avatar_url」が追記部分
```
<%= image_tag current_user.avatar_url, class: 'rounded-circle mr15', width: '40', height: '40' %>
```
リンクを「profile_path」に修正
```
<%= link_to t('header.profile'), profile_path, class: 'dropdown-item' %>
```




## 参考にしたサイト
[【Rails】CarrierWaveの使い方をざっくりまとめてみた](https://zenn.dev/yukihaga/articles/e63a224431bc96)
[【Rails】 CarrierWaveチュートリアル](https://pikawaka.com/rails/carrierwave)
[rails 画像アップロード機能の追加](https://note.com/tumu1632/n/nb17b11cf0ba4)