Search code examples
ruby-on-railscollection-select

collection_select not inserting value from other model


I have two models, Roaster and Roast

I want to have the user select the value of :roaster in the new roast form, from the Roaster model. I am using a collection_select which displays the list of roasters in the dropdown ok, but it doesn't insert the value into the table. From the console, it actually looks like it's trying to pass the roaster_id

"roast"=>{"roaster_id"=>"1", "name"=>"Rugby", "beans"=>"", "countries_attributes"=>{"0"=>{"country_name"=>"", "regions_attributes"=>{"0"=>{"region_name"=>""}}}, "1"=>{"country_name"=>"", "regions_attributes"=>{"0"=>{"region_name"=>""}}}, "2"=>{"country_name"=>"", "regions_attributes"=>{"0"=>{"region_name"=>""}}}}, "bestfor"=>"", "roast"=>"", "tastingnotes"=>""}, "commit"=>"Create Roast"}

My select:

<%= form.collection_select(:roaster_id, Roaster.all, :id, :roaster_name, :prompt => 'Select Roaster') %>

I've tried

<%= form.collection_select(:roaster_name, Roaster.all, :id, :roaster_name, :prompt => 'Select Roaster') %>

but this gives and undefined method error.

My roast_params

params.require(:roast).permit(:roaster, :roaster_id, :name, :bestfor, :beans, :roast, :tastingnotes, :notes, :slug, :avatar, countries_attributes: [:country_id, :country_name, regions_attributes: [:id, :region_name]])

Adding in :roaster_name doesn't solve either.

As requested full form:

<%= form_with(model: roast, local: true, multipart: true) do |form| %>
  <% if roast.errors.any? %>
    <div id="error_explanation">
      <div class="alert alert-danger" role="alert">
      <h2><%= pluralize(roast.errors.count, "error") %> prohibited this roast from being saved:</h2>

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

<form>

<div class="row">
  <div class="col-6">
    <div class="field form-group">
      <%= form.label :roaster, class: 'control-label' %>
      <%= form.collection_select(:roaster_id, Roaster.all, :id, :roaster_name, :prompt => 'Select Roaster') %>
    </div>
  </div>
  <div class="col-6">
    <div class="form-group">
      <%= form.label :name, class: 'control-label' %>
      <%= form.text_field :name, class: "form-control" %>
    </div>
  </div>
</div>

  <div class="form-group">
    <%= form.label :beans, "Blend", class: 'control-label' %><br />
    <%= form.select :beans, [ 'Single Origin','Two Country Blend', 'Three Country Blend' ], :prompt => 'Select One', id: :roast_beans, class: "form-control" %>
  </div>

<div class="row">
  <%= form.fields_for :countries do |countries_form| %>
  <div class="col-6">

    <div class="form-group">

        <%= countries_form.label :country %>
        <%= countries_form.text_field :country_name, class: "form-control" %>
    </div>
  </div>
  <div class="col-6">
  <!-- note the appending of `countries_`  to form.fields to allow for deeper nested to work-->
        <%= countries_form.fields_for :regions do |regions_form| %>
          <%= regions_form.label :region %>
          <%= regions_form.text_field :region_name, class: "form-control" %>
        <% end %>
        <br />
    </div>
    <% end %>
</div>

  <div class="form-group">
    <%= form.label :bestfor, "Style", class: 'control-label' %><br />
    <%= form.select :bestfor, [ 'Espresso','Filter' ], :prompt => 'Select One', id: :roast_bestfor, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= form.label :roast, "Strength", class: 'control-label' %><br />
    <%= form.select :roast, [ 'Light','Medium','Dark' ], :prompt => 'Select One', id: :roast_roast, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= form.label :tastingnotes, "Tasting Notes (separate with commas, e.g chocolate, citrus)", class: 'control-label' %><br  />
    <%= form.text_area :tastingnotes, id: :roast_tastingnotes, class: "form-control" %>
  </div>
