Validating two models from one form


#1

Howdy,

I’m working on my first RoR project, and I want to build a form. The
tricky thing is, this form needs to insert / update against two models,
each with their own validation rules. Unfortunately, I’m having an
incredible amount of difficulty with the error handling, specifically
the fields wrapped in

nevans@bell:app/models$ ls
user_preference.rb user.rb

The User class has_one :user_preference, and UserPreference belongs_to
:user. Both classes have a half dozen validation rules or so.

In my RegisterController, I have two actions: one to display the form,
and one to save the form.


Don’t laugh too hard at how ugly my code is, I’ve only read

Agile Web D. and Why’s Poignant Guide!

class RegisterController < ApplicationController

 def index
     @user = User.new
     @pref = UserPreference.new
     @user.user_preference = @pref
 end

 def save
     @user = User.create(params[:user])
     @pref = UserPreference.create(params[:user_preference])
     @user.user_preference = @pref

     # Default display name to something nice
     @user.user_preference.display_name = @user.username

     User.transaction do
         if @user.save and @user.user_preference.save
             # do something useful
         else
	# breakpoint('User not saved.')
             render(:action => 'index')
         end
     end # end transaction
 end # end save

end

I also have a template that renders my partial template for the new user
form. Yes, it does use the form helper functions to generate fields:

<%= text_field ‘user’, ‘username’, ‘maxlength’ => 20, ‘size’ => 20 %>
<%= text_field ‘user_preference’, ‘email’, ‘size’ => 20, ‘maxlength’ =>
255 %>
etc…

When the form is submitted, both sets of validation rules are run. The
standard error_messages_for, however, only supports rendering out the
errors from one model’s validation. I fixed this with a (really hackish)
helper. It take in an array of instance variables and build the error
box for them.

But, the remaining issue I am unable to solve: . Rails only wraps the tags that fail the
User class’ validation. I’ve been unable to grok how Rails figured out
which fields it needs to put in the fieldWithErrors divs, even after
reading a whole lot of Rails source. :frowning:

What would an optimal solution be? Can I insert my own code anywhere to
take over this part of error handling? Or, failing that, is there a way
I can disable the wrapping of elements in fieldWithErrors for only this
form?

Thank you all very much!

Regards,
Nick Evans


#2

I think the create method actually creates and saves the model to the DB
:

@user = User.create(params[:user])

I think you should use the new method instead :

@user = User.new(params[:user])

Chris


#3

You are correct, create does save the model to the database. My code has
been updated appropriately:

     @user = User.new(params[:user])
     @pref = UserPreference.new(params[:user_preference])

But, I no longer get both sets of errors displayed. I examined @user
with the breakpointer to confirm this. Perhaps it’s because I’m using a
transaction?

After reviewing the material on transaction in Agile Web D., I
corrected my save action:


 def save
     @user = User.new(params[:user])
     @pref = UserPreference.new(params[:user_preference])
     @user.user_preference = @pref

     # Default display name to something nice
# (perhaps this needs to be moved to an initialize or something?)
     @user.user_preference.display_name = @user.username

     begin
         User.transaction do
             @user.save!
             @user.user_preference.save!
         end # end transaction
     rescue
         # Could not save both, catch the exception and show errors.
         render(:action => 'index')
     end

     # SUCCESS!
     # redirect somewhere useful

 end # end save

Unfortunately, the same behaviour I described above is exhibited, with
only the User object’s errors showing up anywhere. The user_preference
instance does not even have the errors in it this time:

