Jose A
Jose A

Reputation: 11077

Updating many to many using a Nested form_for in rails

This is a very common problem, and there are dozens of blog posts that cover this. I just don't know what I'm doing wrong. Sorry for the long post.

I have a Mono-Transitive many-to-many relationship.

A session may have multiple captionists. A captionist may have multiple sessions. The many-to-many relationship is done by "Membership".

class Session < ApplicationRecord

  # This will tell to delete all the memberships
  # in case the current session is deleted.
  has_many :memberships, dependent: :destroy
  # Added this, see:
  # https://www.sitepoint.com/master-many-to-many-associations-with-activerecord/
  has_many :captionists, through: :memberships 

  # https://www.sitepoint.com/complex-rails-forms-with-nested-attributes/
  # Allows us to delete the membersihps if not included. 
  accepts_nested_attributes_for :memberships, :allow_destroy => true

  validates :uuid, presence: true, uniqueness: true
  validates :name, presence: true, uniqueness: false
  validates :url, presence: true, uniqueness: true
  validates :passcode, presence: true, uniqueness: true
  validates :shortcode, presence: true, uniqueness: true

end

class Captionist < ApplicationRecord

  has_many :memberships

  # Added this, see:
  # https://www.sitepoint.com/master-many-to-many-associations-with-activerecord/
  has_many :sessions, through: :memberships

  validates :uuid, presence: true, uniqueness: true
  validates :full_name, presence: true, uniqueness: false
  validates :access_token, presence: true, uniqueness: true
  validates :passcode, presence: true, uniqueness: false

end

# Mono Transitive Association
# https://www.sitepoint.com/master-many-to-many-associations-with-activerecord/

class Membership < ApplicationRecord

  belongs_to :session
  belongs_to :captionist

  validates :uuid, presence: true, uniqueness: true
  validates :hostname, presence: true, uniqueness: false
  validates :web_port, presence: true, uniqueness: false
  validates :tcp_port, presence: true, uniqueness: false

end

Here's my session_controller (It's under an admin namespace)

module Admin
  class SessionsController < Admin::BaseAdminController
    def index
      # List of sessions
      # Careful on not to iterate through everything, as Rails will interpret it 
      # as all. 
      # Default is #25
      # This uses the kaminari gem. 
      # https://github.com/kaminari/kaminari
      @sessions = Session.page(params[:page] || 1)
      @page_count = @sessions.total_pages
    end

    def edit
      @session = Session.includes(:memberships).includes(:captionists).find(params[:id])
      if !@session 
        raise ActionController::RoutingError.new('Not Found')
      end
    end

    def update
      @session = Session.find(params[:id])
      if @session.update_attributes(session_params)
        respond_to do |format|
          format.html { redirect_to edit_admin_session_path , notice: 'Session was successfully updated.' }        
        end
        return
      end
      render 'edit'
    end

    def session_params
      return params.require(:session).permit(:name, :passcode, :shortcode, 
       membership_attributes: [:id, :hostname, :tag_teaming, :web_port, :tcp_port, :allowing_connections, :_destroy])
    end

  end
end 

For last, but not least, the .erb file:

 <h1>Editting!!</h1>

<%= link_to 'Go Back To list', admin_sessions_path %>

<%= form_for :session, method: :patch,  class:'container' do |f| %>
  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: 'form-control' %><br />
  </div>

  <div class="form-group">
    <%= f.label :passcode %>
    <%= f.text_field :passcode, class: 'form-control' %><br />
  </div>


  <%= f.label :shortcode %>
  <%= f.text_field :shortcode, class: 'form-control' %><br />


  <%= f.label :memberships %>
  <table class="table table-striped table-hover">
  <thead class="thead-dark">
    <tr>
    <th scope="col"> Allowing Connections </th>
    <th scope="col">Tag Teaming</th>
    <th scope="col">Hostname</th>
    <th scope="col">Web Port</th>
    <th scope="col">TCP Port</th>
    <th scope="col">Captionist Name </th>
    <th scope="col">Action</th>
    </tr>
  </thead>

