Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add many to many record which having extra column

I have following Models

User

has_many :users_contacts
has_many :contacts, through: :users_contacts
accepts_nested_attributes_for :contacts, allow_destroy: true

Contact

has_many :users_contacts
has_many :users, through: :users_contacts
accepts_nested_attributes_for :users_contacts, allow_destroy: true

UsersContact

belongs_to :users
belongs_to :contacts

I am using following strong parameters

params.require(:user).permit(:id, :email, 
                      contacts_attributes: [:id, :first_name, :last_name, 
                      users_contacts_attributes: [:id, :contact_id, :user_id, :order]])

The problem i am facing is whenever i am updating users with users_contacts_attributes like {contact_id: 5, order: 5} It creates two records one with order: nil& other with order: 5 i am getting order in params.

I want following

  1. Don't want duplicate records to be created.

  2. Save the record in joining table with extra column i.e. order

My params are something like following

{"id"=>4,
 "email"=>"[email protected]",
 "contacts_attributes"=>
  [{"id"=>150,
    "first_name"=>"Pqr",
    "is_shareable"=>true,
    "users_contacts_attributes"=>[{"id"=>87, "user_id"=>4, "contact_id"=>150, "order"=>100}]
   },
   {"first_name"=>"Def",
    "is_shareable"=>true,
    "users_contacts_attributes"=>[{"user_id"=>4, "order"=>101}]
   }
  ]
}
like image 969
Salil Avatar asked Feb 14 '17 06:02

Salil


1 Answers

Not having seen your controllers or form, it's hard to determine what specifically is causing your problem. In addition to the comment from @surya above, I noticed that the two belongs_to statements in your UsersContact class should be singular, not plural (:user, :contact). Also, not sure you need to nest users_contacts_attributes in contacts_attributes which then makes it nested two deep for user when you can just have a separate accepts_nested_attributes_for :users_contacts in the User class.

Beyond that, you'll have to compare your code to the code I have provided below to see if you can find where your trouble lies.

Note: The form is very basic and I have modified some names to indicate more clearly when singular and plural are used and for readability.

Models

class User < ActiveRecord::Base
  has_many :user_contact_pairs, inverse_of: :user
  has_many :contacts, through: :user_contact_pairs
  accepts_nested_attributes_for :contacts, allow_destroy: true
  accepts_nested_attributes_for :user_contact_pairs, allow_destroy: true
end

class Contact < ActiveRecord::Base
  has_many :user_contact_pairs, inverse_of: :contact
  has_many :users, through: :user_contact_pairs
  accepts_nested_attributes_for :user_contact_pairs, allow_destroy: true
end

class UserContactPair < ActiveRecord::Base
  belongs_to :contact
  belongs_to :user
end

Controller

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  def index
    @users = User.all
  end

  def show
  end

  def new
    @user = User.new
  end

  def edit
    @user.user_contact_pairs.build(user_id: @user.id)
   end

  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to @user, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private

    def set_user
      @user = User.find(params[:id])
    end

    def user_params
      params.require(:user).
          permit(:email, :first_name, :last_name, :username,
                  contacts_attributes:
                  [:id, :first_name, :last_name],
                  user_contact_pairs_attributes:
                  [:id, :contact_id, :user_id, :order_number])
    end

end

View (users/index.html.erb)

<p id="notice"><%= notice %></p>

<h1>Listing Users</h1>

<table>
  <thead>
    <tr>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><%= 'Id' %></td>
      <td><%= 'First Name' %></td>
      <td><%= 'Last Name' %></td>
      <td><%= 'Username' %></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.id %></td>
        <td><%= user.first_name %></td>
        <td><%= user.last_name %></td>
        <td><%= user.username %></td>
        <td><%= link_to 'Show', user %></td>
        <td><%= link_to 'Edit', edit_user_path(user) %></td>
        <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

View (users/edit.html.erb)

<h1>Editing User</h1>

<%= render 'form' %>

<%= link_to 'Show', @user %> |
<%= link_to 'Back', users_path %>

