[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
class AddDescriptionToUser < ActiveRecord::Migration[6.1]
def change
add_column :users, :description, :string
end
end
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
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を読み込む
class User < ApplicationRecord
mount_uploader :profile, ProfileUploader
end
単一リソース
プロフィール編集機能を実装するにあたって、単数形のリソースを使います。
一人のユーザーがプロフィール一つしかないのでID
の指定が不要となります。
ユーザーがページを表示する際にidを一切参照しないリソースが使われることがあります。たとえば、/profileでは常に「現在ログインしているユーザー自身」のプロファイルを表示し、他のユーザーidを参照する必要がないとします。このような場合には、単数形リソース (singular resource) を使ってshowアクションに (/profile/:idではなく) /profileを割り当てることができます。
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.rb
のedit
アクションで/profile/edit
にアクセスできるように実装していきます。
Profileコントローラーを作成する
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
編集のビューを作成する
プレビューに関して投稿のサムネと同じやり方で実装しました。
<% 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文字以上で入力してください"]
プロフィール詳細のビューを作成する
<% 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>
ヘッダーにリンクを追加する
プロフィール詳細画面へのリンクを追加します。
<%= link_to User.human_attribute_name(:profile), profile_path, class: 'nav-link' %>
プロフィールの翻訳を追加する
ja:
profiles:
edit:
title: 'プロフィール編集'
show:
title: 'プロフィール'
ja:
activerecord:
attributes:
user:
description: '自己紹介'
profile: 'プロフィール'
終わりに
単一形のリソースは便利ですね。
よく理解していきましょう。
Discussion