====== Error Handling and Validation ====== ===== Standard Methods ===== First of all, take a look at [[http://api.rubyonrails.org/classes/ActiveRecord/Errors.html|Class ActiveRecord::Errors]]. Here are pretty decent error methods to start with. ==== Dirty Details ==== The error_messages_for method outputs neatly formatted error messages for a given object. The catch is that you need to specify the name of the instance variable here, not the object name itself. Take the following example. If you have this instance variable in your controller: @user = PremiumUser.find(your_id) Then you should use this code in your view to output the error messages: <%= error_messages_for 'user' %> ==== Gotcha: validating a hashed password ==== If you play it safe, you store your passwords in some encrypted format, e.g. an md5 hash. Here's the code to do this: def password=(password) write_attribute(:password, Digest::MD5.hexdigest(password) ) end ## NOTE: do NOT use this code Now, you also want to safeguard your web application against brute force password guessing cracks. Nobody should be allowed to have a password of less than 8 characters. So, you add in a validation: validates_length_of( :password, :minimum => 8, :message => 'too short (minimum 8 characters)'.t) But as you test this code, you notice that even one character-sized passwords get validated! This is because every md5 hash generates a string with a predetermined length (usually 128 bits), which almost always exceeds your minimum password length. So, how do you validate the password's length? By converting it into a hash **after** validation has taken place! Scrap the write accessor, and put in this code: def prepare_password self.password = Digest::MD5.hexdigest(self.password) end How do we run this converter after validation? Simple. Put in this callback: after_validation :prepare_password ===== Globalization and localization ===== Use the globalize plugin to localize the error messages. Insert the following code in ''**rails_apps/your_rails_app/vendor/plugins/for-1.1/lib/globalize/rails/active_record_helper.rb**''. This is the only code that really works in Rails 1.1.6 (there are several other code sources, but I couldn't get them to work). I found the code on the [[http://spongetech.wordpress.com/2006/12/24/error_messages_for-you/|Spongecell Techblog]] def error_messages_for(object_list, options = {}) return "" if object_list.nil? options = options.symbolize_keys bullets,main_obj_name = bullets_from_errors(object_list, options) if !bullets.blank? #default to standard error message. replace the ${NUM_ERRORS} with the number of errors. #options[:header_message] ||= "%d errors prohibited this %1 from being saved".t(nil,bullets.length).gsub('%1',"#{(main_obj_name || 'object').t}") #options[:header_message] ||= ("%d errors prohibited this %1 from being saved".t(nil,bullets.length)).gsub('%1',"#{(main_obj_name || 'object').t}") options[:header_message] ||= "The data could not be saved".t options[:header_message] = options[:header_message] % bullets.length content_tag("div", content_tag( options[:header_tag] || "h2", options[:header_message]) + content_tag("p", (options[:header_sub_message] || "There were problems with the following fields:").t) + content_tag("ul", "#{bullets}"), "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" ) else "" end end def bullets_from_errors(object_list,options) subs = options[:sub] ? options[:sub].stringify_keys : {} options[:skip] ||= [] main_obj_name = nil #create a list of bullets (html
  • tags) by concatenating the objects bullets= [object_list].flatten.inject([]) do |msg_list,object| #if it is a string get the instance variable if object.kind_of?(String) main_obj_name ||= "#{object.to_s.gsub("_", " ")}" object = instance_variable_get("@#{object}") elsif object main_obj_name ||= object.class.to_s.titleize.downcase end #if the object exists and responds have the errors object append each error to the list if (object && object.respond_to?('errors') && !object.errors.empty?) object.errors.each do |attr, msg| if (!options[:skip].include?(attr.to_s)) obj_name = (subs[attr] || "#{attr == 'base' ? '' : object.class.human_attribute_name(attr)}") #replace the field names with the names specified in the subs hash msg_list << content_tag('li', "#{obj_name} #{msg}".t("#{obj_name.t} #{msg.to_s.t}")) end end end msg_list end return bullets, main_obj_name end ===== Validating Multiple Form Submits For One Model ===== Suppose you have a model that contains about thirty required fields, with fifty optional fields mixed in. Every interaction designer will tell you that you need to split up your eighty fields form into at least four separate forms. But as you do so, you notice that some required fields end up in the form for the last step. Now how do you validate the data in each steps? You could save up all data and do one massive insert or update, pointing out all validation errors in a separate form. But your interaction designer somehow doesn't seem to like this idea. In fact, what he wants is this: - User submits data - Validation feedback is immediately presented in the same form - Use goes to next form ==== Reporting Relevant Validation Feedback Only ==== This requires us to save the data from each form if the form fields are valid, ignoring all error messages about the attributes that were not on the form. In other words, we need relevant information only. Here's the strategy: * See if there are any errors pertaining to our current form. If there are, store them in a temporary hash. Delete all error messages from the original errors object. * Copy the relevant from our temporary hash back to the errors object. * If there are no relevant errors, use the ''**save_with_validation(false)**'' method to save the data **without** validation. Here's an example I've actually used in one of my controllers: def save_step_two @user = User.new if @user.update_attributes(params[:user]) ## Go ahead as usual flash[:notice] = 'User was successfully updated.'.t redirect_to :controller => 'membership', :action => 'step_three' session[:membership] = @user.membership.system_name else ## See if there are any errors pertaining to our current form. If there are, store them in ## a temporary hash. Delete all error messages from the original errors object. temp_errors = Hash.new @user.errors.each do |attr, msg| temp_errors[attr] = msg[0] if params[:user].has_key?(attr) end @user.errors.clear if temp_errors.empty? ## Save the form without further validation ## - and move on to the next step @user.save_with_validation(false) flash[:notice] = 'User was successfully updated.'.t redirect_to :action => 'step_three' else ## Add the error messages in our temporary hash to the error object ## - and go back to the original form temp_errors.each do |attr, msg| @user.errors.add(attr, msg) end flash[:error] = 'Something went wrong while updating the user.'.t render :action => 'step_two' end end end ===== Generating Required Field Indicators ===== If you define required attributes for your model with ''**validates_presence_of**'', you don't have to repeat this information in your views any longer. This [[http://lists.rubyonrails.org/pipermail/rails/2006-July/056717.html|forum thread]] was a source of inspiration for auto-generating css classes on form fields. Given an example object @user use this method in your views ''**@user.class.attr_presence_required.include?(:firstname)**'' to find out whether ''firstname'' is a required field. You can use this information to output an appropriate css class, as shown in the next sample code:
    <%= text_field 'user', 'firstname' %>
    Here, the html class attribute is set to ''**required**'' if the ''firstname'' field is required, otherwise the html class attribute is set to ''**optional**''. You can combine this approach with an error indicator:
    <%= text_field 'user', 'firstname' %>
    ==== Extending the ClassMethods Module ==== To make it all work, we have to extend the ClassMethods module. Add the following code to a library which you then ''require'' in environment.rb. module ActiveRecord module Validations module ClassMethods alias_method :no_tracking_validates_presence_of, :validates_presence_of @@attr_presence_required = Hash.new def validates_presence_of(*attr_names) self.attr_presence_required = Hash.new unless self.attr_presence_required attr_names.each {|a| self.attr_presence_required[a]=true} no_tracking_validates_presence_of(*attr_names) end def attr_presence_required @@attr_presence_required end def attr_presence_required=(value) @@attr_presence_required = value end end end end Here, the required fields are copied to a hash which is stored in a class variable. Because the ClassMethods module is mixed in with the model classes, you can access the hash from within your model class. Please note that you're using an instance variable in your views (most of the time). So to get to the hash from within your view, you need to get to the class first: ''@user.**class**.attr_presence_required''. Furthermore, you can also access the validation status for each field: the attribute is stored as the key in the ''attr_presence_required'' hash, which points to a boolean indicating whether the attribute is valid (true) or not (false). Finally, there's also a more comprehensive (and complicated) approach by Michael Schuerig. Please visit these links to learn more about it: * [[http://wiki.rubyonrails.org/rails/pages/Validation+Reflection|Validation Reflection]] * [[http://schuerig.de/michael/blog/index.php/2006/12/15/rails-almost-automatic-client-side-validation/|Rails: Almost Automatic Client-Side Validation]] ===== LiveValidation Plugin ===== The [[http://www.livevalidation.com|livevalidation plugin]] reads your model-based validation rules (e.g. "validates_presence_of") and turns them into client-side javascript-based form validation. Here's the API of the plugin: [[http://livevalidation.rubyforge.org/|http://livevalidation.rubyforge.org]]. By and large, this plugin works okay. There are a few pitfalls though. ==== validate_format_of for email ==== Some regular expressions which can be used in Ruby do not translate into JavaScript. Livevalidation literally copies everything out of the validation rules. So: validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :message => "Please check email format. Example: mike@gmail.com" Is turned into: This lovely piece of javascript code is inserted right underneath your email form field. The message here is to check your regular epxressions. See to it that they keep working in javascript! ==== Password Confirmation ==== The normal usage pattern of "validates_confirmation_of :password" is that you first have a password field, and then a confirmation field. Livevalidation, however, scans your model and adds its javascript to the **first** field. For instance, validates_confirmation_of :password results in a situation where the confirmation check is applied immediately when you leave the password field and enter the password-confirmation field. This is very awkward of course, so you'll have to reverse and relabel the fields in your view:


    <%= f.password_field :password_confirmation, :onchange => "$('user_password').value=''" %>


    <%= f.password_field :password %>

    If you take a good look at the ids (as shown in the "for" attribute of the label tag), you'll notice that I've put the confirmation field before the actual password field. This is okay as long as you remember to put your onchange attribute (which clears the second field upon changes in the first field) in the first field as well. ==== You Are Completely Wrong Until Proven Otherwise ==== Livevalidation's default is to immediately check a form field as you start typing. This is very annoying, as it results in error messages before you've even had a chance to complete the field. So I've made some changes in the javascript file (LiveValidation 1.3, prototype.js version). Take a look at the initialize function (the constructor) of the LiveValidation object. Here you get a chance to change the default values. For example, I've set "onlyOnBlur" to true: onlyOnBlur: true Now my form field does not get validated until I leave the field. ==== Repairing Your Mistake Results in Another Error Message ==== Another annoying feature is that empty form fields are reported as invalid as well. The following scenario clearly illustrates this: * You are in an email field where enter your mail address, but you somehow manage to leave out the @ sign * Then you enter the next field, username, whose presence is required (there's a validates_presence_of in your model) * Because your email address is invalid you get to see a message on the mail field * So, you put your cursor back in the mail field to repair things * But all of a sudden, a new error message shows up, because you've left the username field blank! This is unacceptable, so I've arranged for all validates_presence_of check to be made onSubmit only. Here's my rather ugly hack of the ''form_helpers.rb'' file: def live_validation(object_name, method) if validations = self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false field_name = "#{object_name}_#{method}" presence_code = nil validation_types = validations.map do |type, configuration| if type == :presence presence_code = live_validation_code("#{field_name}_presence", type, configuration) '' else live_validation_code(field_name, type, configuration) end ##live_validation_code(field_name, type, configuration) end.join(';') javascript = initialize_validator(field_name) + validation_types if presence_code javascript + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code else javascript end else '' end end def initialize_validator(field_name, options = nil) if options "var #{field_name}_presence = new LiveValidation('#{field_name}',#{options});" else "var #{field_name} = new LiveValidation('#{field_name}');" end end Now, for all validates_presence_of validations, a separate LiveValidation object is instantiated which is called only when you submit the form. ==== Patching LiveValidation to Cooperate with Rails 2.1 ==== ''validates_length_of'' does not take custom messages in Rails 2.1. This needs repairing first. [[http://rails.lighthouseapp.com/projects/8994/tickets/1057-validates_length_of-does-not-accept-custom-messages-when-within-used|Here]] is a patch. Now apply this patch to Globalize too, if you use that module. This is the file: ''plugins/globalize/lib/globalize/rails/active_record.rb'', and this is the patched code: def validates_length_of(*attrs) [... stuff omitted ... ] #too_short = options[:too_short] #too_long = options[:too_long] too_short = (options[:message] || options[:too_short]) % option_value.begin too_long = (options[:message] || options[:too_long]) % option_value.end [... stuff omitted ... ] end Now Rails 2.1 supports custom error messages on ''validates_length_of''. Hurray! So, why not have LiveValidation play along? In ''live_validation.js'', look for the ''Length: function(value, paramsObj)'' line. Apply these changes: Length: function(value, paramsObj){ var value = String(value); var paramsObj = paramsObj || {}; // Onno Schuit inserted paramsObj.failureMessage as fallback, to ensure compatibility with Rails' :within parameter. var params = { wrongLengthMessage: paramsObj.wrongLengthMessage || paramsObj.failureMessage || "Must be " + paramsObj.is + " characters long!", tooShortMessage: paramsObj.tooShortMessage || paramsObj.failureMessage || "Must not be less than " + paramsObj.minimum + " characters long!", tooLongMessage: paramsObj.tooLongMessage || paramsObj.failureMessage || "Must not be more than " + paramsObj.maximum + " characters long!", is: ((paramsObj.is) || (paramsObj.is == 0)) ? paramsObj.is : null, minimum: ((paramsObj.minimum) || (paramsObj.minimum == 0)) ? paramsObj.minimum : null, maximum: ((paramsObj.maximum) || (paramsObj.maximum == 0)) ? paramsObj.maximum : null } That's it! ==== Patching LiveValidation to Cooperate with Rails 2.3.2 ==== Rails 2.3 saw the introduction of nested models / nested attributes. So, to keep LiveValidation (1.3) running, I've patched ''vendor/plugins/livevalidation/lib/form_helpers.rb'' and (in the same directory) ''live_validations.rb''. As an added bonus, the patch also provides: * support for select boxes (i.e. the FormOptionsHelper ''select'' method) * an automatically included destroyer of LiveValidation objects for form elements with the same id (handy for ajax refreshes, where your LiveValidation instances get stale otherwise) I've also customized the livevalidation.js file, which I have included here as an external backup: {{ror:livevalidation_patched2.js.txt|}} Please note that the Rails ajax_wipe method defined below outputs a call to the javascript method destroyById, which is defined in my patched livevalidation.js file. === form_helpers.rb === ''form_helpers.rb'': module ActionView mattr_accessor :live_validations ActionView::live_validations = true module Helpers module FormHelper [ :text_field, :text_area, :password_field ].each do |field_type| define_method "#{field_type}_with_live_validations" do |object_name, method, options| live = options.delete(:live) live = ActionView::live_validations if live.nil? object = options[:object] send("#{field_type}_without_live_validations", object_name, method, options) + ( live ? live_validations_for(object_name, method, object) : '' ) end alias_method_chain field_type, :live_validations end def live_validations_for(object_name, method, object = nil) script_tags(live_validation(object_name, method, object)) end private def tag_id(object_name, method_name) "#{sanitized_object_name(object_name)}_#{sanitized_method_name(method_name)}" end def sanitized_object_name(object_name) object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") end def sanitized_method_name(method_name) method_name.sub(/\?$/,"") end def live_validation(object_name, method, object = nil) if validations = (object) ? object.class.live_validations[method.to_sym] : self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false #if validations = self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false field_name = tag_id(object_name,"#{method}") presence_code = nil validation_types = validations.map do |type, configuration| if type == :presence presence_code = live_validation_code("#{field_name}_presence", type, configuration) '' else live_validation_code(field_name, type, configuration) end ##live_validation_code(field_name, type, configuration) end.join(';') ajax_wipe = "LiveValidationForm.destroyById('#{field_name}'); " javascript = (validations.size == 1 and presence_code) ? "" : initialize_validator(field_name) + validation_types if presence_code #javascript + ";" + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code ajax_wipe + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code + ";" + javascript else ajax_wipe + javascript end else '' end end def initialize_validator(field_name, options = nil) if options "var #{field_name}_presence = new LiveValidation('#{field_name}',#{options});" else "var #{field_name} = new LiveValidation('#{field_name}');" end end def live_validation_code(field_name, type, configuration) "#{field_name}.add(#{ActiveRecord::Validations::VALIDATION_METHODS[type]}" + ( configuration ? ", #{configuration.to_json}" : '') + ')' end def script_tags(js_code = '') ( js_code.blank? ? '' : "" ) end end module FormOptionsHelper def select_with_live_validations(object, method, choices, options = {}, html_options = {}) live = options.delete(:live) live = ActionView::live_validations if live.nil? real_object = options[:object] select_without_live_validations(object, method, choices, options, html_options) + ( live ? live_validations_for(object, method, real_object) : '' ) end alias_method_chain :select, :live_validations end class InstanceTag #def to_input_field_tag_with_script(field_type, options = {}) # "#{to_input_field_tag_without_script(field_type, options = {})}" #end #alias_method_chain :to_input_field_tag, :script end end end === live_validations.rb === ''live_validations.rb'': module ActiveRecord module Validations LIVE_VALIDATIONS_OPTIONS = { :failureMessage => :message, :pattern => :with, :onlyInteger => :only_integer } # more complicated mappings in map_configuration method VALIDATION_METHODS = { :presence => "Validate.Presence", :numericality => "Validate.Numericality", :format => "Validate.Format", :length => "Validate.Length", :acceptance => "Validate.Acceptance", :confirmation => "Validate.Confirmation" } module ClassMethods VALIDATION_METHODS.keys.each do |type| define_method "validates_#{type}_of_with_live_validations".to_sym do |*attr_names| send "validates_#{type}_of_without_live_validations".to_sym, *attr_names define_validations(type, attr_names) end alias_method_chain "validates_#{type}_of".to_sym, :live_validations end def live_validations @live_validations ||= {} end private def define_validations(validation_type, attr_names) conf = (attr_names.last.is_a?(Hash) ? attr_names.pop : {}) attr_names.each do |attr_name| configuration = map_configuration(conf.dup, validation_type, attr_name) add_live_validation(attr_name, validation_type, configuration) end end def add_live_validation(attr_name, type, configuration = {}) @live_validations ||= {} @live_validations[attr_name] ||= {} @live_validations[attr_name][type] = configuration end def map_configuration(configuration, type = nil, attr_name = '') LIVE_VALIDATIONS_OPTIONS.each do |live, rails| configuration[live] = configuration.delete(rails) end if type == :numericality if configuration[:onlyInteger] configuration[:notAnIntegerMessage] = configuration.delete(:failureMessage) else configuration[:notANumberMessage] = configuration.delete(:failureMessage) end end if type == :length and range = ( configuration.delete(:in) || configuration.delete(:within) ) configuration[:minimum] = range.begin configuration[:maximum] = range.end end if type == :confirmation configuration[:match] = self.to_s.underscore + '_' + attr_name.to_s + '_confirmation' end configuration[:validMessage] ||= '' configuration.reject {|k, v| v.nil? } end end end end ==== Validating Radio Buttons With LiveValidation ==== LiveValidation does not support radio button validation. That's perfectly understandable because, after all, there is not much to validate. Or is there? Well, sometimes you want to make sure that a user has made a choice, without preselecting a default choice. For example, if you want to be politically correct on your forms, you should probably abstain from making a default choice for "gender". So, to ensure that the user has made a choice when the form is submitted, store the "choice fact" ("a choice has been made") in a dummy form field. This is the field that we will actually validate.
    <%= main_form.radio_button :male, true, {:onchange => "recordState('dummy')"}%><%= t("guest.form.male") %> <%= main_form.radio_button :male, false, {:onchange => "recordState('dummy')"} %><%= t("guest.form.female") %> <%= text_field_tag "dummy", "", :readonly => "readonly", :style => "width:0px;height:18px;padding:0px;margin:0px;border:0px solid white;visibility:hidden;" %>
    (No, the actual underlying code is not PC, but the user never gets to see that unless she dives into the html source). Here's the accompanying little javascript function: /** * Set a field value to 1 and give it focus (usefull if you want to make sure * that the user has selected at least one item from a range of options) * * @var id {string} - the string of the object */ function recordState(id) { $(id).value = "1"; $(id).focus(); } ==== Patching LiveValidation v. 2007/10/12 for Rails 2.3.8 ==== Rails 2.3.8 escapes all Ruby strings in Erb templates, including our LiveValidation javascript. To prevent this, add the following code (in addition to the changes mentioned above) in ''vendor/plugins/livevalidation/lib/form_helpers.rb''. module ActionView mattr_accessor :live_validations ActionView::live_validations = true module Helpers module FormHelper ## Nothing changed... [ :text_field, :text_area, :password_field ].each do |field_type| define_method "#{field_type}_with_live_validations" do |object_name, method, options| live = options.delete(:live) live = ActionView::live_validations if live.nil? object = options[:object] send("#{field_type}_without_live_validations", object_name, method, options) + ( live ? live_validations_for(object_name, method, object) : '' ) end alias_method_chain field_type, :live_validations end ## Following two methods have been changed to use the 'raw' method def live_validations_for(object_name, method, object = nil) ## As of Rails 2.3.8, strings which end up in Erb templates are always escaped - but we need this one 'raw' raw("") end def custom_livevalidation(html_id, validations) content = "LiveValidationForm.destroyById('#{html_id}');\n" validations.each do |validation, message| message_key = (validation.to_s == "Numericality") ? "notANumberMessage" : "failureMessage" only_on_submit = (validation.to_s == "Numericality") ? "false" : "true" content += "var val_#{html_id}_#{validation} = new LiveValidation('#{html_id}',{onlyOnSubmit: #{only_on_submit}});\n" content += "val_#{html_id}_#{validation}.add(Validate.#{validation}, {\"validMessage\": \"\", \"#{message_key}\": \"#{message}\"});\n" end #script_tags(content) raw("") end ## Nothing changed... private def tag_id(object_name, method_name) "#{sanitized_object_name(object_name)}_#{sanitized_method_name(method_name)}" end def sanitized_object_name(object_name) object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") end def sanitized_method_name(method_name) method_name.sub(/\?$/,"") end def live_validation(object_name, method, object = nil) if validations = (object) ? object.class.live_validations[method.to_sym] : self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false #if validations = self.instance_variable_get("@#{object_name.to_s}").class.live_validations[method.to_sym] rescue false field_name = tag_id(object_name,"#{method}") presence_code = nil validation_types = validations.map do |type, configuration| if type == :presence presence_code = live_validation_code("#{field_name}_presence", type, configuration) '' else live_validation_code(field_name, type, configuration) end ##live_validation_code(field_name, type, configuration) end.join(';') ajax_wipe = "LiveValidationForm.destroyById('#{field_name}'); " javascript = (validations.size == 1 and presence_code) ? "" : initialize_validator(field_name) + validation_types if presence_code #javascript + ";" + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code ajax_wipe + initialize_validator(field_name, "{onlyOnSubmit: true}") + presence_code + ";" + javascript else ajax_wipe + javascript end else '' end end def initialize_validator(field_name, options = nil) if options "var #{field_name}_presence = new LiveValidation('#{field_name}',#{options});" else "var #{field_name} = new LiveValidation('#{field_name}');" end end def live_validation_code(field_name, type, configuration) "#{field_name}.add(#{ActiveRecord::Validations::VALIDATION_METHODS[type]}" + ( configuration ? ", #{configuration.to_json}" : '') + ')' end def script_tags(js_code = '') ( js_code.blank? ? '' : "" ) end end module FormOptionsHelper def select_with_live_validations(object, method, choices, options = {}, html_options = {}) live = html_options.delete(:live) live = ActionView::live_validations if live.nil? real_object = options[:object] select_without_live_validations(object, method, choices, options, html_options) + ( live ? live_validations_for(object, method, real_object) : '' ) end alias_method_chain :select, :live_validations end class InstanceTag #def to_input_field_tag_with_script(field_type, options = {}) # "#{to_input_field_tag_without_script(field_type, options = {})}" #end #alias_method_chain :to_input_field_tag, :script end end end