フィールドが存在しない項目の検証(validate)
前回までに、ユーザーデフォルトの設定が出来るようになったが、現状では入力された値のチェックはしていない。だから予想外の入力がされると、エラーが発生してしまう。入力値の検証機能(validate)が必要だ。
ところが、今回は複数のオブジェクトをyamlに変換して1つのフィールドに保存するようにしている。検証したいのはyamlに変換する前の個々のハッシュオブジェクトの値だ。こんな時はどのように処理するべきなのか?以下は、試行錯誤の結果、その処理方法のメモ。
エラーのチェック
- app/models/default.rb
- モデル(defaultテーブルを管理)
標準的なvalidate
値のチェックの対象がyamlフィールドそのものであれば、以下のように簡潔に表現することが出来る。(例:yamlフィールドはintegerタイプで、値が直接保存されると仮定して、1から100の範囲外の時はエラーにする場合)
class Default < ActiveRecord::Base belongs_to :user validates_exclusion_of :yaml, :in=>(1..100), :message=>"1〜100の範囲の数値でお願いします。" end
簡潔です。とても分かり易い表現。
1フィールドに複数のオブジェクトがまとめて保存されている場合
しかし、今回、yamlフィールドには'per_page=>10'のようなハッシュが、yamlに変換されて保存されている。検証したいのは、default.yaml['per_page']が1〜100の範囲内であるかどうか。以下のようにやってみた。
class Default < ActiveRecord::Base belongs_to :user # unless 1 <= default['per_page'].to_i && # default['per_page'].to_i <= 100 # 上記条件式は、範囲オブジェクトを利用すれば簡潔に表現できる。但し、以下の注意点あり。 # Rubyでは、演算子の優先順位は、=== .. の順である。 # 1..100 === 101 --> 1..(100 === 101)と解釈されて、エラーになる。 # (1..100) === 101 --> falseが返り正常終了。 def validate default = YAML.load(yaml) unless (1..100) === default['per_page'].to_i errors.add('1ページ当りの表示件数:', "1〜100までの数値でお願いします。") end end end
発生したエラーの表現
エラーが発生した場合、標準的なscaffoldと同じように、以下のようなエラー表示を実現したい。
- app/views/defaults/_form.rhtml
- ビュー(環境設定の入力ページ)
- 追記1と、追記2の役割は上記図のようになる。
- app/models/default.rbのvalidateメソッドの中で指定したerrors.add('1ページ当りの表示件数:',...)、オレンジ色の部分が、エラーを表現する場合の項目を区別するキーになる。今までフィールド名である必要があると思っていたが、このような対応関係を理解して自由に書き換えてOKのようだ。
<%= error_messages_for 'default' %><%#<------ 追記1 %> <table> <tr> <th align="right"> <label for="per_page">1ページ当りの表示件数:</label> </th> <td <%= field_with_errors_class(@default, '1ページ当りの表示件数:') %>><%#<------ 追記2 %> <%= text_field_tag 'per_page', @default_hash['per_page'], :name => "default[per_page]" %> </td> </tr> </table>
- app/controllers/defaults_controller.rb
- コントローラー
- 追記1のためには、@defaultにdefaultオブジェクトが代入されている必要がある。(@defaultがnilだとエラーになる。)
- error_messages_for 'default'のオレンジ色の部分defaultは、モデル名を指定している。@defaultとしている変数名は何でも良い。(例:@itemや@objなど自由に設定して大丈夫だと思う。)
class DefaultsController < ApplicationController def edit_preference @default = current_user.default @default_hash = default_user(true) end def set_app_default @default = current_user.default @default_hash = default_app render :action => 'edit_preference' end def update_preference current_user.create_default(:yaml => {}.to_yaml) if current_user.default.nil? @default = current_user.default @default_hash = default_user.merge(params[:default]) # アプリケーションデフォルトと同じ項目は削除したいが、削除してしまうとvalidateが正常に機能しない。 # @default_hash.delete_if {|key, value| default_app[key] == value} if current_user.default.update_attributes(:yaml => @default_hash.to_yaml) flash[:notice] = 'ユーザー環境を更新しました。' redirect_to :controller => 'csvs', :action => 'list' else render :action => 'edit_preference' # 強制リロードを有効にして、キャンセルした時にキャッシュに残らないようにする。 @default_hash = default_user(true) end end end
- app/helpers/application_helper.rb
- ペルパー
- 追記2のペルパメソッドは以下のように定義してある。対応する項目にエラーが発生していたら、class='fieldWithErrors'属性を設定する。
module ApplicationHelper ...(途中省略)... def field_with_errors_class(obj, field) return unless obj "class='fieldWithErrors'" if obj.errors.on(field) end ...(途中省略)...
以上で、ユーザー環境設定で入力した値を検証できるようになった。しかし、フィールドに直接、値を保存する場合と比較して、苦労が多い...。もし、yamlに変換しなければ、今回のような余計なコードは必要なかったはず。Railsにはマイグレーションがあり、フィールドの追加、削除も簡単に対応できるので、素直に1フィールド、1ユーザー設定で標準的に保存した方が良かったかもしれない。