Params hash from a SimpleForm with multiple records has a different structure for one record than for two - ruby-on-rails

I struggled to digest this into a title.
I'm using SimpleForm to construct a bulk-edit page with one or more fieldsets - one for each record in a collection of ActiveRecord models that have been built but not yet saved.
My form looks like this:
= simple_form_for :courses, method: :patch do |f|
- #courses.each do |course|
= field_set_tag do
= f.simple_fields_for 'courses[]', course do |c|
= c.input :title
.row
.medium-6.columns
= c.input :start_date, as: :string, input_html: { class: 'input-datepicker' }
.medium-6.columns
= c.input :end_date, as: :string, input_html: { class: 'input-datepicker' }
= f.submit 'Save', class: 'primary button'
The params hash for one record looks like this:
"courses"=>{"courses"=>[{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2017-07-31"}]}
with an array, while for two records it looks like this:
"courses"=>{"courses"=>{"1"=>{"title"=>"Course X", "start_date"=>"2018-01-16", "end_date"=>"2018-07-30"}, "2"=>{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2018-07-30"}}}
with a stringy-integer-keyed hash.
This becomes a problem when I try and use strong parameters. After much hacking, I ended up with this piece of code, which works for multiple records but fails when only one is submitted:
ActionController::Parameters
.new(courses: params[:courses][:courses].values)
.permit(courses: [:title, :start_date, :end_date])
.require(:courses)
It fails with param is missing or the value is empty: courses highlighting the .require(:courses) line above.
The problem is "solved" by harmonising the single-record case with the multiple-record case:
if params[:courses][:courses].is_a?(Array)
params[:courses][:courses] = { '1': params[:courses][:courses][0] }
end
but it feels like there should be a simpler way of doing it.
Is there a better way to write the form for this use-case? Am I missing a trick with strong parameters?
I'm using rails 5.0.5 and simple_form 3.5.0.

"but it feels like there should be a simpler way of doing it."
Yes, use ajax to send individual create/update requests. This can be done transparently to the user and provides simpler code and a far better user experience.
Rails has fields_for and accepts_nested_attributes that can be used to create/update multiple child records and the parent record in a single request. But it really requires a association that groups the records together and even at this can get really hacky and convoluted when it comes to validations.
You want to set it up so that you have a seperate form for each record:
- courses.each do |c|
= render partial: 'courses/_form', course: c
There is really nothing to the form:
# courses/_form.haml.erb
= simple_form_for course, remote: true, html: { 'data-type' => 'json', class: 'course_form'} do |f|
= c.input :title
.row
.medium-6.columns
= c.input :start_date, as: :string, input_html: { class: 'input-datepicker' }
.medium-6.columns
= c.input :end_date, as: :string, input_html: { class: 'input-datepicker' }
= f.submit 'Save', class: 'primary button'
Instead of using a js.erb template we use 'data-type' => 'json' and write our own handler as its easier to target the correct form:
$(document).on('ajax:success', '.course_form', function(event, xhr, status){
var $form = $(this);
alert('Course created');
if (this.method.post) {
// changes form to update instead.
this.method = 'patch';
this.action = xhr.getResponseHeader('Location');
}
});
$(document).on('ajax:error', '.course_form', function(event, xhr, status){
var $form = $(this);
// #todo display errors
});
Creating the controller is very straight forward:
class CoursesController
def create
#course = Course.new(course_params)
respond_to do |format|
if #course.save(course_params)
format.json { head :created, location: #course }
else
format.json do
render json: {
errors: #course.errors.full_messages
}
end
end
end
end
def update
#course = Course.find(params[:id])
respond_to do |format|
if #course.update(course_params)
format.json { head :ok }
else
render json: {
errors: #course.errors.full_messages
}
end
end
end
end

Keep your form, change strong params to this:
params.require(:courses).permit(
courses: [
:id,
:title,
:start_date,
:end_date
]
)
With this code params should be without index key, #courses is just an array:
# CoursesController
def new
#courses = []
# creating 3 items for example
3.times do
#courses << Course.new
end
end
def create
errors = false
#courses= []
# keep courses in the array for showing errors
courses_params[:courses].each do |params|
course = Course.new(params)
#courses << course
unless course.valid?
errors = true
end
end
if errors
render :new
else
# if no errors save and redirect
#courses.each(&:save)
redirect_to courses_path, notice: 'courses created'
end
end

It turns out that the f.simple_fields_for 'courses[]' ... method only gives that fieldset an ID if the form is populated by an existing record, and the params structure of a string ID mapping to a course hash is only used in this case. For "fresh" records, there is no ID and the course hashes are placed in a plain array.
This bit of code was running in the context of "rolling over" courses from one year to another - copying a previous course and changing the dates. This meant that each fieldset had the ID of the original course.
When the form was submitted, a new record was created and validated with the new attributes, and it was this fresh record with no ID that repopulated the form. The "it only happens when one course is submitted" thing was a red herring - a product of the test scenario.
So worth noting: f.simple_fields_for 'courses[]' ... creates an array for new records and a hash mapping IDs to attributes for existing records.

Related

Change View based on Option selected in select_tag in rails

I am trying to make a form where a user can wither post normally with their username like this - > normal
=form_for #confession , html: {multipart: true} do |f|
=f.label :Confess
=f.text_area :confession , require: true
=f.file_field :confessionimage
=f.select (:id,options_for_select(ID))
=f.submit 'Confess'
or Anonymously where their Names will be hidden and no one will know that who posted this post .. for this what I thought was I will make a user named anonymous in database and if a user select anonymous in select_form while posting they will post as an anonymous user.
for this in my controller I want something like this and main point that I can't understand is how can the controller know what user has selected ?
this is my controller
def index
#amitian = Amitian.where(institute: current_amitian.institute) if amitian_signed_in?
#confessions = Confession.where(amitian_id: #amitian.ids).order('created_at DESC') if amitian_signed_in?
#confession = current_amitian.confessions.build
#anonymous = Amitian.where(email: anonymous#anonymous.com)
# (if anonymous selected )
do #anonymous.confessions.build
end
To access this :id params you use confession_params[:id], or params[:confession][:id]
If you want to conditionally load different views you could use a respond_to block like this:
if confession_params[:id] == 1 #or params[:confession][:id] == 1
loaded_view = :foo
else
loaded_view = :bar
end
respond_to do |format|
format.html { render loaded_view }
end
This would load foo.html.erb or bar.html.erb, based on you #confession :id parameter, passed by your select on form_for tag, which loads parameter names based on your view's variable model.

Saving arrays in Rails 4.2.3

I am having some trouble saving arrays in Rails.
Rails version: 4.2.3 | Ruby version: 2.2.1 | DB: PostgreSQL
In my view, I have a collection of check boxes that shows the conferences that my member attended.
<%= f.fields_for :conferences_member do |conference| %>
<%= collection_check_boxes(:conferences_member, :conference_id, #all_conferences, :id, :name)%>
<% end %>
I put a break point (binding.pry) after the create action in my MembersController, and surprisingly, it shows the selected check boxes:
Processing by Database::MembersController#create as HTML
Parameters: {"utf8"=>"✓","authenticity_token"=>"XYZ==",
[...] "conferences_member"=> {"conference_id"=>["3", "5", ""]}, [...]
Now, if I go to rails c, and type ConferencesMember.last to check what was saved, I get:
pry(main)> ConferencesMember.last
ConferencesMember Load (0.5ms) SELECT "conferences_members".* FROM
"conferences_members" ORDER BY "conferences_members"."id" DESC LIMIT 1
=> nil
These are my associations:
#=> member.rb
has_one :conferences_member
accepts_nested_attributes_for :conferences_member, allow_destroy: true, reject_if: :all_blank
#=> conferences_member.rb
serialize :conference_id, Array
belongs_to :member
#=> members_controller.rb
params.require(:member).permit( [...]
:conference_member_attributes => [:id, :member_id, :conference_id => []],
[...])
I want to thank you in advance. I've tried almost everything here on StackOverflow, but I don't see my error.
Thank you again.
EDIT:
More of my MembersController:
def new
#member = Member.new
#member.build_conferences_member
end
def create
#member = Member.new(member_params)
binding.pry
end
The log doesn't show any error, it just shows that conferences were not saved at all.
First, your field needs to be renamed to nest the :conference_id in :conferences_member_attributes (not in :conferences_member as you do now). Take advantage of the form object yielded by fields_for:
<%= f.fields_for :conferences_member do |conference| %>
<%= conference.collection_check_boxes :conference_id, #all_conferences, :id, :name %>
<% end %>
You also need to actually save the record in the create action: Member.new builds the record but does not save it. Typically, the create action branches based on whether the record saved or did not (due to validations). So you might rewrite this method like so:
def create
#member = Member.new(member_params)
# when #member.save returns true, it saved to the db successfully
if #member.save
redirect_to members_path, notice: "Member #{#member.id} saved!"
# otherwise, it didn't save because of a validation error, so we render the error
# to the user and give them a chance to fix it
else
flash[:error] = "Member didn't save: #{#member.errors.full_messages.to_sentence}"
render :new
end
end
Lastly, to make sure your data gets through your strong parameters, check your logs for any messages that parameters were filtered out. The messages look like:
Unpermitted parameters: your_favorite_attribute

pre-populating form field from database with "second level" association

I have three models: Appointment, Client, and InsuranceProvider
A client has_many :appointments
And a client has_many :insurance_providers (the idea being I"d like to store historical info there).
in my view to create a new appointment, I have this (among other things):
<%= f.association :client, label_method: lambda { |c| "#{c.first_name} #{c.last_name}" }, collection: current_user.clients %>
this is fine, but I'd like to get to the copay field in insurance_providers.
Basically, this is how you'd get there:
appointment.client.insurance_provider.copay
What I'd like to do is pre-populate the "copay amount" field based on the client selected from the dropdown.
How can I do this?
Please let me know if you need to see my models or views explicitly.
If I understand correctly, you want a second select to be populated with values based on the value in the association.
Basically, you need JQuery/AJAX to do this for you. JQuery to watch the first select, and then AJAX to get data from rails based on the value chosen, and JQuery again to add values to the second select.
An alternative would be to use an in-place editor like best_in_place for each select, which would do the AJAX-y stuff for you.
Use ajax to to fetch the values for copay based on the return of the select.
Because there are a lot of steps, I'll lay them out, but you can find them in probably a dozen other SO questions.
Add the Javascript, this coffeescript but it's just your basic on change -> send-data call - so change at will.
#appointment.js.coffee
$(document).ready ->
$(".client_select").on "change", ->
$.ajax
url: "/appointments/new"
type: "GET"
dataType: "script"
data:
client: $(".client_select").val()
Make sure your form has the 2 jquery elements to get data from and push data to.
# First the field to pull from
<%= f.association :client, label_method: lambda { |c| "#{c.first_name} #{c.last_name}" }, collection: current_user.clients, input_html: { class: 'client_select' } %>
# And then the field to push to
<%= f.input :copay_amount, input_html: { class: 'copay_from_client' } %>
This is going to make a request on your "new" action of your appointments controller, so you'll need to add a javascript respond to to make sure it can render the next step, the UJS file.
# appointments_controller.rb
def new
# ... All the stuff you're normally doing and additionally:
#you'll have to adjust the params argument to match your select field
insurance_copay = Client.find(params[:client]).insurance_provider.copay
respond_to do |format|
format.html # new.html.erb
format.js { render "new", locals:{insurance_copay: insurance_copay} }
format.json { render json: #appointment }
end
end
Now add the UJS, new.js.erb
$(".copay_from_client").val('<%= #insurance_copay %>');

Rails 4 NOT updating nested attributes

Issue: Instead of updating nested attributes, they are being created on top of the existing nested attributes when I hit the #update action of the associated features_controller.rb
Likely Cause: I think the problem lies in my lack of understanding in Rails' form_for. I think the breakdown is in my views, how I render the persisting nested attributes, and/or how I fail to specify the nested attribute's id, causing it to simply create a new one
feature.rb
class Feature < ActiveRecord::Base
...
has_many :scenarios
accepts_nested_attributes_for :scenarios,
allow_destroy: true,
reject_if: :all_blank
...
end
features_controller.rb
def update
...
project = Project.find(params[:project_id])
#feature = Feature.find(params[:id])
if #feature.update_attributes(feature_params)
# checking feature_params looks good...
# feature_params['scenarios'] => { <correct object hash> }
redirect_to project
else
render :edit
end
end
...
private
def feature_params
params.require(:feature).permit(:title, :narrative, :price, :eta, scenarios_attributes[:description, :_destroy])
end
_form.html.haml (simplified)
= form_for [#project, #feature] do |f|
...
- if #feature.new_record? -# if we are creating new feature
= f.fields_for :scenarios, #feature.scenarios.build do |builder|
= builder.label :description, "Scenario"
= builder.text_area :description, rows: "3", autocomplete: "off"
- else -# if we are editing an existing feature
= f.fields_for :scenarios do |builder|
= builder.label :description, "Scenario"
= builder.text_area :description, rows: "3", autocomplete: "off"
I'm sure there's a nicer way to achieve the if #feature.new_record? check. I'm also using a few Javascript hooks to create dynamic nested attribute forms (which I've left out), heavily influenced by Railscast #196 Nested Model Form (revised)
I would love a really nice Rails-y implementation of dealing with these sorts of nested forms.
Try adding :id to the :scenario_attributes portion of your feature_params method. You only have the description field and the ability to allow a destroy.
def feature_params
# added => before nested attributes
params.require(:feature).permit(:id, :title, :narrative, :price, :eta, scenarios_attributes => [:id, :description, :_destroy])
end
As #vinodadhikary suggested, you no longer need to check if feature is a new record, since Rails, specifically using the form_for method, will do that for you.
Update:
You don't need to define if #feature.new_record? ... else in your form. It will be taken care by Rails when you use form_for. Rails checks if the action is going to be create or update based on object.persisted?, so, you can update your form to:
= form_for [#project, #feature] do |f|
...
= f.fields_for :scenarios, #feature.scenarios.build do |builder|
= builder.label :description, "Scenario"
= builder.text_area :description, rows: "3", autocomplete: "off"
As #Philip7899 mentioned as a comment in the accepted answer, allowing the user to set the id means that they could "steal" children records belonging to another user.
However, Rails accepts_nested_attributes_for actually checks the id and raises:
ActiveRecord::RecordNotFound:
Couldn't find Answer with ID=5 for Questionnaire with ID=5
Basically the ids are looked for in the children association (again, as said by #glampr). Therefor, the child record belonging to another user is not found.
Ultimately, 401 is the response status (unlike the usual 404 from ActiveRecord::RecordNotFound)
Follows some code I used to test the behaviour.
let :params do
{
id: questionnaire.id,
questionnaire: {
participation_id: participation.id,
answers_attributes: answers_attributes
}
}
end
let :evil_params do
params.tap do |params|
params[:questionnaire][:answers_attributes]['0']['id'] = another_participant_s_answer.id.to_s
end
end
it "doesn't mess with other people's answers" do
old_value = another_participant_s_answer.value
put :update, evil_params
expect(another_participant_s_answer.reload.value).to eq(old_value) # pass
expect(response.status).to eq(401) # pass
end
In conclusion, adding the id to the permitted params as stated above is correct and safe.
Fascinating Rails.

How to use jquery-Tokeninput and Acts-as-taggable-on

This is how you use autocomplete with jQuery Tokeninput and ActsAsTaggableOn.
In my situation i am using a nested form but it shouldnt matter. Everything below is code that works.
Code
Product Model:
attr_accessible :tag_list # i am using the regular :tag_list
acts_as_taggable_on :tags # Tagging products
Products Controller:
#1. Define the tags path
#2. Searches ActsAsTaggable::Tag Model look for :name in the created table.
#3. it finds the tags.json path and whats on my form.
#4. it is detecting the attribute which is :name for your tags.
def tags
#tags = ActsAsTaggableOn::Tag.where("tags.name LIKE ?", "%#{params[:q]}%")
respond_to do |format|
format.json { render :json => #tags.map{|t| {:id => t.name, :name => t.name }}}
end
end
Routes:
# It has to find the tags.json or in my case /products/tags.json
get "products/tags" => "products#tags", :as => :tags
Application.js:
$(function() {
$("#product_tags").tokenInput("/products/tags.json", {
prePopulate: $("#product_tags").data("pre"),
preventDuplicates: true,
noResultsText: "No results, needs to be created.",
animateDropdown: false
});
});
Form:
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tags.map(&:attributes).to_json %>
Issue 1(SOLVED)
Must have the line:
format.json { render :json => #tags.collect{|t| {:id => t.name, :name => t.name }}}
Note - You can use #tags.map here as well and you dont have to change the form either.
Below are the 2 issues on why you needed to do this:
I have the following Tag: {"id":1,"name":"Food"}. When I save a Product, tagged "Food", it should save as ID: 1 when it searches and finds the name "Food". Currently, it saves a new Tag with a new ID that references the "Food" ID, i.e. {"id":19,"name":"1"}. Instead, it should be finding the ID, showing the name, and doing a find_or_create_by so it doesn't create a new Tag.
Issue 2(SOLVED)
When I go to products/show to see the tags by doing <%= #product.tag_list %>. The name appears as "Tags: 1", when it really should be "Tags: Food".
How can I fix these issues?
You should define a route in your routes.rb which should handle products/tags path. You can define it like:
get "products/tags" => "products#tags", :as => :tags
Thus should give you a tags_path helper which should evaluate to /products/tags. This should get rid of the errors you mentioned in the question. Be sure to add this route before defining resources :product in your routes.rb
Now onto acts-as-taggable-on, I haven't used this gem, but you should look at method all_tag_counts documentation. Your ProductsController#tags method will need some changes on the following lines. I am not sure if its exactly what would be required, as I use Mongoid and can't test it out.
def tags
#tags = Product.all_tag_counts.(:conditions => ["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", "%#{params[:q]}%"])
respond_to do |format|
format.json { render :json => #tags.collect{|t| {:id => t.name, :name => t.name } }
end
end
little add-on:
If you want to create the tags on the fly, you could do this in your controller:
def tags
query = params[:q]
if query[-1,1] == " "
query = query.gsub(" ", "")
Tag.find_or_create_by_name(query)
end
#Do the search in memory for better performance
#tags = ActsAsTaggableOn::Tag.all
#tags = #tags.select { |v| v.name =~ /#{query}/i }
respond_to do |format|
format.json{ render :json => #tags.map(&:attributes) }
end
end
This will create the tag, whenever the space bar is hit.
You could then add this search setting in the jquery script:
noResultsText: 'No result, hit space to create a new tag',
It's a little dirty but it works for me.
There is a bug in Application.js code. There is an extra ) after "/products/tags.json". Remove the extra ). The code should be:
$("#product_tags").tokenInput("/products/tags.json", {
prePopulate: $("#product_tags").data("pre"),
preventDuplicates: true,
noResultsText: "No results, needs to be created.",
animateDropdown: false
});
I don't know if this is the entirety of your error, but you are not hitting the proper URL with the tokenInput plugin.
This
$("#product_tag_list").tokenInput("/products/tags.json"), {
should be
$("#product_tag_list").tokenInput("/products.json"), {
As I said, I don't know if this is the only problem you are having, but if you change this, does it work?
EDIT:
I have never used ActsAsTaggableOn. Does it create a Tag model for you to use?
From the looks of it on github, if you wanted to query all tags, you might have to use its namespace as opposed to just Tag, meaning ActsAsTaggableOn::Tag. For example, you can see how they access Tags directly in some of the specs.
I had problems with editing the tags if for example the model failed to validate,
I changed
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tags.map(&:attributes).to_json %>
to
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tag_list.map {|tag| {:id => tag, :name => tag } }.to_json %>
If the form failed to validate on first submission, it was creating tags as the ID's of the tags it had created on subsequent submissions.
Two notes: if you're getting the tags changed by numbers on the POST request, use:
tokenValue: "name"
And if you're trying to add non-existent tags, use (undocumented):
allowFreeTagging: true

Resources