Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to validate a nested model object based on the state of the parent object?

I am writing a wizard form in rails; e.g. multiple input pages for one model object.

The basics of my approach are those described in Ryan Bates' Multistep form railscast : http://railscasts.com/episodes/217-multistep-forms (in case anyone wants to know the WHY behind some of the code below).

The objects under scrutiny here are "Participant", which has one "Address"

My problem is that I only want to validate the nested object (Address) when the user is trying to progress past the address input screen. This is currently tracked via an attribute on the Participant model called "current_step"

So I have a Participant:

class Participant < ActiveRecord::Base
  has_one :address
  accepts_nested_attributes_for :address
  validates_presence_of :first_name, :last_name, :if => self.current_step == "name"
  ...
  def steps = %w[ name address confirm ] # the steps the wizard will follow
end

And an Address:

class Address < ActiveRecord::Base
  belongs_to :participant
  validates_presence_of :address1, :state, :suburb, :postcode #, :if => participant.current_step == "address"
end

The principle of this approach is the "create" action is called on the controller (not shown) for each step of the wizard, and it only validates a subset of the model when each step is processed.

Currently, when I complete the first screen ("name") and try and go onto the address step, the address validation is getting triggered, and I get sent back to the "name" screen with validation errors for blank address details.

So I have tried a number of approaches here, the final part of which was the commented out condition on Address validation shown above - this I've found doesn't work as I'm only building Participant->Address objects, but not saving them. Therefore @participant.address gets me the address object, but @participant.address.participant is null as the Address does not yet have a participant_id foreign key to lookup it's parent.

The reason for my struggles appears to be the inclusion of the super-handy accepts_nested_attributes_for method. I was expecting to use a validates_associated to be doing the validation, but I see that the accepts_nested_attributes_for tag both propagates form parameters nicely to create nested model objects, but also ensures the participant#valid? method calls down to the address validation in ALL situations.

So my dilemma is how to best use the participant#valid? method to valid the partially complete model, based off the current_step parameter in the participant ?

EDIT - updated to remove extra info, and distill down to the core problem

like image 553
Phantomwhale Avatar asked May 12 '11 08:05

Phantomwhale


1 Answers

Add a virtual attribute on your Address model:

class Address < ActiveRecord::Base
  belongs_to :participant
  attr_accessor :skip_validation
  validates_presence_of :address1, :state, :suburb, :postcode, 
                           :unless => :skip_validation 
end

Set the virtual attribute on the address object when current_step is set.

class Participant < ActiveRecord::Base
  has_one :address
  accepts_nested_attributes_for :address
  attr_accessor :current_step
  validates_presence_of :first_name, :last_name, 
                         :if => lambda {|r| r.current_step == "name"}


  def current_step=(value)
    unless (value == "address")
      address.skip_validation = true
    end    
    @current_step = value 
  end    
end
like image 116
Harish Shetty Avatar answered Oct 15 '22 10:10

Harish Shetty