I've been worrying this bug for too many days and I'm not sure what I'm missing.
I have a parent model named 'product' and a child of it 'product_attachment'.
Validation within child model is successful on create to disallow a blank image field via /product_attachments#new
However, when using its parent form /product#new (files below) I'm expecting it to validate to successfully fail without an image. However, Activerecord is ignoring the error, and my controller is tryign to save as a result if the validation passing then complaining that my product_attachments is null.
I'm currently in the understanding that using the 'validates_associated' would validate the child model as part of the Parent's process but this has not been working.
Instead of passing a happy failed validation mid-form to allow user to take action, we leave form and the controller tries to process the create method which fails due to no attachment.
Since I should always have an attachment have been trying to fix this validation to no avail.
Any help appreciated, I've include similar code samples before for your feedback.
I'm fairly new to rails so I'm hoping I'm mis-using a key syntax or context.
Also curious what cause is and what best way was to troubleshoot as I'm still developing good debug practices.
product.rb
class Product < ActiveRecord::Base
has_many :product_attachments
validates_presence_of :title, :message => "You must provide a name for this product."
accepts_nested_attributes_for :product_attachments, allow_destroy: true#,
validates_associated :product_attachments
end
product_attachment.rb (carrierwave to handle uploading used here, seems to work fine)
class ProductAttachment < ActiveRecord::Base
belongs_to :product
mount_uploader :image, ImageUploader
validates_presence_of :image, :message => "You must upload an image to go with this item."
end
products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
def index
#products = Product.all
end
def show
#product_attachments = #product.product_attachments.all
end
def new
#product = Product.new
#product_attachment = #product.product_attachments.build
end
def edit
end
def create
#product = Product.new(product_params)
respond_to do |format|
if #product.save
params[:product_attachments]['image'].each do |a|
#product_attachment = #product.product_attachments.create!(:image => a)
end
format.html { redirect_to #product, notice: 'Product was successfully created.' }
else #- when would this fire?
format.html { render :new }
end
end
end
def update
respond_to do |format|
if #product.update(product_params)
params[:product_attachments]['image'].each do |a|
#product_attachment = #product.product_attachments.create!(:image => a, :post_id => #post.id)
end
format.html { redirect_to #product, notice: 'Product was successfully updated.' }
else #- when would this fire?
format.html { render action: 'new' }
end
end
end
def destroy
#product.destroy
respond_to do |format|
format.html { redirect_to #product, notice: 'Product was successfully destroyed.' }
end
end
private
def set_product
#product = Product.find(params[:id])
end
# we pass the _destroy so the above model has the access to delete
def product_params
params.require(:product).permit(:id, :title, :price, :barcode, :description, product_attachment_attributes: [:id, :product_id, :image, :filename, :image_cache, :_destroy])
end
end
product_attachments_controller.rb
class ProductAttachmentsController < ApplicationController
before_action :set_product_attachment, only: [:show, :edit, :update, :destroy]
def index
#product_attachments = ProductAttachment.all
end
def show
end
def new
#product_attachment = ProductAttachment.new
end
def edit
end
def create
#product_attachment = ProductAttachment.new(product_attachment_params)
respond_to do |format|
if #product_attachment.save
#product_attachment.image = params[:image]
format.html { redirect_to #product_attachment, notice: 'Product attachment was successfully created.' }
else
format.html { render :new }
end
end
end
def update
respond_to do |format|
if #product_attachment.update(product_attachment_params)
#product_attachment.image = params[:image]
format.html { redirect_to #product_attachment.product, notice: 'Product attachment was successfully updated.' }
else
format.html { render :edit }
end
end
end
def destroy
#product_attachment.destroy
respond_to do |format|
format.html { redirect_to product_attachments_url, notice: 'Product attachment was successfully destroyed.' }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product_attachment
#product_attachment = ProductAttachment.find(params[:id])
end
def product_attachment_params
params.require(:product_attachment).permit(:id, :product_id, :image, :image_cache)
end
end
_form.html.slim (using simple_form + slim + cocoon here...)
= simple_form_for #product do |f|
- if #product.errors.any?
#error_explanation
h2
= pluralize(#product.errors.count, "error")
| prohibited this product from being saved:
ul
- #product.errors.each do |attribute, message|
- if message.is_a?(String)
li= message
= f.input :title
= f.input :price, required: true
= f.input :barcode
= f.input :description
h3 attach product images
#product_attachment
= f.simple_fields_for :product_attachments do |product_attachment|
= render 'product_attachment_fields', f: product_attachment
.links
= link_to_add_association 'add product attachment', f, :product_attachments
= f.submit
_product_attachment_fields.html.slim
Noted I needed to name my file field this way for my controller to use files correctly, but unsure why still.
.nested-fields
= f.file_field :image , :multiple => true , name: "product_attachments[image][]"
= link_to_remove_association "remove", f
Let me know if I can provide anything else.
Thank you for your time to read/reply.
Edit1: My current method to debug I'm working through as of writing this is to strip out code chunks and test functionality by through browser. I've read I should be more familiar with rails console but have not got there yet.
Edit2:
undefined method `[]' for nil:NilClass
params[:product_attachments]['image'].each do |a|
app/controllers/products_controller.rb:47:in `block in create'
app/controllers/products_controller.rb:44:in `create'
Request
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"{mytoken}",
"product"=>{"title"=>"pleasefailwell",
"commit"=>"Create Product"}
Edit3: Reworded original section for clarity
Seeing your latest edit, I figured that was happening, which means product_attachments is nil. Which is correct, the param should be product_attachments_attributes.
This leads to another problem in your product_params method.
You have product_attachment_attributes, the the base form "field" should be pluralised: product_attachments_attributes
In short:
Remove the product attachment loops from your controller. Fix the strong params method, it should work after that.
Edit:
Also remove the name attribute from the file_field That isn't helping.
Related
I have two models: one for contacts ("Contatos") and one for users ("Usuarios"). Contatos has_one Usuario , as follows:
class Contato < ApplicationRecord
has_one :usuario, dependent: :destroy
accepts_nested_attributes_for :usuario,
allow_destroy: true
And
class Usuario < ApplicationRecord
has_secure_password
belongs_to :contato
validates_presence_of :login, :password
validates_uniqueness_of :login
end
I want to use one form for creating and editing both models. The _form partial that I currently have is this:
<%= form_with(model: contato, local: true) do |contato_form| %>
<%= if contato.errors.any?
showferr contato
end %>
#Here are the inputs for contato, I cut them out so it wouldn't be too long to read.
Bellow (same file as above) there is a check box for the Contato model that I left on, it sets a Boolean in the model(and DB) telling if the contact has a user on not, additionally I use some JavaScript (Coffee) to toggle the whole user (Usuario) form part based on the checkboxe's value .
<div class="form-group">
<%= contato_form.label :possui_usuario, :class => 'inline-checkbox' do %>
Possui usuário
<%= contato_form.check_box :possui_usuario, {id: "hasUser", checked: #contato.possui_usuario} %>
<% end %>
</div>
</div>
<div id="userPart" class="findMe" <% unless #contato.possui_usuario %> style="display:none;" <% end %> >
<h2> Usuário: </h2>
<div class="container">
<%= contato_form.fields_for :usuario, #contato.usuario do |usuario_form| %>
<%= render partial: 'usuarios/campos_usuario', locals: {form: usuario_form, object: #contato} %>
<% end %>
</div>
</div>
<br/>
<div class="container-fluid text-right">
<%= contato_form.submit 'Confirmar', :class => 'btn-lg btn-success' %>
</div>
<% end %>
The partial form for the Usuario model is rendering ok, but what I want to do is to only create and/or validate the user part if the checkbox is selected (if I say that the contact does have a user).
Here's what I attempted last (there were many attempts):
At Contato model:
attr_accessor(:has_user)
#has_user = 0
before_validation do |record|
#has_user = record.possui_usuario
end
def self.user?
#has_user == 1
end
validates_presence_of :nome
validates_length_of :nome, in: 1..45
validates_presence_of :email
validates_format_of :email, with: email_regex
validates_associated :usuario, if: user?
Controller for Contato:
class ContatosController < ApplicationController
before_action :set_contato, only: [:show, :edit, :update, :destroy]
# GET /contatos
# GET /contatos.json
def index
#contatos = Contato.all
#page_title = 'Contatos'
end
# GET /contatos/1
# GET /contatos/1.json
def show
#page_title = 'Ver contato: ' + #contato.nome
end
# GET /contatos/new
def new
#contato = Contato.new
#contato.build_usuario
#contato.ativo = true
#page_title = 'Novo contato'
end
# GET /contatos/1/edit
def edit
#page_title = 'Editar contato: ' + #contato.nome
unless #contato.possui_usuario
#contato.build_usuario
end
end
# POST /contatos
# POST /contatos.json
def create
#contato = Contato.new(contato_params)
respond_to do |format|
if #contato.save
flash[:notice] = 'Contato foi criado com sucesso.'
format.html {redirect_to #contato}
format.json {render :show, status: :created, location: #contato}
else
flash[:warn] = "Erro ao criar contato."
format.html {render :new}
format.json {render json: #contato.errors, status: :unprocessable_entity}
end
end
end
# PATCH/PUT /contatos/1
# PATCH/PUT /contatos/1.json
def update
respond_to do |format|
if #contato.update(contato_params)
format.html {redirect_to #contato, notice: 'Contato foi atualizado com sucesso.'}
format.json {render :show, status: :ok, location: #contato}
else
format.html {render :edit}
format.json {render json: #contato.errors, status: :unprocessable_entity}
end
end
end
# DELETE /contatos/1
# DELETE /contatos/1.json
def destroy
#contato.destroy
respond_to do |format|
format.html {redirect_to contatos_url, notice: 'Contato deletado com sucesso.'}
format.json {head :no_content}
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_contato
#contato = Contato.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def contato_params
params.require(:contato).permit(:id, :empresa_id,
:ativo, :nome,
:cargo, :celular,
:email, :nascimento,
:observacoes, :mensagem_instantanea,
:tipo_msg_inst, :possui_usuario,
usuario_attributes: [:login, :password, :permissoes, :id, :contato_id, :_destroy])
end
end
Sorry for the long question and big code blocks.
I see two holes in the data presented currently ...
First, your controller action where create is called is where you should be testing to see if you are calling to the model / activerecord.
Something like ...
def create
if #contato && #contato.usuarios # might be able to just do last half
respond_to do |format|
if #contato = #contato.create!(contato_params) # note the bang or '!'
format.html { redirect_to #contato, notice: 'contato was successfully created.' }
else
format.html { render :new }
end
end
end
end
Without seeing your controller - I am going to guess you didn't nest your controller via Rails strong_param feature properly. Note here - these two won't run, I'm not quite sure what information is needed, but I wanted you to make sure if you are nesting your models and using a single controller - you are away you need to nest your models in strong_params (google search nested rails strong_params for thousands of help / hits).
params.require(:contato).permit(:login, :password, usuario: [id, ...] )
If that's not it - also tell us if all the functionality of create/read/update/destroy works normally & you are just looking to limit it to create in certain circumstances?
Update - based on the controller - just move your check for create from the model & move it to the controller at the start of the #create action ... maybe start with ...
def create
# Note - here you will have to inspect contato_params to find syntax
if contato_params[:usuario_attributes][:contato_id]
... rest of action wrapped in here ...
end
end
... once again ... you will need to work out exact syntax - but just like you did with the edit - this spot is where you control the creation - not in the model.
More specifically I see this #contato.possui_usuario in the form ... that's probably the variable you want to check against in your controller, but perhaps my suggestion is more important - I can't tell you that with certainty - I'm also not sure you need the has_user trick per say in model & might be tempted to do a controller version in the private method section ...
class ContatosController
private
def has_user?
... whatever ...
end
Clarification from comment:
If I move the control over the user form part to the controller (which
makes a lot of sense) how would I about canceling the
validates_associated part of the model in case the user decides that
this contact wont have any users?
You don't move the form control (defined as variable in the form), you move the model method that deals with the form control to the controller - then you can wrap it all in a transaction to rollback any other changes OR if you build your activerecord out with #build it will do it for you.
So I create two models, one "Course" and one "Section" with scaffold and need Section to display Courses in a drop down menu that reflects any courses that were created in the course model and use it to create in section. I've been able to get the drop down menu displaying the courses created from "Course", but when I create the new section is displays the course as blank. Course has a Name, Department, Number, and Credit Hours. Section has Semester, Number, Course, and Room Number.
What I modified to make the drop down menu was ( in _form.html.erb of views of section )
<div class="field">
<%= form.label "Courses", class: 'courses'%>
<%= form.collection_select(:section, Course.all, :id, :name) %>
</div>
This gives an error of "Courses must exist"
Previously I had:
<div class="field">
<%= form.label "Courses", class: 'courses'%>
<%= form.collection_select(:course_ids, Course.all, :id, :name) %>
This did not give an error and allowed me to create a section, just without adding the selected course to the section database.
https://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-collection_select
From reading it appears :name should be defined in the models portion of Course, but when I try it gives an error. I also realize I do not have it set to record Course to a specific section ID which is why it isn't saving it when a new section is created. My question is, what do I add or modify to make that work? Is using collection select the wrong thing to do?
EDIT to include sections_controller.rb
class SectionsController < ApplicationController
before_action :set_section, only: [:show, :edit, :update, :destroy]
# GET /sections
# GET /sections.json
def index
#sections = Section.all
end
# GET /sections/1
# GET /sections/1.json
def show
end
# GET /sections/new
def new
#section = Section.new
end
# GET /sections/1/edit
def edit
end
# POST /sections
# POST /sections.json
def create
#section = Section.new(section_params)
respond_to do |format|
if #section.save
format.html { redirect_to #section, notice: 'Section was successfully created.' }
format.json { render :show, status: :created, location: #section }
else
format.html { render :new }
format.json { render json: #section.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /sections/1
# PATCH/PUT /sections/1.json
def update
respond_to do |format|
if #section.update(section_params)
format.html { redirect_to #section, notice: 'Section was successfully updated.' }
format.json { render :show, status: :ok, location: #section }
else
format.html { render :edit }
format.json { render json: #section.errors, status: :unprocessable_entity }
end
end
end
# DELETE /sections/1
# DELETE /sections/1.json
def destroy
#section.destroy
respond_to do |format|
format.html { redirect_to sections_url, notice: 'Section was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_section
#section = Section.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def section_params
params.require(:section).permit(:semester, :number, :course, :room_number)
end
end
I believe I need to relate them somehow with the last part:
def section_params
params.require(:section).permit(:semester, :number, :course, :room_number)
EDIT:
(source: rubyisonrails.com)
http://rubyisonrails.com/pictures/part2.PNG">
First, you should change courses to course in the Section model. The association name for the belongs_to should always be singular
class Section < ApplicationRecord
belongs_to :course #singular name
end
Second, you should have course_id column instead of course in the sections table. You can generate a migration which will reflect these changes in the table
rails g migration modify_sections
The above command should generate a file like xxxxxxxmodify_sections.rb under db/migrate folder. Open the file and add
def change
add_column :sections, :course_id, :integer
remove_column :sections, :course, :integer
end
and do rake db:migrate
Now change the collection_select like the below
<%= form.collection_select(:course_id, Course.all, :id, :name) %>
And in the sections_controller#create, add
#section.course_id = params[:section][:course_id]
before the respond_to do |format|
Finally, change course to course_id in the section_params method.
def section_params
params.require(:section).permit(:semester, :number, :course_id, :room_number)
end
Note:
As you are very new to the technology, I recommend you to follow the Guides to learn.
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
I have a very straight-forward task to fulfil --- just to be able to write comments under posts and if the comments fail validation display error messages on the page.
My comment model uses a gem called Acts_as_commentable_with_threading, which creates a comment model after I installed.
On my post page, the logic goes like this:
Posts#show => display post and a form to enter comments => after the comment is entered, redisplay the Post#show page which has the new comment if it passes validation, otherwise display the error messages above the form.
However with my current code I can't display error messages if the comment validation fails. I think it is because when I redisplay the page it builds a new comment so the old one was erased. But I don't know how to make it work.
My codes are like this:
Comment.rb:
class Comment < ActiveRecord::Base
include Humanizer
require_human_on :create
acts_as_nested_set :scope => [:commentable_id, :commentable_type]
validates :body, :presence => true
validates :first_name, :presence => true
validates :last_name, :presence => true
# NOTE: install the acts_as_votable plugin if you
# want user to vote on the quality of comments.
#acts_as_votable
belongs_to :commentable, :polymorphic => true
# NOTE: Comments belong to a user
belongs_to :user
# Helper class method that allows you to build a comment
# by passing a commentable object, a user (could be nil), and comment text
# example in readme
def self.build_from(obj, user_id, comment, first_name, last_name)
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
end
end
PostController.rb:
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = Comment.build_from(#post, nil, "", "", "")
end
end
CommentsController:
class CommentsController < ApplicationController
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
private
def comment_params
params.require(:comment).permit(:user, :first_name, :last_name, :body, :commentable_id, :commentable_type, :comment_id,
:humanizer_answer, :humanizer_question_id)
end
def commentable_type
comment_params[:commentable_type]
end
def commentable_id
comment_params[:commentable_id]
end
def comment_id
comment_params[:comment_id]
end
def body
comment_params[:body]
end
def make_child_comment
return "" if comment_id.blank?
parent_comment = Comment.find comment_id
#comment.move_to_child_of(parent_comment)
end
def build_comment(comment_params)
if current_user.nil?
user_id = nil
first_name = comment_params[:first_name]
last_name = comment_params[:last_name]
else
user_id = current_user.id
first_name = current_user.first_name
last_name = current_user.last_name
end
commentable = commentable_type.constantize.find(commentable_id)
Comment.build_from(commentable, user_id, comment_params[:body],
first_name, last_name)
end
end
comments/form: (this is on the Posts#show page)
<%= form_for #new_comment do |f| %>
<% if #new_comment.errors.any? %>
<div id="errors">
<h2><%= pluralize(#new_comment.errors.count, "error") %> encountered, please check your input.</h2>
<ul>
<% #new_comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% end %>
I would instead use nested routes to create a more restful and less tangled setup:
concerns :commentable do
resources :comments, only: [:create]
end
resources :posts, concerns: :commentable
This will give you a route POST /posts/1/comments to create a comment.
In your controller the first thing you want to do is figure out what the parent of the comment is:
class CommentsController < ApplicationController
before_action :set_commentable
private
def set_commentable
if params[:post_id]
#commentable = Post.find(params[:post_id])
end
end
end
This means that we no longer need to pass the commentable as form parameters. Its also eliminates this unsafe construct:
commentable = commentable_type.constantize.find(commentable_id)
Where a malicous user could potentially pass any class name as commentable_type and you would let them find it in the DB... Never trust user input to the point where you use it to execute any kind of code!
With that we can start building our create action:
class CommentsController < ApplicationController
before_action :set_commentable
def create
#comment = #commentable.comments.new(comment_params) do |comment|
if current_user
comment.user = current_user
comment.first_name = current_user.first_name
comment.last_name = current_user.last_name
end
end
if #comment.save
respond_to do |format|
format.json { head :created, location: #comment }
format.html { redirect_to #commentable, success: 'Comment created' }
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: #comment.errors, status: 422 }
end
end
end
private
# ...
def comment_params
params.require(:comment).permit(:first_name, :last_name, :body, :humanizer_answer, :humanizer_question_id)
end
end
In Rails when the user submits a form you do not redirect the user back to the form - instead you re-render the form and send it as a response.
While you could have your CommentsController render the show view of whatever the commentable is it will be quite brittle and may not even provide a good user experience since the user will see the top of the post they where commenting. Instead we would render app/views/comments/new.html.erb which should just contain the form.
Also pay attention to how we are responding. You should generally avoid using redirect_to :back since it relies on the client sending the HTTP_REFERRER header with the request. Many clients do not send this!
Instead use redirect_to #commentable or whatever resource you are creating.
In your original code you have totally mixed up JSON and HTML responses.
When responding with JSON you do not redirect or send flash messages.
If a JSON POST request is successful you would either:
Respond with HTTP 201 - CREATED and a location header which contains the url to the newly created resource. This is preferred when using SPA's like Ember or Angular.
Respond with HTTP 200 - OK and the resource as JSON in the response body. This is often done in legacy API's.
If it fails do to validations you should respond with 422 - Unprocessable Entity - usually the errors are rendered as JSON in the response body as well.
Added.
You can scrap your Comment.build_from method as well which does you no good at all and is very idiosyncratic Ruby.
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = #post.comments.new
end
end
Don't use line contiuation (\) syntax like that - use parens.
Don't:
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
Do:
new(
foo: a,
bar: b
)
Added 2
When using form_for with nested resources you pass it like this:
<%= form_for([commentable, comment]) do |f| %>
<% end %>
This will create the correct url for the action attribute and bind the form to the comment object. This uses locals to make it resuable so you would render the partial like so:
I'm assuming your form_for submits a POST request which triggers the HTML format in CommentsController#create:
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
So, if #comment.save fails, and this is an HTML request, the #create method renders create.html. I think you want to render Posts#show instead.
Keep in mind that if validations fail on an object (Either by calling save/create, or validate/valid?), the #comment object will be populated with errors. In other words calling #comment.errors returns the relevant errors if validation fails. This is how your form is able to display the errors in #new_comment.errors.
For consistency, you'll need to rename #new_comment as #comment in the posts#show action, otherwise you'll get a NoMethodError on Nil::NilClass.
TL;DR: You're not rendering your form again with your failed #comment object if creation of that comment fails. Rename to #comment in posts, and render controller: :posts, action: :show if #comment.save fails from CommentsController#create
I have figured out the answer myself with the help of others here.
The reason is that I messed up with the JSON format and html format (typical noobie error)
To be able to display the errors using the code I need to change two places ( and change #comment to #new_comment as per #Anthony's advice).
1.
routes.rb:
resources :comments, defaults: { format: 'html' } # I set it as 'json' before
2.
CommentsController.rb:
def create
#new_comment = build_comment(comment_params)
respond_to do |format|
if #new_comment.save
make_child_comment
format.html { redirect_to(:back, :notice => 'Comment was successfully added.') }
else
commentable = commentable_type.constantize.find(commentable_id)
format.html { render template: 'posts/show', locals: {:#post => commentable} }
format.json { render json: #new_comment.errors }
end
end
end
In my application I have a "bookings" table, and an "extras" table.
This is a many-many relationship. Therefore I have created a middle table called "additions"
I've used the "has_many :through" to establish the relationship between the tables:
class Booking < ActiveRecord::Base
has_many :additions
has_many :extras, :through => :additions
class Extra < ActiveRecord::Base
has_many :additions
has_many :extras, :through => :additions
class Addition < ActiveRecord::Base
belongs_to :booking
belongs_to :extra
This seems to work. I added a few extras to some existing bookings manually (by adding numbers to the additions table), and wrote code so that when you click to show a booking, it lists all associated extras.
Now I need to make it so that when you make a booking - the "extras" are saved into the middle (additions) table.
I have checkboxes on my bookings form page:
<%= f.label 'Extras:' %>
<%= f.collection_check_boxes :extra_ids, Extra.all, :id, :extra_info %>
But obviously, the choices just get discarded when the user clicks on save.
I need some code to go (in the controller?) to make it save these "extras" into the "additions table" ?
Any ideas, as I can't work out how to do this?!
Thanks!
class BookingsController < ApplicationController
respond_to :html, :xml, :json
before_action :find_room
# before_action :find_extra
def index
#bookings = Booking.where("room_id = ? AND end_time >= ?", #room.id, Time.now).order(:start_time)
respond_with #bookings
end
def new
#booking = Booking.new(room_id: #room.id)
end
def create
#booking = Booking.new(params[:booking].permit(:room_id, :start_time, :length, :user_id))
#booking.room = #room
if #booking.save
redirect_to room_bookings_path(#room, method: :get)
else
render 'new'
end
end
def show
#booking = Booking.find(params[:id])
end
def destroy
#booking = Booking.find(params[:id]).destroy
if #booking.destroy
flash[:notice] = "Booking: #{#booking.start_time.strftime('%e %b %Y %H:%M%p')} to #{#booking.end_time.strftime('%e %b %Y %H:%M%p')} deleted"
redirect_to room_bookings_path(#room)
else
render 'index'
end
end
def edit
#booking = Booking.find(params[:id])
end
def update
#booking = Booking.find(params[:id])
# #booking.room = #room
if #booking.update(params[:booking].permit(:room_id, :start_time, :length, :user_id))
flash[:notice] = 'Your booking was updated succesfully'
if request.xhr?
render json: {status: :success}.to_json
else
redirect_to resource_bookings_path(#room)
end
else
render 'edit'
end
end
private
def save booking
if #booking.save
flash[:notice] = 'booking added'
redirect_to room_booking_path(#room, #booking)
else
render 'new'
end
end
def find_room
if params[:room_id]
#room = Room.find_by_id(params[:room_id])
end
end
# def find_extra
# if params[:extra_id]
# #extra = Extra.find_by_id(params[:extra_id])
# end
# end
# If resource not found redirect to root and flash error.
def resource_not_found
yield
rescue ActiveRecord::RecordNotFound
redirect_to root_url, :notice => "Booking not found."
end
def booking_params
params.require(:booking).permit(:user_id, :extra_id)
end
end
------------------------
class AdditionsController < ApplicationController
before_action :set_addition, only: [:show, :edit, :update, :destroy]
# GET /additions
def index
#additions = Addition.all
end
# GET /additions/1
def show
end
# GET /additions/new
def new
#addition = Addition.new
end
# GET /additions/1/edit
def edit
end
# POST /additions
def create
#addition = Addition.new(addition_params)
if #addition.save
redirect_to #addition, notice: 'Addition was successfully created.'
else
render :new
end
end
# PATCH/PUT /additions/1
def update
if #addition.update(addition_params)
redirect_to #addition, notice: 'Addition was successfully updated.'
else
render :edit
end
end
# DELETE /additions/1
def destroy
#addition.destroy
redirect_to additions_url, notice: 'Addition was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_addition
#addition = Addition.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def addition_params
params.require(:addition).permit(:booking_id, :extra_id, :extra_name)
end
end
--------------------------------------
# #author Stacey Rees <https://github.com/staceysmells>
class ExtrasController < ApplicationController
# #see def resource_not_found
around_filter :resource_not_found
before_action :set_extra, only: [:show, :edit, :update, :destroy]
def index
#extras = Extra.all
end
def show
end
def new
#extra = Extra.new
end
def edit
end
def create
#extra = Extra.new(extra_params)
if #extra.save
redirect_to #extra, notice: 'Extra was successfully created.'
else
render :new
end
end
def update
if #extra.update(extra_params)
redirect_to #extra, notice: 'Extra was successfully updated.'
else
render :edit
end
end
def destroy
#extra.destroy
redirect_to extras_url, notice: 'Extra was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_extra
#extra = Extra.find(params[:id])
end
# If resource not found redirect to root and flash error.
def resource_not_found
yield
rescue ActiveRecord::RecordNotFound
redirect_to root_url, :notice => "Room Category not found."
end
# Only allow a trusted parameter "white list" through.
def extra_params
params.require(:extra).permit(:extraimg, :name, :description, :quantity, :price, :extracat_id)
end
end
What you're doing here is working with nested form attributes. It's a bit complex, but it's also something people do often, so there are some good resources available.
I suggest you look at this post: http://www.sitepoint.com/complex-rails-forms-with-nested-attributes/
In particular, the section named 'More Complicated Relationships' specifically has an example of using nested attributes to set up a many-to-many association using has_many :through.
The key pieces (which commenters have already pointed out) are going to be accepts_nested_attributes_for :extras in your Booking model, and a f.fields_for :extras block in the view. You'll also need to modify your booking_params method to permit the nested values. There are a couple of strong parameters gotchas that you can potentially run into with that, so you may need to review the documentation.
It turns out I was nearly there with the code I had once the accepts_nested_attributes_for was written in.
My main issue was setting up the booking_params method in the controller. I got it to work by declaring :extra_ids => [] in my params.permit.