はじめに
前回の記事からしばらく期間が空いてしまった。全く何もやっていないということはなく、ポートフォリオアプリのコードを粛々と書いていた。
一対多は前回説明したので、今回は多対多の要素を登録するやりかたを記す。
やりたいこととDB構造
開発中の日報アプリには振り返り機能がある。あらかじめ目標を設定しておき、振り返りの段階でそれが完了したかどうかをチェックする。以下の流れになる。
step1. 事前に目標を複数登録
step2. 振り返る目標を複数選択
step3. それぞれの目標に対して進捗を振り返り
step4. 総括のコメントを追加
必要となるテーブルはPlan/Review/ReviewItemの三つ。
Plan → ReviewItem(多) / Review → ReviewItem (多)の多対多の構造をとっている。
というわけで振り返り機能を実装するときに多対多の要素の登録が必要になった。
それぞれは以下のようになっている。
Planモデル
class Plan < ApplicationRecord
belongs_to :user
belongs_to :genre
has_many :review_items
accepts_nested_attributes_for :review_items
end
class CreatePlans < ActiveRecord::Migration[6.0]
def change
create_table :plans do |t|
t.references :user, null: false, foreign_key: true
t.references :genre, null: false, foreign_key: true
t.string :name, null: false
t.datetime :deadline, null: false
t.string :status, null: false
t.timestamps
end
end
end
Reviewモデル
class Review < ApplicationRecord
belongs_to :user
has_many :review_items
accepts_nested_attributes_for :review_items
end
class CreateReviews < ActiveRecord::Migration[6.0]
def change
create_table :reviews do |t|
t.string :content, null: false
t.references :user, null: false, foreign_key: true
t.datetime :reviewed_on, null: false
t.timestamps
end
end
end
ReviewItemモデル
class ReviewItem < ApplicationRecord
belongs_to :review
belongs_to :plan
end
class CreateReviewItems < ActiveRecord::Migration[6.0]
def change
create_table :review_items do |t|
t.references :plan, null: false, foreign_key: true
t.references :review, null: false, foreign_key: true
t.timestamps
end
end
end
再度説明
テーブルの中身も合わせて上記の1-4の流れを説明すると以下のようになる。
step1. 事前に目標を複数登録 (Plan)
step2. 振り返る目標を複数選択 (Plan-ReviewItemのひもづけ)
step3. それぞれの目標に対して進捗を振り返り (Planのstatus 変更)
step4. 振り返りの日時/コメントを追加 (Reviewのcontent, reviewed_on 登録)
3と4は同時に行う。
step1.目標の複数登録
割愛する。seedデータで入れ込んでもいいし、あとから1個ずつ追加してもいい。
step2.目標選択機能の実装
振り返る目標を選択する画面を追加したい。Reviewコントローラの中に新しいアクションを追加しよう。既存のRESTfulな7つのアクションではなく。
ルーティングから考える。
ルーティング
Rails.application.routes.draw do
devise_for :users
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
root "plans#index"
resources :plans
resources :reports
resources :report_items, only: [:index, :destroy]
resources :genres, except: :show
resources :reviews do
collection do
get 'select_plan'
end
end
end
resourcesの中にコレクションルーティングを追加した。コレクションとはidを伴わないアクションのことである。今回は目標選択を新規に行うのでidは必要ない。
参考↓
これによって ルートに select_plan_reviews
が追加された。
$ rails routes | grep reviews
select_plan_reviews GET /reviews/select_plan(.:format) reviews#select_plan
reviews GET /reviews(.:format) reviews#index
POST /reviews(.:format) reviews#create
new_review GET /reviews/new(.:format) reviews#new
edit_review GET /reviews/:id/edit(.:format) reviews#edit
review GET /reviews/:id(.:format) reviews#show
PATCH /reviews/:id(.:format) reviews#update
PUT /reviews/:id(.:format) reviews#update
DELETE /reviews/:id(.:format) reviews#destroy
コントローラー
コントローラーも修正する。自分のアカウントのPlanを全てもってくる。(本当はフィルターをかけて一部だけ抽出を実施しているが、今回は説明しない。)
class ReviewsController < ApplicationController
before_action :authenticate_user!
# 中略
def select_plan
@plans = Plan.where(user_id: current_user.id)
end
# 中略
end
view
viewはreviews/select_plan.html.erb
を追加する。ルーティング先だ。
<h1>振り返り登録-目標選択</h1>
<%= form_with url: new_review_path, method: :get, local: true do |form| %>
<% @plans.each do |plan| %>
<p>
<%= check_box_tag 'checked_plan[]',"#{plan.id}" %>
目標: <%= plan.name %>
締め切り:<%= plan.deadline %>
ステータス:<%= plan.status %>
ジャンル:<%= plan.genre_id %>
</p>
<% end %>
<%= form.submit "登録" %>
<% end %>
check_box_tag について
check_box_tag はその名の通り、チェックボックスをつくるヘルパーだ。ドキュメントを読むとモデルに関係ないチェックボックスを生成する、とある。この引数の設定が難しかったので説明する。
<%= check_box_tag 'checked_plan[]',"#{plan.id}" %>
↓
(submitを押した後 渡されたparams)
params
=> <ActionController::Parameters {"checked_plan"=>["9", "7"], "commit"=>"登録", "controller"=>"reviews", "action"=>"new"} permitted: false>
このように、第一引数の最後に[]
をつけると、選択したPlanの plan.id
を配列としてパラメータに渡してくれる。つけないとチェックを入れた最後のひとつしか表示されない。
step3-step4.目標に対しての振り返り
選択した目標に対して振り返りを行う。具体的には目標モデルのstatusの変更(状況に応じて締め切りの変更も行う)と、全体の振り返りコメントの登録をする。
コントローラ(newアクション)
class ReviewsController < ApplicationController
before_action :authenticate_user!
#中略
def new
@review = Review.new
selected_plan_ids = select_plan_params[:checked_plan].map(&:to_i)
@plans = Plan.where(id: selected_plan_ids)
end
#中略
private
def select_plan_params
params.permit(checked_plan: [])
end
end
newアクションの中身はわりと簡単である。チェックをいれたPlanのidの配列はselect_plan_params
でもってこれるようにしている。
view
<h1>振り返り登録</h1>
<%= form_with model: @review, local: true do |form| %>
<p>振り返り日:<%= form.date_field :reviewed_on %></p>
<% @plans.each do |plan| %>
<%= form.fields_for 'plans[]', plan do |f| %>
<p>
目標: <%= plan.name %>
ジャンル:<%= plan.genre_id %>
締め切り:<%=l plan.deadline %>
ステータス:<%= plan.status %>
</p>
<p>変更後の締め切り:<%= f.date_field :deadline %></p>
<p>変更後のステータス:<%= f.select :status, ["進行中","中止","完了"] %></p>
<% end %>
<% end %>
<p>コメント:<%= form.text_field :content %></p>
<%= form.submit "登録" %>
<% end %>
すこしややこしいことをしている。
fields_forについて
fields_forを使うと、ひとつのフォームで異なるモデルを編集することができる。今回はreviewモデルとplanモデルを編集する。
第一引数で渡すパラメータ名を指定している。第二引数のplan
は直前で繰り返しの処理をしている |plan|
のことである。
下記のように第一引数の末尾に[]
をつけない場合、複数のplanモデルの編集ができない。
<%= form.fields_for 'plans', plan do |f| %>
渡されるパラメータ
( "plans"=>{"deadline"=>"2021-11-25", "status"=>"中止"}
となっている。)
params
=> <ActionController::Parameters {
"authenticity_token"=>"****",
"review"=>{
"reviewed_on"=>"2021-11-23",
"plans"=>{
"deadline"=>"2021-11-25", "status"=>"中止"
},
"content"=>"----"
},
"commit"=>"登録", "controller"=>"reviews", "action"=>"create"
} permitted: false>
indexオプションを付与しても[]
と同様のことができる。
<%= form.fields_for 'plans', index: plan.id do |f| %>
もしくは
<%= form.fields_for 'plans[]', plan do |f| %>
params
=> <ActionController::Parameters {
"authenticity_token"=>"****",
"review"=>{
"reviewed_on"=>"2021-11-23",
"plans"=>{
"4"=>{"deadline"=>"2021-11-25", "status"=>"中止"},
"9"=>{"deadline"=>"2021-11-27", "status"=>"完了"}
},
"content"=>"----"
},
"commit"=>"登録", "controller"=>"reviews", "action"=>"create"
} permitted: false>
コントローラ(createアクション)
new.html.erbで登録を押すと、createアクションを実行する。そんなに大きなことはやっていない。パラメータの指定については、binding.pryを挟みながらparamsになにが入っているかを確認して行った。
一発ではわからなかった。
class ReviewsController < ApplicationController
before_action :authenticate_user!
#中略
def create
review = Review.new(review_params)
review.user_id = current_user.id
review.save
param_plans = params.require(:review)[:plans]
plan_keys = param_plans.keys
item = param_plans.values
plan_keys.each_with_index do |id, i|
@plan = Plan.find(id)
@plan.update!(item[i])
review.review_items.create!(plan_id: id)
end
end
#中略
private
def review_params
params.require(:review).permit(:content, :reviewed_on)
end
end
これで登録は完了した。
# おわりに
まだバリデーションの追加や、reviewの編集時の操作など紹介していないことがいろいろあるが、これで一旦終わりにする。またややこしいことがあったら書く。