I've been playing with Ruby on Rails for a few days now, slowly grokking Ruby's unique syntax, sparsely documented stdlib[1] and lack of semicolons. The first stumbling block, albeit simple, was Rails' seemingly simple error_messages_for.
In a view, <%= error_messages_for 'MODEL' %> will show you any errors from MODEL's validate method. It's straightforward enough... unless you're trying to validate against a second model as well, from a validate_associated. Then MODEL's only error message is "othermodel is invalid". Not terribly helpful. You could draw a second box explaning why, but that's only slightly more enlightening (if at all.)
It would be ideal if we could just put all the error messages in a single error block. So what we need to do is get rid of the "othermodel is invalid" error, and add the other model's errors. For example:
errors = ActiveRecord::Errors.new(nil)
@model.errors.each { |key, message| errors.add(key, message) unless key == "othermodel" }
@othermodel.errors.each { |key, message| errors.add(key, message) }
@model.errors.clear
errors.each { |key, message| @model.errors.add(key, message) }Sort of a hack, I'll admit. But hey, it works, and we can do this at any point after valid?[2] has been called on your models. (valid? actually populates @model.errors.)
Here's a detailed example -- let's say you have two objects, a Transaction and an Account, and you want a view that simply asks for an amount for a transaction and an account number, and you want to show the user if they enter an invalid amount or an invalid account number. Your models and views are going to look something like this:
views/transaction/create.rhtml:
<%= error_messages_for 'transaction' %>
<%= start_form_tag :action => 'create' %>
<label for="transaction_amount">Amount:</label> <%= text_field :transaction, :amount %>
<label for="account_number">Account:</label> <%= text_field :account, :number %>
<%= end_form_tag %>models/transaction.rb:
class Transaction < ActiveRecord::Base
belongs_to :account
validates_presence_of :amount, :message =< "is a required field"
validates_associated :account
endmodels/account.rb:
class Account < ActiveRecord::Base
has_many :transactions
validates_presence_of :number
endcontrollers/transaction_controller.rb:
class TransactionsController < ApplicationController
def create
if request.post?
@account = Account.find(:first, :conditions => [ "number = ?", @params[:account][:number]])
@transaction = Transaction.new()
@transaction.account = @account
@transaction.amount = @params[:transaction][:amount]
if @transaction.save
redirect_to :action => 'show', :id => @transaction
else
# here's where we get the good errors
errors = ActiveRecord::Errors.new(nil)
@transaction.errors.each { |k,m| errors.add(k,m) unless k == "account" }
@account.errors.each { |k,m| errors.add(k,m) }
@transaction.errors.clear
errors.each { |k,m| @transaction.errors.add(k, m) }
end
end
end
endNow, if you were to leave both fields blank, you'd get an error message like:
- Amount is a required field
- Number can't be blank
Which is the error message that one would expect out of this view.[3]
- Really, the stdlib documentation is almost nonexistant! Digest? OpenSSL? Hah! Your best bet is to look at code samples and guess!
-
valid?is called bysave, for the record - Far superior to the default:
- Amount is a required field
- Account is invalid
Thanks for the tip. I would think there would be an easier way to do this sort of thing. Maybe through error_messages_on?
Thank you! Thank you! I've been going round and round with this for two days.
Here is my version:
errors = ActiveRecord::Errors.new(nil)
@party.errors.each { |k,m| errors.add(k,m) unless k =~ /s$/ and m =~ /is invalid$/}
[@party.emails, @party.addresses, @party.phones].flatten!.each do |obj|
obj.errors.each { |k,m| errors.add(k,m) }
obj.errors.clear
end
@party.errors.clear
errors.each { |k,m| @party.errors.add(k,m)}
Hey Rich! Very nice, and much more compact. Thanks for the tip.
-Ed