Search code examples
ruby-on-railsrubycollection-select

Rails Form collection_select not saving to database


I am a rails newbie and building a little application to help with my work.

I have client, site and quote models and controllers with views set up.

I have created a form on the quote model that pulls data from the other two models in a collection_select field. The documentation on collection_select for rails that I have found is pretty bad. I want to take a client name and site name and associate/ display the name on the quote.

I have set this up in the form, but it does not save the data or show it.

I really want to understand the inputs for the collection_select as I am sure mine are probably wrong and causing the issue.

<%= f.collection_select :client, Client.all, :quote_client, :client_name , {:prompt => "Please select a client for the site"} %>

I did some research and learned this from @juanpastas here

My form looks like so quotes/views/_form.html

<%= form_for(quote) do |f| %> 
    <% if quote.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(quote.errors.count, "error") %> prohibited this quote from being saved:</h2><ul>
      <% quote.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %><div class="field">
    <%= f.label :client %>
    <%= f.collection_select :client, Client.all, :quote_client, :client_name , {:prompt => "Please select a client for the site"} %>
  </div><div class="field">
    <%= f.label :site_name %>
    <%= f.collection_select :site, Site.all, :quote_site, :site_name , {:prompt => "Please select a site for the quote"} %>
  </div><div class="field">
    <%= f.label :quote_contact %>
    <%= f.text_field :quote_contact %>
  </div><div class="field">
    <%= f.label :quote_value %>
    <%= f.text_field :quote_value %>
  </div><div class="field">
    <%= f.label :quote_description %>
    <%= f.text_field :quote_description %>
  </div><div class="actions">
    <%= f.submit %>
  </div>
<% end %>

EDIT

Answers/clarifications

Quotes can only have one client and one site. The site would also have to belong to the client.

I have a list of clients called from the Client model via Client.all and a list of sites via the Site Model called via Site.all. I only need the name of one Client and one Site for each quote but want to be able to select in a cascading fashion. Select Client, then selects Site from those available for the Client.

Relations are set up between the three models like so:

 class Quote < ApplicationRecord


            belongs_to :site, optional: true
            belongs_to :client, optional: true
            has_and_belongs_to_many :assets
    end

class Site < ApplicationRecord


    has_attached_file :site_image, styles: { small: "64x64", med: "100x100", large: "200x200" }
    do_not_validate_attachment_file_type :site_image


    belongs_to :client , optional: true
    has_and_belongs_to_many :assets
    has_and_belongs_to_many :quotes
end

class Client < ApplicationRecord

    has_and_belongs_to_many :sites
    has_and_belongs_to_many :assets
    has_and_belongs_to_many :quotes
end

Controllers

