Rails4: has_manyの保存自動化とタブでの表示分け— accepts_nested_attributes_for, Strong Parameters, Bootstrap


has_manyしている項目を fields_for で記述して、これをBootstrapのタブで分けて表示して、さらにその保存はassociation を介して自動化する(コントローラでhas_many項目の保存を意識しないでよいようにする)ということについてまとめて書きます。

今回のポイントは、 accepts_nested_attributes_for と、そのための strong parameters の書き方、fields_for を使ってタブ表示するためにobjectの値を取得する方法です。


1.前提:モデルの構造

人とその住所のアソシエーションを例とします。

personモデル

※今回の説明には以下は必要ないですが、とりあえず以下のようにしたとします。

person_name string 氏名
gender string 性別
birth_day string 誕生日
addressモデル
location string 場所の種類 「自宅」「会社」「出向先」など
zip string 郵便番号
prefecture string 都道府県
street text 住所
person_id integer belong_to する person

2.モデルの記述

person.rb

accepts_nested_attributes_for を使いますね。

  has_many :addresses, :dependent => :destroy
  accepts_nested_attributes_for :addresses

address.rb

  belong_to :person

3.コントローラの記述

person_controller.rb

# update は @person.update(person_params) だけでよい
def update
  if !@person.update(person_params)
    .... something to process ....
  end
  render ....
end

ポイントは person_params で使う Strong Parameter の記述です。
:addresses_attributes を使うのですね。

def person_params
  params.require(:person).permit( :id, :person_name, :gender, :birth_date, 
    :addresses_attributes => [:id, :location, :zip, :prefecture, :street])
end

4.ビューの記述

has_many を表示するのに fields_for を使いますね。
fields_for 自体が存在するaddressの数分ループもしてくれるのですごく便利です。

<%= form_for :person do |f| %>
  ..... person 用の要素の記述 .....
  <%= f.fields_for :addresses do |fa|%>
    <div class="form-group">
      <%= fa.text_field :place, :placeholder => "PLACE NAME", :class => "form-control" %>
      <%= fa.text_field :zip, :placeholder => "ZIP", :class => "form-control" %>
      <%= fa.select :prefecture, PREFECTURE_LIST,{:include_blank => true}, :class => "form-control" %>
      <%= fa.text_area :street, :row => 2, :placeholder => "STREET",  :class => "form-control" %>
    </div>
  <% end %>

5.ビューをタブ表示にする

上の形だと、住所が全てダラダラと表示されるのでBootstrapのタブを付け加えてみます。
タブのラベルには address.location を、 タブのidには address.id を使います。

form.html.erb—タブラベル部分

 ※最初のタブには class=”active” が必要

<% address_first_id = @pesson.addresses.first.id %>
<div class="nav nav-tabs">
  <ul class="nav nav-tabs">
    <% addresses.each do |address|%>
      <li<%= ' class="active"' if address.id == address_first_id %>><a href="#address_<%= address.id.to_s %>" data-toggle="tab"><%= address.location %></a></li>
    <% end %>
  </ul>
</div>

form.html.erb—タブの中身部分

 ※ここでのポイントは fa.object.id というようにobject記述でデータの内容を読み出せること。

<div id="addressesTabContent" class="tab-content">
  <%= f.fields_for :addresses do |fa|%>
    <div class="tab-pane <%= "active" if fa.object.id == address_first_id %> in" id="address_<%= fa.object.id.to_s %>">
      <div class="form-group">
        <!-- ここには address.location はいらない。ラベルに表示しているので -->
        <%= fa.text_field :zip, :placeholder => "ZIP", :class => "form-control" %>
        <%= fa.select :prefecture, PREFECTURE_LIST,{:include_blank => true}, :class => "form-control" %>
        <%= fa.text_area :street, :row => 2, :placeholder => "STREET",  :class => "form-control" %>
      </div>
    </div> <!-- /tab-pane -->
</div> <!-- /addressesTabContent -->

以上です。概念説明のメモとして簡略化してますので、個々に解決が必要な細かい問題はあるかもしれません。

補足:fields_forのchild_indexを使う方法

上記の例は address.id をタグのidとしていたが、単純に頭から0,1,2…と番号を振って、fields_for では、その child_index を使うという方法もある。fields_for の index は fa.options[:child_index] で取得できる。


参考にさせていただいたページ

広告

Rails: send_data でダウンロードした後に表示更新する方法


データベースの集計を CSVファイルでダウンロードする処理が必要になった。

ファイルダウンロードには send_data を使う。

今回は、非同期処理するつもりはなかったが、集計の処理には10秒くらいかかるので、ボタンをクリックして10秒くらい待ってからダウンロードが始まるので、待ち時間に「しばらくお待ちください」を表示したい。

処理開始時にこれを表示して、ダウンロードが終わったらそれを消すという動きにしたかった。

ボタンをクリック→以下のJavaScript

 $("#message").html("しばらくお待ちください");
 window.location.href = "<%= totaling_path %>"; //←集計してsend_dataするアクションのURL


そして「集計してsend_dataするアクション」の最後にメッセージを消すためのレンダリングして完了!

ところが、上記はうまく行かない。

send_data はそれ自体がレンダリングをしているので、send_dataの後にレンダリングすると Double render のエラーになるからだ。どうする?

ここに解決のヒントがあった。↓
How to trigger download with Rails send_data from AJAX post

理屈としては、処理に時間がかかっているのだから、処理とsend_dataをアクション分けして、処理が終わってから send_data のある別アクションを起動すればいいということ。

この場合に、send_data のあるアクションの起動を partial を使って、javascriptをレンダリングする形で行うというのがミソのようだ。いろんな理由から send_dataのあるアクションは javascriptの window.location を使いたいということもあり、これが都合いい。

模式的に書くと以下のような構成
↓コントローラ

# GET /totaling
def totaling
  CSV.open tempfile_path do |csv| .... などで一時ファイルとして生成
   ....時間のかかる集計処理...
   csv << data_array
  end
  @download_url = download_path(:tempfile_path => tempfile_path, :file_name => "result.csv") # ← download_path は URL /download のヘルパー関数 tempfile_pathやfile_nameは?パラメータになる
  render :partial => "downloadfile"
end

# GET /download
def download
    File.open(params[:tempfile_path], 'r') do |f|
        send_data f.read, :type => 'text/csv',:disposition => 'attachment',:filename => params[:file_name]
    end
    File.delete(params[:tempfile_path]) if File.exist?(params[:tempfile_path])
end

↓ _downloadfile.js.erb (パーシャルファイル)

  window.location.href = "<%= @download_url %>";
  $("#message").html(""); // ←ここで「しばらくおまちください」のメッセージを消す

上記で send_data の代わりに send_file を使うことはしなかった。send_file を使うと、その次の行のFile.deleteでの一時ファイルの削除が先に行われて都合がわるいことがあるからだ。上記の方法なら、記述の順番通りになる。

またApacheの場合、send_fileに必要なX-sendfile を人によってはインストールできない場合もあるだろう。

以上です。何かあればコメントをください。