Search code examples
ruby-on-rails-3controllermodel-associations

Rails 3 Controller: Saving associated records from a composite form


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.


Solution

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