🦓

[Rails]プロフィール編集 17/20

に公開

はじめに

ユーザーのプロフィール編集機能を実装していきます。

環境:

Rails 6.1.7.3
ruby 3.0.0

ユーザーモデル

現状ユーザーモデルにメールアドレスとパスワードダイジェストしかないですので、プロフィール画像と紹介を追加していきます。

bin/rails generate migration add_description_to_user description: string
      invoke  active_record
      create    db/migrate/20230706140511_add_description_to_user.rb
bin/rails generate migration AddProfileToUsers  image:string
      invoke  active_record
      create    db/migrate/20230706140717_add_profile_to_users.rb
db/migrate/xxx_add_description_to_user.rb
class AddDescriptionToUser < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :description, :string
  end
end
db/migrate/xxx_add_profile_to_user.rb
class AddImageToUsers < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :profile, :string
  end
end
bin/rails db:migrate
Running via Spring preloader in process 10782
== 20230706140511 AddDescriptionToUser: migrating =============================
-- add_column(:users, :description, :string)
   -> 0.0027s
== 20230706140511 AddDescriptionToUser: migrated (0.0039s) ====================

== 20230706140717 AddProfileToUsers: migrating ==================================
-- add_column(:users, :profile, :string)
   -> 0.0036s
== 20230706140717 AddProfileToUsers: migrated (0.0042s) =========================

uploaderを作成する

bin/rails generate uploader profile
      create  app/uploaders/profile_uploader.rb
app/uploaders/profile_uploader.rb
class ProfileUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick

  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

  # Provide a default URL as a default if there hasn't been a file uploaded:
  def default_url(*args)
    'user.png'
  end


  # Create different versions of your uploaded files:
  version :thumb do
    process resize_to_fit: [50, 50]
  end

  # Add an allowlist of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_allowlist
    %w(jpg jpeg gif png)
  end

end

Userモデルにuploaderを読み込む

app/models/user.rb
class User < ApplicationRecord
    mount_uploader :profile, ProfileUploader
end

単一リソース

プロフィール編集機能を実装するにあたって、単数形のリソースを使います。
一人のユーザーがプロフィール一つしかないのでIDの指定が不要となります。

ユーザーがページを表示する際にidを一切参照しないリソースが使われることがあります。たとえば、/profileでは常に「現在ログインしているユーザー自身」のプロファイルを表示し、他のユーザーidを参照する必要がないとします。このような場合には、単数形リソース (singular resource) を使ってshowアクションに (/profile/:idではなく) /profileを割り当てることができます。

https://railsguides.jp/routing.html#単数形リソース

routes.rbを編集する

単数形のリソースを追加していきます。

config/routes.rb
Rails.application.routes.draw do
...
   resource :profile, only: %i[show edit update]
end
bin/rails routes
edit_profile GET    /profile/edit(.:format)              profiles#edit
     profile GET    /profile(.:format)                   profiles#show
           PATCH    /profile(.:format)                   profiles#update
             PUT    /profile(.:format)                   profiles#update

モデルに紐づかないコントローラー

プロフィールの編集画面への遷移を考えた時に、素直にRailsのCRUDに従うと以下のようになると思います。
/users/:id/edit
しかしプロフィールの編集というのは、自分自身のものに対してしか行いません。
それにもかかわらずURLにidが含まれているのは少々分かりづらい気がします。
また「このidを書き換えたら他人のプロフィールを編集できそう」と容易に想像されてしまうリスクもあります。(もちろん他人のプロフィールを編集できないような制御は必須です)

もしプロフィールの編集画面へのURLが/profile/editになっていれば、上記の問題は解決します。

単数系リソースと組み合わせてprofiles_controller.rbeditアクションで/profile/editにアクセスできるように実装していきます。

Profileコントローラーを作成する

app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
    before_action :set_user, only: %i[edit update]

    def show

    end

    def edit
        
    end

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

    private

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


    def user_params
        params.require(:user).permit(:user_name, :email, :description, :profile, :profile_cache)
    end
end

編集のビューを作成する

プレビューに関して投稿のサムネと同じやり方で実装しました。

app/views/profiles/edit.html.erb
<% content_for(:title, t('.title')) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1">
      <h1><%= t('.title') %></h1>
      <%= form_with model: @user, url: profile_path do |f| %>
        <%= render "shared/error_messages", object: f.object %>
        <div class="form-group">
          <%= f.label :user_name %>
          <%= f.text_field :user_name, class: "form-control mb-3" %>
        </div>
        <div class="form-group">
          <%= f.label :email %>
          <%= f.email_field :email, class: "form-control mb-3" %>
        </div>
        <div class="form-group">
          <%= f.label :description %>
          <%= f.text_field :description, class: "form-control mb-3" %>
        </div>
        <div class="form-group mb-3">
          <%= f.label :profile %>
          <%= f.file_field :profile, class: "form-control js-file-select-preview", accept: 'image/*', data: { target: '#preview-target' }  %>
          <%= f.hidden_field :profile_cache %>
        </div>
        <div class='form-group mb-3'>
          <% if @user.profile.present? %>
            <%= image_tag @user.profile.url %>
          <% else %>
            <%= image_tag 'user.png', id: 'preview-target', size:'50x50', class: 'round-circle' %>
          <% end %>
        </div>
        <%= f.submit  class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>
irb(main):002:0> user = User.first
   (2.4ms)  SELECT sqlite_version(*)
  User Load (0.7ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, user_name: "test_user", email: "user@sample.com", password...
irb(main):003:0> user.description = '自己紹介'
=> "自己紹介"
irb(main):004:0> user.save
  TRANSACTION (0.1ms)  begin transaction
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "user@sample.com"], ["id", 1], ["LIMIT", 1]]
  User Update (3.7ms)  UPDATE "users" SET "updated_at" = ?, "description" = ? WHERE "users"."id" = ?  [["updated_at", "2023-07-07 11:39:29.043990"], ["description", "自己紹介"], ["id", 1]]
  TRANSACTION (1.2ms)  commit transaction
=> true
# バリデーションも試す
irb(main):005:0> user.user_name = ''
=> ""
irb(main):006:0> user.save
  TRANSACTION (0.1ms)  begin transaction
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "user@sample.com"], ["id", 1], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  rollback transaction
=> false
irb(main):007:0> user.errors.full_messages
=> ["ユーザー名を入力してください", "ユーザー名は3文字以上で入力してください"]

プロフィール詳細のビューを作成する

app/views/profiles/show.html.erb
<% content_for(:title, t('.title')) %>
<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(:user_name) %></th>
          <td><%= Current.user.user_name %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:description) %></th>
          <td><%= Current.user.description %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:profile) %></th>
          <td>
            <% if Current.user.profile.present? %>
              <%= image_tag Current.user.profile.thumb.url %>
            <% else %>
              <%= image_tag 'user.png', size:'50x50', class: 'round-circle' %>
            <% end %>
          </td>
        </tr>
      </table>
    </div>
  </div>
</div>

ヘッダーにリンクを追加する

プロフィール詳細画面へのリンクを追加します。

app/views/shared/_header.html.erb
<%= link_to User.human_attribute_name(:profile), profile_path, class: 'nav-link' %>

プロフィールの翻訳を追加する

config/locales/view/ja.yml
ja:
  profiles:
    edit:
      title: 'プロフィール編集'
    show:
      title: 'プロフィール'
config/locales/activerecord/ja.yml
ja:
  activerecord:
    attributes:
      user:
        description: '自己紹介'
        profile: 'プロフィール'

終わりに

単一形のリソースは便利ですね。
よく理解していきましょう。

Discussion