Project and tasks have a one-to-many relationship, and project accepts_nested_attributes_for :tasks.
In the form, my task objects look like:
project[tasks][2][assigned_time]
project[tasks][2][due_time]
When the form is submitted I get a hash like:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"...=",
"project"=>{"id"=>"1", "tasks"=>{"1"=>{"assigned_time"=>"09:00",
"due_time"=>"17:00"}, "2"=>{"assigned_time"=>"09:00",
"due_time"=>"17:00"}}
Then I expect them to be saved by just saving the project object:
project = Project.find(params[:id])
respond_to do |format|
if project.update_attributes(params[:tasks])
But I get:
WARNING: Can't mass-assign protected attributes: id SQL (0.3ms)
ROLLBACK Completed in 169ms
ActiveRecord::AssociationTypeMismatch (Task(#2188181260) expected, got
Array(#2151973780)):
Any ideas how to fix this?
In your Projects model, accepts_nested_attributes_for :tasks. This will define #project.tasks_attributes= if you have a has_many :tasks association or #project.task_attributes= if you have a has_one :task association.
In your form, the following:
= form_for #project do |f|
= f.label :project_attribute
= f.text_field :project_attribute
= f.fields_for :tasks do |t|
= t.label :task_attribute
= t.text_field :task_attribute
In your projects controller, the following:
def new
#project = Project.new
#project.tasks.build #=> if has_many
#project.build_task #=> if has_one
end
I think, you just forget to add task_attributes to attr_accessible list in your Project model:
attr_accessible :tasks_attributes, ...
And also, note, that, maybe you generating wrong form, because in my current application, form with nested attributes uses task_attributes method, not tasks (like in your hash)
Related
I'm getting this error when I try to mass-assign a HABTM relationship in Rails 5:
*** ActiveRecord::RecordNotFound Exception: Couldn't find Job with ID=6 for Profile with ID=
class Profile < ApplicationRecord
has_and_belongs_to_many :jobs
accepts_nested_attributes_for :jobs
end
class Job < ApplicationRecord
has_and_belongs_to_many :profiles
end
class ProfilesController < ApplicationController
def create
#profile = Profile.new(profile_params)
end
private
def profile_params
params.require(:profile).permit(
:name,
:email,
:jobs_attributes: [:id]
)
end
end
=form_for #profile do |f|
=f.fields_for :jobs do |j|
=j.select :id, options_for_select([[1, "Job 1", ...]])
The problem is that accepts_nested_attributes_for is a way to update attributes on an associated object (either already linked or one that you're creating). You can think of it doing something like:
params[:jobs_attributes].each do |job_attributes|
#profile.jobs.find(job_attributes[:id]).attributes = job_attributes
end
Except that the job is then saved with the new attributes in an after-save of the parent object.
This isn't what you want. In fact, you don't need accepts_nested_attributes at all.
Instead, change the attribute in your view to job_ids as if it's an attribute of the profile, something like the following:
=form_for #profile do |f|
=j.select :job_ids, options_for_select([[1, "Job 1", ...]])
This will effectively call profile.job_ids = [1,2,3] which will have the effect that you're looking for.
I am currently creating a WebApp using RoR 4 and I am using has_many, though: associations between my databases.
I have 3 models Users, UsersSubject and Subjects given below:
class UsersSubject < ActiveRecord::Base
belongs_to :users
belongs_to :subjects
end
class Subject < ActiveRecord::Base
has_many :users_subjects
has_many :users, through: :users_subjects
end
class User < ActiveRecord::Base
has_many :users_subjects, :class_name => 'UsersSubject'
has_many :subjects, through: :users_subjects
accepts_nested_attributes_for :subjects
end
I am trying to populate the UsersSubject database when I am updating the User using the User controller. Here is my from the partial form of the User:
<div class="control-group nested-fields">
<div class="contols">
<%= f.fields_for :subject do |subject| %>
<%= subject.label "Subjects" %></br>
<%= subject.collection_select(:subject_id, Subject.all, :id, :name) %>
<% end %>
</div>
</div>
and here is my controller:
def edit
user_id = current_user.id
subject_id = Subject.where(:name => params[:name])
#user_sub = UsersSubject.new(user_id: user_id, subject_id: subject_id)
#user_sub.save
end
When I do this the controller populate the UsersSubject database with the correct user_id but the subject_id is always nil. The Subject database is already populate using the seed.rb file.
Can someone help me understand why this is happening and help my fix it?
Thanks in advance
EDIT
Here is my development.log
Started GET "/users/edit" for ::1 at 2016-05-31 18:27:33 +0100
Processing by Users::RegistrationsController#edit as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ZGvBq1uFUi1RxcInKFz1TnUs2ZzlZsP29aW1mzQBOVBTLm/Dq3C42cQQX0ksmBv95/qHnk08bG3f5u1v9taZgw==", "user"=>{"address"=>"", "city"=>"London", "postcode"=>"", "country"=>"United Kingdom", "subject"=>{"subject_id"=>"1"}}, "commit"=>"Update"}
Moved controller code to the update action
def update
user_id = current_user.id
subject_id = params[:user][:subject][:subject_id] unless params[:user].nil?
#user_sub = UsersSubject.new(user_id: user_id, subject_id: subject_id)
#user_sub.save
end
Added the subject permission in the application_controller.rb:
class ApplicationController < ActionController::Base
before_filter :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:last_name,
:first_name, :email, :password, :current_password,
subject_attributes: [:id, :subject_id]) }
end
end
This permits the update action to access the subject_attributes
Changes in the user model
class User < ActiveRecord::Base
has_many :users_subjects, :class_name => 'UsersSubject', dependent: :destroy
has_many :subjects, through: :users_subjects
accepts_nested_attributes_for :subjects
end
Have you looked into using the cocoon gem?
You might also want to take a look at the params you're passing into subject_id, it looks like the full object that you're grabbing from it's name. Print out the "subject_id" that you currently have.
There is a great railscasts video on this topic you might want to look into, the membership is well worth it.
Check your params in your rails server log. From what I guess it should be coming like this
"user" => {
"name"=>"Michael Jackson",
"subject" => {
"subject_id"=>"1"
}
}
But in your controller method, you're trying to access it like params[:name], instead you should do something like this, based on how you've nested in your form.
subject_id = params[:user][:subject][:subject_id]
subject_id is nested inside subject, which is nested inside the user object.
Hope this helps. :)
Reference code:
Do this to your edit action.
def edit
p params #this print out the inspected params in the console log
fail
#this stops the code from executing further, by raising an error.
#So we can inspect the params passed to our method in server log
...
end
Go to your form in your browser, and after that clear your rails server console, (Command + K) for mac, and For ubuntu, press keep pressing enter to move the current log up the screen. Now click submit, and check the server log. You'll notice something like
Started POST "/users"
Processing by UsersController#edit
Parameters: {"utf8"=>..... "user" => { "name" => "Michael Jackson", "subject" => { "subject_id" => "1" }}}
So what you want to do is, you need to create a new many to many relation between User and Subject. You're getting the user_id from devise's current_user and subject_id in your edit method from the params, as I defined. How you're receiving params might differ from how I mentioned.
Final code
def edit
user_id = current_user.id
subject_id = params[:user][:subject][:subject_id]
#user_sub = UsersSubject.new(user_id: user_id, subject_id: subject_id)
#user_sub.save
end
So I have four database tables.
Users (:name etc..) Recipes (:name, :description, :user_id etc..), Scrapbooks (:name, :description, :user_id) and Scrapbook_Entry (:user_id, recipe_id, :scrapbook_id)
I am able to populate the Users, Recipes and Scrapbooks tables fine but what I now want to do is have an option to save a recipe into a scrapbook. By doing this I need to populate the Scrapbook_Entry table which I have made a model for.
Scrapbook_Entry Model:
has_one :recipe
has_one :scrapbook
Recipe Model:
has_many :scrapbooks, through: :scrapbook_entries
Scrapbook Model
has_many :recipes, through: :scrapbook_entries
User Model
has_many :recipes, dependent: :destroy
has_many :scrapbooks, dependent: :destroy
I want to create a form in the Recipe view to allow me to select a scrapbook to save the recipe into and for it then to submit and populate the Scrapbook_Entry table.
My question is: Will I need to create a new controller for my scrapbook_entries and have a create method in that or would I be able to use the recipes controller and if so, how so?
I am new to rails so still trying to figure it all out. Thank you!
You will not need a new controller for this. You should be able to do something along the lines of
#recipe.scrapbook_entries.build(scrapbook: #scrapbook)
assuming you have a #recipe variable with a Recipe object in it, and a #scrapbook variable with a Scrapbook object in it.
This sounds like a job for accepts_nested_attributes_for
The way it works is that it takes a "nested model" (in your case, ScrapBookEntry), and allows you to send data directly to it from the parent model (Recipe). It's got a learning curve, but comes in very useful especially when you start dealing with lots of modular data
Accepts Nested Attributes For
There's a great RailsCast on this subject here
It works by building an ActiveRecord object for your nested model through your parent model's controller, thus allowing Rails to populate both objects when the form is submitted. This means you can add as much data as you want to your nested model whilst keeping your code efficient
Your Code
Instead of creating a new controller, you should be able to handle all the processing in your Recipes controller, like this:
#app/models/recipe.rb
Class Recipe < ActiveRecord::Base
accepts_nested_attributes_for :scrapbook_entries
end
#app/controllers/recipes_controller.rb
def new
#recipe = Recipe.new
#recipe.scrapbook_entries.build #-> repeat for number of fields you want
end
def create
#recipe = Recipe.new(recipe_params)
#recipe.save
end
private
def recipe_params
params.require(:recipe).permit(:recipe, :params, scrapbook_entries_attributes: [:extra, :data, :you, :want, :to, :save])
end
#app/views/recipes/new.html.erb
<%= form_for #recipe do |f| %>
<%= f.text_field :example_field %>
<%= f.fields_for :scrapbook_entries do |s| %>
<%= f.text_field :extra_data %>
<% end %>
<% end %>
I have two models, Car and Manufacturer. These models are pretty simple:
class Car < ActiveRecord::Base
attr_accessible :manufacturer_id, :car_name, :descr, ...
belongs_to :manufacturer
...
end
and
class Manufacturer < ActiveRecord::Base
attr_accessible :name, :url
has_many :cars
...
end
The view (views/cars/_form.html.haml) with form for entering data:
= form_for #car do |f|
.field
= f.label :car_name
= f.text_field :car_name
...
= f.fields_for #manufacturer do |m|
.field
= m.label :name
= m.text_field :name
...
When I send the form for saving entered information (it goes to CarsController), I get this error:
Can't mass-assign protected attributes: manufacturer
I've tried to add the
accepts_nested_attributes_for :manufacturer
to the Car model, but it didn't help me...
Where is the problem?
EDIT:
How I am saving data in controller:
#manufacturer = Manufacturer.new(params[:car][:manufacturer])
#car = #manufacturer.cars.build(params[:car])
EDIT2:
Data from log:
{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"4vcF5NV8D91DkxpCsqCzfbf05sOYsm7ssxZvPa3+kXo=",
"car"=>{"car_name"=>"...",
"descr"=>"...",
"categroy_ids"=>["2",
"3",
"4"],
"manufacturer"=>{"name"=>"Company",
"url"=>"..."}},
"commit"=>"Save",
"id"=>"..."}
Thank you
Can you save manufacturer through car?
Add to Car model:
accepts_nested_attributes_for :manufacturer
Add manufacturer_attributes among other Car attributes to attr_accessible call in Car model:
attr_accessible :manufacturer_attributes, :car_name, :descr, ...
Save it in your controller action(standard way) something like this:
def create
#car = Car.new(params[:car])
if #car.save
redirect_to #car
else
render :new
end
end
Make sure that everything you are sending in manufacturer_attributes hash is white listed with attr_accessible call in Manufacturer model(:name, :url etc..).
Your params[:car] contains manufacturer attributes.. Try this:
#manufacturer = Manufacturer.new(params[:car].delete(:manufacturer))
#car = #manufacturer.cars.build(params[:car])
You are not making use of has_many relation by doing this way. You can go through this
You need to add
attr_accessible :manufacturer_id, :car_name, :descr, :manufacturer_attributtes
in car the model. Don't bother with #manufacturer in your saving method in the car controller it is taken care of.
You should read this : Active Record Nested Attributes
I hope it helped.
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?
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 :-).
If you inspect the form that is generated, you'll notice that the nested fields have a name like "ingredients_attributes". The reason you're getting the mass-assignment error is because you need to add these fields to the attr_accessible declaration.
Something like this should fix it (you'll need to doublecheck the field names):
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
#...
end
Update: There's a similar answer here
Leave the call as
<%= f.fields_for :ingredients do |i| %>
But before that do
<% #recipe.ingredients.build %>
Im guessing that will allow your form to be created the right way, but there are likely other errors with your models, I can look # it more in detail when I have more time if its still not working, but:
As far as what accepts_nested_attributes_for does, when you pass in a correctly formatted params hash to the Model.new or Model.create or Model.update, it allows those attributes on the related model to be saved if they are in the params hash. In addition though, you do need to make the attributes accessible if they are unaccessible in the parent model as stated by beerlington.
I think you just need set up a one-to-many association, one recipe has many ingredients and one ingredient belongs to one recipe, so your model look like:
class Recipe < ActiveRecord::Base
attr_accessible :name, :ingredients_attributes
has_many :ingredients
accepts_nested_attributes_for :ingredients
end
class Ingredient < ActiveRecord::Base
attr_accessible :name, :recipe_id
belongs_to :recipe
end
You are built right form, so I don't write it again here. Now in your new and create controller will be like this:
def new
#recipe = Recipe.new
# This is create just one select field on form
#recipe.ingredients.build
# Create two select field on form
2.times { #recipe.ingredients.build }
# If you keep code above for new method, now you create 3 select field
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
...
else
...
end
end
How does params[:recipe] look like? If you just have one select field, maybe like this:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 } ] } }
If you have 2 ingredient select field:
params = { recipe: { name: "Stewed Tomatoes", ingredients_attributes: [ { id: 1 }, { id: 2 } ] } }