Rails 5: Why can't I save nested attributes? - ruby-on-rails

I'm struggling with understanding this.
I have two models Person and Address. Person accepts nested attributes for Address.
class Person < ApplicationRecord
has_one :address, dependent: :destroy
accepts_nested_attributes_for :address
I have a form app/views/person/new.html.erb:
<%= bootstrap_form_for(#person) do |f| %>
#...
<%= f.fields_for :address do |ff| %>
<%= ff.text_field :apartment_number %>
<%= ff.text_field :building_name %>
<%= ff.text_field :building_number %>
<%= ff.text_field :street %>
<%= ff.text_field :town %>
<%= ff.text_field :postcode, required: true %>
<% end %>
#...
I have set up my strong params and I create the new record as follows:
def create
#person = Person.new(safe_params)
if #person.save
binding.pry
else
render 'new'
end
end
private
def safe_params
params.require(:person).permit(
:name,
:date_of_birth,
address: %i[
apartment_number
building_name
building_number
street
town
postcode
]
)
end
However, there's two things going on here. If #person fails validation, the re-rendered new view does not contain the submitted address data. Secondly, if the save is successful I can see that the associated address is not created:
#person.address
=> nil
What do I need to do to ensure that an address is created and saved? Have I misunderstood something obvious?

As per the docs on nested attributes the strong parameters have to end with _attributes
params.require(:person).permit(
:name,
:date_of_birth,
address_attributes: %i[ apartment_number ]
)

Related

Triple-nested form with Rails form_with and strong parameters

I'm running Rails 6.0.0.rc1 and having trouble getting a triple nested form working. I have products that have options that have option_values. So a Product might have an option called "Color" and an Option Value named "Red." I'd like to create all of these in a classic nested form in the Product form.
The form works and I can save Product with an Option, but not the Option Value on submit. I'm not sure why it's not working when I try to embed fields_for Option Values inside the fields_for Options.
What am I doing wrong here? I feel like I'm missing something obvious, but can't figure it out. (Probably not relevant, but note that I need to scope each object to account_id and my User has_one :account that's the reason for the hidden field.)
Here is my product model:
class Product < ApplicationRecord
belongs_to :account
has_many :options, dependent: :destroy
accepts_nested_attributes_for :options, allow_destroy: true
validates :account_id, presence: true
validates :name, presence: true
end
Option model:
class Option < ApplicationRecord
belongs_to :account
belongs_to :product
has_many :option_values, dependent: :destroy
accepts_nested_attributes_for :option_values, allow_destroy: true
validates :account_id, presence: true
validates :name, presence: true
end
OptionValue model:
class OptionValue < ApplicationRecord
belongs_to :account
belongs_to :option
validates :account_id, presence: true
validates :name, presence: true
end
Here's the Product form:
<%= form_with(model: product, local: true) do |f| %>
<%= f.fields_for :options do |options_form| %>
<fieldset class='form-group'>
<%= options_form.hidden_field :account_id, value: current_user.account.id %>
<%= options_form.label :name, 'Option' %>
<%= options_form.text_field :name, class: 'form-control' %>
</fieldset>
<%= f.fields_for :option_values do |values_form| %>
<fieldset class='form-group'>
<%= values_form.label :name, 'Value' %>
<%= values_form.text_field :name, class: 'form-control' %>
</fieldset>
<% end %>
<% end %>
<% end %>
ProductsController:
class ProductsController < ApplicationController
def new
#product = Product.new
#product.options.build
end
def create
#account = current_user.account
#product = #account.products.build(product_params)
respond_to do |format|
if #product.save
format.html { redirect_to #product, notice: 'Product was successfully created.' }
else
format.html { render :new }
end
end
end
private
def product_params
params.require(:product).permit(
:account_id, :name,
options_attributes: [
:id, :account_id, :name, :_destroy,
option_values_attributes:[:id, :account_id, :name, :_destroy ]
]
)
end
end
At first, you need to change the form to nest option_values inside option, and add account_id field to option values:
<%= form_with(model: product, local: true) do |f| %>
<%= f.fields_for :options do |options_form| %>
<fieldset class='form-group'>
<%= options_form.hidden_field :account_id, value: current_user.account.id %>
<%= options_form.label :name, 'Option' %>
<%= options_form.text_field :name, class: 'form-control' %>
</fieldset>
<%= options_form.fields_for :option_values do |values_form| %>
<fieldset class='form-group'>
<%= values_form.hidden_field :account_id, value: current_user.account.id %>
<%= values_form.label :name, 'Value' %>
<%= values_form.text_field :name, class: 'form-control' %>
</fieldset>
<% end %>
<% end %>
<% end %>
Also you need to build nested records in the controller. Another option is to build them dynamically via javascript (look at the cocoon gem, for example). To build 3 options with 3 values each:
def new
#account = current_user.account
# it is better to create associated product
#product = #account.products.new
3.times do
option = #product.options.build
3.times { option.option_values.build }
end
end
Update:
Because I was trying to follow along this Nested Form Railscast, the biggest problem was that I didn't realize that the version Ryan Bates has working is on "Edit", not "New", so I added the Products, Options, and Values via the console and got the form working with this code:
_form.html.erb
<%= f.fields_for :options do |builder| %>
<%= render 'option_fields', f: builder %>
<% end %>
_option_fields.html.erb
<fieldset class='form-group'>
<%= f.hidden_field :account_id, value: current_user.account.id %>
<%= f.label :name, 'Option' %>
<%= f.text_field :name, class: 'form-control' %>
<br>
<%= f.check_box :_destroy, class: 'form-check-input' %>
<%= f.label :_destroy, 'Remove Option' %>
<small id="optionHelp" class="form-text text-muted">
(e.g. "Size" or "Color")
</small>
<%= f.fields_for :option_values do |builder| %>
<%= render 'option_value_fields', f: builder %>
<% end %>
</fieldset>
_option_value_fields.html.erb
<fieldset class='form-group'>
<%= f.hidden_field :account_id, value: current_user.account.id %>
<%= f.label :name, 'Value' %>
<%= f.text_field :name, class: 'form-control' %>
<br>
<%= f.check_box :_destroy, class: 'form-check-input' %>
<%= f.label :_destroy, 'Remove Value' %>
<small id="optionValueHelp" class="form-text text-muted">
(e.g. "Small, Medium, Large" or "Red, Green, Blue")
</small>
</fieldset>
Also, the only difference with the Railscast is using strong parameters in the controller, so you'll just need to nest them like this:
ProductsController
def product_params
params.require(:product).permit(:account_id, :name, options_attributes [:id, :account_id, :name, :_destroy, option_values_attributes: [:id, :account_id, :name, :_destroy]])
end
OptionsController
def option_params
params.require(:option).permit(:account_id, :name, option_values_attributes [:id, :account_id, :name, :_destroy])
end
OptionValuesController
def option_value_params
params.require(:option_value).permit(:account_id, :option_id, :name)
end
Rather than building the nested objects in the controller I'm going to do it with Javascript as in the Railscast episode or with the Cocoon gem as Vasilisa suggested in her answer.
Just wanted to share the code that actually ended up working in case someone else runs into similar problems. I think the Railscast, though old, is still a great introduction to nested forms in Rails, but you just have to be aware of the changes required to use form_with and strong parameters. Big thanks to Vasilisa for helping me figure this out.
The main "gotchas" that you need to look out for when following the Rails Nested Form Railscast are this:
form_with has some different syntax than the older rails form_tag
Make sure you don't have any typos or name problems when creating your form blocks because these are nested twice
Same with nesting parameters in your controllers, just be aware of your syntax and typos
Be aware that Ryan Bates is demoing with data that wasn't added through the form he's building so if you'd like to follow along you'll need to create some data in the console
With strong parameters you'll have to explicitly list :_destroy as a parameter in order for his "Remove" checkboxes to work

Create multiple new records from checkboxes in form using nested attributes in Rails

I'm trying to come up with a contact form that creates a contact record and potentially multiple location records, if multiple locations are checked in a list of checkboxes. I thought of having all location records created and then destroyed, if they aren't checked. I don't think that's optimal though.
I'm using many to many relationships in the models.
This is what they look like at the moment:
contact.rb
class Contact < ApplicationRecord
has_many :contact_locations, dependent: :destroy
has_many :locations, through: :contact_locations
accepts_nested_attributes_for :contact_locations, allow_destroy: true, reject_if: :empty_location?
private
def empty_location?(att)
att['location_id'].blank?
end
end
location.rb
class Location < ApplicationRecord
has_many :locations, dependent: :destroy
has_many :contacts, :through => :contact_locations
has_many :contact_locations
end
contact_location.rb
class ContactLocation < ApplicationRecord
belongs_to :location
belongs_to :contact
end
contacts_controller.rb
def new
#contact = Contact.new
#locations = Location.all
4.times {#contact.contact_locations.new}
end
private
def contact_params
params.require(:contact).permit(:name, :phone, ..., contact_locations_attributes: [:location_ids])
end
new.html.rb
<%= form_with model: #contact do |f| %>
...
<%= #locations.each do |location| %>
<%= f.fields_for :contact_locations do |l| %>
<%= l.check_box :location_id, {}, location.id, nil %><%= l.label location.name %>
<% end %>
<% end %>
...
<% end %>
Does anyone how to make it work properly?
I'm working on Ruby 2.5.1 and Rails 5.2.1.
Thanks a lot.
I think your solution is the form objects pattern.
You can have something like this:
<%= form_for #user do |f| %>
<%= f.email_field :email %>
<%= f.fields_for #user.build_location do |g| %>
<%= g.text_field :country %>
<% end %>
<% end%>
And convert it in something more readable that permits you to instance the locations inside the registration object, checking the value of the checkboxes.
<%= form_for #registration do |f| %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.input :password %>
<%= f.text_field :password %>
<%= f.input :country %>
<%= f.text_field :country %>
<%= f.input :city %>
<%= f.text_field :city %>
<%= f.button :submit, 'Create account' %>
<% end %>
Here you will find how to apply the pattern: https://revs.runtime-revolution.com/saving-multiple-models-with-form-objects-and-transactions-2c26f37f7b9a
I ended up making it work with Kirti's suggestion on the following question:
Rails Nested attributes with check_box loop
It turns out I needed to make a small adjustment in my form's fields_for tag.
Thanks a lot the help!

nested forms : update child foreign key

I have a nested association:
class User < ActiveRecord::Base
has_many :hostels
accepts_nested_attributes_for :hostels
end
class Hostel < ActiveRecord::Base
belongs_to :user
end
The form :
<%= form_for #user do |f| %>
<%= f.label :email %><br>
<%= f.text_field :email %>
<% f.object.hostels << #hostel -%>
<%= f.fields_for :hostels do |ff| %>
<%= ff.hidden_field :id %>
<% end -%>
<%= f.submit %>
<% end -%>
the controller
def create
#user = User.new(user_params)
raise #user.hostels.inspect
end
private
def user_params
params.require(:user).permit(:email, hostels_attributes: [:id])
end
I would like to relink existing records of hostels to new users by updating hostel foreign key. This way, it definitly don't work.
Tried update_only: true parameter to nested too.
Any ideas about the subject or am I totally wrong about trying to do the operation like that ?
you can use a multiple select for hotels in you form, then in the controller you must require hostel_ids.
Models:
class User < ActiveRecord::Base
has_many :hostels
end
class Hostel < ActiveRecord::Base
belongs_to :user
end
Form: where :name is what you see in your multiple select
<%= form_for #user do |f| %>
<%= f.label :email %><br>
<%= f.text_field :email %>
<%= f.label "Hostels" %><br>
<%= select_tag :hotel_ids, options_for_select(Hostel.all.map{|h| [h.name, h.id]}), { :multiple => true } %>
<%= f.submit %>
<% end %>
Controller:
def create
#user = User.new(user_params)
end
private
def user_params
params.require(:user).permit(:email, hostel_ids)
end
I did not test the code but must work well.

Rails - How to validate Form with Nested Attributes?

I am creating a nested form with attributes from different models. I expect all the required attributes to be valid, before a new object is saved.
<%= form for #product do |f| %>
<%= f.fields_for #customer do |g| %>
<%= g.label :name %>
<%= g.text_field :name %>
<%= g.label :email %>
<%= g.text_field :email %>
<%= g.label :city %>
<%= g.text_field :city %>
<%= g.label :state %>
<%= g.text_field :state %>
<%= g.label :zipcode %>
<%= g.text_field :zipcode %>
<% end %>
<%= f.label :product %>
<%= f.text_field :product %>
<%= f.label :quantity %>
<%= number_field(:quantity, in 1..10) %>
<% end %>
Here are my models
class Product < ActiveRecord::Base
belongs_to :customer
validates_associated :customer
validates :product, :presence => "true"
end
class Customer < ActiveRecord::Base
has_one :product
validates :name, :email, presence: true
validates :email, format: { with: /[A-Za-z\d+][#][A-Za-z\d+][.][A-Za-z]{2,20}\z/ }
validates :city, presence: true
validates :zipcode, format: { with: /\A\d{5}\z/ }
end
I added validates_associated to my Product Model, so my form_for #product should require all the customer validations to pass. That means name, email, city and zipcode have to be there and have to be formatted properly.
I fiddled around, and submitted the form without filling in the Customer required fields, and the form was considered valid.
I don't understand where my mistake is.
EDIT
Alright, so by adding validates :customer, the customer attributes are now required. But they aren't actually saved to the database. I think this has to do with my params
def product_params
params.require(:product).permit(:product, :quantity)
end
Do I need to add my Customer Params to my permitted params list?
The validates_associated method only validates the associated object if the object exists, so if you leave the form fields blank, the Product you are creating/editing will validate, because there is no associated Customer.
Instead, assuming you're using Rails 4+, you want to use accepts_nested_attributes_for :customer, along with validates :customer, presence: true in order to required the customer fields in your product form.
If you're using Rails 3, then accepts_nested_attributes_for will not work for a belongs_to association. Instead, your Customer class will need to use accepts_nested_attributes_for :product, and you will need to alter your form view accordingly.
UPDATE
You also need to allow your controller action to accept parameters for the :customer association:
def product_params
params.require(:product).permit(:product, :quantity, :customer_attributes => [:name, :email, :city, :state, :zipcode])
end
It's worth noting that because there is no :id field in your customer form fields, and no :customer_id field in your product form fields, you will create a new customer every time you successfully submit the product form.
try this out:
In Controller create an instance of a product and associated customer as follows:
#product = Product.new
#customer = #product.build_customer
in use this code for form
<%= form for #product do |f| %>
<%= f.fields_for :customer do |g| %>
<%= g.label :name %>
<%= g.text_field :name %>
<%= g.label :email %>
<%= g.text_field :email %>
<%= g.label :city %>
<%= g.text_field :city %>
<%= g.label :state %>
<%= g.text_field :state %>
<%= g.label :zipcode %>
<%= g.text_field :zipcode %>
<% end %>
<%= f.label :product %>
<%= f.text_field :product %>
<%= f.label :quantity %>
<%= number_field(:quantity, in 1..10) %>
<% end %>
i.e use :customer symbol instead of #customer instance variable.
and use accepts_nested_attributes_for helper method in Product model as #Charles said
Complementing the other answers, I control what I receive in the controller, avoiding further action and noticing if a value is not the one I want.
def update
if params[:customer][:product_attributes]["0"][:name] == ""
redirect_to customer_path(#incident), alert: 'You need to add a name'
else
respond_to do |format|
if #customer.update(customer_params)
format.html { redirect_to customer_path(#customer), notice: 'Succesfully updated' }
format.json { render :show, status: :ok, location: #customer }
else
format.html { render :edit }
format.json { render json: #customer.errors, status: :unprocessable_entity }
end
end
end
end

rails - polymorphic association - one-to-one - Form is submiting nested fields with wrong name

I have User, which can be one of three types: Admin, Student, Teacher. Everyone has other attributes. I am trying polymorphic association one-to-one like this:
User
class User < ActiveRecord::Base
belongs_to :identity, :polymorphic => true
accepts_nested_attributes_for :identity, :allow_destroy => true
attr_accessible :email, :login, :remember_token,
:password_confirmation, :password, :role
end
Student
class Student < ActiveRecord::Base
attr_accessible :field
has_one :user, :as => :identity
end
Controller
def new
#user = User.new
end
def create
#user = User.new(params[:user]) # It fails here.
#user.identita.build
...
end
View
<%= form_for(#user) do |f| %>
<%= f.label :login %><br />
<%= f.text_field :login %>
<%= f.fields_for [:identity, Student.new] do |i| %>
<%= i.label :field %><br />
<%= i.textfield_select :field %>
<% end %>
<% end %>
When I submit this view (more complex, but this is the core), it sends hash like this:
{"utf8"=>"✓",
"authenticity_token"=>"...",
"user"=>{"login"=>"...",
"student"=> {"field"=>"..."}
}
So it fails on marked line in controller with:
ActiveModel::MassAssignmentSecurity::Error in UsersController#create
Can't mass-assign protected attributes: student
What am I doing wrong? Something like :as=>"student" or twisting the realationship?
Firstly, fix:
<%= f.fields_for [:identity, Student.new] do |i| %>
to:
<%= f.fields_for :identity, Student.new do |i| %>
Secondly, you are trying to use accepts_nested_attributes_for on a belongs_to relationship. This is not supported behavior AFAIK. Perhaps try moving that to the Student model:
class Student < ActiveRecord::Base
attr_accessible :field
has_one :user, :as => :identity
accepts_nested_attributes_for :user, :allow_destroy => true
end
and make the view like this:
<%= form_for(Student.new) do |i| %>
<%= i.fields_for :user, #user do |f| %>
<%= f.label :login %><br />
<%= f.text_field :login %>
<% end %>
<%= i.label :field %><br />
<%= i.textfield_select :field %>
<% end %>
From documentation of attr_accessible
attr_accessible will only set attributes in this list, to assign to
the rest of attributes you can use direct writer methods.
So once you've used attr_accessible, another attributes automatically will become a protected ones.

Resources