Reputation: 2172
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
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
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.
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
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