class QuotesController < ApplicationController
  before_action :set_quote, only: [:show, :edit, :update, :destroy]

  # GET /quotes
  # GET /quotes.json
  def index
  @quotes = Quote.all
  end

  # GET /quotes/1
  # GET /quotes/1.json
  def show
  end

  # GET /quotes/new
  def new
    @quote = Quote.new
  end

  # GET /quotes/1/edit
  def edit
  end

  # POST /quotes
  # POST /quotes.json
  def create
    @quote = Quote.new(quote_params)

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

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

  # DELETE /quotes/1
  # DELETE /quotes/1.json
  def destroy
    @quote.destroy
    respond_to do |format|
      format.html { redirect_to quotes_url, notice: 'Quote was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_quote
      @quote = Quote.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def quote_params
      params.require(:quote).permit(:quote_client, :quote_site, :client_name, :site_name, :quote_contact, :quote_value, :quote_description)
    end
end


class SitesController < ApplicationController
  before_action :set_site, only: [:show, :edit, :update, :destroy]

  # GET /sites
  # GET /sites.json
  def index
    @sites = Site.all
    @clients = Client.all
  end

  # GET /sites/1
  # GET /sites/1.json
  def show
      @sites = Site.all
      @clients = Client.all
  end

  # GET /sites/new
  def new
    @site = Site.new
  end

  # GET /sites/1/edit
  def edit
  end

  # POST /sites
  # POST /sites.json
  def create
    @site = Site.new(site_params)

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

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

  # DELETE /sites/1
  # DELETE /sites/1.json
  def destroy
    @site.destroy
    respond_to do |format|
      format.html { redirect_to sites_url, notice: 'Site was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_site
      @site = Site.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def site_params
      params.require(:site).permit(:site_client, :client_name, :site_name, :site_image, :site_address, :site_contact)
    end
end

class ClientsController < ApplicationController
  before_action :set_client, only: [:show, :edit, :update, :destroy]

  # GET /clients
  # GET /clients.json
  def index
    @clients = Client.all
    @sites = Site.all
  end

  # GET /clients/1
  # GET /clients/1.json
  def show
      @clients = Client.all
      @sites = Site.all
  end

  # GET /clients/new
  def new
    @client = Client.new
  end

  # GET /clients/1/edit
  def edit
  end

  # POST /clients
  # POST /clients.json
  def create
    @client = Client.new(client_params)

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

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

  # DELETE /clients/1
  # DELETE /clients/1.json
  def destroy
    @client.destroy
    respond_to do |format|
      format.html { redirect_to clients_url, notice: 'Client was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_client
      @client = Client.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def client_params
      params.require(:client).permit(:client_name, :client_address, :client_phone, :client_email, :client_website)
    end
end

Additions

You may notice I have tried to scale so that a client is called in a site and a site and a client is called in a quote.


Solution

  • First of all: I'm assuming you have relations set up between the three models! There has to be a has_many relation from quote to client and from quote to site.

    There are two issues which could prevent your form from saving.

    Firstly it is in how you create your collection_select. The third parameter in collection select is what will be sent to the controller. This should be an array of IDs (I assume a quote can have more than ONE client). I see you call it :quote_client. I'd rename it to :client_ids. In the end that's what you want to send to your controller: an array of IDs.

    The second thing you have to take care of is your controller. It would be nice if you shared your controller code, but I assume you have a quotes_controller with a quote_params method inside it. It will probably look like this:

        def quote_params
          params.require(:quote).permit(:quote_contact, etc., etc.)
        end
    

    This controller method has to respond with your form_for, so every field in your form_for (like quote_contact) should be in the permit, otherwise it won't get saved. If you want to save an array of IDs, you have to tell this method you're expecting an array of IDs. You can do that like so: client_ids: [].

    So your new quote_params method should look like this:

    def quote_params
      params.require(:quote).permit(:quote_contact, client_ids: [], site_ids: [], all_other_fields...)
    end
    

    I hope this answer provides you with your much needed help. If I need to clarify more: just ask :)

    Cheers

    EDIT: the answer above is still relevant for those who do want to save multiple records, but because you stated you do only want to save one record here is my updated answer:

    The logic I summed up above stays roughly the same.

    What you do not seem to understand at the moment, and what is (IMO) quite vital to understanding Rails applications is the way forms map to controllers and controllers map to the database. The method quote_params, as stated above, should permit all fields from forms you want to save to the database. This means all fields in your permit-part should BE in your database, otherwise they can't be saved. If you look closely at your quote table in the database, you will see that it has fields for client_id and site_id. These two fields hold the reference for your quote/client and quote/site associations. That is why your permit currently is not working, because you have quote_client and quote_site in place. The database does not have a quote_client or quote_site and hence when trying to save, doesn't update associations. The database does have client_id and site_id, so that's what you should pass into your quote params method.

    This should of course correspond to the fields in your form_for. So you need change two things to make this work:

    1. Change your two collection_selects and swap :quote_client for :client_id and :quote_site for :site_id.

    2. Change your controller method to reflect the changes in your form_for. Here also you have to swap quote_site and quote_client for quote_id and site_id, like this:

      def quote_params params.require(:quote).permit(:client_id, :site_id, etc.) end

    The important thing to remember when using Rails MODELNAME_params methods (which we call strong parameters -> READ IT! http://edgeguides.rubyonrails.org/action_controller_overview.html) is that both your form and your permit action should list the fields EXACTLY like they are in the database, otherwise the database won't understand and your record won't be properly saved.

    I hope with this edit you'll figure it out.

    Cheers