Rails Strong Parameters - allow deep nested hashes within array - ruby-on-rails

How to permit/white-list deep-nested hashes with very un-regular (impossible to declare) structure.
Example:
{"widgets" => [
{
"id" => 75432,
"conversion_goal_id" => 1331,
"options" => {"form_settings"=>{"formbuilder-bg-color"=>"rgba(255, 255, 255, 0)", "font-size"=>"14px", "form-field-depth"=>"42px"}, "linkedWidget"=>""},
"type" => "formbuilder-widget"
},
{
"id" => 75433,
"conversion_goal_id" => nil,
"options" => {"width"=>"200px", "height"=>"185px", "display"=>"block", "left"=>313, "top"=>152, "position"=>"absolute"},
"type" => "social-sharing-widget"
},
{},
]}
So options JSON/hash object doesn't have any specified structure.
It is formless.
It can be something like
{"width"=>"200px", "height"=>"185px", "display"=>"block", "left"=>313, "top"=>152, "position"=>"absolute"}
OR:
{"form_settings"=>{"formbuilder-bg-color"=>"rgba(255, 255, 255, 0)", "font-size"=>"14px", "form-field-depth"=>"44px"},
"linkedWidget"=>"",
"required_height"=>164,
"settings"=>
[{"field_options"=>{"include_other_option"=>true, "size"=>"large", "view_label"=>false},
"field_type"=>"text",
"label"=>"Name:",
"required"=>false,
"asterisk"=>false,
"textalign"=>"left"},
{"field_options"=>{"include_other_option"=>true, "size"=>"large", "view_label"=>false},
"field_type"=>"email",
"label"=>"Email:",
"required"=>false,
"asterisk"=>false,
"textalign"=>"left"},
{"buttonalign"=>"left",
"buttonbgcolor"=>"#ba7373",
"buttonfont"=>"Old Standard TT",
"buttonfontweight"=>"bold",
"buttonfontstyle"=>"normal",
"buttonfontsize"=>"18px",
"buttonheight"=>"46px",
"buttontxtcolor"=>"#ffffff",
"field_options"=>{"include_other_option"=>true, "size"=>"large", "view_label"=>false},
"field_type"=>"button",
"label"=>"START LIVING",
"required"=>true,
"textalign"=>"left"}]}
Widgets node is just Array.
I didn't found any info how to whitelist nested attributes within array of hashes.
How to do this?
I found some info in documentation that I can specify keys directly,
page_params.permit({widgets: [:key1, :key2]})
But this won't work, since I want to permit ALL attributes/keys within options hash.
This solution, also doesn't support arrays, but it allows to white-list nested objects:
params.require(:screenshot).permit(:title).tap do |whitelisted|
whitelisted[:assets_attributes ] = params[:screenshot][:assets_attributes ]
end
So how I can whitelist in every single element options attribute (array of hashes)?
REPLY TO COMMENTS:
I need to allow everything within options attribute in widget node. Widget node is in widgets array. I still need to prevent other fields e.g. link_text, 'text_value' etc in array - I don't want them to be submitted.
I need strong parameters to whitelist used parameters and backlist not used parameters. Some parameters exist only in front-end and don't exist in back-end. If I submit everything - then I will have exception.

Maybe I don't follow what you're trying to do here. Strong params are to prevent a user from submitting malicious data, so it basically whitelists certain keys. If you want to allow for everything, what do you need strong params for?

Related

How can I selectively add query parameters in redirect_to?

In my application, the session hash can contain the keys sort and ratings (in addition to _csrf_token and session_id), depending on what action a user takes. That is, it can contain both of them, either one of them, or neither, depending on what a user does.
Now, I wish to call redirect_to in my application and, at the same time, restore any session information (sort or ratings) the user may have provided.
To do this, I want to insert whatever key-value session has currently got stored (out of sort and ratings) as query parameters in my call to redirect_to. So, the path might look something like /movies?sort=...&ratings=....
I don't know how to write the logic for this. How can I do this? And how do I go about selectively inserting query parameters while calling redirect_to? Is it even possible to do this?
Any help is greatly appreciated. Thank you in advance.
First just compose a hash containing the parameters you want - for example:
opts = session.slice(:sort, :ratings)
.merge(params.slice(:sort, :ratings))
.compact_blank
This example would contain the keys :sort, :ratings with the same keys from the parameters merged on top (taking priority).
You can then pass the hash to the desired path helper:
redirect_to foos_path(**opts)
You can either just pass a trailing hash option or use the params option to explitly set the query string:
irb(main):007:0> app.root_path(**{ sort: 'backwards' })
=> "/?sort=backwards"
irb(main):008:0> app.root_path(params: { ratings: 'XX' })
=> "/?ratings=XX"
irb(main):009:0> app.root_path(params: { })
=> "/"
An empty hash will be ignored.
If your calling redirect_to with a hash instead of a string you can add query string parameters with the params: key:
redirect_to { action: :foo, params: opts }
If you're working with an arbitrary given URL/path and want to manipulate the query string parameters you can use the URI module together with the utilities provided by Rack and ActiveSupport for converting query strings to hashes and vice versa:
uri = URI.parse('/foo?bar=1&baz=2&boo=3')
parsed_query = Rack::Utils.parse_nested_query(uri.query)
uri.query = parsed_query.except("baz").merge(x: 5).to_query
puts uri.to_s # => "/foo?bar=1&boo=3&x=5"

Extracting specific JSON data

