Trying to use accepts_nested_attributes_for and has_and_belongs_to_many but the join table is not being populated
Asked Answered
H

1

12

I am learning RoR and trying to use accepts_nested_attributes_for and has_and_belongs_to_many to submit information that would traditionally be two forms. I have read on some sites they are compatible, some sites they aren't compatible, and some sites don't know. As reference, I am using Rails 2.3.4. I tried modeling my solution from the Ryan's Scraps tutorial on nested models

From what I have tried to debug, it seems that I have two problems but I am not sure why.

  1. When I submit a form with nested models, only part of the nested model information is posted. I only get the first field, not the "n" others the user may have selected
  2. Of the single field that gets posted, there aren't any rows inserted into the join table that I created for the HABTM relationship.

Here is a piece of code and the corresponding logs for my insertion attempt:

Attorney Model:

class Attorney < ActiveRecord::Base 
  has_and_belongs_to_many :associations
  accepts_nested_attributes_for :associations, :reject_if => proc { |a| a['name'].blank? }
end

Association Model:

class Association < ActiveRecord::Base
  has_and_belongs_to_many :attorneys
  accepts_nested_attributes_for :attorneys
  validates_presence_of :name, :message => "Please enter an association name."
end

Attorneys Controller:

def new
  @attorney = Attorney.new
  @attorney.associations.build

  respond_to do |format|
    format.html # new.html.erb
    format.xml  { render :xml => @attorney }
  end
end

def create
  @attorney = Attorney.new(params[:attorney])

  respond_to do |format|
    if @attorney.save
      flash[:notice] = 'Attorney was successfully created.'
      format.html { redirect_to(@attorney) }
      format.xml  { render :xml => @attorney, :status => :created, :location => @attorney }
    else
      format.html { render :action => "new" }
      format.xml  { render :xml => @attorney.errors, :status => :unprocessable_entity }
    end
  end
end

Attorney's New View:

<% form_for(@attorney, :html => {:multipart => true}) do |f| %>
  <%= f.error_messages %>
 <%= f.label :"First name" %> 
 <%= f.text_field :firstname %><br>

 <%= f.label :"Last Name" %> 
 <%= f.text_field :lastname %><br>

 <%= f.label :"Attorney Type" %> 
 <%= f.collection_select :member_type_id, MemberType.all, :id, :name %><br>

 <%= f.text_area :bio, :cols => 70, :rows => 20 %><br><br>

 <%= f.label :"Attorney Location" %> 
 <%= f.collection_select :office_location_id, OfficeLocation.all, :id, :location %><br>

 <div id="associations">
      <%= render :partial => 'shared/membership' %>
 </div>
 <%= add_association_link "Add Association" %>
    <%= f.submit 'Create' %>
<% end %>

Membership Partial:

<div class="association">
  <% fields_for :associations do |assoc_form| %>
    <%= assoc_form.collection_select(:association_id, Association.find(:all), :id, :name, :include_blank => true) %>

<%= link_to_function "remove", "$(this).up('.association').remove()" %> <%= link_to 'New Association', new_association_path %> <% end %>

Attorney Helper Link:

def add_association_link(name)
  link_to_function name do |page|
    page.insert_html :bottom, :associations, :partial => 'shared/membership', :object => AssociationsAttorneys.new
  end
end

Join Table Migration:

class CreateAssociationsAttorneys < ActiveRecord::Migration
  def self.up
    create_table :associations_attorneys do |t|
      t.references :attorney, :null => false
      t.references :association, :null => false
      t.timestamps
    end
  end

  def self.down
    drop_table :associations_attorneys
  end
end

Log capture:

    Processing AttorneysController#new (for 127.0.0.1 at 2009-12-04 08:16:19) [GET]
