Rails: Merging a nested attribute with strong_params

nullnullnull picture nullnullnull · May 22, 2013 · Viewed 8k times · Source

In Rails 4, it's possible to merge extra parameters with user generated ones like so:

 params.require(:post).permit([:title, :body]).merge(user: current_user)

It's also possible to include nested attributes like so:

 params.require(:post).permit([:title, :body, sections_attributes: [:title, :section_type]])

Now, what if I wanted to merge extra parameters into a nested model. I tried this:

params.require(:post).permit([:title, :body, sections_attributes: [:title, :section_type]]).merge(user: current_user, sections_attributes: [user: current_user])

But when I check the params with debugger afterwards, I find that user has overwritten the other section_attributes rather than merging with them. Is there a better way to approach this problem?

Full backtrace
--------------

 - activemodel (4.0.0.rc1) lib/active_model/attribute_methods.rb:436:in `method_missing'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_methods.rb:131:in `method_missing'
 - activerecord (4.0.0.rc1) lib/active_record/nested_attributes.rb:432:in `block in assign_nested_attributes_for_collection_association'
 - activerecord (4.0.0.rc1) lib/active_record/nested_attributes.rb:431:in `assign_nested_attributes_for_collection_association'
 - activerecord (4.0.0.rc1) lib/active_record/nested_attributes.rb:322:in `comments_attributes='
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:42:in `_assign_attribute'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:53:in `block in assign_nested_parameter_attributes'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:53:in `assign_nested_parameter_attributes'
 - activerecord (4.0.0.rc1) lib/active_record/attribute_assignment.rb:33:in `assign_attributes'
 - activerecord (4.0.0.rc1) lib/active_record/core.rb:192:in `initialize'
 - activerecord (4.0.0.rc1) lib/active_record/inheritance.rb:27:in `new'
 - activerecord (4.0.0.rc1) lib/active_record/reflection.rb:189:in `build_association'
 - activerecord (4.0.0.rc1) lib/active_record/associations/association.rb:235:in `build_record'
 - activerecord (4.0.0.rc1) lib/active_record/associations/has_many_through_association.rb:102:in `build_record'
 - activerecord (4.0.0.rc1) lib/active_record/associations/collection_association.rb:114:in `build'
 - activerecord (4.0.0.rc1) lib/active_record/associations/collection_proxy.rb:229:in `build'
 - app/controllers/forum/topics_controller.rb:16:in `create'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/implicit_render.rb:4:in `send_action'
 - actionpack (4.0.0.rc1) lib/abstract_controller/base.rb:189:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/rendering.rb:10:in `process_action'
 - actionpack (4.0.0.rc1) lib/abstract_controller/callbacks.rb:18:in `block in process_action'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:422:in `_run__642753351245287313__process_action__callbacks'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:80:in `run_callbacks'
 - actionpack (4.0.0.rc1) lib/abstract_controller/callbacks.rb:17:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/rescue.rb:29:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/instrumentation.rb:31:in `block in process_action'
 - activesupport (4.0.0.rc1) lib/active_support/notifications.rb:159:in `block in instrument'
 - activesupport (4.0.0.rc1) lib/active_support/notifications/instrumenter.rb:20:in `instrument'
 - activesupport (4.0.0.rc1) lib/active_support/notifications.rb:159:in `instrument'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/instrumentation.rb:30:in `process_action'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/params_wrapper.rb:245:in `process_action'
 - activerecord (4.0.0.rc1) lib/active_record/railties/controller_runtime.rb:18:in `process_action'
 - actionpack (4.0.0.rc1) lib/abstract_controller/base.rb:136:in `process'
 - actionpack (4.0.0.rc1) lib/abstract_controller/rendering.rb:44:in `process'
 - actionpack (4.0.0.rc1) lib/action_controller/metal.rb:195:in `dispatch'
 - actionpack (4.0.0.rc1) lib/action_controller/metal/rack_delegation.rb:13:in `dispatch'
 - actionpack (4.0.0.rc1) lib/action_controller/metal.rb:231:in `block in action'
 - actionpack (4.0.0.rc1) lib/action_dispatch/routing/route_set.rb:80:in `dispatch'
 - actionpack (4.0.0.rc1) lib/action_dispatch/routing/route_set.rb:48:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/journey/router.rb:71:in `block in call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/journey/router.rb:59:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/routing/route_set.rb:654:in `call'
 - request_store (1.0.5) lib/request_store/middleware.rb:9:in `call'
 - warden (1.2.1) lib/warden/manager.rb:35:in `block in call'
 - warden (1.2.1) lib/warden/manager.rb:34:in `call'
 - rack (1.5.2) lib/rack/etag.rb:23:in `call'
 - rack (1.5.2) lib/rack/conditionalget.rb:35:in `call'
 - rack (1.5.2) lib/rack/head.rb:11:in `call'
 -  () home/timothythehuman/.rvm/gems/ruby-2.0.0-p0@whistlr/bundler/gems/remotipart-2d6e0949acc2/lib/remotipart/middleware.rb:30:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/params_parser.rb:27:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/flash.rb:241:in `call'
 - rack (1.5.2) lib/rack/session/abstract/id.rb:225:in `context'
 - rack (1.5.2) lib/rack/session/abstract/id.rb:220:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/cookies.rb:486:in `call'
 - activerecord (4.0.0.rc1) lib/active_record/query_cache.rb:36:in `call'
 - activerecord (4.0.0.rc1) lib/active_record/connection_adapters/abstract/connection_pool.rb:626:in `call'
 - activerecord (4.0.0.rc1) lib/active_record/migration.rb:366:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/callbacks.rb:29:in `block in call'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:392:in `_run__4051735323972233883__call__callbacks'
 - activesupport (4.0.0.rc1) lib/active_support/callbacks.rb:80:in `run_callbacks'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/callbacks.rb:27:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/reloader.rb:64:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/remote_ip.rb:76:in `call'
 - better_errors (0.9.0) lib/better_errors/middleware.rb:84:in `protected_app_call'
 - better_errors (0.9.0) lib/better_errors/middleware.rb:79:in `better_errors_call'
 - better_errors (0.9.0) lib/better_errors/middleware.rb:56:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/debug_exceptions.rb:17:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/show_exceptions.rb:30:in `call'
 - railties (4.0.0.rc1) lib/rails/rack/logger.rb:38:in `call_app'
 - railties (4.0.0.rc1) lib/rails/rack/logger.rb:21:in `block in call'
 - activesupport (4.0.0.rc1) lib/active_support/tagged_logging.rb:67:in `block in tagged'
 - activesupport (4.0.0.rc1) lib/active_support/tagged_logging.rb:25:in `tagged'
 - activesupport (4.0.0.rc1) lib/active_support/tagged_logging.rb:67:in `tagged'
 - railties (4.0.0.rc1) lib/rails/rack/logger.rb:21:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/request_id.rb:21:in `call'
 - rack (1.5.2) lib/rack/methodoverride.rb:21:in `call'
 - rack (1.5.2) lib/rack/runtime.rb:17:in `call'
 - activesupport (4.0.0.rc1) lib/active_support/cache/strategy/local_cache.rb:83:in `call'
 - rack (1.5.2) lib/rack/lock.rb:17:in `call'
 - actionpack (4.0.0.rc1) lib/action_dispatch/middleware/static.rb:64:in `call'
 - railties (4.0.0.rc1) lib/rails/engine.rb:511:in `call'
 - railties (4.0.0.rc1) lib/rails/application.rb:96:in `call'
 - rack (1.5.2) lib/rack/content_length.rb:14:in `call'
 - thin (1.5.1) lib/thin/connection.rb:81:in `block in pre_process'
 - thin (1.5.1) lib/thin/connection.rb:79:in `pre_process'
 - thin (1.5.1) lib/thin/connection.rb:54:in `process'
 - thin (1.5.1) lib/thin/connection.rb:39:in `receive_data'
 - eventmachine (1.0.3) lib/eventmachine.rb:187:in `run'
 - thin (1.5.1) lib/thin/backends/base.rb:63:in `start'
 - thin (1.5.1) lib/thin/server.rb:159:in `start'
 - rack (1.5.2) lib/rack/handler/thin.rb:16:in `run'
 - rack (1.5.2) lib/rack/server.rb:264:in `start'
 - railties (4.0.0.rc1) lib/rails/commands/server.rb:84:in `start'
 - railties (4.0.0.rc1) lib/rails/commands.rb:80:in `block in <top (required)>'
 - railties (4.0.0.rc1) lib/rails/commands.rb:75:in `<top (required)>'
 - bin/rails:4:in `<main>'