View (users/_form.html.erb)

<%= form_for(@user) do |user_form| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% @user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= user_form.label :email %>
    <%= user_form.email_field :email %><br>
    <%= user_form.label :first_name %>
    <%= user_form.text_field :first_name %><br>
    <%= user_form.label :last_name %>
    <%= user_form.text_field :last_name %><br>
    <%= user_form.label :username %>
    <%= user_form.text_field :username %><br>
  </div>

  <h2>User Contact Pair</h2>
  <%= user_form.fields_for :user_contact_pairs do |ucp_fields| %>
    <div class="user-contact-pair">
      <div class="field">
        <%= ucp_fields.label :contact_id %>
        <%= ucp_fields.number_field :contact_id %><br>
        <%= ucp_fields.label :order_number %>
        <%= ucp_fields.number_field :order_number %><br>
      </div>
    </div>
  <% end %>

  <div class="actions">
    <%= user_form.submit %>
  </div>
<% end %>

Migrations

class CreateUser < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :email, index: { unique: true }
      t.string :first_name
      t.string :last_name
      t.string :username, index: { unique: true }
      t.timestamps null: false
    end
  end
end

class CreateContact < ActiveRecord::Migration
  def change
    create_table :contacts do |t|
      t.string :first_name
      t.string :last_name
      t.timestamps null: false
    end
  end
end

class CreateUserContactPair < ActiveRecord::Migration
  def change
    create_table :user_contact_pairs do |t|
      t.integer :contact_id
      t.integer :user_id
      t.integer :order_number
      t.timestamps null: false
    end
  end
end

DB Seed File

User.create(email: '[email protected]',            first_name: 'Lisa',    last_name: 'Hawkins',  username: 'lhawkinsa')
User.create(email: '[email protected]',            first_name: 'Helen',   last_name: 'Taylor',   username: 'htaylorb')
User.create(email: '[email protected]',           first_name: 'Gregory', last_name: 'Taylor',   username: 'gtaylorc')
User.create(email: '[email protected]',        first_name: 'Henry',   last_name: 'Lane',     username: 'hlaned')
User.create(email: '[email protected]', first_name: 'Harry',   last_name: 'Phillips', username: 'hphillipse')
User.create(email: '[email protected]',           first_name: 'Jeffrey', last_name: 'Gonzales', username: 'jgonzalesf')
User.create(email: '[email protected]',           first_name: 'Lori',    last_name: 'James',    username: 'ljamesg')
User.create(email: '[email protected]',               first_name: 'Roger',   last_name: 'Hill',     username: 'rhillh')
User.create(email: '[email protected]',     first_name: 'Raymond', last_name: 'Harvey',   username: 'rharveyi')
User.create(email: '[email protected]',              first_name: 'Stephen', last_name: 'Perry',    username: 'sperryj')

Contact.create(first_name: 'Louis', last_name: 'Harris')
Contact.create(first_name: 'Fred', last_name: 'Adams')
Contact.create(first_name: 'David', last_name: 'Lane')
Contact.create(first_name: 'Kevin', last_name: 'Ryan')
Contact.create(first_name: 'Samuel', last_name: 'Jones')

Rails Console (after two user updates via form)

Running via Spring preloader in process 30656
Loading development environment (Rails 4.2.7.1)

    2.3.3 :001 > UserContactPair.all
UserContactPair Load (0.7ms)  SELECT "user_contact_pairs".* FROM "user_contact_pairs"
=> #<ActiveRecord::Relation [#<UserContactPair id: 1, contact_id: 1, user_id: 1, order_number: 1, created_at: "2017-02-14 09:05:31", updated_at: "2017-02-14 09:05:31">, #<UserContactPair id: 2, contact_id: 4, user_id: 1, order_number: 4, created_at: "2017-02-14 09:05:50", updated_at: "2017-02-14 09:05:50">]> 

    2.3.3 :002 > 

UPDATE (in response to comment):

Here is the updated code that should do what you are asking. You should look to add validations that confirm presence and uniqueness where necessary in the above code and within this additional code. You many also want to sort the Contact collection used to populate the form select drop-down.

