1対多の要素を1つのviewで登録する/個人開発のつづき

# はじめに
　ずっとコードを書いていたが、それだけではだいじなことを忘れそうな気がしたので、ここに記録しておこう。今、個人開発で日報アプリをつくろうとしている。ユーザーは日々の記録を書ける。各作業のジャンル、作業名、何時間したかさいごにその日のコメントをつける。

　ログインした後、reportを登録する。reportひとつに対してreport_itemを複数登録できる。以下のようにしたい。


```
例：
ユーザー名：satou
登録日: 2021/10/21
report_item 1
  ジャンル: Ruby
  やったこと： メソッド復習
　　　　作業時間: 1時間

report_item 2
  ジャンル: 英語
  やったこと： TOEIC対策
　　　　作業時間: 2時間

report_item 3
  ジャンル: 筋トレ
  やったこと： ランニング
　　　　作業時間: 0.5時間

全体のコメント：
　　よくできた。

```

これを実現するにはどうすればいいのだろう。ひとつずつやっていくことにした。


# モデルを考える
まずはモデルとその関係性を考えないといけない。必要なモデルはUser,Report,Report_itemそしてGenreだ。

```
User
 -メールアドレス
 -パスワード
 　1対多: reports/genres
  
Report
 -コメント
 -登録日
 1対多: report_items

Genre
 -ジャンル名
 1対多:report_items

Report_item
 -内容
 -何時間したか
 
```

　ユーザーは複数とReportと複数のGenreを持ち、Reportは複数のReport_itemを持つ。Genreも複数のReport_itemを持つ。個々のバリデーションは考えながら追加しよう。

# モデルを作る
　そうやってモデルを作っていく。はじめにやるのがUserモデルだ。これにはログイン機能が必要だが、私はdeviseというgemを使ってそれを実装する。このdeviseの兼ね合いがあり、あとから追加すると面倒臭かった記憶がある。

https://github.com/heartcombo/devise#starting-with-rails

　ここを参考にしながらgemを導入してモデルをつくった。`rails g model` ではなく `rails g devise`　をつかう。

```terminal
$ rails g devise:install
$ rails g devise User
```

　あとは普通につくる。モデルの関連付けは以下のようになる。

```ruby:app/models/user.rb 
class User < ApplicationRecord
  has_many :reports, dependent: :destroy
  has_many :genres, dependent: :destroy
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
```

```ruby:app/models/report.rb 
class Report < ApplicationRecord
  belongs_to :user
  has_many :report_items, dependent: :destroy
  accepts_nested_attributes_for :report_items
end
```

```ruby:app/models/report_item.rb 
class ReportItem < ApplicationRecord
  belongs_to :report
  belongs_to :genre
end
```

```ruby:app/models/genre.rb 
class Genre < ApplicationRecord
  has_many :report_items,dependent: :restrict_with_exception
  belongs_to :user
end
```

### accepts_nested_attributes_forがだいじ

　report.rb にサラッと記述されている `accepts_nested_attributes_for`だが、これがかなり重要だ。

https://railsguides.jp/form_helpers.html#%E8%A4%87%E9%9B%91%E3%81%AA%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B

>最初は単一のオブジェクトを編集していただけのシンプルなフォームも、やがて成長し複雑になります。たとえば、Personを1人作成するのであれば、そのうち同じフォームで複数の住所レコード(自宅、職場など)を登録したくなるでしょう。後でPersonを編集するときに、必要に応じて住所の追加・削除・変更が行えるようにする必要もあります。

親のモデルに `accepts_nested_attributes_for` を記述することで関連しているモデルも編集することができるようになる。

# ルーティングを整える
　ルーティングはわりと簡単だ。resourcesを使うだけ。

```ruby:config/routes.rb
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 "reports#index"
  resources :reports
end
```

# コントローラーとをビュー作る
　これがちょっと厳しかった。今回はreportのindex/new/createアクションだけ紹介する。
`rails g controller reports` でコントローラーとviewファイルが生成されるはずだ。

### indexとnew　アクション
indexアクションとnewアクションを書く。

```ruby:app/controllers/reports_controller.rb
class ReportsController < ApplicationController
  before_action :authenticate_user!
  def index
    @reports = Report.where(user_id: current_user.id)
  end

  def new
    @report = Report.new
    3.times { @report.report_items.build }
    @select_genre = Genre.where(user_id: current_user)
  end

  def create

  end

end
```

 indexアクションはそこまで問題ではない。Reportを自分が登録したものだけ引っ張ってきている。
`before_action :authenticate_user!` はdeviseによって使用できるメソッドであり、ログインしていない状態ではログインページへリダイレクトされる。

 newアクションではreportを新しく作り、その子要素を3つ作っている。これは新規投稿画面でreport_itemを一気に3つまで登録したいためだ。

### index/newのビュー

　indexのビューは以下のようになる。

```erb:app/views/reports/index.html.erb
<h1>日報一覧</h1>
<% @reports.each do |report| %>
  <p>
  日報ID：<%= report.id %>
  </p>
  <p>日付：<%= report.reported_on %></p>
  <% report.report_items.each do |item| %>
    <p>
    ID：<%= item.id %>
    ジャンルID：<%= item.genre_id %>
    やったこと：<%= item.content %>
    時間：<%= item.work_hours %>
    </p>
  <% end %>
  コメント： <%= report.content %>
  </p>
  <hr>
<% end %>
```

いたって普通だ。コントローラーの@reportsを呼び出し、繰り返しで中の要素を書き出している。ジャンルIDを普通の名前表示にしたりとかは、また別の話。