I receive (similar to) the following JSON data:
{"accountId"=>"some-private-really-long-account-id",
"stats"=>
{"score"=>
{"globalScore"=>
[{"key"=>"lifetimeScore", "value"=>"571",
"key"=>"someOtherKeyHere", "value"=>"someValue"}]}
I am not quite sure how I would get the lifetime score. I've tried doing stuff like this:
puts data["globalScore"]["lifetimeScore"]["value"]
But that doesn't work. (data is of course the JSON data received).
I believe the problem here is that data["globalScore"]["lifetimeScore"]["value"] doesn't reference a valid "path" within the JSON. Better formatting helps to clarify this:
hash = {
"accountId" => "some-private-really-long-account-id",
"stats" => {
"score" => {
"globalScore" => [
{
"key" => "lifetimeScore",
"value" => "571",
"key" => "someOtherKeyHere",
"value" => "someValue"
}
]
}
}
}
This Ruby hash has some issues since a hash can't actually have multiple values for a given key, but that aside,
hash['stats']['score']['globalScore'][0]['value']
is a perfectly valid way to access the 'value' field.
My point is that the problem with the original question is not that hash#dig(...) should be used (as shown by #Phlip), it is that the "path" through the Hash data structure was actually invalid.
hash.dig("globalScore", "lifetimeScore", "value)
will fail just like the bracketed syntax in the original question.
Use JSON.parse(body) to convert your json to a hash. Then use hash.dig('stats', 'score', 'globalScore', 0, 'value') to run queries on that hash.

When to use slice vs. permit in ActionController::Parameters?

Assuming I have an ActionController::Parameters object like
params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
I can call slice on it or permit to get/allow only certain parameters.
At first sight, they return the same thing
> params.slice(:a)
=> {"a"=>1}
> params.permit(:a)
[18:21:45.302147] Unpermitted parameters: b, c
=> {"a"=>1}
But if I call to_h on it params.slice(:a).to_h returns an empty hash, while params.permit(:a).to_h returns an hash with key :a. As far as I understood, this is the case, because :a was not permitted.
What I wonder now is, what is the use case of slice, if I could just use permit?
One difference I could think of is permit cuts nested hash if you don't explicitly specify the nested keys while slice allows nested hash:
# params = { a: 'a', nested: { nested_1: 1, nested_2: 2 } }
params.permit(:a, :nested) # => { a: 'a' }
params.slice(:a, :nested) # => { a: 'a', { nested_1: 1, nested_2: 2 } }
Another difference is in Rails 4, permit won't raise ActiveModel::ForbiddenAttributes when calling in .update_attributes(...) (answered here):
user.update_attributes(params.slice(:email)) # will raise ActiveModel::ForbiddenAttributes
user.update_attributes(params.permit(:email)) # wont raise error
slice gives ability to slice a hash with selected keys.
where as .permit returns a new ActionController::Parameters instance that includes only the given filters and sets the permitted attribute for the object to true. This is useful for limiting which attributes should be allowed for mass updating.
I would say slice is for everything dealing with Hash and permit is created using slice pattern but more in context of url params.
Hope it helps!
Also read this: http://apidock.com/rails/ActionController/Parameters/permit

Using best_in_place gem how do I specify a nil value for a select tag?

The answer on this question has provided me with a nice roadmap for how to generate select tags with data from a collection on an association.
This works nicely and everything is going great.
The issue I have now is, how do I handle an empty collection?
With the regular :type => :input, I can just specify :nil => "Some nil message here".
But that doesn't seem to work for the collection, and to make matters worse, when there is nothing in the collection it seems to be displaying some integers (i.e. 1 and 2). I am assuming those are the IDs from the previously displayed objects in the collection, but for obvious reasons that doesn't make much sense.
Any ideas on how I can handle an empty collection with this gem?
Thanks.
Edit 1:
One alternative is to just put my original best_in_place helper tag inside an if statement for when a collection is not nil. But then how does the user edit it when it is blank? Perhaps there may be no way to handle this, because it would involve creating a new record in the collection.
I use a "workaround" for the empty options in a select tag, it could help you:
:type => :select, :collection => #my_colletion || [[I18n.t('common.none'), -1]]
When #my_colletion is nil, it shows a choice named 'None' with id = -1 (wich is not that bad to handle in the backend).
This part of code assumes the #my_collection is an array of arrays like [ [key, value], [key, value], ... ] OR nil.
Although, if you want your MyModel.all collection to fit the conditions for best_in_place, you can use the following:
#my_collection = MyModel.all.map{ |object| [object.name, object.value] }
# => this returns an array like [ [key, value], [key, value], ... ]
# => returns an empty array (NOT nil) if there is no entry in the DB
About the -1 id:
Using the -1 id as 'none' is easy because you don't need to explicitly handle the value nil (tests, etc). With the -1 id, you can use the following:
MyModel.where(id: params[:id]).first # => Returns the first object that has the params[:id]
# if params[:id] is -1, it will return nil and not raise an error.
I hope it helped :)

Ruby/Rails - Can't access JSON object attributes directly in controller

I am setting up an API.
The client (using HTTParty) posts this to the API:
{:body =>
{
:product=> {:description=>"some text", :cost => "11.99"},
:brand=> {:name=>"BrandName", :etc =>"hey"}
}
}
The server/api receives the post.
Now, if I access params[:brand] I get:
{"name"=>"BrandName", "etc" =>"hey"}
If I do this:
Brand.new(params[:brand])
Then I get a new Brand object with the "name" and "etc" attributes populated correctly.
However, if I try to access params[:brand][:name], I just get nil
Any ideas?
Thanks.
Use params[:brand]["name"] or params["brand"]["name"]
Hash keys can be any sort of object. Common rails practice is to use symbols as hash keys, but when translated from JSON, the keys are likely to be strings.

Resources