Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails -- how to populate parent object id using nested attributes for child object and strong parameters?

I've got a situation much like is presented in Railscast 196-197: Nested Model Form. However, I've encountered a conflict between this approach and strong parameters. I can't figure out a good way to populate the parent record id field on the child object, since I don't want that to be assignable through the form (to prevent users from associating child records to parent records they don't own). I have a solution (see code below) but this seems like the kind of thing Rails might have a clever, easy way to do for me.

Here's the code...

There's a parent object (call it Survey) that has_many child objects (call them Questions):

# app/models/survey.rb class Survey     belongs_to :user     has_many :questions     accepts_nested_attributes_for :questions end  # app/models/question.rb class Question     validates :survey_id, :presence => true     belongs_to :survey end 

There's a form that allows users to create a survey and the questions on that survey at the same time (for simplicity, the code below treats surveys as though they have only question):

# app/views/surveys/edit.html.erb <%= form_for @survey do |f| %>     <%= f.label :name %>     <%= f.text_field :name %><br />     <%= f.fields_for :questions do |builder| %>         <%= builder.label :content, "Question" %>         <%= builder.text_area :content, :rows => 3 %><br />     <% end %>     <%= f.submit "Submit" %> <% end %> 

The problem is the controller. I want to protect the survey_id field on the question record via strong parameters, but in doing so the questions don't pass validation, since the survey_id is a required field.

# app/controllers/surveys_controller.rb class SurveysController     def edit         @survey = Survey.new         Survey.questions.build     end      def create         @survey = current_user.surveys.build(survey_params)         if @survey.save             redirect_to @survey         else             render :new         end     end      private      def survey_params         params.require(:survey).permit(:name, :questions_attributes => [:content])     end end 

The only way I can think to solve this problem is to build the questions separately from the survey like this:

def create     @survey = current_user.surveys.build(survey_params)     if @survey.save         if params[:survey][:questions_attributes]             params[:survey][:questions_attributes].each_value do |q|                 question_params = ActionController::Parameters.new(q)                 @survey.questions.build(question_params.permit(:content))             end         end         redirect_to @survey     else         render :new     end end  private  def survey_params     params.require(:survey).permit(:name) end 

(Rails 4 beta 1, Ruby 2)

UPDATE

Perhaps the best way to handle this problem is to factor out a "Form object" as suggested in this Code Climate blog post. I'm leaving the question open, though, as I'm curious to other points of view

like image 826
Nathan Wallace Avatar asked May 28 '13 02:05

Nathan Wallace


1 Answers

So the problem you are running into is that the child objects don't pass validation, right? When the child objects are created at the same time as the parent, the child objects could not possibly know the id of their parent in order to pass validation, it's true.

Here is how you can solve that problem. Change your models as follows:

# app/models/survey.rb class Survey     belongs_to :user     has_many :questions, :inverse_of => :survey     accepts_nested_attributes_for :questions end  # app/models/question.rb class Question     validates :survey, :presence => true     belongs_to :survey end 

The differences here are the :inverse_of passed to the has_many association, and that the Question now validates on just :survey instead of :survey_id.

:inverse_of makes it so that when a child object is created or built using the association, it also receives a back-reference to the parent who created it. This seems like something that should happen automagically, but it unfortunately does not unless you specify this option.

Validating on :survey instead of on :survey_id is kind of a compromise. The validation is no longer simply checking for the existence of something non-blank in the survey_id field; it now actually checks the association for the existence of a parent object. In this case it is helpfully known due to :inverse_of, but in other cases it will actually have to load the association from the database using the id in order to validate. This also means that ids not matching anything in the database will not pass validation.

Hope that helps.

like image 176
Comatose Turtle Avatar answered Sep 21 '22 20:09

Comatose Turtle