Rails 3: nested_form, collection_select, accepts_n

2019-03-21 02:32发布

问题:

Update: answered here.

There are lots of good questions and answers here and on the interweb about getting nested_form, collection_select, accepts_nested_attributes_for and fields_for to play nicely together, but I'm still stumped. Thanks in advance if you can help me.

Aim: To generate a new isbn record. An isbn can have many contributors. I am successfully using the ryanb nested_form gem to dynamically produce new contributor fields on a form, as required. One of these fields uses a collection_select drop down of all the name records in Contributor. When the new record is created, the many contributor ids need to be written to the join table (contributors_isbns).

I have got bits of this working, but only to the point where I can save a single contributor ID to the new record in the isbns table. I can't seem to get anywhere in writing any data to the join table.

I have three models. Contributors and Isbns have a many to many relationship, which I've done using has_many :through. An isbn can have many contributors, and a contributor can have many isbns. They are joined via contributors_isbn.

isbn.rb

  attr_accessible               :contributor_id
  has_many                      :contributors, :through => :contributors_isbns
  has_many                      :contributors_isbns
  accepts_nested_attributes_for :contributors
  accepts_nested_attributes_for :contributors_isbns

contributor.rb

  attr_accessible               :isbn_id
  has_many                      :contributors_isbns
  has_many                      :isbns, :through => :contributors_isbns
  accepts_nested_attributes_for :isbns

contributors_isbn.rb

  class ContributorsIsbn
  attr_accessible               :isbn_id, :contributor_id
  belongs_to                    :isbn
  belongs_to                    :contributor
  accepts_nested_attributes_for :contributors

In the isbns controller:

 def new
    @isbn  = Isbn.new
    @title = "Create new ISBN"
    1.times {@isbn.contributors.build}
    @isbn.contributors_isbns.build.build_contributor
  end

