Rails 4 Strong Params with multiple objects and integer keys - ruby-on-rails

I'm submitting a form with 2-4 objects at once, depending on how many the parent has. I realize that this is probably unconventional, but I really wanted the user to be able to edit all of the objects at once on one form. On my form, I'm doing:
<%= simple_fields_for "derps[]", derp do |f| %>
<% end %>
Then I'm doing this in the controller:
def update
#derps = []
#rejects = []
derps_params.each do |key, hash|
derp = Derp.find(key)
derp.assign_attributes(hash)
#rejects << derp unless derp.save
end
if #rejects.empty?
redirect_to #parent, flash: {success: 'Derps were successfully updated.'}
else
#derps = #rejects
render :edit
end
end
Lets say there are two objects - the params are coming through as:
"derps"=>{"1"=>{"attribute"=>"39", "another_attribute"=>"serp", "a_third_attribute"=>"yerp"}, "2"=>{"attribute"=>"30", "another_attribute"=>"49", }}
I had this working in Rails 3 without strong params. I'm upgrading to rails 4 and I'm struggling with how to get this working - I keep getting "Unpermitted parameters: 1, 2"
I'm assuming I need to do something like:
def mashes_params
params.require(:derps).permit(
id: []
or
def mashes_params
params.require(:derps).permit(
:id,
Something along those lines, but I've tried it every way I can think of without luck.
Any ideas here?

I've found that the command line is immensely helpful for debugging Strong Parameters in Rails 4. Here's how I tested your problem in the console:
rails c # From within your project directory, short for 'rails console'
params = ActionController::Parameters.new( { derps: { 1 => { attribute: 39, another_attribute: "serp" }, 2 => { attribute: 30, another_attribute: 49 } } } )
params # To make sure that the object looks the same
permitted = params.require( :derps ).permit( 1 => [ :attribute, :another_attribute ], 2 => [ :attribute, :another_attribute ] )
permitted # To see what you'd get back in your controller
Hopefully with this tool, you'll be able to debug anything that my answer didn't provide more easily than trial and error.

Final Edit (hopefully):
Had to rethink this from the ground up. I came to the conclusion: Since :id works as a wildcard, but is not allowed as the key of the hash, why not always make the keys 1-4, so I can whitelist them explicitly, then get the ID from a key-value in the hash, much like is done in traditional form nesting? Thats how I ended up solving it. Here's the final implementation that I have working:
<% i = #parent.derps.index(derp) + 1 %>
<%= simple_fields_for "derps[#{i}]", derp do |f| %>
<%= f.hidden_field :id, value: derp.id %>
<%= render "rest_of_the_fields" %>
<% end %>
Then in the controller:
def update
#derps = []
#rejects = []
derp_params.each do |key, hash|
derp = Derp.find(hash.delete("id"))
derp.assign_attributes(hash)
#rejects << derp unless derp.save
end
if #rejects.empty?
redirect_to #parent, flash: {success: "Derps updated successfully."}
else
#derps = #rejects
render :edit
end
end
Then here are the strong params:
def derp_params
p = [:id, :attribute_1, :another_attribute, ...]
params.require(:derps).permit(
"1" => p, "2" => p, "3" => p, "4" => p
)
end
Phew. Hope this helps someone.

The absolute best solution I've seen is here:
def product_params
properties_keys = params[:product].try(:fetch, :properties, {}).keys
params.require(:product).permit(:title, :description, properties: properties_keys)
end
I made one more change to iterate through the unnamed keys since my property_keys have more nested keys and values:
response_keys = params[:survey][:responses].try(:fetch, :properties, {}).keys
params.require(:survey).permit(responses: response_keys.map {|rk| [rk => [:question_id, :answer_id, :value]]})

Here is the approach I am currently using. You can permit each nested params one by one like this:
params = ActionController::Parameters.new(
"derps" => {
"1" => {
"attribute" => "39",
"another_attribute" => "serp",
"a_third_attribute" => "yerp"
},
"2" => {
"attribute" => "30",
"another_attribute" => "49"
}
}
)
# => <ActionController::Parameters {"derps"=>{"1"=>{"attribute"=>"39", "another_attribute"=>"serp", "a_third_attribute"=>"yerp"}, "2"=>{"attribute"=>"30", "another_attribute"=>"49"}}} permitted: false>
params.fetch(:derps).map do |i, attrs|
[
i,
ActionController::Parameters.new(attrs).permit(
:attribute,
:another_attribute,
:a_third_attribute,
)
]
end.to_h.with_indifferent_access
#=> {"1"=><ActionController::Parameters {"attribute"=>"39", "another_attribute"=>"serp", "a_third_attribute"=>"yerp"} permitted: true>, "2"=><ActionController::Parameters {"attribute"=>"30", "another_attribute"=>"49"} permitted: true>}

Here is a sort of dirty way of accomplishing this which builds on the answer above by Greg Blass
This can handle an infinite number of indexes with nested params
def foo_bar_params
num_keys = params[:foo_bars].keys.size
the_params = [:id, :attr1, :attr2, :another]
permit_hash = {}
i = 0
while i < num_entries
permit_hash[i.to_s] = the_params
i += 1
end
params.require(:foo_bars).permit(permit_hash)
end
Im sure there is a fancier way to do this, but this way is readable and I can easily tell what is going on...and most importantly it works

Related

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

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.

update_all ruby on rails

Im trying to update all post where a condition is true. If the condition is true should the field category_id be set to params[:category_id]
Every time im trying to do it will my code update all post where the condition is true and set it to "--- !ruby/hash:ActionController::Parameters categori_id: '169'"
Instead of just 169.
My controller action look like this
def update_all_notes
#deletefolder = Categori.find(params[:id])
System.where(:categori_id => params[:id]).update_all(:categori_id => params[:categori_id])
redirect_to :back
end
My form look like this:
<%= form_tag update_all_notes_path(category.id) do %>
<%= collection_select :kategori_id, :id, #current_company.category.where.not(:name => category.name), :id, :name %>
<button>move</button>
this is the parameters i send to the action
"categori_id"=>{"categori_id"=>"169"},
"id"=>"168"}
Thanks in advance
From your hash you should replace params[:categori_id] to be params[:categori_id][:categori_id]
as the hash is { "categori_id" => {"categori_id" => 169}, "id" => X }
Example of update_all use:
ids = [1,2,3]
records = Mammal::Human.where(id: ids)
records.update_all(status: :enlightenment, enlightenment_at: Time.zone.now, enlightenment_by: "myself")
I will rather use status: value over :status => value syntax

Rails accessing variable in email template

I'm still on the rather steep side of the Rails learning curve, so please pardon the rather simplistic nature of this question, but Google's just not proving very helpful.
So, my issue is this. I have a controller that is calling a mailer. (Code snippet below)
The problem I can't seem to get passed is that no matter how I try to access the values in the rhtml page, I either get errors or nothing at all.
This is the controller snippet (#person is working just fine. #item is what's not working)
if params[:id] == 'username'
item_value = #user[:login]
elsif params[:id] == 'password'
item_value = #user[:new_password]
end
#item = { 'name' => params[:id], 'val' => item_value }
ApplicantMailer.deliver_forgot(#person.email, #person, #item)
This is the mailer method snippet:
def forgot(recipient, person, item, sent_at = Time.now)
#subject = 'Site Password Retrieval'
#body['person'] = person
#body['item'] = item
#recipients = 'rdavis#localhost'
#from = CONTACT_EMAIL
#sent_on = sent_at
#headers = {}
logger.debug #body.to_yaml
end
This is the rhtml snippet:
Dear <%= #person.first_name %>,
You are receiving this email because you or someone else has used the lost <%= #item[:name] %> page from the login page.
Your <%= #item[:name] %> for your account is: <%= #item[:val] %>
So, like I said, when I try to access the values for the #item, if I use #item.name it throws a missing method error and if I use the version listed above, it doesn't show anything.
I know I'm working with a hash & thought that trying to access the keys like I showed here was the right way. Obviously, I'm missing something here.
Can someone point me in the right direction, please? Thanks!
You are using strings when creating your hash and symbols when accessing it.
You want the following:
#item = { :name => params[:id], :val => item_value }
Which is distinct from:
#item = { 'name' => params[:id], 'val' => item_value }
You can try this in irb with the following.
hash = {"a" => "val1", :a => "val2", "b" => "val3"}
hash["a"] => "val1"
hash[:a] => "val2"
hash["b"] => "val3"
hash[:b] => nil

Rails: Preserving GET query string parameters in link_to

I have a typical search facility in my app which returns a list of results that can be paginated, sorted, viewed with a different records_per_page value, etc. Each of these options is controlled by parameters in the query string. A simplified example:
/search?q=test&page=2
Now say I need to display a set of links that set records_per_page value to 10, 20, 30. Each link must include the existing query parameters, which can be a very long set, plus a new per_page parameter.
/search?q=test&page=2& ... &per_page=10
/search?q=test&page=2& ... &per_page=20
/search?q=test&page=2& ... &per_page=30
Is there an easy way to do it with just link_to helper or I need to parse and reproduce the query string from previous request somehow?
link_to 'Link', request.query_parameters.merge({:per_page => 20})
link_to 'Link', params.merge({:per_page => 20})
The simplest way to merge the new params with the query parameters and NOT with all parameters (including those obtained through the path) is to merge with request.query_parameters
link_to 'Search', search_path(request.query_parameters.merge({ per_page: 20 }))
Otherwise you end up with query strings duplicating the path parameters, for example ?action=index&controller=products&foo=bar instead of ?foo=bar.
If you want to keep existing params and not expose yourself to XSS attacks, be sure to clean the params hash, leaving only the params that your app can be sending:
# inline
<%= link_to 'Link', params.slice(:sort).merge(per_page: 20) %>
If you use it in multiple places, clean the params in the controller:
# your_controller.rb
#params = params.slice(:sort, :per_page)
# view
<%= link_to 'Link', #params.merge(per_page: 20) %>
You can just throw elements of the params hash at link_to. Like
link_to "some_other_link", "/search", :page => params[:page]
This works if the links you are processing aren't given to you by request.params.
require 'rack/utils'
require 'uri'
def modify_query url, options={}
uri = URI(url)
query_hash = Rack::Utils.parse_query(uri.query)
query_hash.merge!(options)
uri.query = Rack::Utils.build_query(query_hash)
uri.to_s
end
puts modify_query('/search?q=test&page=2&per_page=10', 'per_page' => 20)
puts modify_query('/search?q=test&page=2', 'per_page' => 30)
# Outputs
# /search?q=test&page=2&per_page=20
# /search?q=test&page=2&per_page=30
What about
<%= link_to 'Whatever', :overwrite_params => { :pear_page => 20 } %>
?
A bit late i know..
If your using this as a way to filter search results have a look at my helper :)
This automagicly removes all blank and unneeded params and add the class "selected" if all of it's new params were already set.
def search_to s, args={}
selected = 0
args.each do |k, v|
selected = selected + 1 if params[k] == v.to_s || ( params[k].nil? && v.blank? )
end
if #search_params_base.nil?
#search_params_base = request.parameters.clone
#search_params_base.delete(:action)
#search_params_base.delete(:controller)
#search_params_base.delete(:page)
#search_params_base.delete_if{|k, v| v.nil? || v.blank?}
#search_params_base.delete(:utf8) if #search_params_base[:keywords].nil?
end
search_params = #search_params_base.merge(args)
search_params.delete_if{|k, v| v.nil? || v.blank?}
link_to s, search_path + '?' + search_params.to_param, :class => selected == args.length ? 'selected' : nil
end
You can then just use this in your view:
search_to '$80 to $110', :price => 80..110
Or in your case:
search_to '30 per page', :page => params[:page], :per_page => 30

How to make optional :conditions for a find

Hello I have the followong struggle in my head. I want a text-field in which the use can type in some parameters, which will be used as filter-criteria for the :conditions hash in my find method.
I have created a helper, with takes an option and merge the hash to the options:
In my controller:
#bills = adminbill_filter(:limit=>params[:limit] || 50,:offset=>params[:offset] || 0, :conditions=>params[:options])
In my helper:
def link_to_with_current(text, link, condition, *args)
options = args.first || {}
options[:class] = condition ? 'current' : nil
link_to text, link, options
end
In my view:
<%= text_field :filter ,:criteria, :class=>'roundRect',:id=>'name', :value=>12009%>
<%= button_to_with_filter 'Start Filter', 'index', :filter_condition=>true, :options=>{:id=>81}%>
Is it somehow possible to pass the value of text_field into the :option=>{...} of the button_to_with_filter? I find this solution (if it is working) quite unhandy. Your comments are as always very helpful.
Greetings
Matthias
It seems kind of terrifying to put in the contents of user-submitted params without vetting them in any capacity. You're probably going to run into all kinds of exceptions if the data doesn't come in as expected, or is formulated to be malicious.
I've found it's often easier to use a chained scopes approach:
def index
bills_scope = Bill
# Use an example Bill.with_id scope
if (params[:with_id])
bills_scope = bills_scope.with_id(params[:with_id])
end
# Repeat as required
# Finally, use the scope to retrieve matching records
#bills = bills_scope.paginated
end
Using something like will_paginate can help with your offset and limit values.
If the text field and button were encapsulated in a form, and the button was the submit button, the text field's value would automatically be brought into the params hash. Then you wouldn't have to deal with it. I can't recall at the moment the exact Rails helpers that will do this for you, but you want the resulting form to probably be something like this:
<% form_for :options, :url => {:action => :index}, :html => { :method => :get } do |f| %>
<%= f.text_field :filter ,:criteria, :class=>'roundRect',:id=>'name', :value=>12009%>
<%= f.submit 'Start Filter' %>
<% end %>
Which may change some, since I don't know the underlying code behind your methods.
Otherwise, the only thing I can think of is using a Javascript event on the button that grabs the value of the text field before it submits.
Thanks for your help, I came across named_scope and solved the problem with the following code:
Bill model:
class Bill < ActiveRecord::Base
# named_scope werden fuer Filterfunktionen bei Adminbill benoetigt
named_scope :standard, :order => "created_at DESC"
named_scope :limit, lambda {|*args| {:limit=>(args.first)|| 50}}
named_scope :offset, lambda {|*args| {:offset=>(args.first || 10)}}
named_scope :paid, :conditions=>"paid IS NOT NULL"
named_scope :not_paid, :conditions=>{:paid=>nil}
named_scope :test_bill, :conditions => {:test_bill=>true}
named_scope :no_test_bill, :conditions => {:test_bill=>false}
named_scope :find_via_bill_id, lambda {|*args|{:conditions=>{:id=>(args.first || 210)}}}
named_scope :find_via_email, lambda {|*args| {:conditions=>{:buyer_id=>args.first}}}
controller:
def index
logger.debug "The object is #{current_user}"
if params[:filterInput] != nil && !params[:filterInput].empty?
filter_array = params[:filterInput].split('&')
bill_scope = Bill.scoped({})
bill_scope = bill_scope.standard
# Filtere via Regexp-Matching die Zahlen der Eingabe heraus
filter_array.each do |el|
if el =~ /limit\([0-9]+\)/
number =
bill_scope = bill_scope.limit(el.scan(/\d+/)[0])
elsif el =~ /offset\([0-9]+\)/
bill_scope = bill_scope.offset(el.scan(/\d+/)[0])
elsif el == 'paid'
bill_scope = bill_scope.paid
elsif el == 'not_paid'
bill_scope = bill_scope.not_paid
elsif el == 'test_bill'
bill_scope = bill_scope.test_bill
elsif el =~ /find_via_bill_id\([0-9]+\)/
bill_scope = bill_scope.find_via_bill_id(el.scan(/\d+/)[0])
elsif el =~ /find_via_email\([A-Za-z0-9.#-]+\)/
email = el.scan(/\([A-Za-z0-9.#-]+\)/)[0]
# TODO geht bestimmt auch eleganter durch besseres Matching
email = email.gsub("(", "")
email = email.gsub(")", "")
user = User.find_by_email(email) unless User.find_by_email(email).blank?
bill_scope = bill_scope.find_via_email(user.id)
end
end
#bills = bill_scope
else
#bills = Bill.standard.limit.offset
end
And in the view:
<% form_tag(:action => 'index') do %>
<%= text_field_tag 'filterInput', nil, :size => 40 %>
<%= submit_tag 'Start Filter'%>
<% end %>
Now you can pass in the tex-field e.g.the following valid expression: paid&limits(20)
I know that the controller solution isn't very elegant but for me it was the fastest way to solve this problem.

Resources