2
votes

One StackOverflow question has asked what I need, but the self-answer didn't help me know what to do next. The scenario presented in that question (whitelisting deeply nested strong parameters in rails) is pretty much what I've got going on, but I'll post an abbreviation of mine (still very long) and hope someone--maybe even the Dave of the post--can help (I don't have enough reputation to comment and ask him). There are few links about nested strong parameters I haven't read, and I've dealt with some on many controllers and API endpoints, but this is the most complex in the app. (I'm including such a long example so you can see the full complexity.)

This is on the sales_controller, and one of the attributes we can't get to is the timezone_name, which is in the run_spans_attributes, which is in the options_attributes under sale. I've tried just about all of the different syntax approaches that match most of the nested attributes with strong parameters issues here on StackOverflow, but none of it has worked. Do I need more classes? Are there magic brackets? I need new suggestions. Please.

It should be noted that this is with the strong_parameters gem and Rails 3.2.21, but I want to get the app ready for Rails 4, so I'm hoping to avoid a short-term solution.

Sorry it's so long:

 Parameters:
    "sale"=>{
      "cloned_from"=>"",
      "type"=>"Localsale",
      "primary_contact_attributes"=>{
          "primary"=>"true",
          "first_name"=>"Fred",
          "id"=>"1712"
        },
      "contract_signed_on"=>"March 20, 2015",
      "billing_addresses_attributes"=>{
          "0"=>{
              "billing"=>"1",
              "city"=>"San Diego",
              "id"=>"29076"
            }
        },
      "other_contacts_attributes"=>{
          "0"=>{
              "first_name"=>"Fred",
              "_destroy"=>"false",
              "id"=>"170914"
            },
          "1"=>{
              "first_name"=>"Fred",
              "last_name"=>"Smith",
              "_destroy"=>"false",
              "id"=>"1798"
            }
        },
      "opportunity_detail_attributes"=>{
          "original_salesperson_id"=>"",
          "id"=>"10130"
        },
      "production_editor"=>"1868097",
      "event_sale_attributes"=>{
          "0"=>{
              "name"=>"is_super_sale",
              "value"=>"0",
              "id"=>"15326"
            },
          "1"=>{
              "name"=>"super_show_code",
              "value"=>""
            },
        },
      "scheduling_note"=>"",
      "category_ids"=>["2", "364"],
      "options_attributes"=>{
          "0"=>{
              "title"=>"T-Shirt and Bag Check",
              "event_starts_at(1i)"=>"2015",
              "event_starts_at(2i)"=>"6",
              "event_doors_open_at_attributes"=>{
                  "option_id"=>"8682604",
                  "doors_time(1i)"=>"",
                  "id"=>"278382"
                },
              "event_option_attributes"=>{
                  "0"=>{
                      "name"=>"event_duration",
                      "value"=>""
                    },
                  "1"=>{
                      "name"=>"send_pre_event_email",
                      "value"=>"1",
                      "id"=>"632546"
                    }
                },
              "language_id"=>"1",
              "run_spans_attributes"=>{
                  "0"=>{
                      "timezone_name"=>"Eastern Time (US & Canada)",
                      "_destroy"=>"false",
                      "id"=>"560878"
                    },
                  "1429320288130"=>{
                      "timezone_name"=>"Eastern Time (US & Canada)",
                      "_destroy"=>"false"
                    }
                },
              "_destroy"=>"false",
              "id"=>"8682604"
              }#ends 0 option
          },#ends options
      "coupons_per_redemption"=>"1",
      "methods_attributes"=>{
          "0"=>{
              "redemption_code"=>"0",
              "_destroy"=>"0",
              "id"=>"9797012"
            },
          "1"=>{
              "redemption_code"=>"4",
              "_destroy"=>"1",
              "vendor_provided_promo_code"=>"0",
              "promo_code"=>""
            }
        }, #ends redemption methods
      "addresses_attributes"=>{
          "0"=>{
              "street_address_1"=>"2400 Cat St",
              "primary"=>"0",
              "id"=>"2931074",
              "_destroy"=>"false"
            }
        },
      "zoom"=>"",
      "video_attributes"=>{
          "youtube_id"=>"",
        },
      "updated_from"=>"edit"
      }

Help me do this right? By the way, all kinds of .tap do |whitelisted| approaches have failed.

 private
 def_sale_strong_params
    params.require(:sale).permit(:how, :the, :heck, :do, :the_attributes =>
       [:make, themselves => [:known, :outside => [:of, :these => [:darn,
        :parentheses], :and], :brackets]])
 end
5
what are your failed trials? give one at least!caesarsol
It would be great if you could trim down your example to the bit that is causing problems - if addresses_attributes or other_contacts_attributes (for example) are ok, then no need to include them in your post.Frederick Cheung
you are using arrays and giving it keys and values, you should try replacing arrays with hashes when they have sub values. like so permit(:how, :the, :heck, :do, the_attributes: {:make, themselves: {:known, outside: {:of, these: [:darn, :parentheses], :and}, :brackets}}), of course keys like :of and :and ideally would have values tooMohammad AbuShady
@caesarsol, the format I included at the bottom failed, as did the format @Mohammad AbuShady suggests. At one point, @Frederick, the code was not able to access some of the attributes even in the addresses_attributes or other_contracts_attributes. I THINK I have managed to get them seen, but until the run_spans_attributes works, I won't know for sure. I thought it best to include them, just in case they're still an issue.wisetara

5 Answers

4
votes

1. Validate Your Progress in the Console

To configure Strong Parameters, it can help to work in the Rails console. You can set your parameters into an ActiveSupport::HashWithIndifferentAccess object and then start to test out what you'll get back from ActionController::Parameters.

For example, say we start with:

params = ActiveSupport::HashWithIndifferentAccess.new(
  "sale"=>{
    "cloned_from"=>"",
    "type"=>"Localsale"}
)

We can run this:

ActionController::Parameters.new(params).require(:sale).permit(:cloned_from, :type)

And we'll get this as the return value and we'll know we've successfully permitted cloned_from and type:

{"cloned_from"=>"", "type"=>"Localsale"}

You can keep working up until all parameters are accounted for. If you include the entire set of parameters that you had in your question...

params = ActiveSupport::HashWithIndifferentAccess.new(
  "sale"=>{
    "cloned_from"=>"",
    "type"=>"Localsale",
    ...
    "options_attributes"=>{
      "0"=>{
        "title"=>"T-Shirt and Bag Check",
        ...
        "run_spans_attributes"=>{
          "0"=>{
            "timezone_name"=>"Eastern Time (US & Canada)",
            ...
)

...you can get down to timezone_name with a structure that will look something like this:

ActionController::Parameters.new(params).require(:sale).permit(:cloned_from, :type, options_attributes: [:title, run_spans_attributes: [:timezone_name]])

The return value in the console will be:

{"cloned_from"=>"", "type"=>"Localsale", "options_attributes"=>{"0"=>{"title"=>"T-Shirt and Bag Check", "run_spans_attributes"=>{"0"=>{"timezone_name"=>"Eastern Time (US & Canada)"}, "1429320288130"=>{"timezone_name"=>"Eastern Time (US & Canada)"}}}}}

2. Break the Work Into Smaller Parts

Trying to handle all of the allowed attributes for each model all on one line can get confusing. You can break it up into several lines to make things easier to understand. Start with this structure and fill in the additional attributes for each model:

video_attrs = []  # Fill in these empty arrays with the allowed parameters for each model
addresses_attrs = []
methods_attrs = []
run_spans_attrs = [:timezone_name]
event_option_attrs = []
options_attrs = [:title, event_option_attributes: event_option_attrs, run_spans_attributes: run_spans_attrs]
event_sale_attrs = []
opportunity_detail_attrs = []
other_contacts_attrs = []
billing_addresses_attrs = []
primary_contact_attrs = []
sales_attributes = [
  :cloned_from, 
  :type, 
  primary_contact_attributes: primary_contact_attrs, 
  billing_addresses_attributes: billing_addresses_attrs,
  other_contacts_attributes: other_contacts_attrs,
  options_attributes: options_attrs, 
  opportunity_detail_attributes: opportunity_detail_attrs,
  event_sale_attributes: event_sale_attrs,
  methods_attributes: methods_attrs,
  addresses_attributes: addresses_attrs,
  video_attributes: video_attrs
]

Then you can just send *sales_attributes into the permitted parameters. You can verify in the console with:

ActionController::Parameters.new(params).require(:sale).permit(*sales_attributes)
1
votes

Try this out:

params.require(:sale).permit(:options_attributes => [:other_attributes, { :run_spans_attributes => [:timezone_name] }]
1
votes

Going by what I did in my controller, here's what I'd assume would work:

params.require(:sale).permit(:cloned_form, ... billing_addresses_attributes: [:id, :city, :building] ...)

Edfectively, put brackets around the attributes like "0"=> and don't forget to adf :id and :_destroy like I mentioned in my answer, as I ended up creating other models and used accepts_nested_attributes_for.

1
votes

For posterity, I want to let folks know what we finally discovered: Well, the information was out there, and in fact, I read it in Russ Olsen's Eloquent Ruby, which I thankfully remembered during a debugging session with a more advanced developer: "With the advent of Ruby 1.9, however, hashes have become firmly ordered."

Why was that important? I'd alphabetized the attributes on the controller, and because of how the run_span_attributes were set up, they NEEDED timezone_name to be in the front. And if it wasn't, then it couldn't get to it. You can only imagine how debugging this tormented us. But at least we know now: ORDER MATTERS. So, if all else fails, remember that.

0
votes

After some experimentation, I found this problem only seemed to exist with hash keys that were integer strings.

When I replaced integers strings with non-integer strings, strong parameters accepted the hash.

Assuming we can't change the data format in our params, one workaround is to allow arbitrary hashes at the level where you run into trouble (eg. :run_spans_attributes => {}).

If your form is used to modify a model, this open up the risk of mass assignment for the models you open up with the arbitrary hash -- you will want to keep that in mind.