Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 3, many-to-many form using accepts_nested_attributes_for, how do I set up correctly?

I have a many-to-many relationship between Recipes and Ingredients. I am trying to build a form that allows me to add an ingredient to a recipe.

(Variants of this question have been asked repeatedly, I have spent hours on this, but am fundamentally confused by what accepts_nested_attributes_for does.)

Before you get scared by all the code below I hope you'll see it's really a basic question. Here are the non-scary details...

Errors

When I display a form to create a recipe, I am getting the error "uninitialized constant Recipe::IngredientsRecipe", pointing to a line in my form partial

18:   <%= f.fields_for :ingredients do |i| %>

If I change this line to make "ingredients" singular

<%= f.fields_for :ingredient do |i| %>

then the form displays, but when I save I get a mass assignment error Can't mass-assign protected attributes: ingredient.

Models (in 3 files, named accordingly)

class Recipe < ActiveRecord::Base
  attr_accessible :name, :ingredient_id
  has_many :ingredients, :through => :ingredients_recipes
  has_many :ingredients_recipes

  accepts_nested_attributes_for :ingredients
  accepts_nested_attributes_for :ingredients_recipes
end

class Ingredient < ActiveRecord::Base
  attr_accessible :name, :recipe_id
  has_many :ingredients_recipes
  has_many :recipes, :through => :ingredients_recipes

  accepts_nested_attributes_for :recipes
  accepts_nested_attributes_for :ingredients_recipes
end

class IngredientsRecipes < ActiveRecord::Base
  belongs_to :ingredient
  belongs_to :recipe

  attr_accessible :ingredient_id, :recipe_id
  accepts_nested_attributes_for :recipes
  accepts_nested_attributes_for :ingredients
end

Controllers

As RESTful resources generated by rails generate scaffold

And, because the plural of "recipe" is irregular, inflections.rb

ActiveSupport::Inflector.inflections do |inflect|
    inflect.irregular 'recipe', 'recipes'
end

View (recipes/_form.html.erb)

<%= form_for(@recipe) do |f| %>
  <div class="field">
    <%= f.label :name, "Recipe" %><br />
    <%= f.text_field :name %>
  </div>
  <%= f.fields_for :ingredients do |i| %>
    <div class="field">
      <%= i.label :name, "Ingredient" %><br />
      <%= i.collection_select :ingredient_id, Ingredient.all, :id, :name %>
    </div>
  <% end %>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Environment

  • Rails 3.2.9
  • ruby 1.9.3

Some things tried

If I change the view f.fields_for :ingredient then the form loads (it finds Recipe::IngredientRecipe correctly, but then when I save, I get a mass-assignment error as noted above. Here's the log

Started POST "/recipes" for 127.0.0.1 at 2012-11-20 16:50:37 -0500
Processing by RecipesController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"/fMS6ua0atk7qcXwGy7NHQtuOnJqDzoW5P3uN9oHWT4=", "recipe"=>{"name"=>"Stewed Tomatoes", "ingredient"=>{"ingredient_id"=>"1"}}, "commit"=>"Create Recipe"}
Completed 500 Internal Server Error in 2ms

ActiveModel::MassAssignmentSecurity::Error (Can't mass-assign protected attributes: ingredient):
  app/controllers/recipes_controller.rb:43:in `new'
  app/controllers/recipes_controller.rb:43:in `create'

and the failing lines in the controller is simply

@recipe = Recipe.new(params[:recipe])

So the params being passed, including the nested attributes, are incorrect in some way. But I have tried lots of variants that fix-one-break-another. What am I failing to understand?

like image 950
Tom Harrison Avatar asked Dec 08 '22 19:12

Tom Harrison


1 Answers

Thanks to clues from all, I have found what was wrong with my approach. Here's how I solved it.

I had originally tried with a simple HABTM many-to-many relationship, where the join table was named following standard Rails convention: ingredients_recipes. Then I realized that in a way, accepts_nested_attributes_for is designed for a 1-to-many relationship. So I converted to using has_many_through, creating a model IngredientsRecipes.

That name was the core problem, because Rails needs to be able to convert from plural to singular when using build to create form elements. This caused it to look for the non-existant class Recipe::IngredientsRecipe. When I changed my form so it used fields_for :ingredient the form displayed, but still failed to save with a mass assignment error. It even failed when I added :ingredients_attributes to attr_accessible. It still failed when I added @recipe.ingredients.build to RecipesController#new.

Changing the model to a singular form was the final key to resolve the problem. IngredientsRecipe would have worked, but I chose RecipeIngredients, as it makes more sense.

So to summarize:

  • can't use accepts_nested_attributes_for with has_and_belongs_to_many; need has_many with through option. (Thanks @kien_thanh)
  • adding accepts_nested_attributes_for creates a accessor that must be added to attr_accessible in the form <plural-foreign-model>_attributes, e.g. in Recipe I added attr_accessible :name, :ingredients_attributes (Thanks @beerlington)
  • before displaying the form in the new method of the controller, must call build on the foreign model after creating a new instance, as in 3.times { @recipe.ingredients.build }. This results in HTML having names like recipe[ingredients_attributes][0][name] (Thanks @bravenewweb)
  • join model must be singular, as with all models. (All me :-).
like image 108
Tom Harrison Avatar answered Dec 29 '22 01:12

Tom Harrison