Rendering template within layouts/default
Rendering attorneys/new
  [4;35;1mMemberType Load (0.4ms)[0m   [0mSELECT * FROM "member_types" [0m
  [4;36;1mOfficeLocation Load (18.6ms)[0m   [0;1mSELECT * FROM "office_locations" [0m
  [4;35;1mAssociation Load (0.6ms)[0m   [0mSELECT * FROM "associations" [0m
Rendered shared/_membership (3.5ms)
  [4;36;1mCACHE (0.0ms)[0m   [0;1mSELECT * FROM "associations" [0m
Rendered shared/_membership (1.5ms)
Rendered shared/_nav (0.6ms)
Rendered shared/_footer (0.1ms)
Completed in 149ms (View: 114, DB: 20) | 200 OK [http://localhost/attorneys/new]

Processing ApplicationController#index (for 127.0.0.1 at 2009-12-04 08:16:19) [GET]

Processing AttorneysController#create (for 127.0.0.1 at 2009-12-04 08:16:57) [POST]
  Parameters: {"commit"=>"Create", "authenticity_token"=>"Jh7aMCcOY7jUu/D1YtiCswg2n6iwqnS98VnVn46psp0=", "associations"=>{"association_id"=>"3"}, "attorney"=>{"birthstate"=>"Alabama", "office_location_id"=>"1", "birthdate"=>"December 3, 2009", "birthcity"=>"Test", "middlename"=>"Test", "lastname"=>"Testing", "image_temp"=>"", "member_type_id"=>"2", "firstname"=>"Test", "bio"=>"testing testing testing", "suffix"=>"", "email"=>"[email protected]"}}
  [4;35;1mAttorney Load (15.6ms)[0m   [0mSELECT "attorneys".id FROM "attorneys" WHERE ("attorneys"."email" = '[email protected]') LIMIT 1[0m
  [4;36;1mAttorney Create (0.8ms)[0m   [0;1mINSERT INTO "attorneys" ("birthstate", "created_at", "birthdate", "office_location_id", "birthcity", "updated_at", "middlename", "lastname", "firstname", "member_type_id", "suffix", "bio", "image", "email") VALUES('Alabama', '2009-12-04 15:16:57', 'December 3, 2009', 1, 'Test', '2009-12-04 15:16:57', 'Test', 'Testing', 'Test', 2, '', 'testing testing testing', NULL, '[email protected]')[0m
Redirected to http://localhost:3000/attorneys/11
Completed in 150ms (DB: 16) | 302 Found [http://localhost/attorneys]

I can see that associations"=>{"association_id"=>"3"} it is only getting the last of the multiple associations that I had for the particular person and it isn't creating any entries into the join table. Where might my code have gone wrong?

Halfhour answered 4/12, 2009 at 15:21 Comment(0)
I
29

You two have problems here, unfortunately one of them is masked by the other.

Both problems stem from this part of the view:

<div class="association">
  <% fields_for :associations do |assoc_form| %>
    <%= assoc_form.collection_select(:association_id, Association.find(:all),
      :id, :name, :include_blank => true) %>

Problem 1: You've misunderstood what accept_nested_fields_for does.

accepts_nested_fields_for is used to create and modify related objects in a form. It can be used to populate join table, which is kind of what you're trying to do. However, using accepts_nested_fields_for to populate the join table is impossible with a HABTM relationship. A good use of accepts_nested_fields_for would be if you wanted to create a new Association that will be linked with the new Attorney. Or if you had a rich join model that required additional information for each record.

Problem 2: You're not linking the fields in this form to the attorney form. Which is necessary to use accepts_nested_fields_for.

We've already established that accepts_nested_fields_for is not what you need to accomplish this, but, you're still not associating the select association_id field with the form. Which is why params[associations][association_id] was set and not params[attorney][associations][association_id].

Problem 3: The form structure is all wrong for what it looks like you're trying to accomplish.

There's a too much that needs correcting for me to give a proper break down. You're better off checking out the complex-forms-example repository. It's a working example of accepts_nested_attributes_for, it doesn't deal with any HABTM relationships, but it should teach you every thing you need to know. The corrected code below is 90 % of what you need. The complex-forms-examples linked above will teach you what you need to know to fill in the blanks that are add_association_link and create_association_link.

The correction involves the following steps:

  1. Create a join model, and change the relationship to a has many through one, accepting nested attributes on the join model.
  2. Make a minor adjustment in the controller, in terms of building things.
  3. Pass the form builder object to the partial.
  4. Rewrite the form in the partial so it is focuses on the newly created join model.

You can accomplish this with the following changes.

class Attorney < ActiveRecord::Base
  has_many :attorney_associations
  has_many :associations, :through => :attorney_associations

  accepts_nested_attributes_for :attorney_associations, :reject_if => proc { |a| 
     a['association_id'].blank? }
  accepts_nested_attributes_for :associations, :reject_if => proc {|a|
     a['name'].blank?}
end

class AttorneyAssociations < ActiveRecord::Base
  belongs_to :attorney
  belongs_to :association
end

Attorney Controller:

def new
  @attorney = Attorney.new
  @attorney.associations.build
  @attorney.attorney_associations.build


  respond_to do |format|
    format.html # new.html.erb
    format.xml  { render :xml => @attorney }
  end
end

New Attorney View:

<% form_for(@attorney, :html => {:multipart => true}) do |f| %>
  <%= f.error_messages %>
 <%= f.label :"First name" %> 
 <%= f.text_field :firstname %><br>

 <%= f.label :"Last Name" %> 
 <%= f.text_field :lastname %><br>

 <%= f.label :"Attorney Type" %> 
 <%= f.collection_select :member_type_id, MemberType.all, :id, :name %><br>

 <%= f.text_area :bio, :cols => 70, :rows => 20 %><br><br>

 <%= f.label :"Attorney Location" %> 
 <%= f.collection_select :office_location_id, OfficeLocation.all, :id, :location %><br>

 <div id="associations">
   <% f.fields_for :attorney_association do |aa_form| %>
     <%= render :partial => 'attorney_association', :locals => {:f => aa_form} %>
   <% end %>
   <%= add_association_link "Add Another Existing Association" %>
   <% f.fields_for :associations do |assoc_form| %>
     <%= render :partial => 'attorney', :locals => {:f => assoc_form} %>       
   <%= create_association_link, "Create a New Association for this Attorney" %>
 </div>



 <%= f.submit 'Create' %>
<% end %>

I'm assuming that add_association_link is a javascript helper that creates a link to clone an empty instance of what was the Membership partial. create_association_link is a place holder for a similar helper that will add a partial for a new association.

Attorney Association Partial:

  <div class="attorney_association">
    <%= f.collection_select(:association_id, Association.find(:all),
      :id, :name, :include_blank => true) %>
    <%= link_to_function "remove", "$(this).up('.attorney_association').remove()" %>
  </div>

Association Partial:

  <div class="association">
    <%= f.label_for :name %>
    <%= f.text_field :name %>
    <%= link_to_function "remove", "$(this).up('.attorney_association').remove()" %>
  </div>
Incoherence answered 4/12, 2009 at 17:59 Comment(5)
By changing the view to pass in the form 'f', I am now getting the error: undefined method `association_id' for #<Association:0x1031aff28> on this line in the partial: <%= assoc_form.collection_select(:association_id, Association.find(:all), :id, :name, :include_blank => true) %>. Any ideas?Halfhour
Looks like I overlooked a number of other problems you had going on. I've updated the solution to address those too.Incoherence
so i have worked on this for several weeks and i am still having problems. when it tries to render the attorney_association partial, its complaining about not knowing what 'f' is...i thought the form object was passed in with the :locals hash?Halfhour
That complex-forms-example repository does have a HABTM using nested attributes. But of course, it doesn't work.Onepiece
Sh*t. One should change framework if implementing this case takes "several weeks", but I guess what that what "Ruby black magic" is. Unfortunately I have to change an existing project, and I'm getting less fund of Rails every day. The model and view is all to mashed together. However, the example above is great - thanks for the help!Dent

© 2022 - 2024 — McMap. All rights reserved.