The blog of Rahoul Baruah from 3hv Ltd

What's going on?

My name is Rahoul Baruah (aka Baz) and I'm a software developer in Leeds (England).

This is a log of things I've discovered while writing software in Ruby on Rails. In other words, geek stuff.

However, I've decided to put this blog on ice - I would ask you to check out my business blog here (or subscribe here).

08 November, 2006

Active Record validations and transient objects

There I was, handling a multi-stage function using a nice transient object stored in the session. And I got to thinking about the error states. Important you see, those error states.

And I thought to myself "I want to use error_messages_for". So I grab my copy of "Rails Recipes" (if you haven't got it, you probably should) and turn to Recipe 64 "Using Validations outside of Active Record". The recipe is reasonably simple: all the validations stuff is in a module called ActiveRecord::Validations. In theory, you just need to include that and you're all set. Except it's not quite that easy. ActiveRecord::Validations expects you to have a few extra methods - save! and the like. So Chad Fowler defines a new module, Validateable, that includes ActiveRecord::Validations and defines a few blank methods. I faithfully copied all this code, deliberately typed in some invalid data and hit "Save", expecting the nice .field_with_errors stuff to appear. Instead I got a "method missing human_attribute_name". Search through the docs. Nothing. Search through the source. Nothing. I tried defining my own, both in the Validateable module and in my transient object. Arses.

Mr Fowler does state, in his book, that the recipe depends upon the internals of ActiveRecord, which are subject to change. The book is from Rails 1.1. I'm on Rails 1.16. So I guess they have changed.

I asked for help on a mailing list, and got the response "when Rails starts making something hard, it's probably the wrong thing to do". Which is true. So I cheated.

I created a new ActiveRecord model called Pseudo. The migration for this model looks like:


class CreatePseudos < ActiveRecord::Migration
def self.up
create_table :pseudos do |t|
t.column :type, :string
end
end
def self.down
drop_table :pseudos
end
end


In other words, there's not much to it.

Then I defined Pseudo itself.


class Pseudo < ActiveRecord::Base
def save
raise "Cannot save a Pseudo Model"
end
def method_missing(symbol, *params)
send $1 if (symbol.to_s =~ /(.*)_before_type_cast$/)
end
def transfer_attributes(params)
params.each do | name, value |
self.send("#{name}=", value) if self.respond_to?("#{name}=")
end
end
end


Again, not much to it. I overrode save as these models are never intended to be stored in the database. I overrode method_missing to deal with ActiveRecord's field_before_type_cast method. And I created a new method, transfer_attributes, for reasons that will become clear later.

Finally, I defined my transient object.


class Transient < Pseudo
attr_accessor :field1
attr_accessor :field2
validates_numericality_of :field1
validates_inclusion_of :field2, :in => ['value1', 'value2']
end


So Pseudo, the base class, and its associated table have no real storage associated with them. The descendant class defines all its properties using attr_accessor and just validates against these. To check you call transient.valid? rather than transient.save - and your view can then call error_messages_for to show the errors collection. The only issue remaining is that you cannot use

@transient.update_attributes params[:transient]

in your controller because none of your 'fields' exist in the underlying table - meaning ActiveRecord complains. Which is the reason for the 'transfer_attributes' method in Pseudo - it does the same thing but works with any property setter, regardless of whether there is an underlying field or not.

So, it's not quite "using validations outside of ActiveRecord" but it is "using ActiveRecord validations on transient objects".

1 comment:

Per Olesen said...

I got the same error as you did about missing method human_attribute_name when I followed the recipe in Rails Recipes. But, I fixed it by adding this method definition in the transient model:

def self.human_attribute_name(arg)
arg.to_s
end

Did you remember to make it a class method? (e.g. using "self."?)

eXTReMe Tracker