Rails+JQuery.validation: Radio Button の errorPlacement


複数ページのフォームでバリデーションを行うケースがあり、railsのモデルのバリデーションが使えないように思ったので、jquery-validation-rails を使うことにした。

しかし、デフォルトのエラー表示の位置が気に入らない。
例えば、下の[     ]が入力フィールドであるとして

姓[     ] 名[     ]

これらを必須項目にすると、姓の入力がない場合、

姓[     ] 姓を入力してください 名[     ]

のように「姓」の後ろにエラーが表示され「名」が離れてしまう。

そこでerrorPlacement を使う。例えば、以下のようにすれば、「姓」も「名」もエラーメッセージを後ろに集めることができる。これは Jquery Validation plug-in custom error placement からの受け売り。

姓<%= f.text_field :family_name %> 名<%= f.text_field :first_name %>
<span id="invalid-registration_family_name"></span> <!-- <=ここでエラー表示 -->
<span id="invalid-registration_first_name"></span>
$("form").validate({
  rules: { ..... 略 ..... },
  messages: { ..... 略 ..... },
  errorPlacement: function(error, element) {
      error.insertAfter('#invalid-' + element.attr('id'));
  }
});

ここからが本題。

ラジオボタンなんかはどうすればいいのだろう。ラジオボタンでは、elementのIDがみんな違っていて、それら全てを span で書かなければならないだろうか?スゴイたくさんの選択肢がある場合に困る。

elementのIDではなくname を使えれば問題ないはずだ。以下のように書き直してみる。

error.insertAfter('#invalid-' + element.attr('id'));
↓
error.insertAfter('#invalid-' + element.attr('name'));

しかし、rails の form_for 要素では element name は例えば name=”registration[xxxx]” のようになっていて、この [ ] 記号がIDとしてはどうもまずいらしい。どうする?

結論↓

いろんなやり方がある。element name を使って、その[]を例えば _ に文字置換するとか。
僕は以下のようにしてみました。正規表現でregistration[]を取り除いてデータのアイテム名だけにしている。
動いてはいるが問題ないだろうか。

以下から選択してください。<span id="invalid-category"></span> <!-- <=ここでエラー表示 -->
<%= f.radio_button :category, '010', {} %><%= f.label :category, "ソフトウェア開発",'010'  %>
<%= f.radio_button :category, '020', {} %><%= f.label :category, "プロバイダ",'020' %>
<%= f.radio_button :category, '030', {} %><%= f.label :category, "製造",'030' %>
$("form").validate({
  rules: { ..... 略 ..... },
  messages: { ..... 略 ..... },
  errorPlacement: function(error, element) {
    var element_name = element.attr('name');
    var field_name = element_name.match(/.+\[(.+)\]/);
    error.insertAfter('#invalid-' + field_name[1]);
  }
});

Rails: 1つの form_for でコンテンツとは別にFile Upload


1つの form_for の中で、添付ファイルをアップロードもやりたいのだけれど、その file_field は別に form_for のコンテンツの項目でもアソシエーションでも何でもないということがあり、以下のように書いたが、うまく行かなかった。

↓View

<%= form_for @note do |f| %>
  <%= f.text_field :title %>
  <%= f.text_field :body %>
  <%= file_field_tag :attachment %>
<% end %>

↓Controller

def create
  @note = Note.new(note_params)
  respond_to do |format|
    if @note.save
      if params[:attachment]
        attachment = params[:attachment] # <= ここでファイル情報を持つオブジェクトが取得できない
        filename = attachment.original_filename # <= したがってoriginal_filenameも取得できない
        ..... 略 ......
      end
      ..... 略 ......
    end
  end
end

def note_params
  params.require(:note).permit(:title, :body)
end

上記で、attachment = params[:attachment] は、ファイル情報を持つオブジェクトが取得できない。ファイル名の文字列だけが取得されている。

attachment を form_for の傘下に含めれば、ファイル情報を持つオブジェクトが取得できる。つまり、以下のようにviewを書けばよいことはわかったが……。

<%= form_for @note do |f| %>
  <%= f.text_field :title %>
  <%= f.text_field :body %>
  <%= f.file_field :attachment %> # <= f. を付けた file_field
<% end %>

そうすると、Strong Parameter も変更か。permitを記述するのはいいことだ……などと考える。

def note_params
  params.require(:note).permit(:title, :body, :attachment)
end

いやいや、これではNoteモデルにない項目を追加することになるし、@note = Note.new(note_params) も当然エラーになってしまう。どうする?

とりあえず、以下のようにしてStrong Paramaterを別にして回避したが、これでいいのだろうか。

↓結論(今のところの)

def create
  @note = Note.new(note_params)
  respond_to do |format|
    if @note.save
      if attachment_params[:attachment]
        attachment = attachment_params[:attachment] # <= ファイル情報を持つオブジェクトが取得できた!
        filename = attachment.original_filename # <= したがってoriginal_filenameも取得OK
        ..... 略 ......
      end
      ..... 略 ......
    end
  end