newのビューは以下のようになった。

```erb:app/views/reports/new.html.erb
<h1>日報登録</h1>
<%= form_with model: @report, local: true do |f| %>
  <p>登録日：<%= f.date_field :reported_on , value:Date.today%></p>
  <%= f.fields_for :report_items do |item| %>
    <p>ジャンル：<%= item.collection_select :genre_id, @select_genre, :id, :name, :include_blank => true %></p>
    <p>やったこと：<%= item.text_field :content %></p>
    <p>作業時間：<%= item.text_field :work_hours %></p>
  <% end %>
  <p>コメント：<%= f.text_field :content %></p>
  <%= f.submit "登録" %>
<% end %>
```

ここが少し大変だった。form_withとfields_forの使い方を調べるのが難しかった。Railsのバージョンが新しくなってから、form_forとform_tagではなくform_withを利用することが推奨され始めた。

`　form_with` 　のあとに登録するモデルを書く。繰り返しの構文のように `|f|` を指定してやる。`f.date_field` や`f.text_field` を設定することでいろんなスタイルのフォームを作成できる。

　あとは`fileds_for` を使うとreportsとは関係のないモデルへの登録も可能となる。ここで`|item|` をつかって繰り返しをすると、さきほどつくった3つの子要素のフォームが表示される。

このあたりは公式のgithubのコメント欄にいろいろ書いてある。rails ドキュメントより記載が豊富だ。

https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/actionview/lib/action_view/helpers/form_helper.rb#L710


### createアクション

　これが本当に面倒くさかった。なにをどう登録するかがわからなかったのだ。まずはbinding.pryを挟んで、newアクション後のparamsになにが渡っているのか確認した。

```ruby:app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 略

  def create
   binding.pry
  end

end
```

以下のようなものが入っていた。


```terminal
pry(#<ReportsController>)> params
=> <ActionController::Parameters {"authenticity_token"=>"qyEey4ZqOElVMESUggFdPNOjsgsmDDK7yLZDtwuxv9yxVsI8kPkzspTbkVhyYyeppK1qR/mhG7ed1sjDR95wDA==", "report"=><ActionController::Parameters {"reported_on"=>"2021-10-17", "report_items_attributes"=>{"0"=>{"genre_id"=>"1", "content"=>"a", "work_hours"=>"3"}, "1"=>{"genre_id"=>"", "content"=>"", "work_hours"=>""}, "2"=>{"genre_id"=>"", "content"=>"", "work_hours"=>""}}, "content"=>"test"} permitted: false>, "commit"=>"登録", "controller"=>"reports", "action"=>"create"} permitted: false>
```

なんとなくわかった。

このまま利用するとセキュリティ的にあぶないとどこかに書いてあった。

railsにはマスアサイメントといって複数の項目を一括で更新/登録ができる機能が備わっている。しかし、これは渡されるハッシュを読み取っているだけなので、一般ユーザが管理者権限がないと更新できない項目もいぢれてしまう！という可能性もでてくるらしい。

ストロングパラメータを使って、設計者側から利用できるパラメータを指定すべき、らしい。

https://railsguides.jp/action_controller_overview.html#strong-parameters


ネストされているので少し工夫が必要だ。

```
params.require(:report).permit(:content, :reported_on, report_items_attributes: [:content, :genre_id, :work_hours, :id])
```

上記のようにした。`report_items_attributes` には`:id`も必要だった。これをストロングパラメータとしてコントローラーに追記する。

```ruby:app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 略

  def create
   binding.pry
  end

# 略

  private

  def report_params
    params.require(:report).permit(:content, :reported_on, report_items_attributes: [:content, :genre_id, :work_hours, :id])
  end
end
```

あとは3つの子要素のうち、1つだけ/2つだけ登録したいというときの処理を考える必要がある。ストロングパラメータにしたreport_paramsにはなにがはいっているのか。

```
report_params
=> <ActionController::Parameters {
 "content"=>"eee", "reported_on"=>"2021-10-17",
 "report_items_attributes"=>
  <ActionController::Parameters {
    "0"=><ActionController::Parameters {"content"=>"a", "genre_id"=>"2", "work_hours"=>"1"} permitted: true>, 
    "1"=><ActionController::Parameters {"content"=>"", "genre_id"=>"", "work_hours"=>""} permitted: true>, 
    "2"=><ActionController::Parameters {"content"=>"", "genre_id"=>"", "work_hours"=>""} permitted: true>
   } 
 permitted: true>
} 
permitted: true>
```

なるほど。いろいろ入っていた。"1","2"のなかみがcontent=""になっているので、これをうまいこと除去してやればよさそうだ。

そうして以下のようになった。

```ruby:app/controllers/reports_controller.rb
class ReportsController < ApplicationController
# 略

  def create
    para = report_params[:report_items_attributes]
    first_key = para.keys.first
    first_value = para.values.first
    para.reject! { |_key, value| value[:content] == "" }
    para[first_key] = first_value unless para.key?("content")
    formatted_para = report_params
    formatted_para[:report_items_attributes] = para
    report = Report.new(formatted_para)
    report.user_id = current_user.id
    report.save!
  end
# 略

end
```

中央のpara.rejecet!でcontentが空白になっているものは除去するようにした。3つの子要素すべのcontentが空白だとバリデーションではじくようになっている。


#　できた

　やっとひとつの機能ができた。なんだかうれしい気分になった。でも次に行った実装はもっと面倒くさかった。多対多の要素の登録である。

　それはまた別のお話。
↓やっていく　github
https://github.com/kyokucho1989/simple-record