How to access an instance relation using strings in Ruby on Rails? - ruby-on-rails

Supose I have a class named Classroom and I have another class named Student.
From their relationship I can access Student by using Classroom.students which returns an array of students.
Now, taking that in consideration, if I assign 'Classroom' to a variable and call it with variable.constantize it will return the Class from where I can query normally.
eg:
[1] pry(main)> variable = 'Classroom'
[2] pry(main)> variable.constantize.students
=> [#<Student id: 1, name: 'Foo Smith'>, <Student id: 2, name: 'Bar Obama'>]
All of that works for me but I have no idea on how to access students if the key name is also stored as a string. This works if I'm trying to access a Classroom field but for relations it seems to be impossible.
eg:
[1] pry(main)> variable = 'Classroom'
[2] pry(main)> class_key = 'name'
[3] pry(main)> relation_key = 'students'
[4] pry(main)> variable.constantize.first[class_key]
=> "Happy Classroom"
[5] pry(main)> variable.constantize.first[key]
=> nil
There's also another case in which the string is a method from the class.
What I wanted to do (mock code):
[1] pry(main)> variable = 'Classroom'
[2] pry(main)> method = 'last'
[3] pry(main)> variable.constantize[method]
=> <#Classroom id: 3, name: 'Dubious Classroom'>
Any of those are feasible in Ruby?
Thanks!

Use Object.public_send, like this:
'Classroom'.constantize.public_send('students').public_send('first')
Edit: original answer suggested using send instead of public_send. The difference is that the former will call even the private methods, so it's less safe to use than the latter.

Related

How to correctly merge one hash into another, replacing the first key. RoR

Im currently trying to merge two hashes, I don't really have much else to go on but this is the result I need is showed in this example;
{key_1: 'i want to replace this', key_2: 'i want to keep this'}.merge({key_1: 'new text'})
=> {key_1: 'new text', key_2: 'i want to keep this'}
Currently what I've got looks like this;
#notification.attributes.merge({body: ()}).to_json Im attempting to merge an replace the first key with the body element. What I'm really missing is the argument to perform the key replacement. If anyone has any direction, advice or even answers it would be much appreciated, thanks.
In Rails #attributes returns a hash with string keys:
irb(main):001:0> note = Notification.new(title: 'All your base are belong to us', body: 'Loren Ipsum...')
irb(main):002:0> note.attributes
=> {"id"=>nil, "title"=>"All your base are belong to us", "body"=>"Loren Ipsum...", "read_at"=>nil, "created_at"=>nil, "updated_at"=>nil}
If you want to replace a key in the hash you either need to use a hash with string keys:
irb(main):003:0> note.attributes.merge("body" => "Moahahahahahaha")
=> {"id"=>nil, "title"=>"All your base are belong to us", "body"=>"Moahahahahahaha", "read_at"=>nil, "created_at"=>nil, "updated_at"=>nil}
Or you need to change the keys of the hash to symbols which can be done with Hash#symbolize_keys:
irb(main):004:0> note.attributes.symbolize_keys.merge(body: "Moahahahahahaha")
=> {:id=>nil, :title=>"All your base are belong to us", :body=>"Moahahahahahaha", :read_at=>nil, :created_at=>nil, :updated_at=>nil}
This is a pretty common source of errors for new developers as Rails abstracts away the difference between symbol and string keys in many places through the use of ActiveSupport::HashWithIndifferentAccess or hash like objects like ActionController::Parameters that have indifferent access while Ruby itself is strict about the difference.
irb(main):008:0> { "foo" => "bar" }.merge(foo: 'baz')
=> {"foo"=>"bar", :foo=>"baz"}
irb(main):009:0> { "foo" => "bar" }.with_indifferent_access.merge(foo: 'baz')
=> {"foo"=>"baz"}
If you ever need to do this with a nested hash you can use the recursive versions deep_symbolize_keys and deep_merge.
You have to add ! in your merge operation, as when you are doing operation it is not effecting your actual object but will create new object. So your example as you said I did following to change values
{key_1: 'i want to replace this', key_2: 'i want to keep this'}.merge!({key_1: 'new text'})
# result
{:key_1=>"new text", :key_2=>"i want to keep this"}
please change following from
#notification.attributes.merge({body: ()}).to_json
to
#notification.attributes.merge!({body: ()}).to_json
Please try to change the order of master hash (the master is what you want to keep)
[9] pry(main)> a = {:key_1=>"i want to replace this", :key_2=>"i want to keep this"}
=> {:key_1=>"i want to replace this", :key_2=>"i want to keep this"}
[10] pry(main)> b = {:key_1=>"new text"}
=> {:key_1=>"new text"}
[11] pry(main)> c = b.merge(a)
=> {:key_1=>"i want to replace this", :key_2=>"i want to keep this"}
[12] pry(main)> d = a.merge(b); // <===== This is what you want.
=> {:key_1=>"new text", :key_2=>"i want to keep this"}
Hope it help. Thanks

Apply deep_symbolize_keys! to array of hashes

deep_symbolize_keys! converts string keys to symbol keys. This works for hashes and all sub-hashes. However, I have a data like this:
arr = [
{'name': 'pratha', 'email': 'p#g.com', 'sub': { 'id': 1 } },
{'name': 'john', 'email': 'c#d.com', 'sub': { 'id': 2 } }
]
arr.deep_symbolize_keys! # this is not working for array of hashes.
In this case, hashes are in an array. So how can i symbolize all at once?
Using Ruby 2.6.3
I also read somewhere that this is deprecated (probably on one of the Rails forum). Is that true? If so, what is the best way to convert keys to symbols in my case?
Currently using this:
def process(emails)
blacklist = ["a", "john", "c"]
e = emails.map do |hash|
blacklist.include?(hash['name']) ? nil : hash.deep_symbolize_keys!
end
e
end
Do you need a copy or an in-place transformation? In-place you can use arr.each(&:deep_symbolize_keys!). For a copy you should use arr.map(&:deep_symbolize_keys). Remember that map does not mutate but returns a new array.
The implementation already handles nested arrays, it just doesn't define the method on Array. So, nest it in a temporary hash and symbolize that. This works for arbitrary types:
[1] pry(main)> def deep_symbolize_keys(object) = {object:}.deep_symbolize_keys[:object];
[2] pry(main)> deep_symbolize_keys([{"a" => 1}, {"b" => {"c" => 2}}])
=> [{:a=>1}, {:b=>{:c=>2}}]
Also, be careful with your key syntax. In your example, your keys are already symbols - they're just quoted symbols:
[3] pry(main)> {a: 1}.keys.first.class
=> Symbol
[4] pry(main)> {'a': 1}.keys.first.class
=> Symbol
[5] pry(main)> {'a' => 1}.keys.first.class
=> String
The syntax is necessary to handle cases like {'a-b': 1}[:'a-b'], but it's very often misleading since they look so much like string keys. I recommend avoiding it entirely unless absolutely necessary - stick to {a: 1} for symbol keys and {'a' => 1} for string keys.

Why elem_match is returning 0 elements?

I am trying to get one record result from a collection of objects, but after following the Mongoid documentation I don't know what more to try.
I have this single element:
> contacts
=> #<Contact _id: 55ace6bc6xx, device_fields: {"app_id"=>"55ace6bc65195efc8200xxxx"}, created_at: 2015-07-20 12:17:00
UTC, updated_at: 2015-07-20 12:17:00 UTC, name_first: "Kory",
name_last: "Funk", ...>
this list of matchers:
> apps = []
> apps << App.where(id: "55ace6bc65195efc8200xxxx").first.id
=> ["55ace6bc65195efc8200xxxx"]
And this code trying to get the elements that match:
> contacts.elem_match(device_fields: {:app_id.in => apps }).to_a
=> []
> contacts.elem_match(device_fields: { "app_id": "55ace6bc65195efc8200xxxx"}).to_a
=> []
Why is it returning an empty array it there is one that matches?
According to official mongodb manual
The $elemMatch operator matches documents that contain an array field
And you are trying to use it with the hash field so you basically misunderstood this selection. So there is no object that matches.
Instead of it you should do:
contacts.where(:'device_fields.app_id'.in => apps).to_a
I can not achieve to resolve this with match_elem method, so finally I decided to make it through and. I am not very happy with this solution and I still don't understand why match_elem is not returning records, but at least I have found a solution to unblock this feature.
contacts.and(:device_fields.exists => true,
:device_fields.nin => ['', nil],
:"device_fields.app_id".in => apps).to_a
You don't need elemMatch here. It's for finding object array elements by partial matches (where you don't need full object equality, but only one or several fields)
This should work for your case.
contacts.where('device_fields.app_id' => {'$in' => apps})

Why is the "where" query in rails returning a different object?

I'm testing chats between users in my app. I'm using RSpec and FactoryGirl
The test that's not passing:
it "creates a chat if one does not exist" do
bob = create(:user, username: "bob")
dan = create(:user, username: "dan")
new_chat = Chat.create(user_id: #dan.id, chatted_user_id: bob.id)
expect(Chat.where("chatted_user_id = ?", bob.id).first).to equal(new_chat)
end
The failure message says:
Failure/Error: expect(Chat.where("chatted_user_id = ?", bob.id).first).to equal(new_chat)
expected #<Chat:70120833243920> => #<Chat id: 2, user_id: 2, chatted_user_id: 3>
got #<Chat:70120833276240> => #<Chat id: 2, user_id: 2, chatted_user_id: 3>
Compared using equal?, which compares object identity,
but expected and actual are not the same object. Use
`expect(actual).to eq(expected)` if you don't care about
object identity in this example.
Why is my query returning a different object id?
equal checks object identity. The objects you are testing are two objects (instances) referencing the same record, but they are actually different objects from a Ruby virtual machine point of view.
You should use
expect(Chat.where("chatted_user_id = ?", bob.id).first).to eq(new_chat)
To better understand the problem, look at the following example
2.0.0-p353 :001 > "foo".object_id
=> 70117320944040
2.0.0-p353 :002 > "foo".object_id
=> 70117320962820
Here I'm creating two identical strings. They are identical, but not equal because they are actually two different objects.
2.0.0-p353 :008 > "foo" == "foo"
=> true
2.0.0-p353 :009 > "foo".equal? "foo"
=> false
That's the same issue affecting your test. equal checks if two objects are actually the same at the object_id level. But what you really want to know is if they are the same record.

How to convert from a string to object attribute name?

I am trying to convert a string value into a name of an attribute that belongs to an object. For example, in the following code, I need all the string values in the column_array turned into attribute names. The names "student_identification", "email", etc. are actual column names of my Student table. In the real scenario, column_array will be set by the user (by ticking check boxes). And new_array will be replaced by csv, as I want the data go into a csv file.
At the moment I am really struggling at the following line:
new_array << r."#{column_array[i]}"
I want "#{column_array[i]}" to be turned into the attribute name so I can access the data.
def exp_tst
#records = Student.find(:all, :conditions=> session[:selection_scope],
:order => sort_order('laboratory_id'))
column_array = ["student_identification", "laboratory_id", "email", "current_status"]
new_array = Array.new()
#records.each do |r|
(0..(column_array.size-1)).each do |i|
new_array << r."#{column_array[i]}"
end
end
end
Let's say column_array[i] = "foo", for an example.
If you want to call the method r.foo, use Object#send:
r.send(column_array[i], arg1, arg2, arg3, ...)
If you want to access r's instance variable #foo, use Object#instance_variable_get and Object#instance_variable_set:
r.instance_variable_get('#'+column_array[i])
r.instance_variable_set('#'+column_array[i], new_value)
In this case we have to prepend the given name with an # sigil, since that is required at the start of all instance variable names.
Since this is rails, and there's a whole lot of ActiveRecord magic going on with your models (and I'm guessing Student is a subclass of ActiveRecord::Base) you probably want to use the former, since ActiveRecord creates methods to access the database, and the values stored in instance variables may not be what you want or expect.
I'll use an example from some test data I've got lying around:
% script/console
Loading development environment (Rails 2.3.2)
irb> Customer
#=> Customer(id: integer, date_subscribed: datetime, rental_plan_id: integer, name: string, address: string, phone_number: string, credit_limit: decimal, last_bill_end_date: datetime, balance: decimal)
irb> example_customer = Customer.find(:all)[0]
#=> #<Customer id: 6, date_subscribed: "2007-12-24 05:00:00", rental_plan_id: 3, name: "Evagation Governessy", address: "803 Asbestous St, Uneradicated Stannous MP 37441", phone_number: "(433) 462-3416", credit_limit: #<BigDecimal:191edc0,'0.732E3',4(12)>, last_bill_end_date: "2009-05-15 04:00:00", balance: #<BigDecimal:191e870,'0.743E3',4(12)>>
irb> example_customer.name
#=> "Evagation Governessy"
irb> field = 'name'
#=> "name"
irb> example_customer.instance_variable_get(field)
NameError: `name` is not allowed as an instance variable name
from (irb):8:in `instance_variable_get`
from (irb):8
irb> example_customer.instance_variable_get('#'+field)
#=> nil
irb> example_customer.send(field)
#=> "Evagation Governessy"
irb> example_customer.send(field+'=', "Evagation Governessy Jr.")
#=> "Evagation Governessy Jr."
irb> example_customer.send(field)
#=> "Evagation Governessy Jr."
irb> example_customer.name
#=> "Evagation Governessy Jr."
So you can see how #send(field) accesses the record information, and trying to access the attributes doesn't.
Also, we can use #send(field+'=') to change record information.
Look at instance_eval method ...
if you have 'attribute' and need do
object.attribute = 'ololo'
you can do:
object.instance_eval('attribute') = 'ololo'

Resources