I have 3 models, a Property has_many units and each unit belongs to a Property. A Unit has_one unit_amenity and a unit_amenity belongs to a unit. The issue is that I need to nest the unit_amenity form which is 2 levels down in the new property form and I'm getting an Unpermitted parameter error. I also see just "unit_amenity"=>{"heat"=>"1"} in the logs instead of "unit_amenity_attributes"=>{"heat"=>"1"}.
Property.rb
class Property < ApplicationRecord
has_many :units, dependent: :destroy
accepts_nested_attributes_for :units, allow_destroy: true
end
Unit.rb
class Unit < ApplicationRecord
belongs_to :property
has_one :unit_amenity, dependent: :destroy
accepts_nested_attributes_for :unit_amenity, allow_destroy: true
end
Unit_amenity.rb
class UnitAmenity < ApplicationRecord
belongs_to :unit
end
I have nested the unit_amenity params within the unit params in properties_controller.rb
# Only allow a list of trusted parameters through.
def property_params
params.require(:property).permit(:city, :state, :zip_code,
units_attributes: [ :id, :_destroy, :unit_number, :bedrooms, :bathrooms, :property_id,
unit_amenity_attributes: [ :id, :_destroy, :unit_id, :heat, :air_conditioning ] ])
end
end
The form has a nested fields_for for unit_amenity
<%= form_with(model: property) do |form| %>
<div class="col-span-6">
<%= form.label :city %>
<%= form.text_field :city %>
</div>
....
<%= form.fields_for :unit_amenity do |amenity_field| %>
<div>
<%= amenity_field.check_box :heat, class: '', type: 'checkbox', id: 'heat' %>
<%= amenity_field.label :heat, class: "text-sm font-medium text-gray-700" %>
</div>
....
<% end %>
<% end %>
The new action of the properties_conroller.rb
def new
#property = Property.new
units = #property.units.build # has_many association
units.build_unit_amenity # has_one association
end
You need to nest the calls to fields for:
<%= form_with(model: property) do |form| %>
# ...
<%= form.fields_for :units do |unit_field| %>
<%= unit_field.fields_for :unit_amenity do |amenity_field| %>
# ...
<% end %>
<% end %>
Aparat from that you should also remove property_id and unit_id from the parameters whitelist. Never have the "parent id" in the params whitelist.
They are assigned when you create nested records and could actually be used to create records belonging to another property if a malicous user felt like it.
Related
So im working through the Odin Project's "Flight Booker" project. https://www.theodinproject.com/courses/ruby-on-rails/lessons/building-advanced-forms. Which essentially is what it sounds like and im running into a problem with passing nested attributes.
First and foremost the Relevant Models:
class Booking < ApplicationRecord
belongs_to :passenger
belongs_to :flight
accepts_nested_attributes_for :passenger
end
class Flight < ApplicationRecord
has_many :bookings, dependent: :destroy
has_many :passengers, through: :bookings
belongs_to :to_airport, class_name: 'Airport', foreign_key: 'origin_id'
belongs_to :from_airport, class_name: 'Airport', foreign_key: 'destination_id'
end
class Passenger < ApplicationRecord
has_many :bookings, dependent: :destroy
has_many :flights, through: :bookings
end
The passenger schema just contains an email and name for right now. But the problem is when I pass the information to the "booking" controller. Here is my "New" form for booking.
<%= form_for #booking do |f| %>
<%= f.hidden_field :flight_id, value: params[:booking][:flight_num] %>
<%= f.hidden_field :passengers_num, value: params[:booking][:passengers_num] %>
<% params[:booking][:passengers_num].to_i.times do |passenger| %>
<%= fields_for :passenger do |passenger| %>
<%= passenger.label :name, 'Name', class: "Label" %>
<%= passenger.text_field :name %>
<%= passenger.label :email, 'email', class: "Label" %>
<%= passenger.email_field :email %>
<% end %>
<% end %>
<%= f.submit "Book Flight" %>
<% end %>
(Ignore the hidden fields for now, they are passed from the "Flights" search page and Im getting those just fine.)
So I am getting the multiple forms (name and email fields) but when I "Submit" I am only getting parameters for the last field sets. (So if there are 3 sets of name/email fields, I only get parameters for the last one).
It's possible im not understanding the fields_for however as I can't find a ton of good examples.
Thanks!
There could be many issues with your implementation...I'll layout a few...
Move <% params[:booking][:passengers_num].to_i.times do |passenger| %> logic into the new action of your bookings controller...ie
def new
#booking = Booking.new
3.times { #booking.passengers.new } # or whatever your logic is to display x amount of passenger fields
end
Make sure that in your bookings controller you are permitting the nested attributes like this...
params.require(:booking).permit(passengers_attributes: [:name, :email])
As far as the form, you'll need to treat it like a form within a form (makes sense...nested attributes created from a nested form!) and use the block variable...like this
<ul>
<%= f.fields_for :passengers do |passenger_form| %>
<li>
<%= passenger_form.label :name
<%= passenger_form.text_field :name %>
</li>
<!-- other permitted fields -->
<% end %>
</ul>
This is a new error to me, and struggling to resolve it. It also states: Roaster(#70130698993440) expected, got "1" which is an instance of String(#70130675908140)
It's highlighting my create method in my Roasts Controller:
def create
#roast = Roast.new(roast_params)
The scenario is that I'm trying to create a triple nested form. for three models Roasts Countries and Regions where roasts has many countries and countries has many regions.
I'm assuming there is something wrong with the roast params, but I can see what it is. I have added the associations there for the nested models
def roast_params
params.require(:roast).permit(:roaster, :name, :bestfor, :beans, :roast, :tastingnotes, :notes, :slug, :avatar, :countries_attributes => [:country_name, :regions_attributes => [:region_name]])
end
my form
<div class="form-group">
<%= form.fields_for :countries do |countries_form| %>
<%= countries_form.label :country %>
<%= countries_form.text_field :name, class: "form-control" %>
</div>
<div class="form-group">
<%= form.fields_for :regions do |regions_form| %>
<%= regions_form.label :region %>
<%= regions_form.text_field :region_name, class: "form-control" %>
<% end %>
<% end %>
</div>
Roast Controller
...
def new
#roast = Roast.new
#roast.countries.build.regions.build
end
...
roast model
class Roast < ApplicationRecord
has_many :tastings
has_many :countries
has_many :notes, through: :tastings
has_many :comments, as: :commentable
belongs_to :roaster
accepts_nested_attributes_for :countries
country model
class Country < ApplicationRecord
has_many :regions, inverse_of: :country
accepts_nested_attributes_for :regions
belongs_to :roasts
region model
class Region < ApplicationRecord
belongs_to :country
I've nested the regions params in the country params, is that correct? I also saw on SO other issues with suggestions for setting config.cache_classes to true in development.rb but that didn't help here.
Update
So looking at this further, I believe it's not related to the nested forms, but rather a collection_select I'm using.
<%= form.label :roaster, class: 'control-label' %>
<%= form.collection_select(:roaster, Roaster.order(:roaster_name).all, :id, :roaster_name, prompt: true, class: "form-control") %>
So this select is pulling the roaster_name from a model called Roaster.
My params now look like the below:
params.require(:roast).permit(:roaster_name, :roaster, :name, :bestfor, :beans, :roast, :tastingnotes, :notes, :slug, :avatar, :countries_attributes => [:country_id, :country_name, :regions_attributes => [:region_id, :region_name]])
And looking at the console when submitting the form, it seems that just the :id of Roaster is getting passed, rather than the value of :roaster_name.
{"utf8"=>"✓",
"authenticity_token"=>"EG+zty85IiVsgipm1pjSAEZ7M66ELWefLq8Znux+cf89sSnVXxielRr1IaSS9+cJvdQD8g1D4+v2KqtKEwh6gw==",
"roast"=>{"roaster"=>"1", "name"=>"Espress", "countries_attributes"=>{"0"=>{"country_name"=>"UK"}}, "regions"=>{"region_name"=>"Highlands"}, "bestfor"=>"", "roast"=>"", "tastingnotes"=>""},
"commit"=>"Create Roast"}
Can't work this out
ActiveRecord::AssociationTypeMismatch is raised when an association-setter (Roast#roaster= in this case) is called with a value that is not an instance of the expected class. Roaster was expected, got String.
The issue seems to be with passing roaster in as a param, which is "1" (String) in your example. I'm guessing this is actually an ID of a Roaster, the form code in the question does not show it.
Perhaps you meant to permit and pass a roaster_id param?
def roast_params
params.require(:roast).permit(:roaster_id, # ...
end
I have a few models in my project : Request, Work, Car and Employee. Work is an intermediate model between Request and Car/Employee.
Here are the associations:
Request
has_many :works, dependent: :destroy
def performers
works.map {|x| x.performer}
end
Work
belongs_to :request
belongs_to :performer, polymorphic: true
Car
has_many :works, as: :performer
has_many :requests, through: :works, as: :performer
Employee
has_many :works, as: :performer
has_many :requests, through: :works, as: :performer
View used to create works:
<%= form_for([#request, #work]) do |f| %>
<%= (f.collection_select :performer_id, Employee.all, :id, :name) if #request.type == "StaffRequest" %>
<%= (f.collection_select :performer_id, Car.all, :id, :select_info) if #request.type == "CarRequest" %>
<%= f.submit 'OK' %>
<% end %>
Work controller
def new
#work = #request.works.new
end
def create
#work = #request.works.new(work_params)
end
def work_params
params.require(:work).permit(:performer_id, :request_id)
end
The problem is that my performer_type column is always empty, it does not save the class name. What can be the problem? Any ideas?
It's empty because you did't pass it, you should add a hidden field for you form:
<%= form_for([#request, #work]) do |f| %>
<% if #request.type == "StaffRequest" %>
<%= (f.hidden_field :performer_type, value: "Employee") %>
<%= (f.collection_select :performer_id, Employee.all, :id, :name) %>
<% elsif #request.type == "CarRequest" %>
<%= (f.hidden_field :performer_type, value: "Car") %>
<%= (f.collection_select :performer_id, Car.all, :id, :select_info) %>
<% end %>
<%= f.submit 'OK' %>
<% end %>
Beside :performer_id, you have to pass the :performer_type also, one way to do this is via the form select_tag :
def create
#work = #request.works.new(work_params)
end
def work_params
# use :performer if you use :performer as in the select form field
params.require(:work).permit(:performer, :request_id)
# OR
# use :performer_id & :performer_type if you also use the hidden field
params.require(:work).permit(:performer_id, :performer_type, :request_id)
end
There is a good example (for Rails 4.2) of using a single select form field for polymorphic so you can write like:
<%= f.grouped_collection_select :global_performer, [ Car, Employee ], :all, :model_name, :to_global_id, :name %>
How to create grouped select box in Rails for polymorphic association using Global ID?
I'm attempting to build a recipe-keeper app with three primary models:
Recipe - The recipe for a particular dish
Ingredient - A list of ingredients, validated on uniqueness
Quantity - A join table between Ingredient and Recipe that also reflects the amount of a particular ingredient required for a particular recipe.
I'm using a nested form (see below) that I constructed using an awesome Railscast on Nested Forms (Part 1, Part 2) for inspiration. (My form is in some ways more complex than the tutorial due to the needs of this particular schema, but I was able to make it work in a similar fashion.)
However, when my form is submitted, any and all ingredients listed are created anew—and if the ingredient already exists in the DB, it fails the uniqueness validation and prevents the recipe from being created. Total drag.
So my question is: Is there a way to submit this form so that if an ingredient exists whose name matches one of my ingredient-name fields, it references the existing ingredient instead of attempting to create a new one with the same name?
Code specifics below...
In Recipe.rb:
class Recipe < ActiveRecord::Base
attr_accessible :name, :description, :directions, :quantities_attributes,
:ingredient_attributes
has_many :quantities, dependent: :destroy
has_many :ingredients, through: :quantities
accepts_nested_attributes_for :quantities, allow_destroy: true
In Quantity.rb:
class Quantity < ActiveRecord::Base
attr_accessible :recipe_id, :ingredient_id, :amount, :ingredient_attributes
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient
And in Ingredient.rb:
class Ingredient < ActiveRecord::Base
attr_accessible :name
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
Here's my nested form that displays at Recipe#new:
<%= form_for #recipe do |f| %>
<%= render 'recipe_form_errors' %>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<h3>Ingredients</h3>
<div id='ingredients'>
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient_attributes do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name %>
<% end %>
<%= ff.label :amount %>
<%= ff.text_field :amount, size: "10" %>
<%= ff.hidden_field :_destroy %>
<%= link_to_function "remove", "remove_fields(this)" %><br>
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
</div><br>
<%= f.label :description %><br>
<%= f.text_area :description, rows: 4, columns: 100 %><br>
<%= f.label :directions %><br>
<%= f.text_area :directions, rows: 4, columns: 100 %><br>
<%= f.submit %>
<% end %>
The link_to and link_to_function are there to allow the addition and removal of quantity/ingredient pairs on the fly, and were adapted from the Railscast mentioned earlier. They could use some refactoring, but work more or less as they should.
Update: Per Leger's request, here's the relevant code from recipes_controller.rb. In the Recipes#new route, 3.times { #recipe.quantities.build } sets up three blank quantity/ingredient pairs for any given recipe; these can be removed or added to on the fly using the "Add ingredient" and "remove" links mentioned above.
class RecipesController < ApplicationController
def new
#recipe = Recipe.new
3.times { #recipe.quantities.build }
#quantity = Quantity.new
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
redirect_to #recipe
else
render :action => 'new'
end
end
You shouldn't put the logic of ingredients match into view - it's duty of Recipe#create to create proper objects before passing 'em to Model. Pls share the relevant code for controller
Few notes before coming to code:
I use Rails4#ruby2.0, but tried to write Rails3-compatible code.
attr_acessible was deprecated in Rails 4, so strong parameters are used instead. If you ever think to upgrade your app, just go with strong parameters from the beginning.
Recommend to make Ingredient low-cased to provide uniform appearance on top of case-insensitivity
OK, here we go:
Remove attr_accessible string in Recipe.rb, Quantity.rb and Ingredient.rb.
Case-insensitive, low-cased Ingredient.rb:
class Ingredient < ActiveRecord::Base
before_save { self.name.downcase! } # to simplify search and unified view
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
end
<div id='ingredients'> part of adjusted form to create/update Recipe:
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name, size: "10" %>
<% end %>
...
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
We should use :ingredient from Quantity nested_attributes and Rails will add up _attributes-part while creating params-hash for further mass assignment. It allows to use same form in both new and update actions. For this part works properly association should be defined in advance. See adjusted Recipe#new bellow.
and finally recipes_controller.rb:
def new
#recipe = Recipe.new
3.times do
#recipe.quantities.build #initialize recipe -> quantities association
#recipe.quantities.last.build_ingredient #initialize quantities -> ingredient association
end
end
def create
#recipe = Recipe.new(recipe_params)
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
def update
#recipe = Recipe.find(params[:id])
#recipe.attributes = recipe_params
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
private
def prepare_recipe
#recipe.quantities.each do |quantity|
# do case-insensitive search via 'where' and building SQL-request
if ingredient = Ingredient.where('LOWER(name) = ?', quantity.ingredient.name.downcase).first
quantity.ingredient_id = quantity.ingredient.id = ingredient.id
end
end
end
def recipe_params
params.require(:recipe).permit(
:name,
:description,
:directions,
:quantities_attributes => [
:id,
:amount,
:_destroy,
:ingredient_attributes => [
#:id commented bc we pick 'id' for existing ingredients manually and for new we create it
:name
]])
end
In prepare_recipe we do the following things:
Find ID of ingredient with given name
Set foreign_key quantity.ingredient_id to ID
Set quantity.ingredient.id to ID (think what happens if you don't do that and change ingredient name in Recipe)
Enjoy!
Currently, an Item belongs_to a Company and has_many ItemVariants.
I'm trying to use nested fields_for to add ItemVariant fields through the Item form, however using :item_variants does not display the form. It is only displayed when I use the singular.
I have check my associations and they seem to be correct, could it possibly have something to do with item being nested under Company, or am I missing something else?
Thanks in advance.
Note: Irrelevant code has been omitted from the snippets below.
EDIT: Don't know if this is relevant, but I'm using CanCan for Authentication.
routes.rb
resources :companies do
resources :items
end
item.rb
class Item < ActiveRecord::Base
attr_accessible :name, :sku, :item_type, :comments, :item_variants_attributes
# Associations
#-----------------------------------------------------------------------
belongs_to :company
belongs_to :item_type
has_many :item_variants
accepts_nested_attributes_for :item_variants, allow_destroy: true
end
item_variant.rb
class ItemVariant < ActiveRecord::Base
attr_accessible :item_id, :location_id
# Associations
#-----------------------------------------------------------------------
belongs_to :item
end
item/new.html.erb
<%= form_for [#company, #item] do |f| %>
...
...
<%= f.fields_for :item_variants do |builder| %>
<fieldset>
<%= builder.label :location_id %>
<%= builder.collection_select :location_id, #company.locations.order(:name), :id, :name, :include_blank => true %>
</fieldset>
<% end %>
...
...
<% end %>
You should prepopulate #item.item_variants with some data:
def new # in the ItemController
...
#item = Item.new
3.times { #item.item_variants.build }
...
end
Source: http://rubysource.com/complex-rails-forms-with-nested-attributes/
try this way
in your item controller new action write
def new
...
#item = # define item here
#item.item_variants.build if #item.item_variants.nil?
...
end
and in item/new.html.erb
<%= form_for #item do |f| %>
...
...
<%= f.fields_for :item_variants do |builder| %>
...
<% end %>
...
...
<% end %>
for more see video - Nested Model Form