Forum: Ruby on Rails Validating two models from one form

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
38c94343d160b921ee22e9f0fb4c18b4?d=identicon&s=25 Nicholas Evans (Guest)
on 2006-04-09 19:56
(Received via mailing list)
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 <div class='fieldWithErrors'></div>

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 Development 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: <div
class='fieldWithErrors'></div>. 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. :-(

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
Cd8c9864d88bcafc164d8fdb820cc451?d=identicon&s=25 Chris (Guest)
on 2006-04-09 20:41
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
38c94343d160b921ee22e9f0fb4c18b4?d=identicon&s=25 Nicholas Evans (Guest)
on 2006-04-09 21:37
(Received via mailing list)
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 Development, 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
4a2e23da65a5f0b2f156797537a125e8?d=identicon&s=25 Gd (Guest)
on 2006-04-09 22:07
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
38c94343d160b921ee22e9f0fb4c18b4?d=identicon&s=25 Nicholas Evans (Guest)
on 2006-04-09 22:26
(Received via mailing list)
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. :-)

Regards,
Nick Evans
Cd8c9864d88bcafc164d8fdb820cc451?d=identicon&s=25 Chris (Guest)
on 2006-04-09 22:31
try this instead :

@user.valid?
@pref.valid?
Cd8c9864d88bcafc164d8fdb820cc451?d=identicon&s=25 Chris (Guest)
on 2006-04-09 22:37
Chris wrote:
> try this instead :
>
> @user.valid?
> @pref.valid?

on second thoughts, i dont think that will do anything
Cd8c9864d88bcafc164d8fdb820cc451?d=identicon&s=25 Chris (Guest)
on 2006-04-09 22:39
Rails might use the labels around the text boxes to figure out which
elements to wrap the error box around.  COuld be wrong.
38c94343d160b921ee22e9f0fb4c18b4?d=identicon&s=25 Nicholas Evans (Guest)
on 2006-04-09 23:09
(Received via mailing list)
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
38c94343d160b921ee22e9f0fb4c18b4?d=identicon&s=25 Nicholas Evans (Guest)
on 2006-04-10 00:53
(Received via mailing list)
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
59ea1b450935b9d70abfec4186b7a4d5?d=identicon&s=25 Jeff Coleman (progressions)
on 2006-04-10 11:08
Nicholas Evans 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 Coleman
E65d5a8983bbd47e66361a7ab68be659?d=identicon&s=25 Gerry Shaw (Guest)
on 2006-04-10 17:13
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/enhanci...

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.
E3ef407496e3e8192a7775e0850ee9da?d=identicon&s=25 a plugin that fixes this problem and so easy to us (Guest)
on 2006-06-25 21:34
http://railtie.net/articles/2006/01/26/enhancing_r...

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.
This topic is locked and can not be replied to.