resourceを使ったプロフィール編集機能の実装
ルーティングの設定
今回、プロフィール詳細画面と編集画面へのurlは/profile, /profile/editと 現在ログインしているユーザーの詳細と編集画面だけ表示できればいいので、urlでユーザーのidを参照する必要がない。このような時に使うのが単数型のリソース routes.rbに
#routes.rb resource :profile, only: %i[show edit update]
を記載、bundle exec 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
とidを使わないルーティングを生成できるし、urlヘルパーも使えるようになる ちなみに複数リソース(resources)と単数リソース(resource)もルーティング先のコントローラはデフォルトでリソース名の複数形名称である
コントローラー
bundle exec rails g controller profilesを実行してprofilesコントローラーの作成
コードは以下のようになる
#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.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
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)
end
end
set_userで@user = current_userとしてしまうとプロフィール名変更に失敗したときに、画面上で名前が変わってしまう。これは@userにはcurrent_userの保存されている場所の値(メモリ番地)が代入されており、これによりcurrent_userの値を参照しているので、@userをupdateしてしまうと@userを通してcurrent_userの値が影響を受けてしまうためである。なので、@userにはdbから取得したオブジェクトを利用している
アバターカラムの追加
今回は詳細ページにユーザーにアバター画像を追加するのでアバター画像のカラムを用意する
カラム名はavatarにするのでbundle exec rails g uploader Avatarを実行してアバター画像の設定ファイルを生成したらbundle exec rails g migration add_avatar_to_users avatar:stringを実行して、bundle exec rails db:migrateを実行してavatarカラムを追加
user.rbにmount_uploader :avatar, AvatarUploaderを記載して設定ファイルを適用させる
設定ファイルは以下の感じ
#avatar_uploader.rb
class AvatarUploader < 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
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# Provide a default URL as a default if there hasn't been a file uploaded:
def default_url
'sample.jpg'
end
# Process files as they are uploaded:
# process scale: [200, 300]
#
# def scale(width, height)
# # do something
# end
# Create different versions of your uploaded files:
# version :thumb do
# process resize_to_fit: [50, 50]
# end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_whitelist
%w[jpg jpeg gif png]
end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
# def filename
# "something.jpg" if original_filename
# end
end
ビュー
ユーザー詳細ページと編集ページのビューは以下のように記載
#show.html.erb
<% content_for :title do %>
プロフィール
<% end %>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-1 col-lg-6 offset-lg-2">
<h1><%= t '.title' %></h1>
</div>
<div class="col-md-4 offset-md-1 col-lg-2 offset-lg-1">
<%= button_to t('.button'), {controller: 'profiles', action: 'edit' }, method: :get, class: "btn btn-success float-right" %>
</div>
<div class="col-md-11 offset-md-1 col-lg-9 offset-lg-2">
<table class="table">
<tr>
<td>
<strong><%= User.human_attribute_name(:email) %></strong>
</td>
<td><%= current_user.email %></td>
</tr>
<tr>
<td>
<strong><%= t('.name') %></strong>
</td>
<td><%= current_user.decorate.full_name %></td>
</tr>
<tr>
<td>
<strong><%= User.human_attribute_name(:avatar) %></strong>
</td>
<td><%= image_tag current_user.avatar.url,class: 'rounded-circle mr15',width: '40',height: '40' %></td>
</tr>
</table>
</div>
</div>
</div>
#edit.html.erb
<% content_for :title do %>
プロフィール編集
<% end %>
<div class="container">
<div class="row">
<div class=" col-md-10 offset-md-1 col-lg-8 offset-lg-2">
<h1><%= t '.title' %></h1>
<%= form_with model: current_user, url: profile_path, method: :patch, local: true do |form| %>
<%= render 'shared/error_messages', object: form.object %>
<div class="form-group">
<%= form.label :email, User.human_attribute_name(:email) %>
<%= form.text_field :email, class: "form-control" %>
</div>
<div class="form-group">
<%= form.label :last_name, User.human_attribute_name(:last_name) %>
<%= form.text_field :last_name,class: "form-control" %>
</div>
<div class="form-group">
<%= form.label :first_name, User.human_attribute_name(:first_name) %>
<%= form.text_field :first_name,class: "form-control" %>
</div>
<div class="form-group">
<%= form.label :avatar, User.human_attribute_name(:avatar) %>
<%= form.file_field :avatar,class: "form-control" %>
</div>
<div class="actions">
<%= form.submit t('defaults.update'),class: "btn btn-primary" %>
</div>
<% end %>
</div>
</div>
</div>
レイアウトは
Bootstrapの使い方 導入方法と基本・レスポンシブデザインを徹底解説 - WEBST8のブログ
あたりを参考に作成
参考文献
掲示板の検索機能の実装
ransackとは?
簡単に検索フォームを作成するためのgem
インストール
Gemfileに
gem 'ransack'
を記載して、bundle install
使い方
使い方はシンプルモードとアドバンスモードの2つがある
今回は、シンプルモードを使って、検索時、掲示板のタイトルまたは本文、ブックマークされた掲示板一覧ページで検索した場合はブックマークした掲示板の中から検索条件に一致したものを表示させるようにする
コントローラ
掲示板一覧(boards_controller.rbのindexアクション)とブックマーク一覧(boards_controller.rbのbookmarkアクション)を以下のように記述
#boards_controller.rb
def index
@search = Board.ransack(params[:q])
@boards = @search.result(distinct: true).includes(%i[user bookmarks]).order(created_at: :desc).page(params[:page])
end
def bookmarks
@search = current_user.bookmark_boards.ransack(params[:q])
@boards = @search.result(distinct: true).includes(:user).order(created_at: :desc).page(params[:page])
end
params[:q]に検索パラメーターが渡されて、モデル名.ransack(params[:q])と記載することで、@searchと検索オブジェクトが生成される。これに@boards = @search.result(distinct: true)~~と記載することで検索結果を得られるようになる
distinct: trueで取得するデータの重複を取り除いている。これは掲示板と紐づいたコメントなどを検索する場合、同一の掲示板で複数のコメントの検索に引っかかっても、同じ掲示板が複数表示されないようにしてくれる。今回は掲示板だけのカラム検索なので特に必要ない。
ビュー
掲示板一覧(boards/index.html.erb)とブックマーク一覧(boards/bookmarks.html.erb)にそれぞれ以下を記載
#boards/index.html.erb
<%= search_form_for @search do |f| %>
<div class="input-group mb-3">
<%= f.search_field :title_or_body_cont, class:"form-control", placeholder: t('defaults.search_word') %>
<div class="input-group-append">
<%= f.submit "検索", class: "btn btn-primary" %>
</div>
</div>
<% end %>
#boards/bookmarks.html.erb
<%= search_form_for @search ,url: bookmarks_boards_path do |f| %>
<div class="input-group mb-3">
<%= f.search_field :title_or_body_cont, class:"form-control", placeholder: t('defaults.search_word') %>
<div class="input-group-append">
<%= f.submit "検索", class: "btn btn-primary" %>
</div>
</div>
<% end %>
search_form_forはform_forの検索フォームバージョンみたいなもの
f.search_field :title_or_body_cont, ~は掲示板のタイトル(title)と本文(body)カラムに対して、最後の_contでLIKE句を利用した部分一致検索ができるようにしている
複数の属性を検索条件に指定する場合はattribute_name_orattribute_name・・・と記載する
この_cont部分を述語(Predicate)と言い、他にも条件と一致したものを検索する_eqや
~以上を満たすものを検索する_gteqとかなど様々な検索条件を指定できる述語がある
掲示板一覧とブックマーク一覧の検索フォームビューの違いはurlである
それぞれ、検索条件をindexアクションbookmarksアクションに送りたいのでurlでルーティングヘルパーを使って指定している
参考文献
GitHub - activerecord-hackery/ransack: Object-based searching.
掲示板のページネーション
kaminariのインストール
今回はページネーションをkaminari gemを使って実装する
Gemfileに gem 'kaminari' を追記してbundle install —path vendor/bundle
今回の場合掲示板一覧画面とブックマーク一覧に1ページあたり20件の掲示板を表示するようにする
bundle exec rails g kaminari:config
を実行すると
config/initializers/kaminari_config.rbとページネーションのデフォルトの設定ファイルを生成する
boardモデルに1ページあたり20件の掲示板を取得するようにboard.rbに
paginates_per 20を記載
または、先ほど生成されたconfig/initializers/kamakiri_config.rbに
Kaminari.configure do |config| config.default_per_page = 20 end
と記載、設定の記載なので後者の書き方の方が良い?
コントローラの修正
boardsコントローラに
def index
@boards = Board.all.includes(%i[user bookmarks]).order(created_at: :desc).page(params[:page])
end
def bookmarks
@boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc).page(params[:page])
end
と.page params[:page]を記載 ページネーションのビューはbootstrap4のレイアウトを適用するために
bundle exec rails g kaminari:views bootstrap4
を実行することでapp/views/kaminari/以下にページネーションの指定したレイアウトを適用したビューファイルを生成してくれる
ページネーションの表示
あとは掲示板一覧のビューとブックマーク一覧に
#boards/index.html.erb
<div class="row">
<div class="col-12">
<div class="row">
<% if @boards.blank? %>
<p><%= t('.no_board') %></p>
<% else %>
<%= render @boards %>
<% end %>
</div>
</div>
</div>
<%= paginate @boards %>
</div>
#bookmarks.html.erb
<div class="row">
<div class="col-12">
<div class="row">
<% if @boards.blank? %>
<p><%= t('.no_bookmark_board') %></p>
<% else %>
<%= render @boards %>
<% end %>
</div>
</div>
</div>
<%= paginate @boards %>
</div>
と<%= paginate @boards %>を記載してサーバーを起動してページネーション機能が実装されているか確認する
rails cで@boards.page(1).limit_valueで1ページあたりの取得件数が20になっているか確認
参考文献
【Rails】kaminariの使い方をマスターしよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
コメント投稿、削除機能のajax化
コメント作成、削除処理のajax
・コメントの投稿フォームを処理をajax化するので、form_withをremote: trueにして、サーバーのログでjs形式で送信されているか確認 ちなみに、form_withはデフォルトでremote: trueなので明示的に記載しなくても良い
#comments/_form.html.erb
<div class="row mb-3">
<div class="col-lg-8 offset-lg-2">
<%= form_with model: [board, comment], remote: true,id: 'new_comment' do |form| %>
<%= form.label :body %>
<%= form.text_area :body, class: "form-control mb-3", row: "4", placeholder: Comment.human_attribute_name(:body) %>
<%= form.submit t('defaults.post'), class: "btn btn-primary" %>
<% end %>
</div>
</div>
コメントの削除のリンクのlink_toにremote: trueを追加、リンク先をcomment_path(comment)と指定
#comments/_comment.html.erb
<tr id="comment-<%= comment.id %>">
中略
<% if current_user.own?(comment) %>
<td class="action">
<ul class="list-inline justify-content-center" style="float: right;">
中略
<li class="list-inline-item">
<%= link_to comment_path(comment), method: :delete, remote: true, class: 'js-delete-comment-button', data: {confirm: 'よろしいですか'} do %>
<%= icon 'fas', 'trash' %>
<% end %>
</li>
</ul>
</td>
<% end %>
</tr>
comments_controller.rbの修正
#comments_controller.rb
class CommentsController < ApplicationController
before_action :set_comment, only: %i[update destroy]
def create
@comment = current_user.comments.create(comment_params)
end
def destroy
@comment.destroy!
end
private
def set_comment
@comment = current_user.comments.find(params[:id])
end
redirect_backの処理を削除して、set_commentで削除するコメントのインスタンスを取得して置くことでアクションを簡潔にしている
コメント作成、削除時の動的レンダリング処理を追加
#comments/create.js.erb
$("#error-messages").remove();
<% if @comment.errors.present? %>
$("#new_comment").prepend("<%= j(render('shared/error_messages', object: @comment)) %>");
<% else %>
$("#js-table-comment").prepend("<%= j(render('comments/comment', comment: @comment)) %>");
$("#js-new-comment-body").val('');
<% end %>
#comments/destroy.js.erb
$("#comment-<%= @comment.id %>").remove();
create.js.erbではもしコメントインスタンスがバリデーションに引っかかり、エラーオブジェクトを持っている場合、prependメソッドで指定した要素の子要素の先頭にshared/error_messagesとパーシャルファイルを追加している。エラーオブジェクトがなければ、同じくprependメソッドを使って、コメントのパーシャル部分だけをレンダリングするようにしている。また投稿成功時、コメントフォームvalue属性をvalで空文字を指定してリセットしている。
destroy.js.erbではremoveメソッドで指定した要素を全て削除している
参考文献
Rails で JavaScript を使用する - Railsガイド
【Rails】Ajax(非同期通信)でコメント投稿、削除 - Qiita
ブックマークボタンのajax化
ajaxとは
非同期通信と呼ばれる通信方法、通常、クライアントがリクエストを出したらサーバーからのレスポンスが返ってくるまで他の作業をすることができない同期通信と呼ばれる方法が用いられているが、クライアントからサーバーに対して、一部の情報だけを返すようにリクエストを送ることで、それ以外の部分は変わらないので他の作業ができる
ブックマークボタンをクリック時にajax通信を行うには
#_bookmark.html.erb
<%= link_to bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :post do %>
<%= icon 'far', 'star' %>
<% end %>
#_unbookmark.html.erb
<%= link_to bookmark_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :delete do %>
<%= icon 'fas', 'star' %>
<% end %>
と現在のブックマークボタンの状態でクリックすると
サーバーログ Processing by BookmarksController#create as HTML
のように通常のブックマーク機能だとHTML形式でリクエストが送信される ここで、ブックマークのリンクヘルパーにremote: trueを指定することでJS形式でリクエストを送信できる
#_bookmark.html.erb
<%= link_to bookmarks_path(board_id: board.id),
id: "js-bookmark-button-for-board-#{board.id}",
class:"float-right",
method: :post,
remote: true do %>
<%= icon 'far', 'star' %>
<% end %>
#_unbookmark.html.erb
<%= link_to bookmark_path(current_user.bookmarks.find_by(board_id: board.id)),
id: "js-bookmark-button-for-board-#{board.id}",
class:"float-right",
method: :delete,
remote: true do %>
<%= icon 'fas', 'star' %>
<% end %>
これによりブックマークボタンをクリックすると
サーバーログ Processing by BookmarksController#create as JS
となり、処理の流れは、bookmarks_controllerのcreate,destroyアクション の通過後はcreate.js.erb,destroy.js.erbファイルに向かうようになる
bookmarks_controller.rbの修正
修正前
#bookmarks_controller.rb
class BookmarksController < ApplicationController
def create
@board = Board.find(params[:board_id])
current_user.bookmark(@board)
redirect_back fallback_location: root_path, success: t('.success')
end
def destroy
@board = current_user.bookmarks.find(params[:id]).board
current_user.unbookmark(@board)
redirect_back fallback_location: root_path, success: t('.success')
end
end
修正後
#bookmarks_controller.rb
class BookmarksController < ApplicationController
def create
@board = Board.find(params[:board_id])
current_user.bookmark(@board)
end
def destroy
@board = current_user.bookmarks.find(params[:id]).board
current_user.unbookmark(@board)
end
end
とredirect_backの記述を削除
ブックマーク切り替えの処理のJSファイルを作成
HTML形式のリクエストで送信していた時はcreateアクションやdestroyアクションでビューの作成はしていないのでリダイレクト処理でどのページに遷移するか指定していたが、今はremote: trueでJS形式のリクエストで送信しているのでアクション通過後に部分的に更新させるためのJSファイルを作成する
#views/bookmarks/create.js.erb
$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/unbookmark', board: @board)) %>");
#views/bookmarks/destroy.js.erb
$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/bookmark', board: @board)) %>");
それぞれ、replaceWithでboards/unbookmark、boards/bookmarkをrenderして置き換えているが、javascripthは''や""があるとダメなのでエスケープする必要があるのでj(render~とexcape_javascript(renderのaliasを使っている、replaceWithと似たメソッドとしてhtmlメソッドがあるが、htmlにすると、指定している要素(今回はlink_toで生成されるaタグ)に設定したした値をセットする、今回それぞれ、指定している要素はbookamarkファイルとunbookmarkファイル内に含まれていて、それぞれ設定した値はbookmarkファイルとunbookmarkファイルをセットするので、link_toで生成されたaタグにネストされた形になってしまう。htmlは指定した要素に引数で渡した値をセットするのに、replaceWithは指定した要素に引数で渡した値を置き換えるといった違いがある
参考文献
Rails で JavaScript を使用する - Railsガイド
ブックマーク機能の追加
アソシエーション
ユーザーは複数の掲示板をブックマークでき、掲示板は複数のユーザーにブックマークされるというようにモデル間で多対多の関係がある時、中間テーブル(今回の場合はブックマーク)を作成してモデル間の情報をやり取りする このテーブルによって、今回ならどのユーザーがどの掲示板と関連づいているのかという情報を管理することで、ユーザーが掲示板をブックマークといった機能を実装できる UserモデルとBoardモデルとBookmarkモデルの関係は以下のようになる User : Bookmark = 1 : 多 Board : Bookmark = 1 : 多
bundle exec rails g model Bookmark user:references board:references
を実行、bookmarkのモデルファイルとマイグレーションファイルを作成
#bookmark.rb
class Bookmark < ApplicationRecord
belongs_to :user
belongs_to :board
validates :user_id, uniqueness: { scope: :board_id }
end
uniqunessヘルパーのscopeオプションにboard_idを指定することで、各掲示板(board_id)別にユーザー(user_id)との関係性を一意にすることができる
class CreateBookmarks < ActiveRecord::Migration[5.2]
def change
create_table :bookmarks do |t|
t.references :user, foreign_key: true
t.references :board, foreign_key: true
#user_idとboard_idカラムにuniqueインデックスを作成して、dbレベルで一意性を保つ
t.index [:user_id, :board_id], unique: true
t.timestamps
end
end
end
これにより一人のユーザーが同じ投稿を複数回bookmarkできないようにしている
bundle exec rails db:migrate
を実行して中間テーブルは作成完了
#user.rb class User < ApplicationRecord #dependent: :destroyでユーザーが削除された時、そのユーザーに関連する情報を削除するようにしている has_many :bookmarks, dependent: :destroy #throughオプションでbookmarkテーブルを経由してuserモデルとアソシエーションを作成して、bookmark_boardsは仮のテーブル名なので、sourceオプションで参照するテーブルを指定している has_many :bookmark_boards, through: :bookmarks, source: :board end
ブックマークの処理
ブックマークの処理はモデルに記載すること 理由はコントローラーに記載すると可読性が落ちるため
#user.rb
class User < ApplicationRecord
#<<演算子でbookmarks.create!(board_id: board.id)と同様の処理を行う
def bookmark(board)
bookmark_boards << board
end
def unbookmark(board)
bookmark_boards.destroy(board)
end
#ログインしているユーザーに紐づいたブックマークのレコードに引数で渡された掲示板モデルと紐づいたレコードが存在するか。存在すればユーザーはその掲示板をブックマークしていると判定、存在しなければブックマークされていないと判定できる
def bookmark?(board)
bookmark_boards.include?(board)
end
end
ルーティングの設定
今回ブックマーク一覧ページをboards/bookmarksにルーティングを設定したい urlから何を表示しているか推測しやすくするため また、bookmarksコントローラのcreateとdestroyアクションだけルーティングを設定したいので
#routes.rb
resources :boards do
collection do
get :bookmarks
end
resources :bookmarks, only; %i[create destroy]
とcollectionオプションを使うことでboardsリソースの中にbookmarksアクションを追加できる、この時bookmarks_boards_urlやbookmarks_boards_pathといったルーティングヘルパーも使えるようになる。
Bookmarkコントローラ作成
bundle exec rails g controller bookmarks create destroy
を実行してbookmarks_controller.rbを作成して、以下を記述
#bookmarks_controller.rb
class BookmarksController < ApplicationController
#ブックマークボタンをクリックされた掲示板を取得して、Userモデルのインスタンスメソッドとして作成したbookmarkメソッドの引数に渡してブックマークする
def create
@board = Board.find(params[:board_id])
current_user.bookmark(@board)
redirect_back fallback_location: root_path, success: t('.success')
end
#ログインしているユーザーに関連づいてクリックしたbookmarksモデルに関連づいたboardモデルを取得してunbookmarkメソッドの引数に渡してブックマーク解除をしている
def destroy
@board = current_user.bookmarks.find(params[:id]).board
current_user.unbookmark(@board)
redirect_back fallback_location: root_path, success: t('.success')
end
end
redirect_backを使うとユーザーを直前のページに戻すことができる。この時fallback_locationは必ず設定する必要がある。HTTP_REFERER(リンク元のURLが入っているためどこのURLからアクセスしてきたかを知ることができる)がブラウザが必ずしも設定しているとは限らないため
boards_controllerでbookmarksアクションの実装
#boards_controller.rb
class BoardsController < ApplicationController
def bookmarks
@bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
end
end
ビュー実装
#boards/_board.html.erb
中略
<% if current_user.own?(board) %> #ログインしているユーザーが掲示板作成者であるならcrud_menusのパーシャルを表示する、そうでなければ、bookmark_buttonのパーシャルを表示する
<%= render 'crud_menus', board: board %>
<% else %>
<%= render 'bookmark_button', board: board %>
<% end %>
#boards/_bookmark.html.erb
user.rbにて作成したbookmark?メソッドによってログインしているユーザーが掲示板をブックマークしているか判定、ブックマークしていたら、unbookmarkパーシャルを表示、そうでなければbookmarkパーシャルを表示
<% if current_user.bookmark?(board) %>
<%= render 'unbookmark', { board: board } %>
<% else %>
<%= render 'bookmark', { board: board } %>
<% end %>
#boards/_bookmark.html.erb
<%= link_to bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :post do %>
<%= icon 'far', 'star' %>
<% end %>
#boards/_unbookmark.html.erb
<%= link_to bookmark_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :delete do %>
<%= icon 'fas', 'star' %>
<% end %>
#boards/bookmarks.html.erb
中略
#ブックマークされた掲示板があればbookmark_boardパーシャルを表示、なければ、ブックマーク中の掲示板がありませんと表示
<% if @bookmark_boards.present? %>
<%= render @bookmark_boards %>
<% else %>
<p><%= t('.no_result') %></p>
<% end %>
参考文献
Active Record の関連付け - Railsガイド
https://qiita.com/niwa1903/items/8a0374155cd18adbd7cf
掲示板の編集、削除機能の実装
掲示板編集ページの作成
フォーム部分はパーシャルを用意して、新規作成ページと共有できるようにしておく ので、今回編集ページのビューは以下のようになる
#boards/edit.html.erb
<div class="container">
<div class="row">
<div class="col-lg-8 offset-lg-2">
<h1><%= t('.title') %></h1>
<%= render 'form', { board: @board } %> #掲示板フォームのパーシャル
</div>
</div>
</div>
現在ログインしているユーザーのidと掲示板のuser_idが同じ場合編集、削除アイコンを表示させる。まず、この判定を行うメソッドをuserモデルのインスタンスメソッドとして記述しておく
#user.rb
def own?(object)
id == object.user_id
end
ビューに直接ifなどを使って判定してもいいが、userモデルに判定のロジックを記載することで、ロジックを見直したりしたいときなどにuserモデルを見るだけで済む。また引数にobjectを受け取るようにすることで、掲示板だけでなく、コメントなどユーザーに他の関連づいたインスタンスの判定も行えるようにしている。 ビューはこのメソッドを使うことで以下のようになる
<% if current_user.own?(board) %>
<%= link_to edit_board_path(board), id: 'button-edit-#{board.id}' do %>
<%= icon 'fa', 'pen' %>
<% end %>
<%= link_to '#', id: 'button-delete-#{board.id}', method: :delete, data: {confirm: ''} do %>
<%= icon 'fas', 'trash' %>
<% end %>
<% end %>
edit,update,destroyアクションの作成 先にコードを記載すると以下のようになる
before_action :set_board, only: %i[edit update destroy]
def edit; end
def update
if @board.update(board_params)
redirect_to board_path(@board), success: t('.success')
else
flash.now[:danger] = t('.fail')
render :edit
end
end
def destroy
@board.destroy!
redirect_to boards_path, success: t('.success')
end
private
def set_board
@board = current_user.boards.find(params[:id])
end
今回は,edit,update,destroyアクションにおいて、ルーティングのboardオブジェクトのidから現在のユーザーに関連付いたBoardオブジェクトを取得してプロセスが共通なので、コールバック(オブジェクトに対して、特定のアクションを実行した瞬間に呼び出されるメソッドのこと)としてprivateに登録してある。
privateに登録するのは記載したクラスの外から呼び出されて、悪用されるのを防ぐため
before_action :set_board, only: %i[edit update destroy]の記述によりedit,update,destroyアクションの実行前にset_boardアクションが実行される
確認ダイアログの表示
削除アイコンは
<%= link_to board_path(board), id: 'button-delete-#{board.id}', method: :delete, data: {confirm: ‘よろしいですか?’} do %>
と記述method: :deleteによってHTTPにDELETEリクエストを発行してくれるこの時
HTTPはDELETEリクエストを受け取ることができないので,Rails側はJavascriptを使って偽造してくれる。つまりjavascriptがオフになっていると削除のリンクも無効になってしまうということ。また、リンクやフォームにdata-confirm属性を追加することで、確認ダイアログを表示してくれる。confirm: に表示したい文字列を追加する
また、destroyアクションでdestroyメソッドではなくdestroy!メソッドを使っているのはデータを削除するとき失敗する想定(バリデーションなど)に引っかかるなどを今回の仕様では想定していないため、万が一削除の処理に失敗した場合には処理を中断させて例外として扱う必要があるためである。
参考文献