I have a Category model and a Standard model. A category has many standards through a "join table" model CategoryStandard. In my view, I have a form where I can edit the category description, and then add or remove standards from that category. So, my nested attributes are for :category_standards, because I'm not editing the standard itself, just adding or removing relationships, if that makes sense.
Here's the important part of the view:
<%= form_for(#category) do |f| %>
...
<div class="field">
<%= f.label :description %>
<%= f.text_field :description %>
</div>
<%= label_tag nil, "Standards in this Category" %>
<div id="standard-list">
<%= f.fields_for :category_standards do |ff| %>
<div class="field">
<%= ff.object.standard.number_with_exceptions %>
<%= ff.hidden_field :standard_id %>
<%= ff.hidden_field :_destroy %>
<%= link_to "<span class='glyphicon glyphicon-remove'></span>".html_safe, "", class: "del-std-btn", title: "Remove standard from category" %>
</div>
<% end %>
<div class="hidden" id="std-add-new-template">
<div class="field">
<%= f.fields_for :category_standards, CategoryStandard.new, child_index: "new_id" do |ff| %>
<%= ff.collection_select :standard_id, #standards - #category.standards, :id, :number_with_exceptions, prompt: "Select a standard to add" %>
<% end %>
</div>
</div>
</div>
...
<% end %>
There's some jQuery under the hood to manipulate the "rows", but that works fine and I don't think it's part of my problem, so I'll omit it.
In my Category model, I have:
class Category < ActiveRecord::Base
has_many :category_standards, dependent: :destroy
has_many :standards, through: :category_standards
validates :description, presence: true,
uniqueness: true
accepts_nested_attributes_for :category_standards, allow_destroy: true, reject_if: proc { |attributes| attributes['standard_id'].blank?}
end
And in my Categories controller, I have:
def category_params
params.require(:category).permit(:description, category_standards_attributes: [:id, :standard_id, :_destroy])
end
But when I try to add a standard to a category, I get these lines in my server log (reformatted in the hopes of making it more readable):
Parameters: {"utf8"=>"✓",
"authenticity_token"=>"***********",
"category"=>{
"description"=>"Drinking Water System Components",
"category_standards_attributes"=>{
"0"=>{
"standard_id"=>"2",
"_destroy"=>"false",
"id"=>"1"
},
"new_id"=>{
"standard_id"=>""
},
"1424899001814"=>{
"standard_id"=>"1"
}
}
},
"commit"=>"Save Changes",
"id"=>"2"
}
User Load (5.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 ORDER BY "users"."id" ASC LIMIT 1
Category Load (4.0ms) SELECT "categories".* FROM "categories" WHERE "categories"."id" = $1 LIMIT 1 [["id", "2"]]
Unpermitted parameters: 0, new_id, 1424899001814
(4.0ms) BEGIN
Category Exists (6.0ms) SELECT 1 AS one FROM "categories" WHERE ("categories"."description" = 'Drinking Water System Components' AND "categories"."id" != 2) LIMIT 1
SQL (6.0ms) UPDATE "categories" SET "description" = $1, "updated_at" = $2 WHERE "categories"."id" = 2 [["description", "Drinking Water System Components"], ["updated_at", Wed, 25 Feb 2015 21:16:44 UTC +00:00]]
It updates the description field just fine, but what's up with the Unpermitted parameters? My attributes hash comes out just like the example in the Rails Guide on nested forms, and it even says "The keys of the :addresses_attributes hash are unimportant, they need merely be different for each address." And yet it's the keys that are getting denied for me.
Where have I gone wrong? Thanks!
Figured it out, after a lot of reading. The missing piece was here.
Hashes with integer keys are treated differently and you can declare the attributes as if they were direct children. You get these kinds of parameters when you use accepts_nested_attributes_for in combination with a has_many association:
# To whitelist the following data:
# {"book" => {"title" => "Some Book",
# "chapters_attributes" => { "1" => {"title" => "First Chapter"},
# "2" => {"title" => "Second Chapter"}}}}
params.require(:book).permit(:title, chapters_attributes: [:title])
The important part of that was "Hashes with integer keys". My hash keys were passing as "0", "new_id", "1240934304343". It isn't important that I use "new_id", because that's just a placeholder value that gets changed in my jQuery when new rows are added. Only the template row retains that value, which is fine, because it gets filtered out by my reject_if clause.
But the fact that "new_id" isn't an integer apparently was the thing that was mucking it all up. So I changed it to "-1", which Rails accepts (even though it is still filtered out by reject_if, as it should be).
<div class="hidden" id="std-add-new-template">
<div class="field">
<%= f.fields_for :category_standards, CategoryStandard.new, child_index: "-1" do |ff| %>
<%= ff.collection_select :standard_id, #standards - #category.standards, :id, :number_with_exceptions, prompt: "Select a standard to add" %>
<% end %>
</div>
</div>
Your attribute keys don't seem to match what you are expecting in your strong parameters, "new_id" and "1424899001814" certainly will not be permitted.
"new_id"=>{
"standard_id"=>""
},
"1424899001814"=>{
"standard_id"=>"1"
}
I suspect the way you are constructing your form is invalid. Try breaking it down to the simplest working form.. like:
<div id="standard-list">
<%= link_to "<span class='glyphicon glyphicon-remove'></span>".html_safe, "", class: "del-std-btn", title: "Remove standard from category" %>
<div class="hidden" id="std-add-new-template">
<div class="field">
<%= f.fields_for :category_standards do |ff| %>
<%= ff.object.standard.number_with_exceptions %>
<%= ff.hidden_field :standard_id %>
<%= ff.hidden_field :_destroy %>
<%= ff.collection_select :standard_id, #standards - #category.standards, :id, :number_with_exceptions, prompt: "Select a standard to add" %>
<% end %>
</div>
</div>
</div>
The intention is to have only one nested form, and by stripping it down create only one nested hash.
"category_standards_attributes"=>{
"0"=>{
"standard_id"=>"2",
"_destroy"=>"false",
"id"=>"1"
}
}
What happens with this?
Related
I have an Exam and an ExamBattery that is just a collection of Exams. They have a has_and_belong_to_many declaration for each other, and ExamBattery accepts nested attributes for Exam, like so:
class Exam < ApplicationRecord
has_and_belongs_to_many :exam_batteries
validates_presence_of :name
end
class ExamBattery < ApplicationRecord
has_and_belongs_to_many :exams
accepts_nested_attributes_for :exams, reject_if: lambda { |attrs| attrs['name'].blank? }
validates_presence_of :name
end
When I create a new Exam, I want to be able to assign it to one or many ExamBatteries, so in ExamsController I whitelisted the array exam_battery_ids to accept multiple ExamBatteries to assign them to the current Exam (no other change was made, the controller is just from the scaffold):
def exam_params
params.require(:exam).permit(:name, :description, :order, :price, exam_battery_ids: [])
end
Also, in the view exams/new I added a multiple select to send the desired exam_battery_ids as params:
<%= form_with(model: exam, local: true) do |form| %>
# ... typical scaffold code
<div class="field">
<% selected = exam.exam_batteries.collect { |eb| eb.id } %>
<%= form.label :exam_battery_ids, 'Add batteries:' %>
<%= form.select :exam_battery_ids,
options_from_collection_for_select(ExamBattery.all, :id, :name, selected),
{ prompt: 'None' },
multiple: true %>
</div>
<% end %>
The idea is to be able to create a new ExamBattery with new Exams in it, in the same form (I haven't wrote that part yet, I can only edit for now). Also, when I edit an ExamBattery I want to be able to edit its Exams and even assign them to other ExamBatteries (if I select 'None', or JUST another exam battery, it would stop being assigned to the current ExamBattery), so in exam_batteries/edit (actually, the form partial in it) I have this code:
<%= form_with(model: exam_battery, local: true) do |form| %>
# ... normal scaffold code
<div class="field">
<!-- it should be exam_battery[exams_attributes][#_of_field][order] -->
<!-- it is exam_battery[exam_battery_ids][] -->
<% selected = exam_battery.exams.map { |exam| exam.id } %>
<%= form.label :exam_battery_ids, 'Edit batteries:' %>
<%= form.select :exam_battery_ids,
options_from_collection_for_select(ExamBattery.all, :id, :name, selected),
{ prompt: 'None' },
multiple: true %>
</div>
<% end %>
And in ExamBatteriesController I whitelisted the exam_batteries_attributes, with exam_battery_ids: [] as a param:
params.require(:exam_battery).permit(:name, :certification, exams_attributes: [:name, :description, :order, :price, exam_battery_ids: []])
But when in the ExamBattery form I try to edit the Exam's exam_batteries, the info doesn't update, because the params are like this:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"blah", "exam_battery"=>{"name"=>"Battery1", "certification"=>"test1", "exams_attributes"=>{"0"=>{"name"=>"Exam1", "description"=>"", "order"=>"", "id"=>"3"}, "1"=>{"name"=>"Exam2", "description"=>"", "order"=>"", "id"=>"4"}, "2"=>{"name"=>"Exam3", "description"=>"", "order"=>"", "id"=>"5"}}, "exam_battery_ids"=>["", "", "", "", "", "3"]}, "commit"=>"Update Exam battery", "id"=>"3"}
The exam_battery_ids are sent as a different param because the select name is exam_battery[exam_battery_ids][] instead of something like exam_battery[exams_attributes][0][name], as it happens with the other fields. How can I fix that?
Thanks.
I had an error in the form. In exam_batteries/edit I didn't notice I was using the form_with variable (form) and not the fields_for variable (builder), so it should be like this:
<div class="field">
<!-- it should be exam_battery[exams_attributes][0][order] -->
<!-- it is exam_battery[exam_battery_ids][] -->
<% selected = exam_battery.exams.map { |exam| exam.id } %>
<%= builder.label :exam_battery_ids, 'Escoge una batería' %>
<%= builder.select :exam_battery_ids,
options_from_collection_for_select(ExamBattery.all, :id, :name, selected),
{
include_hidden: false,
prompt: 'Ninguna'
},
multiple: true %>
</div>
With that it should work.
The only issue now is that I can't get the selected batteries when I show them in the fields_for, but I'm working on it.
UPDATE: I can show the current exam_batteries of the exam in the nested form by replacing the selected variable in the view with this:
<% selected = exam_battery.exams[builder.options[:child_index]].exam_batteries.map { |eb| eb.id } %>
If you know about a cleaner method, please let me know.
I have a form submitting a value to my create controller. In the server log, I see the parameter (:style) being passed, but not inserted into database. Does someone know how that could happen?
Server log
Started POST "/users/25/cupboards" for ::1 at 2016-08-19 19:11:39 -0400
Processing by CupboardsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"asWeoSFSisr62oMOPXZnb//KJ8LnAhT1h07NOm1Yn2O8t06N4yrDGugkd01AMQcujYzSahH+O7bEA/jpH+L9fQ==", "style"=>"WEEKEND", "commit"=>"Make a new closet", "user_id"=>"25"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 25]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 25]]
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO "cupboards" ("user_id", "created_at", "updated_at") VALUES (?, ?, ?) [["user_id", 25], ["created_at", "2016-08-19 23:11:39.219003"], ["updated_at", "2016-08-19 23:11:39.219003"]]
(0.7ms) commit transaction
Redirected to http://localhost:3000/users/25/cupboards/35/edit
Completed 302 Found in 46ms (ActiveRecord: 1.4ms)
Form
<%= form_for #cupboard, url: { action: "create" }, html: { class: "listform"} do |f| %>
<%= f.label "ATHLEISURE", class: "welcomelistitem" %>
<%= radio_button_tag(:style, "ATHLEISURE") %>
<%= f.label "CASUAL", class: "welcomelistitem" %>
<%= radio_button_tag(:style, "CASUAL") %>
<%= f.label "PROFESISONAL", class: "welcomelistitem" %>
<%= radio_button_tag(:style, "PROFESSIONAL") %>
<%= f.label "WEEKEND", class: "welcomelistitem" %>
<%= radio_button_tag(:style, "WEEKEND") %>
<%= f.label "FESTIVAL", class: "welcomelistitem" %>
<%= radio_button_tag(:style, "FESTIVAL") %>
<%= f.label "DATENIGHT", class: "welcomelistitem" %>
<%= radio_button_tag(:style, "DATENIGHT") %>
<%= f.submit 'Make a new closet' %>
<% end %>
Controller
def create
#user = User.find(params[:user_id])
#cupboard = #user.cupboards.new(cupboard_params)
if #cupboard.save
redirect_to edit_user_cupboard_path(#user, #cupboard)
else
render :new
end
end
Params
private
def cupboard_params
# I used .fetch because .permit wasnt working. workaround found at http://stackoverflow.com/questions/24944871/actioncontrollerparametermissing-param-is-missing-or-the-value-is-empty-film
params.fetch(:cupboard, {}).permit(:style, :season, :neutral1, :neutral2, :accent1, :accent2)
end
end
Thank you for the assistance!
This happens because your form submits the styleattribute value like style instead of cupboard[style]. Looks like you're mixing form_for methods with form_tag ones and you need to replace radio_button_tag with radio_button. Also note that there are 2 forms of notations: either the function invocation radio_button(:model_name, :col_name, 'value') or the method invocation on the form object itself f.radio_button(:col_name, 'value'). Another issue with your form is that you're using label for each radio while it's designed to be used once for every group of radio buttons. Hence, the form should look like this:
<%= form_for #cupboard, html: { class: 'listform' } do |f| %>
<%= f.label :style, class: 'welcomelistitem' %>
<%= f.radio_button :style, 'ATHLEISURE' %>
<%= f.radio_button :style, 'CASUAL' %>
<%= f.radio_button :style, 'PROFESSIONAL' %>
<%= f.radio_button :style, 'WEEKEND' %>
<%= f.radio_button :style, 'FESTIVAL' %>
<%= f.radio_button :style, 'DATENIGHT' %>
<%= f.submit 'Make a new closet' %>
<% end %>
You'll then have to rewrite the cupboard_params method like this:
def cupboard_params
params.require(:cupboard).permit(:style)
end
Didn't have time to check if this works, but seems like it should.
The parameter "style" is not being passed inside of params[:cupboard]. It is directly in params.
I don't see the other params (besides style) in your form so I'm not sure what is going on there but try this
def cupboard_params
params.permit(:style)
end
Or change your radio buttons in the view to this
radio_button_tag('cupboard[style]', "ATHLEISURE")
This is my first question on here, so I am hoping I have not asked it incorrectly.
I have a generic new action on my tickets controller. Whenever I load tickets/new, it is creating a new item in the DB and committing it.
Here is the output from the server when the page is loading.
Started GET "/tickets/new" for ::1 at 2016-02-10 21:14:47 -0800
Processing by TicketsController#new as HTML
Customer Load (0.4ms) SELECT `customers`.* FROM `customers` WHERE `customers`.`email` = 'tim#tim.com' LIMIT 1
(0.3ms) BEGIN
SQL (0.5ms) INSERT INTO `tickets` (`category`, `created_at`, `updated_at`) VALUES (3, '2016-02-11 05:14:47', '2016-02-11 05:14:47')
(6.4ms) COMMIT
Rendered tickets/_new_form.html.erb (23.3ms)
Rendered tickets/new.html.erb within layouts/application (48.4ms)
Rendered layouts/_user_nav.html.erb (0.8ms)
Rendered layouts/_navbar.html.erb (0.5ms)
Rendered layouts/_flashes.html.erb (0.5ms)
Rendered layouts/_minimal.html.erb (759.5ms)
Completed 200 OK in 893ms (Views: 822.1ms | ActiveRecord: 21.3ms)
This is the from the tickets controller.
def new
#ticket = Ticket.new
end
Here is the code for the form.
<%= form_for(#ticket, html: { class: 'form-horizontal' }) do |f| %>
<%= f.error_notification %>
<%= f.hidden_field(:category) %>
<%= f.hidden_field(:severity) %>
<br>
<%= f.form_group :summary do |f| %>
<%= f.label :summary, class: 'control-label col-md-2' %>
<div class='col-md-8'>
<%= f.text_field :summary, class: 'form-control' %>
<%= f.error_messages %>
</div>
<% end %>
<%= f.form_group :detail do |f| %>
<%= f.label :detail, class: 'control-label col-md-2' %>
<div class='col-md-8'>
<%= f.text_area :detail, class: 'form-control' %>
<%= f.error_messages %>
</div>
<% end %>
<br>
</div>
<div class="form-actions col-md-offset-2 col-md-10">
<%= f.submit 'Create', class: 'btn btn-primary' %>
<%= link_to "Cancel", tickets_path, class: 'btn' %>
</div>
<% end %>
Here are the relevant routes.
resources :tickets do
collection do
get :step_1
get :new_ticket
get :billing_new_1
get :internet_step_1
get :internet_step_2
get :internet_modem_reset
get :internet_step_1
get :internet_step_2
get :internet_create_1
get :internet_create_2
get :tv_step_1
get :tv_step_2
get :tv_step_3
get :tv_create_1
get :tv_create_2
get :tv_create_3
get :closed
get :sidenav
end
member do
put :close
end
resources :notes
resources :appointments
end
Help!!
--Tim
INSERT INTO `tickets` (`category`, `created_at`, `updated_at`) VALUES (3, '2016-02-11 05:14:47', '2016-02-11 05:14:47')
This is getting category (3) from somewhere, suggesting that there is some functionality somewhere which is saving the #ticket.
The simplest explanation I can see is that you have a before_action somewhere. It would benefit to show your entire TicketsController:
#app/controllers/tickets_controller.rb
class TicketsController < ApplicationController
before_action :set_user #-> something like this??
end
Since you're new, you can make your routes much more succinct (multiple resources):
#config/routes.rb
methods = %i(step_1 new_ticket billing_new_1 internet_step_1 internet_step_2 internet_modem_reset internet_create_1 internet_create_2 tv_step_1 tv_step_2 tv_step_3 tv_create_1 tv_create_2 tv_create_3 closed sidenav)
resources :tickets do
resources :notes, :appointments
collection do
methods.each {|method| get method }
end
put :close, on: :member
end
I ended up starting the whole ticket class over.
I think the error was in my html.
My guess is that the if statement with the bang was causing the ticket to save, because of an enum with that name on the model.
Here is what I think was the bad html.
<% if #ticket.category == :tv %>
Ok. Your tv is down, but your internet is still working.
<br>
Do you have any more details to add? If so, add them here. If not, just hit sumbit and we will open up a ticket with all of the information that you have provided us.
<% elsif #ticket.internet! %>
Ok. Your internet is down, but your tv is still working.
<br>
Do you have any more details to add? If so, add them here. If not, just hit sumbit and we will open up a ticket with all of the information that you have provided us.
<% elsif #ticket.billing %>
I am fresh out of questions.
<br>
Do you have any more details to add? If so, add them here. If not, just hit sumbit and we will open up a ticket with all of the information that you have provided us.
<% elsif #ticket.category == :internet_and_tv %>
Ok. Your cable and internet are both down.'%>
<br>
Do you have any more details to add? If so, add them here. If not, just hit sumbit and we will open up a ticket with all of the information that you have provided us.'%>
<% else #ticket.category == :plant %>
<%end%>
In my rails project I have two models, Car Make & Car Model, with a 1:M relationship (i.e. one Audi has many Audi models).
In my Views page, I want a form with two input fields for car make & car model. Ideally, I will be able to input a car make (i.e. Audi) and the second input field will have a drop down menu with all the models available for the make (2016 Audi A6, 2017 Audi A7).
I've set up all the relations and in the models I have saved a foreign key of the make.
currently in _form.html.erb I have
<div class="field">
<%= f.label :make_id, "Make:"%><br>
<%#= f.number_field :make_id %>
<%= f.collection_select :make_id, Make.all,
:id,:makes_info, {:include_blank => 'Please Select'} %>
</div>
<div class="field">
<%= f.label :model_id, "Model:" %><br>
<%= f.collection_select :model_id, Model.all,
:id,:model_info, {:include_blank => 'Please Select'} %>
</div>
If you want it to truly be dynamic, you would need to use an AJAX request to update the second select after the first is picked. You'd also need to use the options_for_select method inside of the select tag
Some more info to accompany what was already provided.
It's known as dynamic select boxes:
#config/routes.rb
resources :makes do
get :models, on: :collection #-> url.com/makes/models
end
#app/controllers/makes_controller.rb
class MakesController < ApplicationController
def models
#make = Make.find(params[:make][:make_id])
respond_to do |format|
format.js
end
end
end
#app/views/makes/models.js.erb
$select = $("select#models");
$select.empty();
<% #make.models.each do |model| %>
$select.append($('<option>').text(<%=j model.name %>).attr('value', <%= model.id %>));
<% end %>
#views
<%= f.collection_select :make_id, Make.all, :id, :makes_info, {include_blank: 'Please Select'}, { data: { remote: true, url: make_models_path }} %>
<%= f.collection_select :model_id, Model.all, :id,:model_info, {include_blank: 'Please Select'}, { id: "models" } %>
I have implemented nested attributes using railscasts and I am using devise in application. Here is the simple structure.
I have two model ie Group and Invite.
Group Model has
accepts_nested_attributes_for :invites, :reject_if => lambda { |a| a[:email].blank? }, :allow_destroy => true
and it works perfectly fine.
here is the hash of the parameters.
Parameters: {"utf8"=>"✓", "authenticity_token"=>"uI0HmJrIE9Qh3pZW2qIJymeON/yjbFSgcH8bRx/x5Oo=", "group"=>{"name"=>"Test Group", "description"=>"", "invites_attributes"=>{"0"=>{"name"=>"Test User", "email"=>"test#example.com", "_destroy"=>"false"}}}, "commit"=>"Create Group"}
Now the problem is I want to store invitee user_id in invites table, But I can access current_user in invite model. So how can I modify the hash in order to save current_user id in invite model.
Basically things will be easy if my hash parameters is:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"uI0HmJrIE9Qh3pZW2qIJymeON/yjbFSgcH8bRx/x5Oo=", "group"=>{"name"=>"Test Group", "description"=>"", "invites_attributes"=>{"0"=>{"name"=>"Test User", "email"=>"test#example.com", "_destroy"=>"false", :invited_by => "1" }}}, "commit"=>"Create Group"}
How can I modify hash at controller end? or is there is better way to achieve this.
Maybe you could try using hidden_field.
So, you may write something like this (based on the Railscasts):
# /app/views/surveys/_question_fields.html.erb
<div class="fields">
<p>
<%= f.hidden_field :user_id, :value => "1" %>
<%= f.label :content, "Question" %>
<%= link_to_remove_fields "remove", f %><br />
<%= f.text_area :content, :rows => 3 %><br />
</p>
<% f.fields_for :answers do |builder| %>
<%= render 'answer_fields', :f => builder %>
<% end %>
<p><%= link_to_add_fields "Add Answer", f, :answers %></p>
</div>
From APIdock:
One important thing to remember is that this is NOT hidden in the
source code and can be modified by an evil user so all input in a
hidden field should be considered as untrustworthy and checked just
like a visible field.
Ref:
http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-hidden_field
http://apidock.com/rails/ActionView/Helpers/FormHelper/hidden_field