(Note: Due to the fact that the use of nested attributes causes the addition of number keys (e.g. ["0"]) within param hashes, which vary depending upon the number of associated records of those nested attribute classes an object has, I've added the hashie gem to the gem file, which provides the ability to find deeply nested keys within a hash via .deep_find, enabling direct access to the desired keys, bypassing these varying number keys.)

Models (added method to Contact class for form select drop-down)

  def last_name_first
    self.last_name + ', ' + self.first_name
  end

Controller (edit and update actions updated)

  def edit
    @contacts = Contact.all
    @user.contacts.build
    @user.user_contact_pairs.build
  end

  def update
    @user = User.find(params[:id])

    user_only_params = { email: params[:user][:email], first_name: params[:user][:first_name], last_name: params[:user][:last_name], username: params[:user][:username] }
    contact_params = params[:user][:contacts_attributes]
    user_contact_pair_params = params[:user][:user_contact_pairs_attributes]

    contact_params.extend(Hashie::Extensions::DeepFind)
    contact_first_name = contact_params.deep_find(:first_name)
    contact_last_name = contact_params.deep_find(:last_name)

    user_contact_pair_params.extend(Hashie::Extensions::DeepFind)
    user_contact_pair_contact_id = user_contact_pair_params.deep_find(:contact_id)
    user_contact_pair_order_number = user_contact_pair_params.deep_find(:order_number)

    @user.assign_attributes(user_only_params)

    if user_contact_pair_order_number != ''
      if contact_first_name != '' && contact_last_name != ''
        @contact = Contact.create(first_name: contact_first_name, last_name: contact_last_name)
        @user.user_contact_pairs.new(contact_id: @contact.id, order_number: user_contact_pair_order_number.to_i)
      elsif user_contact_pair_contact_id != ''
        @user.user_contact_pairs.new(contact_id: user_contact_pair_contact_id.to_i, order_number: user_contact_pair_order_number.to_i)
      end
    end
    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }
      else
        @contact.destroy if @contact.exists?
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

View (users/_form.html.erb)

<%= form_for(@user) do |user_form| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% @user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="user">
    <h2 style="margin-bottom: 4px">User</h2>
    <%= user_form.label :email %><br>
    <%= user_form.email_field :email %><br><br>
    <%= user_form.label :first_name %><br>
    <%= user_form.text_field :first_name %><br><br>
    <%= user_form.label :last_name %><br>
    <%= user_form.text_field :last_name %><br><br>
    <%= user_form.label :username %><br>
    <%= user_form.text_field :username %><br><br>
  </div>

  <%= user_form.fields_for :user_contact_pairs do |ucp_fields| %>
    <% if ucp_fields.object.new_record? %>
      <h2 style="margin-bottom: 4px">Use Existing Contact</h2>
      <%= ucp_fields.collection_select(:contact_id, @contacts, :id, :last_name_first, prompt: "Select...") %><br><br>
    <% end %>
  <% end %>

  <%= user_form.fields_for :contacts do |contact_fields| %>
    <% if contact_fields.object.new_record? %>
      <h2 style="margin-bottom: 4px">Or Create New Contact</h2>
      <%= contact_fields.label 'First Name' %><br>
      <%= contact_fields.text_field :first_name %><br><br>
      <%= contact_fields.label 'Last Name' %><br>
      <%= contact_fields.text_field :last_name %><br><br>
    <% end %>
  <% end %>

  <%= user_form.fields_for :user_contact_pairs do |ucp_fields| %>
    <% if ucp_fields.object.new_record? %>
      <h2 style="margin-bottom: 4px">Order</h2>
      <%= ucp_fields.label 'Order Number' %><br>
      <%= ucp_fields.number_field :order_number %><br><br>
    <% end %>
  <% end %>

  <div class="actions">
    <%= user_form.submit %>
  </div>

<% end %>
like image 122
eggroll Avatar answered Sep 23 '22 17:09

eggroll