(obviously I can't make my mind up on which build method to use.)

In the isbns new.html.erb view:

<%= nested_form_for @isbn, :validate => false do |f| %>
<h1>Create new ISBN</h1>
<%= render 'shared/error_messages', :object => f.object %>
<%= render 'fields', :f => f %>
  <div class="actions">
    <%= f.submit "Create" %>
  </div>  

<% end %>

In the _fields partial, a version with a very plain text_field:

<%= field_set_tag 'Contributor' do %>
<%= f.link_to_add "Add Additional Contributor", :contributors %>
<li>
<%= f.label 'Contributor Sequence Number' %>
<%= f.text_field :descriptivedetail_contributor_sequencenumber%>
</li>

<%= f.fields_for :contributors_isbns, :validate => false do |contrib| %>
<li>
<%= contrib.label :id, 'contributors_isbns id' %>
<%= contrib.text_field :id %>
</li>
<% end %>

<li>
<%= f.label 'Contributor Role' %>
<%= f.text_field :descriptivedetail_contributor_contributorrole  %>
</li>

<% end %>

And here, a fancier version which doesn't work either:

<%= f.fields_for :contributors_isbns, :validate => false do |contributors| %>
<li>
<%= f.label :personnameinverted, 'Contributor Name' %>
<%= f.collection_select(:contributor_id,  Contributor.all, :id, :personnameinverted ) %>
</li>
<% end %>

This code uses the answer from here. Both result in a 'Missing block" error on the nested_form_for @isbn line.

Thanks so much again in advance.

Update: here is some info about the nested_form gem which might come in handy for looking at this sort of problem. And here's a [2009 but still relevant post][4] on accepts_nested_attributes_for.

Update 2: well, here's a thing. I've been poking around on a cut-down version of this in two different models, not using collection_select or has_many through, but just on a simple belongs_to / has_many association. The parent model is Contract and the child model is Istc. I couldn't even create a record through the nested form. However, after looking in the stack and googling the error message "Warning. Can't mass-assign protected attributes" I've just added :istcs_attributes to my :attr_accessible line and now I can add records. A rather crucial bit missing, and a case of RTFM, as it's right there in the gem readme. I'll update later to see if this works on the more complicated has_many through association.

Update 4: [Here][5] is another useful post about how to deal with a nil record error message.

Update 5: Slight detour - When I dynamically added a new set of fields to the form, one one of the child records was being created. Duh - I had the "Add" link inside the child forms area. Here's the before:

<%= f.fields_for :istcs do |istc_form| %>
<h4> Istc</h4>
<%= istc_form.label "istc name" %>
<%= istc_form.text_field :title_title_text %>
<%= istc_form.link_to_remove "[-] Remove this istc"%>
<%= f.link_to_add "[+] Add an istc", :istcs  %>
<% end %>

and here's the after:

<%= f.fields_for :istcs do |istc_form| %>
<h4> Istc</h4>
<%= istc_form.label "istc name" %>
<%= istc_form.text_field :title_title_text %>
<%= istc_form.link_to_remove "[-] Remove this istc"%>
<% end %>
<%= f.link_to_add "[+] Add an istc", :istcs  %>

Update 6, post-answer:

Oh noes. The collection_select isn't working. It's adding new contributor records, not using an existing one from the contributor model. Someone else had this problem too. Any ideas?

回答1:

Huzzah! Here's the code which makes all this work. Bit verbose but didn't want to leave anything out. My main learnings:

  • you need to make the child attributes attr_accessible in the parent model

  • you need to make the parent and child ids attr_accessible in the join table model

  • it makes life easier if you build at least one child instance in the parent controller.

contributor.rb model

class Contributor < ActiveRecord::Base
  attr_accessible  #nothing relevant 
  has_many :contributors_isbns
  has_many :isbns, :through => :contributors_isbns

isbn.rb model

class Isbn < ActiveRecord::Base
  attr_accessible :contributors_attributes, :contributor_id, :istc_id #etc
  belongs_to  :istc
  has_many   :contributors, :through => :contributors_isbns
  has_many   :contributors_isbns
  accepts_nested_attributes_for :contributors #if you omit this you get a missing block error

contributors_isbn model

class ContributorsIsbn < ActiveRecord::Base
  belongs_to :isbn
  belongs_to :contributor
  attr_accessible :isbn_id, :contributor_id

isbn controller

 def new
    @isbn  = Isbn.new
    @title = "Create new ISBN"
    1.times {@isbn.contributors.build}
  end

new.html.erb

<td class="main">
<%= nested_form_for @isbn, :validate => false do |f| %>
<h1>Create new ISBN</h1>
<%= render 'shared/error_messages', :object => f.object %>
<%= render 'fields', :f => f %>
  <div class="actions">
    <%= f.submit "Create" %>
  </div>  

<% end %>

_fields.html.erb

<%= field_set_tag 'Identifier Details' do %>

<li>
<%= f.label 'Title prefix' %>
<%= f.text_field :descriptivedetail_titledetail_titleelement_titleprefix %>
</li>
<li>
<%= f.label 'Title without prefix' %>
<%= f.text_field :descriptivedetail_titledetail_titleelement_titlewithoutprefix %>
</li>
<li>
<%= f.label 'ISTC' %>
<%= f.collection_select(:istc_id, Istc.all, :id, :title_title_text, :prompt => true) %>
</li>

<% end %>


<%= field_set_tag 'Contributor' do %>
<li>
<%= f.label 'Contributor Sequence Number' %>
<%= f.text_field :descriptivedetail_contributor_sequencenumber%>
</li>

<%= f.fields_for :contributors, :validate => false do |contributor_form| %>
<li>
<%= contributor_form.label :personnameinverted, 'Contributor Name' %>
<%= contributor_form.collection_select(:isbn_id, Contributor.all, :id, :personnameinverted ) %>
</li>
<%= contributor_form.link_to_remove "[-] Remove this contributor"%>
<% end %>
<%= f.link_to_add "[+] Add a contributor", :contributors  %>


<li>
<%= f.label 'Contributor Role' %>
<%= f.text_field :descriptivedetail_contributor_contributorrole  %>
</li>

<% end %>