Rails update_attributes with dynamic hash - ruby-on-rails

I have a rails app in which I am trying to update a model with the attributes I am getting in the hash.
My code is:
attr_hash = {"name"=>"cat_name"}
#category.update_attributes(attr_hash, :type => 'sample')
Here is what I want that type will be fixed and the attr hash can be any attribute base on the form submit. But this gives me an error. Any ideas?

attr_hash = {"name"=>"cat_name"}
#category.update_attributes(attr_hash.merge(type: "sample"))
(because update_attributes takes only one hash)
Explanation:
Currently you're passing this:
update_attributes({"name"=>"cat_name"}, {type: "sample"})
but you want this:
update_attributes({"name"=>"cat_name", type: "sample"})
So you need to merge these two hashes.

Related

Ruby update existing hash value inside value

I've got a model which one of the column is a hash value, like below
[3] pry(main)> Activity.find(14)
=> #<Activity:0x00007fe625694070
id: 14,
content_basic: {
"content_type"=>"Audio",
"session_overview_image"=>"https://some_domain.com/392/123-woman.png"}
I want to change content_basic['session_overview_image'] to be /392/123-woman.png - how to do that in a rails c?
How about doing it like this:
Activity.find_each do |activity|
img = activity.content_basic['session_overview_image'].gsub('https://some_domain.com', '')
activity.content_basic['session_overview_image'] = img
activity.save
end
This assumes that all of the urls have the same domain that you want to remove. If they don't, then a little bit of URI parsing would be required first.
It also assumes that you are not on one of the older Rails versions (3 of before I believe) where you had to tell ActiveRecord that something has changed in the hash when changing serialized column values.

Rails find_or_create_by add cast to hash value json type attribute?

I have a model with an :extra_fields column that is :jsonb datatype, I want to add in the attr hashes to the column, something like this below but I am unsure of the syntax to cast the hash values' datatypes here, and if not here what is the best practice for casting hash value data ?
instance = Model.find_or_create_by(ref_id: hash[:ref_id]) do |a|
a.extra_fields = {
'attr1' : hash[:attr1], <-- //possible to cast type here ie ::type ?
'attr2' : hash[:attr2] <--
}
instance.save!
end
Bonus: how would I cast the hash values as type :decimal, :string, :boolean, :date for example?
All incoming parameters in Rails/Rack are strings. Well except except array/hash parameters which still have strings as values. Rails does the actual casting when you pass parameters to models.
You can cast strings to any other type in Ruby with the .to_x methods:
irb(main):006:0> "1.23".to_f
=> 1.23
irb(main):007:0> "1.23".to_d
=> #<BigDecimal:7ff7dea40b68,'0.123E1',18(18)>
irb(main):008:0> 1.23.to_s
=> "1.23"
irb(main):008:0> 1.23.to_i
=> 1
Boolean casting is a Rails feature. You can do it by:
# Rails 5
ActiveModel::Type::Boolean.new.cast(value)
ActiveModel::Type::Boolean.new.cast("true") # true
ActiveModel::Type::Boolean.new.cast("t") # true
ActiveModel::Type::Boolean.new.cast("false") # false
ActiveModel::Type::Boolean.new.cast("f") # false
# This is somewhat surprising
ActiveModel::Type::Boolean.new.cast("any arbitrary string") # true
# Rails 4.2
ActiveRecord::Type::Boolean.new.type_cast_from_database(value)
# Rails 4.1 and below
ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
Note that this is very different then the Ruby boolean coercion done by ! and !!.
irb(main):008:0> !!"false"
(irb):8: warning: string literal in condition
=> true
In Ruby everything except nil and false are true.
Dates are somewhat more complex. The default Rails date inputs use multi-parameters to send each part of the date (year, month, day) and a special setter that constructs a date from these inputs.
Processing by PeopleController#create as HTML
Parameters: { "person"=>{"birthday(1i)"=>"2019", "birthday(2i)"=>"2", "birthday(3i)"=>"16"}, ...}
You can construct a date from these parameters by:
date_params = params.fetch(:person).permit("birthday")
Date.new(*date_params.values.map(&:to_i))
what is the best practice for casting hash value data ?
There is no best practice here. What you instead should be pondering is the use of a JSON column. Since you seem to be want to apply some sort of schema to the data it might be a good idea to actually create a separate table and model. You are after all using a relational database.
JSON columns are great for solving some complex issues like key/value tables or storing raw JSON data but they should not be your first choice when modelling your data.
See PostgreSQL anti-patterns: Unnecessary json/hstore dynamic columns for a good write up on the topic.

How to access Chewy results with the dot notation?

