This one has been giving me grief for a while. I was dealing with a model that accepts_nested_attributes_for a second model, which accepts_nested_attributes_for a third model, and I was running into trouble with validations.
Now, ideally I shouldn’t have been running into validation errors at all, because if the input doesn’t pass :reject_if, it doesn’t even get as far as being validated. Of course, my :reject_if was wrong.
The trouble is that this is the usual construction you’ll find if you want to :reject_if something is blank:
accepts_nested_attributes_for :bars, :reject_if => proc { |attributes| attributes.all? {|k,v| v.blank?} }
This is generally useful enough that :all_blank was added to Rails as a shortcut for that proc.
But this doesn’t necessarily work in a case where something with nested attributes itself has nested attributes. I had:
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars, :reject_if => proc { |attributes| attributes.all? {|k,v| v.blank?} }
end
class Bar < ActiveRecord::Base
has_many :bazzes
accepts_nested_attributes_for :bazzes, :reject_if => proc { |attributes| attributes.all? {|k,v| v.blank?} }
protected
def validate
if [bazzes, quux].all?(&:blank?)
errors.add_to_base("quux or a baz must be specified")
end
end
I meant it so that if you try to create a Foo with a Bar whose quux and bazzes are blank, it’d reject the Bar. Instead, it bypassed the :reject_if and threw the validation error.
Now, of course the reason is simple: the attributes in that case are not actually all blank. The presence of the third model in the hash throws things off:
>> { :foo => "", :bar => { "1" => {:baz => "" } } }.all? {|k,v| v.blank?}
=> false
So clearly that won’t work.
Since my validation was only checking for blanks anyway I did the moderately DRY thing and let the model decide:
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars, :reject_if => proc {|attrs| Bar.new(attrs).invalid? }
end
Really I think it is generally up to the model to decide whether it’s rejectable just as much as if it’s valid; if circumstances were more complicated I might put a rejectable? method in Bar. Edit: And in fact I ended up doing this, because ‘new’ wasn’t playing well with a form that also passed a “_delete” attribute.