irb(#RegisterController:0xb76beab0):006:0> @user.user_preference
=> #<UserPreference:0xb76a8a08
@attributes=
{
“gender”=>“MALE”,
“display_name”=>“a”,
“user_id”=>“0”,
“age”=>nil,
“email”=>""
},
@new_record=true>

Thank you for your reply, Chris.

Regards,
Nick Evans


#4

OK. Using the valid? method on a model will check if all validations
passed and add the errors to the model without saving the model. Also,
saving @user will save any models it contains (e.g.
@user.user_preference will be auto saved). SO do this:

@user = User.new(params[:user])
@pref = UserPreference.new(params[:user_preference])
@user.user_preference = @pref
@user.user_preference.display_name = @user.username

if @user.valid? and @pref.valid?
@user.save
else
render :action=>…

end

Its nice and simple too.

Its nice to give something back after asking a million questions myself
in this great forum!!

Chris


#5

It looks to me like one cannot do both valid? calls in an if-and
statement. When doing:


if @user.valid? and @pref.valid?
@user.save
else
render :action=>…

end

I would only get the errors for @user. A tinkered a bit and came up with
this:


@user.valid?
@user.user_preference.valid?

breakpoint(‘Validated, stopping before save.’)

if @user.save
# SUCCESS!
else
render(:action => ‘index’)
end

I get both sets of errors into my error handler, finally. But, this
still doesn’t solve the original issue of only User fields in the form
wrapped with fieldWithErrors.

Additionally, the UserPreference form elements do not get defaulted to
the value I entered on the first screen when its reporting errors. I
guess the cause is probably the same thing as the error div’s cause.

Thanks for your help, though! I feel like I’m making progress. :slight_smile:

Regards,
Nick Evans


#6

try this instead :

@user.valid?
@pref.valid?


#7

Rails might use the labels around the text boxes to figure out which
elements to wrap the error box around. COuld be wrong.


#8

Chris wrote:

try this instead :

@user.valid?
@pref.valid?

on second thoughts, i dont think that will do anything


#9

I don’t think that is the case. I believe this:


alias_method :tag_without_error_wrapping, :tag
def tag(name, options)
if object.respond_to?(“errors”) && object.errors.respond_to?(“on”)
error_wrapping(tag_without_error_wrapping(name, options),
object.errors.on(@method_name))
else
tag_without_error_wrapping(name, options)
end
end

Is what puts the the divs around the elements. Besides, label tags are
supposed to go around the field labels.

I think my issue is that the tag method above is that, like the
error_messages_for, it only wraps the error hash from the top-leve
object [I don’t think I’m using the right terminology there, please forgive me].

Actually, now that I think about it, all I need to do is concat the
error lists together and Rails should handle the rest. Hopefully.

Lessee’ what I can cook up…

Regards,
Nick Evans


#10

I’m just an idiot. The field names in the form doesn’t map to a /model/,
but to an instance variable. Once I changed @pref to @user_preference, I
was home free.

I also had to add a hack to concatonate all of the error messages from
preference into @user.errors in order for error_messages_for to display
the errors for both models’ validation rules.


class RegisterController < ApplicationController

 def index
     @user = User.new
     @user_preference = UserPreference.new
     @user.user_preference = @user_preference

     # For some reason, gender defaults to male before
     # submission.
     if params[:commit]
         @user.user_preference.gender = ''
     end
 end

 def save
     @user = User.new(params[:user])
     @user_preference = UserPreference.new(params[:user_preference])
     @user.user_preference = @user_preference

     # Default display name to something nice
     @user.user_preference.display_name = @user.username

# Get any error messages.
     @user.valid?
     @user.user_preference.valid?

     if @user.save
         # SUCCESS!
     else
    # Hack
         @user.user_preference.errors.each do
             |attribute, error|
             @user.errors.add(attribute, error)
         end
         render(:action => 'index')
     end

 end # end save

end

And that was all. Thanks guys!

Regards,
Nick Evans


#11

Nicholas E. wrote:

I’m just an idiot. The field names in the form doesn’t map to a /model/,
but to an instance variable. Once I changed @pref to @user_preference, I
was home free.

I also had to add a hack to concatonate all of the error messages from
preference into @user.errors in order for error_messages_for to display
the errors for both models’ validation rules.

Would it not work to simply call “error_messages_for” in your view for
each of the models you’re dealing with?

Jeff C.man


#12

For displaying validation errors from more than object on a form take a
look at this plugin:
http://www.railtie.net/articles/2006/01/26/enhancing_rails_errors

I used it as a basis to figure out how to improve error_messages_for so
that it can take multiple models.

Calling error_messages_for twice gives you two blocks on the page which
looks odd. This way, all your errors will be contained into a single
block. The syntax is a bit cumbersome but it’s flexible.


#13

http://railtie.net/articles/2006/01/26/enhancing_rails_errors#commentform

install it in vendor/plugins and take a look at the readme - and your
good to go!

make sure you restart your server once you install the plugin.