Greg Blass
Greg Blass

Reputation: 3650

Rails 4 Strong Params with multiple objects and integer keys

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?

Upvotes: 10

Views: 3816

Answers (6)

Steve
Steve

Reputation: 7098

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.

Upvotes: 0

MikePopsicle
MikePopsicle

Reputation: 19

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

Upvotes: 0

mnishiguchi
mnishiguchi

Reputation: 2241

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>}

Upvotes: 0

Ryan Clark
Ryan Clark

Reputation: 764

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]]})

Upvotes: 1

Greg Blass
Greg Blass

Reputation: 3650

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.

Upvotes: 0

s_dolan
s_dolan

Reputation: 1276

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.

Upvotes: 6

Related Questions