<br />

  <div class="form-group">
    <%= form.label :avatar, "Upload image...", class: 'control-label' %>
    <%= form.file_field :avatar %>
  </div>

  <div class="actions">
    <%= form.submit class: "btn btn-success" %> <%= link_to "Cancel", "/roasts", class: "btn btn-secondary"%>
  </div>
<% end %>

</form>

roast_controller.rb

class RoastsController < ApplicationController
  before_action :set_roast, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, only: [:create, :edit, :update, :destroy]
  before_action :set_search

  # GET /roasts
  # GET /roasts.json
  def index
      @q = Roast.ransack(params[:q])
      @roastsalpha = @q.result.order(:name)
      @roastcount = Roast.count(:country)
      @roasts = Roast.order(:name).count
      @countroastschart = Roast.order("roaster DESC").all

  end

  # GET /roasts/1
  # GET /roasts/1.json
  def show
    @roast = Roast.friendly.find(params[:id])
    @commentable = @roast
    @comments = @commentable.comments
    @comment = Comment.new
    @sameroaster = Roast.where(roaster: @roast.roaster)
    @samecountry = Roast.where(country: @roast.country)
    @roastcount = Roast.where(roaster: @roast.roaster)



  end

  # GET /roasts/new
  def new
    @roast = Roast.new
    3.times {@roast.countries.build.regions.build}
  end

  # GET /roasts/1/edit
  def edit
    3.times {@roast.countries.build.regions.build}
  end

  # POST /roasts
  # POST /roasts.json
  def create
    @roast = Roast.new(roast_params)

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

  # PATCH/PUT /roasts/1
  # PATCH/PUT /roasts/1.json
  def update
    respond_to do |format|
      if @roast.update(roast_params)
        format.html { redirect_to @roast, notice: 'Roast was successfully updated.' }
        format.json { render :show, status: :ok, location: @roast }
      else
        format.html { render :edit }
        format.json { render json: @roast.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /roasts/1
  # DELETE /roasts/1.json
  def destroy
    @roast.destroy
    respond_to do |format|
      format.html { redirect_to roasts_url, notice: 'Roast was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_roast
      @roast = Roast.friendly.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def roast_params
      params.require(:roast).permit(:roaster, :roaster_id, :name, :bestfor, :beans, :roast, :tastingnotes, :notes, :slug, :avatar, countries_attributes: [:country_id, :country_name, regions_attributes: [:id, :region_name]])
    end

end

Solution

  • I think you are doing many wrong things. By looking your other questions I saw your models. I put some important things:

    class Roast < ApplicationRecord
      has_many :countries
      accepts_nested_attributes_for :countries
    end
    
    class Country < ApplicationRecord
      has_many :regions, inverse_of: :country
      accepts_nested_attributes_for :regions
      belongs_to :roast
    end
    
    class Region < ApplicationRecord
      belongs_to :country, inverse_of: :regions
    end
    

    In these models I didn't see the Roaster. I assume a Roast belongs_to :roaster.

    So: your Roast has many countries and each country has many regions. But you are passing country names and region names in your view to the create controller. You need to pass the ids, so that you save references to these models.

    You have many unnecessary field in params, and some missing ones. This is how it should be:

    def roaster_params
      params.require(:roast).permit(:roaster_id, :name, :bestfor, :beans, :tastingnotes, :notes, :slug, :avatar, countries_attributes: [:id, regions_attributes: [:id]])
    end
    

    You don't need roast, roaster, country_name, region_name. You need the id of the country (and not the country_id), and the id of the region (and not the region_id)

    In your form you should ask for country and region ids:

    <%= countries_form.collection_select(:id, Country.all, :id, :name, :prompt => 'Select Country') %>
    
    <%= regions_form.collection_select(:id, Region.all, :id, :name, :prompt => 'Select Region') %>
    

    In fact this is more difficult, because a region belongs to a country, but here you are showing all regions. You should only show regions for the selected country (which is dynamic).