How to convert from a string to object attribute name? - ruby-on-rails

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'

Related

How to access an instance relation using strings in 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.

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.

Ruby/Rails convert string to a class attribute

Suppose I have a class Article, such that:
class Article
attr_accessor :title, :author
def initialize(title, author)
#title = title
#author= author
end
end
Also, variable atrib is a String containing the name of an attribute. How could I turn this string into a variable to use as a getter?
a = Article.new
atrib='title'
puts a.eval(atrib) # <---- I want to do this
EXTENDED
Suppose I now have an Array of articles, and I want to sort them by title. Is there a way to do the compact version using & as in:
col = Article[0..10]
sorted_one = col.sort_by{|a| a.try('title') } #This works
sorted_two = col.sort_by(&:try('title')) #This does not work
You can use either send or instance_variable_get:
a = Article.new 'Asdf', 'Coco'
a.pubic_send(:title) # (Recommended) Tries to call a public method named 'title'. Can raise NoMethodError
=> "Asdf"
# If at rails like your case:
a.try :title # Tries to call 'title' method, returns `nil` if the receiver is `nil` or it does not respond to method 'title'
=> "Asdf"
a.send(:title) # Same, but will work even if the method is private/protected
=> "Asdf"
a.instance_variable_get :#title # Looks for an instance variable, returns nil if one doesn't exist
=> "Asdf"
Shot answer to your extended question: no. The &:symbol shortcut for procs relies on Symbol#to_proc method. So to enable that behavior you'd need to redifine that method on the Symbol class:
class Symbol
def to_proc
->(x) { x.instance_eval(self.to_s) }
end
end
[1,2,3].map(&:"to_s.to_i * 10")
=> [10, 20, 30]
ActiveRecord instances have an attributes hash:
a = Article.new(title: 'foo')
#=> <#Article id: nil, title: "foo">
atrib = 'title'
a.attributes[atrib]
#=> "foo"
You can use order to get sorted objects from your database:
Article.order('title').first(10)
#=> array of first 10 articles ordered by title

Rails Model.where how do I get ID?

I have the following where statement:
<% location = Location.where('locname' == client.locname) %>
How do I get the .id of the location record that it found?
This didn't work:
<% location = Location.where('locname' == client.locname).id %>
Thanks for the help!
<% location = Location.where("locname = ?", client.locname).first.id %>
The reason is that where will return an ActiveRecord::Relation, thus you can either loop through the elements or just grab the first one as I did above.
You may also use the find method provided by ActiveRecord like:
<% location = Location.find(:first, :conditions => ["locname = ?", client.locname]).id %>
be also aware that you need to paramterize your query properly to eliminate all possibilities of SQL injection.
The reason why your first code sample you provided doesn't allow you to obtain the id, is it isn't an instance of the Location class. Using some code from my own project:
1.9.2p290 :001 > ch = Character.where(name: 'Catharz')
Character Load (2.9ms) SELECT "characters".* FROM "characters" WHERE "characters"."name" = 'Catharz'
=> [#<Character id: 2, name: "Catharz", player_id: 2, archetype_id: 4, created_at: "2012-03-29 07:10:31", updated_at: "2012-11-26 05:36:11", char_type: "m", instances_count: 348, raids_count: 148, armour_rate: 5.1, jewellery_rate: 5.29, weapon_rate: 5.48>]
1.9.2p290 :002 > ch.class
=> ActiveRecord::Relation
This is because returns an instance of the ActiveRecord:Relation class which mimics your class. You can see this by calling #klass on the returned value.
1.9.2p290 :002 > ch.klass
=> Character(id: integer, name: string, player_id: integer, archetype_id: integer, created_at: datetime, updated_at: datetime, char_type: string, instances_count: integer, raids_count: integer, armour_rate: float, jewellery_rate: float, weapon_rate: float)
But if you try and get an id, you'll get the following exception:
1.9.2p290 :004 > ch.id
NoMethodError: undefined method `id' for #<ActiveRecord::Relation:0xce58344>
The ActiveRecord::Relation class allows you to chain together scopes, without executing the SQL until you need it to be executed. This is why Luis' answer above will work. Calling #first on the ActiveRecord::Relation will force the query to be executed.
As a pointer on design, you should probably be assigning your location as #location in your controller then using the instance variable in your view.

invalid decimal becomes 0.0 in rails

I have the following rails model:
class Product < ActiveRecord::Base
end
class CreateProducts < ActiveRecord::Migration
def self.up
create_table :products do |t|
t.decimal :price
t.timestamps
end
end
def self.down
drop_table :products
end
end
But when I do the following in the rails console:
ruby-1.9.2-p180 :001 > product = Product.new
=> #<Product id: nil, price: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :002 > product.price = 'a'
=> "a"
ruby-1.9.2-p180 :003 > product.save
=> true
ruby-1.9.2-p180 :004 > p product
#<Product id: 2, price: #<BigDecimal:39959f0,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
=> #<Product id: 2, price: #<BigDecimal:3994ca8,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
As you can see, I wrote 'a' and it saved 0.0 in the database. Why is that? This is particularly annoying because it bypasses my validations e.g.:
class Product < ActiveRecord::Base
validates :price, :format => /\d\.\d/
end
anything that is invalid gets cast to 0.0 if you call to_f on it
"a".to_f #=> 0.0
you would need to check it with validations in the model
validates_numericality_of :price # at least in rails 2 i think
i dont know what validating by format does, so i cant help you there, but try to validate that it is a number, RegExs are only checked against strings, so if the database is a number field it might be messing up
:format is for stuff like email addresses, logins, names, etc to check for illegeal characters and such
You need to re-look at what is your real issue is. It is a feature of Rails that a string is auto-magically converted into either the appropriate decimal value or into 0.0 otherwise.
What's happening
1) You can store anything into an ActiveRecord field. It is then converted into the appropriate type for database.
>> product.price = "a"
=> "a"
>> product.price
=> #<BigDecimal:b63f3188,'0.0',4(4)>
>> product.price.to_s
=> "0.0"
2) You should use the correct validation to make sure that only valid data is stored. Is there anything wrong with storing the value 0? If not, then you don't need a validation.
3) You don't have to validate that a number will be stored in the database. Since you declared the db field to be a decimal field, it will ONLY hold decimals (or null if you let the field have null values).
4) Your validation was a string-oriented validation. So the validation regexp changed the 0.0 BigDecimal into "0.0" and it passed your validation. Why do you think that your validation was bypassed?
5) Why, exactly, are you worried about other programmers storing strings into your price field?
Are you trying to avoid products being set to zero price by mistake? There are a couple of ways around that. You could check the value as it comes in (before it is converted to a decimal) to see if its format is right. See AR Section "Overwriting default accessors"
But I think that would be messy and error prone. You'd have to set the record's Error obj from a Setter, or use a flag. And simple class checking wouldn't work, remember that form data always comes in as a string.
Recommended Instead, make the user confirm that they meant to set the price to 0 for the product by using an additional AR-only field (a field that is not stored in the dbms).
Eg
attr_accessor :confirm_zero_price
# Validate that when the record is created, the price
# is either > 0 or (price is <= 0 && confirm_zero_price)
validates_numericality_of :price, :greater_than => 0,
:unless => Proc.new { |s| s.confirm_zero_price},
:on => :create
Notes The above is the sort of thing that is VERY important to include in your tests.
Also I've had similar situations in the past. As a result of my experiences, I now record, in the database, the name of the person who said that the value should indeed be $0 (or negative) and let them have a 255 char reason field for their justification. Saves a lot of time later on when people are wondering what was the reason.

Resources