I'm using Toptal's Chewy gem to connect and query my Elasticsearch, just like an ODM.
I'm using Chewy along with Elasticsearch 6, Ruby on Rails 5.2 and Active Record.
I've defined my index just like this:
class OrdersIndex < Chewy::Index
define_type Order.includes(:customer) do
field :id, type: "keyword"
field :customer do
field :id, type: "keyword"
field :name, type: "text"
field :email, type: "keyword"
end
end
end
And my model:
class Order < ApplicationRecord
belongs_to :customer
end
The problem here is that when I perform any query using Chewy, the customer data gets deserialized as a hash instead of an Object, and I can't use the dot notation to access the nested data.
results = OrdersIndex.query(query_string: { query: "test" })
results.first.id
# => "594d8e8b2cc640bb78bd115ae644637a1cc84dd460be6f69"
results.first.customer.name
# => NoMethodError: undefined method `name' for #<Hash:0x000000000931d928>
results.first.customer["name"]
# => "Frederique Schaefer"
How can I access the nested association using the dot notation (result.customer.name)? Or to deserialize the nested data inside an Object such as a Struct, that allows me to use the dot notation?
try to use
results = OrdersIndex.query(query_string: { query: "test" }).objects
It converts query result into active record Objects. so dot notation should work. If you want to load any extra association with the above result you can use .load method on Index.
If you want to convert existing ES nested object to accessible with dot notation try to reference this answer. Open Struct is best way to get things done in ruby.
Unable to use dot syntax for ruby hash
also, this one can help too
see this link if you need openStruct to work for nested object
Converting the just-deserialized results to JSON string and deserializing it again with OpenStruct as an object_class can be a bad idea and has a great CPU cost.
I've solved it differently, using recursion and the Ruby's native Struct, preserving the laziness of the Chewy gem.
def convert_to_object(keys, values)
schema = Struct.new(*keys.map(&:to_sym))
object = schema.new(*values)
object.each_pair do |key, value|
if value.is_a?(Hash)
object.send("#{key}=", convert_to_object(value.keys, value.values))
end
end
object
end
OrdersIndex.query(query_string: { query: "test" }).lazy.map do |item|
convert_to_object(item.attributes.keys, item.attributes.values)
end
convert_to_object takes an array of keys and another one of values and creates a struct from it. Whenever the class of one of the array of values items is a Hash, then it converts to a struct, recursively, passing the hash keys and values.
To presence the laziness, that is the coolest part of Chewy, I've used Enumerator::Lazy and Enumerator#map. Mapping every value returned by the ES query into the convert_to_object function, makes every entry a complete struct.
The code is very generic and works to every index I've got.

Ruby on Rails: How can i iterate over params and convert any empty strings into nil values in a controller?

Rails newbie here.
I have an integration with stripe where users can update the billing address on their card, however, stripe doesn't accept empty strings, only nil values, and it's possible that users won't need to fill in the second address line for example.
How would I go about iterating through params received from a form and convert empty strings into nil?
I have a Stripe Tool module that handles stripe related tasks.
In my controller i have:
def add_billing_address
account_id = current_user.account_id
account = Account.find_by(id: account_id)
stripe_id = account.stripe_customer_id
# convert params empty strings to nil here
StripeTool.add_billing_address(stripe_id: stripe_id,
stripe_token: params[:stripeToken],
address_line1: params[:address_line1],
address_line2: params[:address_line2],
address_city: params[:address_city],
address_state: params[:address_state],
address_zip: params[:address_zip]
)
# redirects and error handling happens after this
You can call .map .each on the params hash in the controller like this:
params.each do |key, value|
params[key] = nil if value === ''
end
But it's probably better to let your form return a nil value when a field contains no data.
I would recommend to avoid modifying the values in the params object, cause it is not good practice to change them in place. It is better to create a new object the has the values you want to use.
stripe_params = params.select { |_,v| v.present? }
This will create a new object without any of the blank attributes. I'm guessing that if an attribute is nil, you might as well not pass it at all.

Rails group by id uses a string for hash key

My code looks like this:
hash = MyModel.count(:group => 'id', :conditions => 'bla = "bla"')
The returned Hash has keys that are strings. I want them to be ints. I know it would be possible to convert the Hash manually using something like a map construct.
Edit:
Thanks for the responses. Have realised it was a json conversion process that was turning the ids into Strings and rails does in fact use the Fixnum as one might expect.
hash = MyModel.count(group: 'id', conditions: 'bla = "bla"')
should have Fixnum keys by default since id is an instance of Fixnum.
What happens is that ActiveRecord always fetch result as strings and then Rails takes care of converting them to other datatypes according to the type of the database column (we say that they are typecast).
So it's maybe a Rails bug or the 'id' column is not set as integer(which would be surprising).
If you can't fix it, convert them manually:
hash.each_with_object({}) do |(key, value), hash|
hash[key.to_i] = value
end
When I use your code I get integer keys (rails 3.07), what's the column type of id?
If you want to do it manually:
new_hash = hash.inject({}){|h,a| h[a.first.to_i] = a.last; h}
new_hash = Hash[hash.map { |k, v| [k.to_i, v] }

Resources