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
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)
Try this out:
params.require(:sale).permit(:options_attributes => [:other_attributes, { :run_spans_attributes => [:timezone_name] }]
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.
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.
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.
Related
I'm doing some custom logging in my Rails application and I want to automatically sensor some parameters. I know that we have fitler_parameter_logging.rb which does this for the params object. How can I achieve something like this for my custom hash.
Let's say I'm logging something like this:
Rails.logger.info {name: 'me', secret: '1231234'}.inspect
So my secret key should be sensored in the logs.
I know I can personally delete the key before logging, but it adds noise to my application.
The question title talks about removing the parameters, but your question refers to censoring the parameters similar to how Rails.application.config.filter_parameters works. If it's the latter, it looks like that's already been answered in Manually filter parameters in Rails. If it's the former, assuming a filter list, and a hash:
FILTER_LIST = [:password, :secret]
hash = {'password' => 123, :secret => 321, :ok => "this isn't going anywhere"}
then you could do this:
hash.reject { |k,v| FILTER_LIST.include?(k.to_sym) }
That'll cope with both string and symbol key matching, assuming the filter list is always symbols. Additionally, you could always use the same list as config.filter_parameters if they are going to be the same and you don't need a separate filter list:
hash.reject { |k,v| Rails.application.config.filter_parameters.include?(k.to_sym) }
And if you wanted to make this easier to use within your own logging, you could consider monkey patching the Hash class:
class Hash
def filter_like_parameters
self.reject { |k,v| Rails.application.config.filter_parameters.include?(k.to_sym) }
end
end
Then your logging code would become:
Rails.logger.info {name: 'me', secret: '1231234'}.filter_like_parameters.inspect
If you do monkey patch custom functionality to core classes like that though for calls you're going to be making a lot, it's always best to use a quite obtuse method name to reduce the likelihood of a clash with any other library that might share the same method names.
Hope that helps!
I am trying to update a columns value to a hash in database. The column in database is text.
In model i have,
serialize :order_info
In controller i have update action
def update
Order.update_order_details(update_params, params[:order_info])
head :no_content
end
I am not doing strong parameters for order_info because order_info is an arbitrary hash and after doing research, strong params doesnt support an arbitrary hash
The value that i am trying to pass is like below
"order_info": {
"orders": [
{
"test": "AAAA"
}
],
"detail": "BBBB",
"type": "CCCC"
}
But when i try to update the value it gets updated in database like
--- !ruby/object:ActionController::Parameters parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess comments: - !ruby/hash:ActiveSupport::HashWithIndifferentAccess test: AAAA detail: BBBB type: CCCC permitted: false
serialize is an instance of ActiveSupport::HashWithIndifferentAccess so i am guessing thats why its in the value. How can i get rid of the extra stuff and just update the hash?
If you want to unwrap all the ActionController::Parameters stuff from params[:order_info] without any filtering then the easiest thing to do is call to_unsafe_h (or its alias to_unsafe_hash):
hash = params[:order_info].to_unsafe_h
In Rails4 that should give you a plain old Hash in hash but AFAIK Rails5 will give you an ActiveSupport::HashWithIndifferentAccess so you might want to add a to_h call:
hash = params[:order_info].to_unsafe_h.to_h
The to_h call won't do anything in Rails4 but will give you one less thing to worry about when you upgrade to Rails5.
Then your update call:
Order.update_order_details(
update_params,
params[:order_info].to_unsafe_h.to_h # <-------- Extra DWIM method calls added
)
should give you the YAML in the database that you're looking for:
"---\n:order_info:\n :orders:\n - :test: AAAA\n :detail: BBBB\n :type: CCCC\n"
You might want to throw in a deep_stringify_keys call too:
params[:order_info].to_unsafe_h.to_h.deep_stringify_keys
depending on what sort of keys you want in your YAMLizied Hash.
I am working on an app in Rails 4 using i18n-active_record 0.1.0 to keep my translations in the database rather than in a .yml-file. It works fine.
One thing that I am struggling with, however, is that each translation record is one record per locale, i.e.
#1. { locale: "en", key: "hello", value: "hello")
#2. { locale: "se", key: "hello", value: "hej")
which makes updating them a tedious effort. I would like instead to have it as one, i.e.:
{ key: "hello", value_en: "hello", value_se: "hej" }
or similar in order to update all instances of one key in one form. I can't seem to find anything about that, which puzzles me.
Is there any way to easily do this? Any type of hacks would be ok as well.
You could make an ActiveRecord object for the translation table, and then create read and write functions on that model.
Read function would pull all associated records then combine them into a single hash.
Write function would take your single hash input and split them into multiple records for writing/updating.
I ended up creating my own Translation functionality using Globalize. It does not explicitly rely on I18n so it is a parallell system but it works, although not pretty and it is not a replacement to I18n but it has the important functionality of being able to easily add a locale and handle all translations in one form.
Translation model with key:string
In Translation model:
translates :value
globalize_accessors :locales => I18n.available_locales, :attributes => [:value]
In ApplicationHelper:
def t2(key_str)
key_stringified = key_str.to_s.gsub(":", "")
t = Transl8er.find_by_key(key_stringified)
if t.blank?
# Translation missing
if t.is_a? String
return_string = "Translation missing for #{key_str}"
else
return_string = key_str
end
else
begin
return_string = t.value.strip
rescue
return_string = t.value
end
end
return_string
end
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?
Currently my JSON request is returning the below, where each person/lender has many inventories.
#output of /test.json
[
{"id":13, "email":"johndoe#example.com", "inventories":
[
{"id":10,"name":"2-Person Tent","category":"Camping"},
{"id":11,"name":"Sleeping bag","category":"Camping"},
{"id":27,"name":"6-Person Tent","category":"Camping"}
]
},
{"id":14, "email":"janedoe#example.com", "inventories":
[
{"id":30,"name":"Electric drill","category":"Tools"},
{"id":1,"name":"Hammer","category":"Tools"},
{"id":37,"name":"Plane","category":"Tools"}
]
}
]
I need to nest in one more thing and am having trouble doing so. For context, each inventory item is referenced via it's id as a foreign key in a borrow record. Each borrow record belongs to a request parent that stores returndate and pickupdate. What I need now, is for each inventory item, to nest an array of all the request records, with information on pickupdate and returndate. In other words, desired output:
[
{"id":13, "email":"johndoe#example.com", "inventories":
[
{"id":10,"name":"2-Person Tent","category":"Camping", "requests":
[
{"id":1, "pickupdate":"2014-07-07","returndate":"2014-07-10"},
{"id":2, "pickupdate":"2014-06-02","returndate":"2014-06-05"},
{"id":3, "pickupdate":"2014-08-14","returndate":"2014-08-20"}
]
},
{"id":11,"name":"Sleeping bag","category":"Camping", "requests":
[
{"id":4, "pickupdate":"2014-05-27","returndate":"2014-05-30"},
{"id":5, "pickupdate":"2014-04-22","returndate":"2014-04-25"}
]
},
{"id":27,"name":"6-Person Tent","category":"Camping", "requests":
[
{"id":6, "pickupdate":"2014-07-10","returndate":"2014-07-12"}
]
}
]
},
{"id":14, "email":"janedoe#example.com", "inventories":
...
I have written the following code:
json.array!(#lenders) do |json, lender|
json.(lender, :id, :email)
json.inventories lender.inventories do |json, inventory|
json.id inventory.id
json.name Itemlist.find_by_id(inventory.itemlist_id).name
#code below says, json.requests should equal all the Requests where there is a Borrows within that Request that is using the Inventory in question
json.requests Request.select { |r| r.borrows.select { |b| b.inventory_id == inventory.id }.present? } do |json, request|
json.pickupdate request.pickupdate
json.returndate request.returndate
end
end
end
When I refresh the page, I get wrong number of arguments (0 for 2..5)
I feel like the issue is that the Request.select... is returning an Array which isn't what needs to go here... but in the earlier nested function lender.inventories is an Inventory::ActiveRecord_Associations_CollectionProxy though I'm not sure how to correct for this.
NOTE: Someone said the problem could be that unlike with the nesting between inventories and lender, there's not an explicit association between inventory and request, but then again the line json.name Itemlist.find_by_id(inventory.itemlist_id).name worked so I'm not sure this is right. (Also if this is the case, I'm not sure how to bypass this limitation... I currently don't want to create a relationship between the two.)
Thanks!
ARG. Ok so this code is perfectly right. The issue was that I"m using the Gon gem in conjunction with Jbuilder, and Request is a predefined class in Gon.
So just changed code to
#requestrecords.select....
And in the controller:
#requestrecords = Request.all
-__-