Rails - How to validate Form with Nested Attributes? - ruby-on-rails

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

Related

Rails 5: Why can't I save nested attributes?

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 ]
)

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

Rails Submitting Multiple Has_Many Attributes via Form

I am running a Rails 5.1 app with the following information:
Models
class Company < ApplicationRecord
has_many :complaints
accepts_nested_attributes_for :complaints
validates :name, presence: true
end
class Complaint < ApplicationRecord
belongs_to :company
validates :username, :priority, presence: true
end
Controller
class ComplaintController < ApplicationController
def new
#company = Company.new
#company.complaints.build
end
def create
#company = Company.new(company_params)
respond_to do |format|
if #company.save
format.html { redirect_to complaint_url }
else
format.html { render :new }
end
end
end
private
def company_params
params.require(:company).permit(:name, complaints_attributes: [:username, :priority])
end
Form in view
<%= form_for #company do |f| %>
<%= f.label :name, "Company" %>
<%= f.text_field :name, type: "text" %>
<%= f.fields_for :complaints do |complaint| %>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
If I have just one input field for the complaint_attributes part of the form (in other words just one field for username and one field for priority as shown above), this works just fine.
However, if I want to have multiple fields for username/priority in the form, so that I can submit multiple username/priority combinations in a single submission, I find that submitting the form will only save the last username/priority values from the form. Example of this view would be:
<%= form_for #company do |f| %>
<%= f.label :name, "Company" %>
<%= f.text_field :name, type: "text" %>
<%= f.fields_for :complaints do |complaint| %>
<div>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
</div>
<div>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
</div>
<% end %>
<%= f.submit 'Submit' %>
<% end %>
I noticed that when submitting the form, I get a hash like this (for submitting single complaint):
{"utf8"=>"✓", "authenticity_token"=>"...", "company"=>{"name"=>"Test", "complaints_attributes"=>{"0"=>{"username"=>"test_person", "priority"=>"1"}}}, "commit"=>"Submit"}
Is there any way to modify the params to make it similar to this and have it saved to the DB?:
{"utf8"=>"✓", "authenticity_token"=>"...", "company"=>{"name"=>"Test", "complaints_attributes"=>{"0"=>{"username"=>"test_person", "priority"=>"1"}"1"=>{"username"=>"test_person", "priority"=>"2"}}}, "commit"=>"Submit"}
Or if not the above, what would be the best way to have the username/priority values saved if using multiple fields for them in a single form?
EDIT: I should point out that I can dynamically add the username/priority field groups as needed, so I don't want to be restricted to a set number.
the second block will override the first fields... you should instead build many complaints in the controller:
def new
#company = Company.new
3.times { #company.complaints.build }
end
and then with the following form it should generate to inputs according to the number of complaints you have built:
<%= form_for #company do |f| %>
<%= f.label :name, "Company" %>
<%= f.text_field :name, type: "text" %>
<%= f.fields_for :complaints do |complaint| %>
<%= complaint.label :username, "Username" %>
<%= complaint.text_field :username %>
<%= complaint.label :priority, "Priority" %>
<%= complaint.text_field :priority %>
<% end %>
<%= f.submit 'Submit' %>
<% end %>

Rails - Convert hours and minutes to seconds then assign value to key

I would like to combine both values :hours and :minutes and convert them to to_i in seconds. Next is to assign this value (which should be in seconds) to the :time_duration which is a column in the cars db before it creates a new service. The :time_duration is in a hidden_field because there's no reason to render this data in the view.
views
This is my _car_fields.html.erb which is a nested partial inside a view template called, _form.html.erb .
_car_fields.html.erb
<div class="nested-fields">
<div class="field">
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<%= f.label :hours %>
<%= f.select :hours, '0'..'8' %>
<%= f.label :minutes %>
<%= f.select :minutes, options_for_select( (0..45).step(15), selected: f.object.minutes )%><br>
<%= f.label :price %><br>
<%= f.text_field :price, :value => (number_with_precision(f.object.price, :precision => 2) || 0) %> <br>
<%= f.label :details %><br>
<%= f.text_area :details %></div>
<%= link_to_remove_association "Remove Car", f, class: 'btn btn-default' %>
<%= f.hidden_field :time_duration, value: %>
<br>
<hr>
</div>
_form.html.erb
<%= simple_form_for #service do |f| %>
<div class="field">
<%= f.label "Select service category" %>
<br>
<%= collection_select(:service, :service_menu_id, ServiceMenu.all, :id, :name, {:prompt => true }) %>
<%= f.fields_for :cars do |task| %>
<%= render 'car_fields', :f => task %>
<% end %>
</div>
<div class="links">
<%= link_to_add_association 'Add New Car', f, :cars, class: 'btn btn-default' %>
</div><br>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
controller
services_controller
def new
#service = current_tech.services.build
end
def create
#service = current_tech.services.build(service_params)
respond_to do |format|
if #service.save
format.html { redirect_to #service, notice: 'Service was successfully created.' }
format.json { render :show, status: :created, location: #service }
else
format.html { render :new }
format.json { render json: #service.errors, status: :unprocessable_entity }
end
end
end
private
def service_params
params.require(:service).permit(:name, :service_menu_id, cars_attributes: [:tech_id, :name, :hours, :minutes, :price, :details, :_destroy])
end
models
service.rb
class Service < ActiveRecord::Base
belongs_to :tech
belongs_to :service_menu
has_many :cars, dependent: :destroy
accepts_nested_attributes_for :cars, :reject_if => :all_blank, :allow_destroy => true
end
car.rb
class Car< ActiveRecord::Base
belongs_to :service
belongs_to :tech
has_many :appointments
end
First, you can remove the hidden time_duration field from the form, since it is not needed.
Then, you'll create a before_save method for your car model:
car.rb
class Car < ActiveRecord::Base
belongs_to :service
belongs_to :tech
has_many :appointments
before_save :generate_time_duration
def generate_time_duration
self[:time_duration] = hours.hours.to_i + minutes.minutes.to_i
end
end
What this does: Before the car object is saved, it will run the generate_time_duration method. What this method does is it simply sums the hours.to_i and minutes.to_i and assigns it to the car's time_duration attribute.
Update old DB records
Since you're adding this functionality in your application AFTER records have already been created, here is a quick way to update all of your current records:
In your command line, open a rails console by running the command rails c (or rails console)
In the console, run this command: Car.all.each { |c| c.save! }
This is a quick, one-time fix that will loop through all Car records, save them, and subsequently update their time_duration fields.

Rails nested Attributes of join table won't be saved

Nested attributes of join model won't be saved. relation id's seems to be missing. The following error messages are added when the fields get validated:
* Assigned projects user can't be blank
* Assigned projects project can't be blank
The submitted params look like this ( <%= debug(params) %> )
--- !map:ActionController::Parameters
utf8: "\xE2\x9C\x93"
authenticity_token: HrF1NHrKNTdMMFwOvbYFjhJE1ltlKbuz2nsfBYYBswg=
project: !map:ActionController::Parameters
name: Peter Pan
assigned_projects_attributes: !map:ActiveSupport::HashWithIndifferentAccess
"0": !map:ActiveSupport::HashWithIndifferentAccess
position: Group Leader
currency: " Neverland Dollars"
commit: Add Project
action: create
controller: projects
I have 3 models, as followed:
class User < ActiveRecord::Base
has_many :assigned_projects
has_many :projects, :through => :assigned_projects
has_many :created_projects, :class_name => "Project", :foreign_key => :creator_id
end
class AssignedProject < ActiveRecord::Base
belongs_to :user, class_name: "User"
belongs_to :project, class_name: "Project"
attr_accessible :project_id, :user_id, :position, :project_attributes
accepts_nested_attributes_for :project
validates :user_id, presence: true
validates :project_id, presence: true
validates :position, presence: true
end
class Project < ActiveRecord::Base
belongs_to :user
has_many :assigned_projects
has_many :users, :through => :assigned_projects
belongs_to :creator, :class_name => "User", :foreign_key => :creator_id
attr_accessible :name, :creator_id, :currency :assigned_projects_attributes
accepts_nested_attributes_for :assigned_projects
validates :name, presence: true, length: { minimum: 3, maximum: 100 }
validates :currency, presence: true, length: { minimum: 1, maximum: 5 }
validates :creator_id, presence: true
end
So each User can create a Project. He can add any User to the Project through the join model.
Each Project belongs to a User resp. Creator and has_many user through assigned_projects
I want to give each user of a project a "position", which should be saved in the join model: assigned_project :position
the Project controller looks like that:
class ProjectsController < ApplicationController
def new
#project = Project.new
#project.assigned_projects.build(user_id: current_user)
end
def create
#project = current_user.assigned_projects.build.build_project(params[:project])
#project.creator = current_user
if #project.save
redirect_to current_user
else
render 'new'
end
end
end
and the project/new.html.erb form looks like that:
<%= form_for( #project ) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :assigned_projects do |ff| %>
<%= ff.label :position %>
<%= ff.text_field :position%>
<% end %>
<%= f.label :currency %>
<%= f.text_field :currency %>
<%= f.submit "Add Project", class: "" %>
<% end %>
UPDATE: current controller & view
def create
#project = Project.new(params[:project])
if #project.save
redirect_to current_user
else
render 'new'
end
end
<%= form_for( #project ) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.hidden_field :creator_id, value: current_user.id %>
<%= f.fields_for :assigned_projects, #project.assigned_projects do |ff| %>
<%= ff.label :position %>
<%= ff.text_field :position%>
<% end %>
<%= f.label :currency %>
<%= f.text_field :currency %>
<%= f.submit "Add Project", class: "" %>
<% end %>
View:
I think you need to pass the objects collection #project.assigned_projects you built in the new action to the fields_for:
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :assigned_projects, #project.assigned_projects do |ff| %>
<%= ff.label :position %>
<%= ff.text_field :position%>
<% end %>
<%= f.label :currency %>
<%= f.text_field :currency %>
<%= f.submit "Add Project", class: "" %>
Controller:
If i understood the first line in the create action i think you try to re-build the project assigned_projects in-order to stamp the creator attribute !!
Instead you could remove this line and put a hidden field in the nested for, something like:
<%= ff.hidden_field :creator, current_user %>
So your controller looks pretty basic now:
def create
#project = Project.new(params[:prject])
if #project.save #nested objects will be saved automatically
redirect_to current_user
else
render 'new'
end
end
What does the build_project method do?
I think in your controller you should just have build, not build.build_project, so like this:
#project = current_user.assigned_projects.build(params[:project])
of if build_project is a method used to create the params then
#project = current_user.assigned_projects.build(project_params)
in the case of rails 4 you would need something like this:
def project_params
params.require(:project).permit(:your_params)
end
In the case of rails 3 I think you need to add
attr_accessible :param1, :param2
in the project model for the parameters you want to set.
Problem is solved by removing the validations for project_id and user_id in the join table "AssignedProject"
So the join Model looks like that:
# Join Model AssignedProject
class AssignedProject < ActiveRecord::Base
belongs_to :user#, class_name: "User"
belongs_to :project#, class_name: "Project"
attr_accessible :project_id, :user_id, :position, :project, :project_attributes
accepts_nested_attributes_for :project
validates :position, presence: { message: " can't be blank." }
end
The New and Create methods look like that:
# Projects Controller
class ProjectsController < ApplicationController
def new
#project = Project.new
#project.assigned_projects.build(user_id: current_user)
end
def create
#project = Project.new(params[:project])
if #project.save
#project.assigned_projects.create(user_id: current_user)
redirect_to current_user
else
render 'new'
end
end
end
And the form in the view for the new method looks like that:
<%= form_for( #project ) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.hidden_field :creator_id, value: current_user.id %>
<%= f.fields_for :assigned_projects, #project.assigned_projects do |ff| %>
<%= ff.hidden_field :project_id, value: #project %>
<%= ff.hidden_field :user_id, value: current_user.id %>
<%= ff.label :position %>
<%= ff.text_field :position%>
<% end %>
<%= f.label :currency %>
<%= f.text_field :currency %>
<%= f.submit "Add Project", class: "" %>
<% end %>

Resources