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.
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:
Change your two collection_selects and swap :quote_client
for :client_id
and :quote_site
for :site_id
.
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