s_dolan
s_dolan

Reputation: 1276

Rails 4.2 - how to build a has_many, through: association with collection_select

I'm currently using Rails 4.2 with these models:

class User < ActiveRecord::Base
  has_many :user_accessories, dependent: :destroy
  accepts_nested_attributes_for :user_accessories,
    reject_if: :all_blank,
    allow_destroy: true
  has_many :accessories, through: :user_accessories
end

class UserAccessory < ActiveRecord::Base
  belongs_to :user,      required: true
  belongs_to :accessory, required: true
end

class Accessory < ActiveRecord::Base
  has_many :user_accessories, dependent: :destroy
  has_many :users, through: :user_accessories
end

This is a simple many-to-many relationship, where a user can "own" many accessories, and an accessory can be "owned" by many users.

I've set up my routes in the following way:

resources :users, only: [ :index, :show, :edit, :update, :accessories ] do
  member do
    get 'accessories'
  end
end

This way, a user can go to their "accessories" path (/users/1/accessories) to see a list of their accessories, as well as to add new accessory associations to their account.

Here's the view that renders for the accessories path:

<table>
  <thead>
    <tr>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
    <%= render @accessories %>
  </tbody>
</table>

<%= form_for( @user ) do |f| %>
  <%= f.collection_select( 
    :accessory_ids, 
    Accessory.all, 
    :id, 
    :name, { include_hidden: false }, { multiple: true } ) 
  %>

  <%= f.button "Add Equipment" %>
<% end %>

This just renders a table of current accessories, and then builds a collection_select box where you can choose multiple accessories to add. I've simplified this by removing Bootstrap styling.

Finally, here are the relevant controller actions:

def update
  @user = User.find( params[:id] )
  if( @user.update_attributes( update_params ) )
    redirect_to @user
  else
    render 'edit'
  end
end

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

private 
  def update_params 
    params.require( :user ).permit( 
      :first_name, 
      :last_name, 
      :region_id, 
      :country, 
      :profile_image, 
      :remove_profile_image, 
      accessory_ids: [:id], 
    )
  end

Now the Problems:

  1. Submitting the form when I already have a value manually entered in the database actually just deletes all currently stored values and does not add any new ones:

    Started PATCH "/users/nuggles" for 127.0.0.1 at 2015-02-18 13:31:50 -0500
    
    Processing by UsersController#update as HTML
    
    Parameters: {
      "utf8"=>"✓",····
      "authenticity_token"=>"my_token",·
      "user"=>{
        "accessory_ids"=>["5"]
      },· 
      "button"=>"",·
      "id"=>"nuggles"
    } 
    
    User Load (40.0ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1  ORDER BY "users"."id" ASC LIMIT 1  [["id", 4]] 
    
    User Load (36.4ms)  SELECT  "users".* FROM "users" WHERE "users"."slug" = $1  ORDER BY "users"."id" ASC LIMIT 1  [["slug", "nuggles"]]·
    
    (33.5ms)  BEGIN
    
    Accessory Load (34.1ms)  SELECT "accessories".* FROM "accessories" INNER JOIN "user_accessories" ON "accessories"."id" = "user_accessories"."accessory_id" WHERE "user_accessories"."user_id" = $1  [["user_id", 4]]
    
    **SQL (34.1ms)  DELETE FROM "user_accessories" WHERE "user_accessories"."user_id" = $1 AND "user_accessories"."accessory_id" IN (1, 2, 3)  [["user_id", 4]]**
    
    User Exists (37.7ms)  SELECT  1 AS one FROM "users" WHERE (LOWER("users"."username") = LOWER('Nuggles') AND "users"."id" != 4) LIMIT 1
    
    (34.2ms)  COMMIT
    
    Redirected to http://localhost:3000/users/nuggles
    Completed 302 Found in 304ms (ActiveRecord: 250.0ms)
    
  2. When submitting the form with nothing selected, I get the following error:

    param is missing or the value is empty: user
    
    private
      def update_params
        params.require( :user ).permit(
          :first_name,
          :last_name,
          :region_id,
    

Upvotes: 0

Views: 1179

Answers (1)

s_dolan
s_dolan

Reputation: 1276

For anyone who is interested, I've got the form working. This may not be the ideal way to create multiple records at one time, but it's working for me right now.

First, I took out the weird accessories definition in the Users controller. Since I was going to be directly creating, updating, and destroying a user's accessories, the join model deserved its own controller:

class UserAccessoriesController < ApplicationController
  def index
    @user = User.friendly.find( params[:id] )
    @user_accessories = @user.user_accessories
  end

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

    create_params[:accessory].each do |accessory|
      @user_accessory = UserAccessory.new( user: @user, accessory_id: accessory )

      @user_accessory.save
    end

    redirect_to user_accessories_path( @user )
  end

  def destroy
    @user_accessory = UserAccessory.friendly.find( params[:id] )

    @user_accessory.destroy
  end

  private
    def create_params
      params.require( :user_accessory ).permit(
        :id,
        accessory: []
      )
    end
end

I only have one view for UserAccessories, the index. This page lists all of a user's accessories, and if the displayed user is the same as the logged in user, the following form is displayed:

  <%= form_for @user_accessories.new(), { method: :post } do |f| %>
    <div class="form-group">
      <%= f.collection_select(
                               :accessory,
                               # Get only accessories not on the user's account
                               Accessory.where.not( id: @user.accessories ),
                               :id,
                               :name,
                               {
                                 include_hidden: false
                               },
                               {
                                 multiple:    true,
                                 class:       'form-control',
                                 placeholder: 'Select Equipment to Add'
                               }
                             ) %>
    </div>

    <div class="form-group">
      <%= f.submit "Update Equipment" %>
    </div>
  <% end %>

This allows the user to select multiple accessories (using select2), then on submit creates the association one by one in the controller.

If anyone has a better method for the creation of the user accessories (i.e. not looping through my params and creating that many times), then please leave a comment!

Upvotes: 1

Related Questions