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...
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
.
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
As RESTful resources generated by rails generate scaffold
inflections.rb
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'recipe', 'recipes'
end
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 %>
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?
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:
accepts_nested_attributes_for
with has_and_belongs_to_many
; need has_many
with through
option. (Thanks @kien_thanh)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)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)If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With