<!-- HERE'S THE PART THAT is giving me the problem -->
          <% @session.memberships.map do |membership| %>

      <%= f.fields_for membership do |ff| %>
        <tr class="nested-fields">
          <%= ff.hidden_field :id %>
          <td>
          <%= ff.label :allowing_connections %>
          <%= ff.check_box :allowing_connections %>
          </td>
          <td>
          <%= ff.label :tag_teaming %>
          <%= ff.check_box :tag_teaming %>
          </td>
          <td>
          <%= ff.label :hostname %>
          <%= ff.text_field :hostname %>
          </td>
          <td>
          <%= ff.label :web_port %>
          <%= ff.text_field :web_port %>
          </td>
          <td>
          <%= ff.label :tcp_port %>
          <%= ff.text_field :tcp_port %>
          </td>
          <td>
          <%= membership.captionist.full_name %>

          </td>
          <td>
            <%= ff.check_box :_destroy %>

          </td>
        </tr>
        <% end %>
      <% end %>
  </table>
  <%= f.submit 'Update', class: 'btn btn-primary' %>
<% end %>

Here are the params that are sent:

 {"name"=>"Session Correct name", "passcode"=>"A Passcode", "shortcode"=>"A Shortcode", "membership"=>{"id"=>"106", "allowing_connections"=>"0", "tag_teaming"=>"0", "hostname"=>"delete hostname", "web_port"=>"80", "tcp_port"=>"3001", "_destroy"=>"0"}} permitted: false>

Here's how I'm whitelisting the params:

return params.require(:session).permit(:name, :passcode, :shortcode, 
       membership_attributes: [:id, :hostname, :tag_teaming, :web_port, :tcp_port, :allowing_connections, :_destroy])

What are the problems?

1) Right now, if I submit the form I get: unpermitted_params=["membership"] at the end of the console 2) I've checked that the database has been correctly migrated. The foreign keys are properly set. I can navigate through the properties without any problems. What I did do was to add the relationships has_many after I performed the migrations. I don't know if I need to add some sort of migrations for the update to work. 3) I have tried replacing

<%= form_for :session, method: :patch,  class:'container' do |f| %>

for: <%= form_for @session, method: :patch, class:'container' do |f| %>

But I receive an error:

Could not find a valid mapping for #

4) I've also tried removing <% @session.memberships.map do |membership| %>, but I wouldn't have the fields populated. Another problem that I see is that the name of the inputs are all the same. Whenever I send the parameters to update, it's only the last of the nested portion that gets reflected.

Here's the response from the server (params[:session]):

Parameters {"name"=>"Session Correct name", "passcode"=>"A Passcode", "shortcode"=>"A Shortcode", "membership"=>{"id"=>"106", "allowing_connections"=>"0", "tag_teaming"=>"0", "hostname"=>"delete hostname", "web_port"=>"80", "tcp_port"=>"3001", "_destroy"=>"0"}} permitted: false>

Here's an example of the visuals: enter image description here

he part that is not nested gets updated.

Any ideas?

Thanks!

Edit (April 9th, 2018): Added the params sent (That I didn't have it here). Fixed an issue with the formatting and the HTML.

Upvotes: 0

Views: 487

Answers (2)

Pablo
Pablo

Reputation: 3005

You don't need:

<% @session.memberships.map do |membership| %>

  <%= f.fields_for membership do |ff| %>

You just need:

<%= f.fields_for @session.memberships do |ff| %>

Rails will iterate through all memberships in the session and will put all fields inside memberships_attributes.

Upvotes: 1

Petercopter
Petercopter

Reputation: 1258

I'm pretty sure this is just a pluralization problem. I'm poking around in code that's similar to yours here, and it looks like you want memberships_attributes in your strong params.

Check also the HTML that is being generated, and make sure the keys you're permitting match up.

I would recommend putting a binding.pry or whatever you like to debug with inside the controller, and try saving the object with the params that are coming in.

Upvotes: 1

Related Questions