Evaluating URL params, not returning expected results - ruby-on-rails

My URL is as follows:
http://localhost:3000/movies?ratings[PG-13]=1&commit=Refresh
I'm experimenting evaluating URL params and am unsure why this works the way is does. In my controller I evaluate the parameters and build an array as follows:
In my View I use the following debug statement to see what gets placed into #selected_ratings
=debug(#selected_ratings)
In my controller I have tried two statements.
Test one returns the following, this should work?
#selected_ratings = (params["ratings[PG-13]"].present? ? params["ratings[PG-13]"] : "notworking")
output: notworking
However if I use the following ternary evaluation n my controller:
#selected_ratings = (params["ratings"].present? ? params["ratings"] : "notworking")
output:!map:ActiveSupport::HashWithIndifferentAccess
PG-13: "1"
Why will my evaluation not find the literal params["ratings[PG-13]"]?

Rails parses string parameters of the form a[b]=c as a hash [1] where b is a key and c is its associated value: { :a => { :b => "c" } }.
So the url http://localhost:3000/movies?ratings[PG-13]=1&commit=Refresh will result in the hash:
{ :ratings => { :'PG-13' => "1"}, :commit => "Refresh" }
In your first assignment, you check if params["ratings[PG-13]"] is present, and since it is not, it returns "notworking". In the second case, you check if params["ratings"] is present, and it is, so it returns params["ratings"], which is a hash with the key PG-13 and value "1".
[1] Or rather, a HashWithIndifferentAccess, a special kind of hash that converts symbol and string keys into a single type.

Related

Retrieve a value from a hash in a hash in an array in a hash

I am trying to get the value of zap in a hash that looks like:
hash = {
:foo => 1,
:bar => [{
:baz => 2,
:zot => {
:zap => 3
}
}]
}
hash.dig breaks as soon as it gets to the array.
If it's important, this is a step in an if/elsif/else statement checking for different error messages. (i.e. elsif zap == 3)
I would do something like this:
hash[:bar].first.dig(:zot, :zap)
I believe you are incorrect, and dig in fact works on any object with a dig method. Dig is defined both for arrays and hashes. Also, if I define a dig method on a custom object:
o = Object.new
def o.dig(*args)
puts args.inspect
return :result
end
then when called like so:
{custom_object: o}.dig(:custom_object,1,2,3)
#-> output: [1,2,3]
#=> :result
you can see that dig gets called on o with the remaining arguments ([1,2,3]) and returns whatever the custom dig method returns.
What you may have missed is that for arrays, you need to use a numeric index, or dig raises a type error when it gets called on the array. So hash.dig(:bar, 0, :zot, :zap) is what you probably want. (credit to Alex for beating me to the punch).

Is there any function to get an array value from string except `eval`?

I have a string params, whose value is "1" or "['1','2','3','4']". By using eval method, I can get the result 1 or [1,2,3,4], but I need the result [1] or [1,2,3,4].
params[:city_id] = eval(params[:city_id])
scope :city, -> (params) { params[:city_id].present? ? where(city_id: (params[:city_id].is_a?(String) ? eval(params[:city_id]) : params[:city_id])) : all }
Here i don't want eval.
scope :city, -> (params) { params[:city_id].present? ? where(city_id: params[:city_id]) : all }
params[:city_id] #should be array values e.g [1] or [1,2,3,4] instead of string
Your strings look very close to JSON, so probably the safest thing you can do is parse the string as JSON. In fact:
JSON.parse("1") => 1
JSON.parse('["1","2","3","4"]') => ["1","2","3","4"]
Now your array uses single quotes. So I would suggest you to do:
Array(JSON.parse(string.gsub("'", '"'))).map(&:to_i)
So, replace the single quotes with doubles, parse as JSON, make sure it's wrapped in an array and convert possible strings in the array to integers.
I've left a comment for what would be my preferred approach: it's unusual to get your params through as you are, and the ideal approach would be to address this. Using eval is definitely a no go - there are some big security concerns to doing so (e.g. imagine someone submitting "City.delete_all" as the param).
As a solution to your immediate problem, you can do this using a regex, scanning for digits:
str = "['1','2','3','4']"
str.scan(/\d+/)
# => ["1", "2", "3"]
str = '1'
str.scan(/\d+/)
# => ["1"]
# In your case:
params[:city_id].scan(/\d+/)
In very simple terms, this looks through the given string for any digits that are in there. Here's a simple Regex101 with results / an explanation: https://regex101.com/r/41yw9C/1.
Rails should take care of converting the fields in your subsequent query (where(city_id: params[:city_id])), though if you explictly want an array of integers, you can append the following (thanks #SergioTulentsev):
params[:city_id].scan(/\d+/).map(&:to_i)
# or in a single loop, though slightly less readable:
[].tap { |result| str.scan(/\d+/) { |match| result << match.to_i } }
# => [1, 2, 3, 4]
Hope that's useful, let me know how you get on or if you have any questions.

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

Cannot get exact value on parameter

I have sample parameter below:
Parameters: {
"utf8"=>"✓",
"authenticity_token"=>"xxxxxxxxxx",
"post" => {
"product_attributes" => {
"name"=>"Ruby",
"product_dtls_attributes" => {
"0"=>{"price"=>"12,333.00"},
"1"=>{"price"=>"111,111.00"}
},
},
"content"=>"Some contents here."
}
Now, the scenario is, I cannot get the price exact value in model.
Instead of:
price = 12,333.00
price = 111,111.00
I get:
price = 12.00
price = 11.00
And now here is what I did in my code:
before_validation(on: :create) do
puts "price = #{self.price}" # I also tried self.price.to_s, but didn't work.
end
UPDATE:
(I am trying do to here is to get the full value and strip the comma).
before_validation(on: :create) do
puts "price = #{self.price.delete(',').to_f}" # I also tried self.price.to_s, but didn't work.
end
Note:
column price is float
The question is, how can I get the exact value of params price.
Thanks!
Looking at the 'price' parameter you provided:
"price"=>"12,333.00"
The problem is with the comma.
For example:
irb(main):003:0> "12,333.00".to_i
=> 12
But you can fix that:
Example:
irb(main):011:0> "12,333.00".tr(",", "_").to_i
=> 12333
The key point is replacing the comma with an underscore. The reason is that 12_333 is the same integer as 12333 (the underscores are ignored). You could just remove the comma with tr(",", "") as well. In this case, you could replace tr with gsub and have the same effect.
By the way, are you aware that your validation method is not doing anything besides printing? Anyway, a before_validation method is not the right approach here because the number will already have been incorrectly converted when the code reaches this point. Instead, you can override the setter on the model:
class MyModel
def price=(new_price)
if new_price.is_a?(String)
new_price = new_price.tr(",", "")
end
super(new_price)
end
end
You can do it like this too:
2.1.1 :002 > "12,333.00".gsub(',', '').to_f
=> 12333.0
This will replace the comma and if you have any decimal value then too it will interpret it:
2.1.1 :003 > "12,333.56".gsub(',', '').to_f
=> 12333.56
The solution I made is to handle it on controller. Iterate the hash then save it. Then it get the proper value which I want to get and save the proper value.
Iterate the following hash and save.
"post" => {
"product_attributes" => {
"name"=>"Ruby",
"product_dtls_attributes" => {
"0"=>{"price"=>"12,333.00"},
"1"=>{"price"=>"111,111.00"}
},
},
"content"=>"Some contents here."
I can't get the full value of price in model because of comma separator. This comma separator and decimal points + decimal places is made by gem.
Price is float, but your data contains a non-numeric character (comma, ","). When the field is converted to a float, parsing likely stops at this character and returns just 12.
I had expected an error to be thrown, though.
I suggest you remove the comma before putting it into the database.

find_all elements in an array that match a condition?

I've an array of hash entries, and want to filter based on a paramater passed into the function.
If there are three values in the hash, A, B, and C, I want to do something similar to:
data = [{A:'a1', B:'b1', C:'c1'},
{A:'a1', B:'b2', C:'c1'},
{A:'a1', B:'b2', C:'c2'},
{A:'a2', B:'b1', C:'c1'},
{A:'a2', B:'b2', C:'c1'}]
data.find_all{ |d| d[:A].include?params[:A] }
.find_all{ |d| d[:B].include?params[:B] }
.find_all{ |d| d[:C].include?params[:C] }
find all where A == 'a1' AND B='b2'
so for above I get:
{A:'a1', B:'b2', C:'c1'} and {A:'a1', B:'b2', C:'c2'}
* put if / else statements, like params.has_key? 'B' then do something.
* modify my code each time a new type of value is added to the Hash map (say now I have 'D').
Note: The key of the hash is a symbol, but the value is a string and I want to do a "contains" not "equals".
I think of it as SQL Select statement with where zero or more '==' clause
If I understand your question correctly, then I believe that the select method of Array class may be helpful to you.
select takes a block of code which is intended to be a test condition on each element in your array. Any elements within your array which satisfy that test condition will be selected, and the result is an array of those selected elements.
For example:
arr = [ 4, 8, 15, 16, 23, 42 ]
result = arr.select { |element| element > 20 }
puts result # prints out [23, 42]
In your example, you have an array of hashes, which makes it only slightly more complicated than my example with a simple array of numbers. In your example, we have:
data = [{A:'a1', B:'b1', C:'c1'},
{A:'a1', B:'b2', C:'c1'},
{A:'a1', B:'b2', C:'c2'},
{A:'a2', B:'b1', C:'c1'},
{A:'a2', B:'b2', C:'c1'}]
I believe what you want your code to do is something like: Select from my data array all of the hashes where, within the hash, :A equals some value AND :B equals some other value.
Let's say you want to find all of the hashes where :A == 'a1' and :B == 'b2'. You would do that like this:
data.select { |hash_element| hash_element[:A] == 'a1' && hash_element[:B] == 'b2' }
This line returns to you an array with those hashes from your original data array which satisfy the condition we provided in the block - that is, those hashes where :A == 'a1' and :B == 'b2'. Hope that that helps shed some light on your problem!
More information about the select method here:
http://www.ruby-doc.org/core-2.1.0/Array.html#method-i-select
edited - below is an addition to original answer
To follow up on your later question about if/else clauses and the addition of new parameters... the block of code that you pass to select can, of course, be much more complicated than what I've written in the example. You just need to keep this in mind: If the last line of the block of code evaluates to true, then the element will be selected into the result array. Otherwise, it won't be selected.
So, that means you could define a function of your own, and call that function within the condition block passed to select. For example, something like this:
def condition_test(hash_element, key_values)
result = true
key_values.each do |pair|
if hash_element[pair[:key]] != pair[:value]
result = false
end
end
return result
end
# An example of the key-value pairs you might require to satisfy your select condition.
requirements = [ {:key => :A, :value => 'a1'},
{:key => :B, :value => 'b2'} ]
data.select { |hash_element| condition_test(hash_element, requirements) }
This makes your condition block a bit more dynamic, rather than hard-wired for :A == 'a1' and :B == 'b2' like we did in the earlier example. You can tweak requirements on the fly based on the keys and values that you need to look for. You could also modify condition_test to do more than just check to see if the hash value at some key equals some value. You can add in your if/else clauses in condition_test to test for things like the presence of some key, etc.
I think you want to use .values_at(key)
http://www.ruby-doc.org/core-2.1.0/Hash.html#method-i-values_atvalues_at method

Resources