The params:

{"name"=>"stuff", "description"=>"", "topical_id"=>"1", "topical_type"=>"User", "comments_attributes"=>{"0"=>{"body"=>"1111111111111111111"}, "user"=>#<User id: 1, active: true, bio: nil, birthday: nil, image: nil, location: nil, real_name: nil, twitter_name: nil, username: "tbaron", website: nil, whuffie: #<BigDecimal:6365488,'0.0',9(18)>, slug: nil, created_at: "2013-05-31 00:42:28", updated_at: "2013-05-31 00:42:35", email: "[email protected]", encrypted_password: "$2a$10$jSrDsC9Ai.yFU5sttCxIiuRBthDUYiy9wWyZnie70qbp...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2013-05-31 00:42:35", last_sign_in_at: "2013-05-31 00:42:35", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", confirmation_token: nil, confirmed_at: nil, confirmation_sent_at: nil, unconfirmed_email: nil, failed_attempts: 0, unlock_token: nil, locked_at: nil>}, "user"=>#<User id: 1, active: true, bio: nil, birthday: nil, image: nil, location: nil, real_name: nil, twitter_name: nil, username: "tbaron", website: nil, whuffie: #<BigDecimal:6365488,'0.0',9(18)>, slug: nil, created_at: "2013-05-31 00:42:28", updated_at: "2013-05-31 00:42:35", email: "[email protected]", encrypted_password: "$2a$10$jSrDsC9Ai.yFU5sttCxIiuRBthDUYiy9wWyZnie70qbp...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 1, current_sign_in_at: "2013-05-31 00:42:35", last_sign_in_at: "2013-05-31 00:42:35", current_sign_in_ip: "127.0.0.1", last_sign_in_ip: "127.0.0.1", confirmation_token: nil, confirmed_at: nil, confirmation_sent_at: nil, unconfirmed_email: nil, failed_attempts: 0, unlock_token: nil, locked_at: nil>}

Answer

charleyc picture charleyc · May 29, 2013

The merge method that you're calling is just the usual Ruby Hash#merge method. For each key in the argument of merge, the value overwrites the value that is currently present for that key, if any. In this case, you are overwriting the value of sections_attributes.

Since sections_attributes is a "pseudo-array" of the form {"0" => first_hash, "1" => second_hash} and so on, you need a way to duplicate your user once per entry. You can do that by using the ability of merge take a block that changes the merged value. Here's one approach:

filtered_params = params.require(:post)
                        .permit([:title, 
                                 :body, 
                                 sections_attributes: [:title, :section_type]])
additional_params = {user: current_user, sections_attributes: [user: current_user]}
result = filtered_params.merge(additional_params) do |key, oldval, newval|
  if newval.is_a? Array
    # Arrays are expected to be one-element arrays containing a Hash that is
    #   supposed to be merged into each element of the currently-existing
    #   "pseudo-array"
    oldval ||= {}
    Hash[oldval.map {|k, v| [k, v.merge(newval.first)]}]
  elsif newval.is_a? Hash
    # Hashes are merged into existing hashes
    oldval ||= {}
    oldval.merge newval
  else
    # Other types are passed as-is (and replace any existing value)
    newval
  end
end

# This marks the newly added parameters as permitted.  It's only necessary because we
# made new Hashes when we modified the "pseudo-array"
result.permit!

If you find that this works for you, you could encapsulate this in a method, either by taking the inside of the block and making it a separate method, or by installing the whole thing as a named method on the Parameters class.

Note also that the above only handles one layer of recursion. Going deeper than this is a bit tricky. If you need to do it, I'd be happy to write up an explanation.