Elijah Murray
Elijah Murray

Reputation: 2172

Validate Associated Object Presence Before Create

I've been following the Getting Started rails tutorial and am now trying some custom functionality.

I have 2 models, Person and Hangout. A Person can have many Hangouts. When creating a Hangout, a Person has to be selected and associated with the new Hangout. I'm running into issues however when I call my create action. This fires before the validate_presence_of for person.

Am I going about this the wrong way? Seems like I shouldn't have to create a custom before_create validation to make sure that a Hangout was created with a Person.

#hangout_controller
def create
  @person = Person.find(params[:hangout][:person_id])

  @hangout = @person.hangouts.create(hangout_params)
  @hangout.save

  redirect_to hangouts_path(@hangout)
end

#hangout.rb
class Hangout < ActiveRecord::Base
  belongs_to :person

  validates_presence_of :person 
end

#person.rb
class Person < ActiveRecord::Base
  has_many :hangouts

  validates :first_name, presence: true
  validates :met_location, presence: true
  validates :last_contacted, presence: true

  def full_name
    "#{first_name} #{last_name}"
   end
end

Upvotes: 2

Views: 904

Answers (3)

Richard Peck
Richard Peck

Reputation: 76774

Nested Attributes

I think you'll be better using accepts_nested_attributes_for - we've achieved functionality you're seeking before by using validation on the nested model (although you'll be able to get away with using reject_if: :all_blank):

#app/models/person.rb
Class Person < ActiveRecord::Base
   has_many :hangouts
   accepts_nested_attributes_for :hangouts, reject_if: :all_blank
end

#app/models/hangout.rb
Class Hangout < ActiveRecord::Base
   belongs_to :person
end

This will give you the ability to call the reject_if: :all_blank method -

Passing :all_blank instead of a Proc will create a proc that will reject a record where all the attributes are blank excluding any value for _destroy.

--

This means you'll be able to create the following:

#config/routes.rb
resources :people do
   resources :hangouts # -> domain.com/people/:people_id/hangouts/new
end

#app/controllers/hangouts_controller.rb
Class HangoutsController < ApplicationController
   def new
      @person = Person.find params[:people_id]
      @hangout = @person.hangouts.build
   end

   def create
      @person = Person.find params[:people_id]
      @person.update(hangout_attributes)
   end

   private

   def hangout_attributes
       params.require(:person).permit(hangouts_attributes: [:hangout, :attributes])
   end
end

Although I've not tested the above, I believe this is the way you should handle it. This will basically save the Hangout associated object for a particular Person - allowing you to reject if the Hangout associated object is blank

The views would be as follows:

#app/views/hangouts/new.html.erb
<%= form_for [@person, @hangout] do |f| %>
    <%= f.fields_for :hangouts do |h| %>
       <%= h.text_field :name %>
    <% end %>

    <%= f.submit %>
<% end %>

Upvotes: 0

Mandeep
Mandeep

Reputation: 9173

Create action fires before the validate_presence_of for person

I think you are confused about rails MVC. Your form contains a url and when you submit your form your form params are send to your controller action according to the routes you have defined in routes.rb Your controller action, in this case create action, interacts with model this is very it checks for your validations and if all the validations are passed your object is saved in databse so even though in your app the control is first passed to your controller but your object is saved only once if all the validations are passed.

Rails MVC


Now lets comeback to your code. There are couple of things you are doing wrong

a. You don't need to associate your person separately:

In your create action you have this line:

@person = Person.find(params[:hangout][:person_id])

You don't need to do this because your person_id is already coming from your form and it'll automatically associate your hangout with person.

b. You are calling create method instead of build:

When you call .association.create method it does two things for you it first initialize your object, in your case your hangout and if all the validations are passed it saves it. If all the validations are not passed it simply rollback your query.

If you'll use .association.build it'll only initialize your object with the params coming from your form

c. Validation errors won't show:

As explained above, since you are calling create method instead of build your validation error won't show up.


Fix

Your create method should look like this:

def create
  @hangout = Hangout.new(hangout_params) # since your person_id is coming from form it'll automatically associate your new hangout with person
  if @hangout.save
    redirect_to hangouts_path(@hangout)
  else
    render "new"  # this will show up validation errors in your form if your hangout is not saved in database
  end
end

private

  def hangout_params
    params.require(:hangout).permit(:person_id, :other_attributes)
  end

Upvotes: 2

Nobita
Nobita

Reputation: 23713

You are confused with the controller and model responsibilities.

Let me try to explain what I think is confusing you:

First try this in your rails console:

Hangout.create

It shouldn't let you because you are not passing a Person object to the create method. So, we confirm that the validation is working fine. That validation means that before creating a Hangout, make sure that there is a person attribute. All this is at the model level, nothing about controllers yet!

Let's go to the controllers part. When the create action of the controller 'is fired', that controller doesn't know what you are trying to do at all. It doesn't run any validations. It is just an action, that if you want, can call the Hangout model to create one of those.

I believe that when you say 'it fires' you are saying that the create action of the HangoutController is called first than the create method on the Hangout model. And that is completely fine. The validations run at the model level.

Upvotes: 0

Related Questions