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.
开发者_Go百科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
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
How about just assigning it in your controller before you call participant.valid?
:
@participant.address.participant = @participant
Although the ActiveRecord "validate" methods are very convenient, there's nothing to stop you from "rolling your own", perhaps using before_save
hooks. You could replace your validates_presence_of
validator on Address
with a before_save
hook which does validation only under certain conditions. Then accepts_nested_attributes_for
presumably wouldn't see it.
精彩评论