Rails 4 Strong Params with multiple objects and integer keys
Asked Answered
L

6

10

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?

Latoyia answered 11/8, 2015 at 14:48 Comment(2)
what are mashes in mashes_params? Should that be derps?Scrivenor
Yes, thanks! Corrected that typo. That was my real code. I was using derps as a generic example.Latoyia
L
0

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.

Latoyia answered 11/8, 2015 at 16:31 Comment(1)
This only works if you can guarantee that you have no more than 4 nested records, right? So it doesn't seem to be a general-purpose solution to the underlying problem.Decimalize
H
5

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.

Hurling answered 11/8, 2015 at 15:8 Comment(0)
K
1

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]]})
Kail answered 13/12, 2016 at 19:50 Comment(1)
This answer is really excellent and deserves more visibility. I struggled with it a bit and ended up with something like: cu_keys = params[:company_users].keys params.require(:company_users).permit(cu_keys.map {|cuk| [cuk => [:manager, :supervisor]]}) Not as error-proof as Ryan's original post, but it works and seems way better than many of the other solutions!Hypochondriac
L
0

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.

Latoyia answered 11/8, 2015 at 16:31 Comment(1)
This only works if you can guarantee that you have no more than 4 nested records, right? So it doesn't seem to be a general-purpose solution to the underlying problem.Decimalize
V
0

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>}
Valdes answered 15/7, 2020 at 16:15 Comment(0)
R
0

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

Replevy answered 7/4, 2021 at 19:33 Comment(0)
P
0

In the controller, the cleanest solution I've found so far is this:

derp_params = params
  .permit(:derp => [:attribute, :another_attribute])
  .fetch(:derp) { {} }
  .values

If you actually want an exception to be raised when the key is missing, either call #require first or raise the exception in #fetch.

params.require(:derp)
derp_params = params
  .permit(:derp => [:attribute, :another_attribute])
  .fetch(:derp) { {} }
  .values

# or
derp_params = params
  .permit(:derp => [:attribute, :another_attribute])
  .fetch(:derp) { |key| raise ActionController::ParameterMissing, key, params.keys }
  .values

Of course, you could wrap all that up in a convenient method in your ApplicationController if you use this pattern a lot.

Bonus tip: the fields_for form helper accepts an :index option.

<% derps.each_with_index do |derp, index| %>
  <%= fields_for 'derps', derp, index: index do |f| %>
    <%= f.text_field :attribute %>
  <% end %>
<% end %>

# Will produce:
# <input name="derps[0][attribute]" ...etc.
Perform answered 11/10, 2023 at 19:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.