rubyonrails: April 2006 Archives

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
end

models/account.rb:

class Account < ActiveRecord::Base
  has_many :transactions
  validates_presence_of :number
end

controllers/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
end

Now, 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]

  1. Really, the stdlib documentation is almost nonexistant! Digest? OpenSSL? Hah! Your best bet is to look at code samples and guess!
  2. valid? is called by save, for the record
  3. Far superior to the default:
    • Amount is a required field
    • Account is invalid
Edward Thomson is a Software Engineer at Teamprise, where he develops cross-platform client solutions for Microsoft Team Foundation Server, with an emphasis on Macintosh compatibility and IDE integration.