end

def note_params
  params.require(:note).permit(:title, :body)
end
def attachment_params # <= コレ追加
  params.require(:note).permit(:attachment)
end

Rails4: ActiveRecord polymorphicのjoins検索?(追記あり)


polymorphicをjoinsで記述する方法が分からず、ほとんどSQLで書いたことのメモです。

例えば動物と植物のデータテーブルがあって、研究報告書のテーブルを共有して使うような構造。

class Report < ActiveRecord::Base
  belongs_to :reportable, polymorphic: true
end
class Animal < ActiveRecord::Base
  has_many :report, as: :reportable, dependent: :destroy
end
class Plant < ActiveRecord::Base
  has_many :report, as: :reportable, dependent: :destroy
end

動物のコアラと植物のユーカリのレポートを検索して一覧に表示したい。

ここでの前提は、何らかの検索の結果としてのReportのコレクション reports が手元にあり、ここから検索を追加記述しなければならないということ。(つまり AnimalやPlan側からの開始でReportを抽出できない)
joinsで検索を追加できるだろうか?

調べたが分からなかったので、とりあえず期待を込めてこうだったらいいなで書いてみる。

koala_reports = repors.joins(:animals).where(:animals => {:specie => KOALA})
eucalyptus_reports = repors.joins(:plants).where(:plants => {:classification => EUCALYPTUS})
result = koala_reports | eucalyptus_reports

残念、やっぱりダメで1〜2行目で report は animalを持っていない、plants を持っていないという意味でエラーになる。ではreportabelだろうかとヤマカンをはる(多分かなり疲れていた)。

koala_reports = repors.joins(:reportabel).where(:animals => {:specie => KOALA})
eucalyptus_reports = repors.joins(:reportabel).where(:plants => {:classification => EUCALYPTUS})
result = koala_reports | eucalyptus_reports

当然、これも間違い。
仕方ないので、SQLでベタに書くことにした。

koala_reports = repors.joins("JOIN animals ON animais.id = reports.reportable_id AND reports.reportable_type = 'Animal' AND animals.specie = #{KOALA}")
eucalyptus_reports = repors.joins("JOIN plants ON plants.id = reports.reportable_id AND reports.reportable_type = 'Plant' AND plants.classification = #{EUCALYPTUS}")
result = koala_reports | eucalyptus_reports

なにかよい記述方法があるのだろうか。


【追記】

多分、repors.animals と repors.plants でアクセスできるようにモデルを書きかえた上で(前提が変わるか?)、以下で行けるのでは? Arelを使う。2015/03/04

result = repors.includes(:animals, :plants)
.where(Animal.arel_table[:specie].eq(KOALA)
.or(Plant.arel_table[:classification].eq(EUCALYPTUS)))
.references(:animals, :plants)

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 を人によってはインストールできない場合もあるだろう。

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

Rails4: ActiveRecord カラム演算してその合計を出す—例えば数量×単価の合計


テーブルのカラムをかけ算してその合計を出す。例えば検索されたデータ行の数量×単価の合計を出したい。

結論を言えば下記。
Eitem というモデルに数量(quantity)と単価(price)がある。これらは同じestimate_idという見積書データのidを持っているものとする。

    eitems = Eitem.where(:estimate_id => estimate_id).select("sum(eitems.quantity * eitems.price) as total, eitems.estimate_id").group(:estimate_id).order(nil)
    total = eitems.first.total

余談だが、order(nil)しておかないと eitems.id で order by してしまってエラーになる。

ActiveRecordネイティブぽい記述(sum()とか)でできると思ってググったり、modelにメソッドを追加したり、半日試行錯誤したが、結局 selectを使う方法しか考えられなかった。

もっとActiveRecordっぽいいい方法があるのだろうか。

参考:Rails 3.1 – find using count and select as

Rails4: includesで除外をする


Rails4 です。
Personモデルが複数の Emailモデルを持っているという has_many なアソシエーションで、ある特定の email を持っているいる人を除外したい。
とりあえず、除外でなく、持っている人を検索するなら……

@people_with_email = Person.all.includes(:emails).where("emails.email='xxx@xxx.com' or emails.email='yyy@yyy.com').references(:emails)

で良いようだ。
では、除外するには……

@people_except_email = Person.all.includes(:emails).where("emails.email != 'xxx@xxx.com' or emails.email != 'yyy@yyy.com'").references(:emails)

でよいか……どうも良くないみたいだ。
試行錯誤の結果以下のようにした。

ids = Person.connection.select_rows(Person.joins(:emails).where("emails.email = 'xxx@xxx.com' or emails.email = 'yyy@yyy.com'").select("people.id").to_sql)
@people_except_email = Person.all.includes(:emails).where.not(:id => ids).references(:emails)

参考にさせていただいたサイトは以下です。本当に助かりました。ありがとうございます。