I'm using Rails 3 and have a form that incorporates fields from multiple associated records using fields_for. My models w/relationships are as follows:
class Company < ActiveRecord::Base
has_many: locations, dependent: :destroy
has_many :addresses, through: :locations
has_many :contacts
accepts_nested_attributes_for :locations, :addresses, :contacts
end
class Address < ActiveRecord::Base
has_many :locations
has_many :companies, through: :locations
accepts_nested_attributes_for :locations, :companies
end
class Contact < ActiveRecord::Base
belongs_to :company
accepts_nested_attributes_for :company
end
class Location < ActiveRecord::Base
belongs_to :company
belongs_to :address
accepts_nested_attributes_for :company, :address
end
My controller currently looks like this:
class CompaniesController < ApplicationController
def new
@company = Company.new
@location = @company.locations.build
@address = @company.addresses.build
@contact = @company.contacts.build
end
def create
@company = Company.new(params[:company])
if @company.save
#handle a successful save
flash[:success] = "Company Created Successfully"
redirect_to @company
else
render 'new'
end
end
end
When the form is submitted I get this error: Can't mass-assign protected attributes: addresses_attributes, locations_attributes, contacts_attributes
I've tried changing the create method in the controller to the following:
def create
@company = Company.new(params[:company_name])
@company.addresses.build(params[:address])
@company.locations.build(params[:location])
@company.contacts.build(params[:contact])
if @company.save
#handle a successful save
flash[:success] = "Company Created Successfully"
redirect_to @company
else
render 'new'
end
end
The result with this create method is a server log that says:
> Processing by CompaniesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"SVDIk5IzY7foo9DULhzY+RWgh/HAA9NqRp6FafWwFDg=", "company"=>{"company_name"=>"New Co", "
addresses_attributes"=>{"0"=>{"address_line_1"=>"231 Main", "address_line_2"=>"", "address_line_3"=>"", "city"=>"Dallas", "state"=>"AL",
"country"=>"USA", "zipcode"=>"74343"}}, "locations_attributes"=>{"0"=>{"location_type"=>"11", "location_name"=>"DFW"}}, "contacts_attribu
tes"=>{"0"=>{"first_name"=>"Joe", "last_name"=>"User", "title"=>"CEO"}}}, "commit"=>"Save Info"}
SQL (24.9ms) BEGIN TRANSACTION
Address Exists (27.6ms) EXEC sp_executesql N'SELECT TOP (1) 1 AS one FROM [addresses] WHERE ([addresses].[address_line_1] IS NULL AND
[addresses].[address_line_2] IS NULL AND [addresses].[address_line_3] IS NULL AND [addresses].[address_line_4] IS NULL AND [addresses].[a
ddress_line_5] IS NULL AND [addresses].[city] IS NULL AND [addresses].[state] IS NULL AND [addresses].[county] IS NULL AND [addresses].[c
ountry] IS NULL AND [addresses].[zipcode] IS NULL)'
SQL (50.8ms) IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION
CACHE (0.0ms) SELECT @@TRANCOUNT
Rendered companies/new.html.erb within layouts/application (9.4ms)
Rendered layouts/_shim.html.erb (0.0ms)
User Load (32.1ms) EXEC sp_executesql N'SELECT TOP (1) [users].* FROM [users] WHERE [users].[remember_token] = N''TZlKZ6Sx06p3mMS9kUJY
GA'''
Notice that although the address_attributes are populated, the params[:address] that's queried is null for all fields. (*Note I have a validator in the address model to ensure each address is unique. There are currently no records in the address table).
How can I properly build and store the records for each model upon submit? Thanks!
UPDATE: I didn't have addresses_attributes, locations_attributes, and contacts_attributes listed in the Company model attr_accessible block. Adding these attributes seems to have resolved the problem of getting the child attributes loaded from the form in the controller and
@company = Company.new(params[:company])
now populates the addresses, locations and contacts, however when I call
if @company.save
the transaction still gets rolled back with the following server log
Started POST "/companies" for 127.0.0.1 at 2013-12-11 11:12:03 -0600
SQL (25.3ms) BEGIN TRANSACTION SQL (50.4ms) IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION CACHE (0.0ms) SELECT @@TRANCOUNT
Not sure why the transaction appears to get rolled back on save. I'm using sql server 2008 and tinytds if that helps.
Ok, so there were multiple problems with my code. I've weeded through them all and wanted to post the final working solution, in case someone else is dealing with has_many through associations and nested forms.
My final models look like this (using Rails 3.2.13):
Company.rb
class Company < ActiveRecord::Base
attr_accessible :locations_attributes, :contacts_attributes, :company_name
has_many :locations, dependent: :destroy
has_many :addresses, through: :locations
has_many :contacts
accepts_nested_attributes_for :locations, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :addresses
accepts_nested_attributes_for :contacts
end
Location.rb
class Location < ActiveRecord::Base
attr_accessible :address_attributes, :address, :created_by, :is_active, :location_name, :location_type, :region_id, :updated_by, :website
belongs_to :company
belongs_to :address
accepts_nested_attributes_for :address, :reject_if => :all_blank
end
Address.rb
class Address < ActiveRecord::Base
attr_accessible :address_line_1, :address_line_2, :address_line_3, :address_line_4, :address_line_5, :city, :country, :county, :created_by, :province, :state, :updated_by, :zipcode
has_many :locations, dependent: :destroy
has_many :companies, through: :location
accepts_nested_attributes_for :locations
end
companies_controller.rb
class CompaniesController < ApplicationController
def new
@company = Company.new
@location= @company.locations.build
@address = @company.addresses.build
@contact = @company.contacts.build
end
def show
@company = Company.find(params[:id])
end
def create
@company = Company.new(params[:company])
if @company.save
#handle a successful save
flash[:success] = "Company Created Successfully"
redirect_to @company
else
render 'new'
end
end
end
new.html.erb
<% provide(:title, 'Create Company')%>
<h1>Create Company</h1>
<div class="container">
<%= form_for(@company) do |f| %>
<% if @company.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@company.errors.count, "error") %> prohibited this company from being saved:</h2>
<ul>
<% @company.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="row">
<%= f.label :company_name, "Company Name" %>
<%= f.text_field :company_name %>
<hr>
</div>
<div class="row">
<div class="col-md-6"><h3>Primary Location</h3><hr></div>
<div class="col-md-6"><h3>Location Information</h3><hr></div>
</div>
<%= f.fields_for(:locations) do |lf| %>
<%= lf.fields_for(:address_attributes) do |af| %>
<%= f.fields_for(:contacts) do |cf| %>
<div class="row" >
<div class="col-md-6"><%= af.label "Address 1"%><%= af.text_field :address_line_1 %></div>
<div class="col-md-6"><%= lf.label "Location Type" %> <%= lf.select(:location_type, options_for_select([["Headquarters",11], ["Office", 12]])) %></div>
</div>
<div class="row" >
<div class="col-md-6"><%= af.label "Address 2"%><%= af.text_field :address_line_2 %></div>
<div class="col-md-6"><%= lf.label "Location Name" %> <%= lf.text_field :location_name %></div>
</div>
<div class="row" >
<div class="col-md-6"><%= af.label "Address 3"%><%= af.text_field :address_line_3 %></div>
<div class="col-md-6"><h3>Other Location Information</h3><hr></div>
</div>
<div class="row" >
<div class="col-md-6"><%= af.label "City"%><%= af.text_field :city %></div>
<div class="col-md-3"><button type="button" class="btn btn-primary btn-small btn-block">Services Offered</button></div>
</div>
<div class="row" >
<div class="col-md-6"><%= af.label "State"%><%= af.select(:state, options_for_select([["Alabama","AL"], ["Alaska","AK"]])) %></div>
<div class="col-md-3"><button type="button" class="btn btn-primary btn-small btn-block">Materials Accepted</button></div>
</div>
<div class="row" >
<div class="col-md-6"><%= af.label "Country"%><%= af.select(:country, options_for_select([["United States","USA"], ["United Kingdom","UK"]])) %></div>
<div class="col-md-3"><button type="button" class="btn btn-primary btn-small btn-block">Location Certifications</button></div>
</div>
<div class="row" >
<div class="col-md-6"><%= af.label "Zipcode"%><%= af.text_field :zipcode %></div>
</div>
<div class="row" >
<div class="col-md-12"><h3>Tell us about you</h3></div>
<hr>
</div>
<div class="row">
<div class="col-md-6"><%= cf.label "First Name"%><%= cf.text_field :first_name %></div>
</div>
<div class="row">
<div class="col-md-6"><%= cf.label "Last Name"%><%= cf.text_field :last_name %></div>
</div>
<div class="row">
<div class="col-md-6"><%= cf.label "Title"%><%= cf.text_field :title %></div>
</div>
<div class="row">
<div class="col-md-2"><%= f.submit "Save Info", class: "btn btn-small btn-primary" %></div>
</div>
<% end %>
<% end %>
<% end%>
<% end %>
The things to notice are how the accepts_nested_attributes_for statements link up the associations, the need to add the appropriate model_attributes in each models attr_accessible statement, and how the form nests the address form (af) within the locations_form (lf) using :addresses_attributes.
Additionally, I had to remove foreign Key (company_id, address_id) validations from Location.rb because they caused the transaction to rollback prior to the Address or Company record being created (a prereq for creating a location)