Referencing an object attribute by a string - ruby-on-rails

I'm trying to DRY up a method in which I need to perform the same task on three different attributes. Like this:
if !#item.picture.blank?
picture_copy = Picture.new
picture_copy.save!
item_copy.picture = picture_copy
end
if !#item.picture_for_x.blank?
picture_for_x_copy = PictureForX.new
picture_for_x_copy.save!
item_copy.picture_for_x = picture_for_x_copy
end
if !#item.picture_for_y.blank?
picture_for_y_copy = PictureForY.new
picture_for_y_copy.save!
item_copy.picture_for_y = picture_for_y_copy
end
So basically I'm running the same code, but instantiating different objects and then assigning them to different attributes. It feels like there should be a way to DRY up this view using reflection. Is there a way that I can refer to these attributes and objects as strings passed into a helper method?
For various reasons, I can't just use .clone or .dup: mainly because there's binary file pointers involved and I also need deep copies.

{
picture: Picture,
picture_for_x: PictureForX,
picture_for_y: PictureForY
}.each do |name,klass|
if !#item.send(name).blank?
copy = klass.new
copy.save!
item_copy.send("#{name}=",copy)
end
end
Remember that in Ruby there are no properties or attributes available externally, just methods (which you may choose to call without parentheses so that it looks like you're accessing a property, and which sometimes might be just returning the value of an instance variable).
Object#send is the magic method that lets you invoke a method based on a name stored in a variable.

def picture_for_x_blank?(s = "")
s = "_for_#{s}" unless s.empty?
m = "picture#{s}"
unless #item.send(m).blank?
copy = Kernel::const_get(m.camelize).new
copy.save!
item_copy.send("#{m}=", copy)
end
end
picture_for_x_blank?
picture_for_x_blank?("x")
picture_for_x_blank?("y")

Related

Using Variables for Model Names in Active Record

I am writing a seed file that will make several API calls via HTTParty in order to populate the database. I am pulling the same information for several different models and I would like to be able to use a single method for all of them. However, I cannot figure out how to reference the model name through a variable. Specifically I am having difficulties because each of these must belong to another model. I have tried the following:
def create_assets(subject, model, geokit_hoods)
response = HTTParty.get("https://raw.githubusercontent.com/benbalter/dc-maps/master/maps/#{subject}.geojson")
parsed = JSON.parse(response)
collection = parsed["features"]
collection.each do |station|
coordinates = station["geometry"]["coordinates"].reverse
point = Geokit::LatLng.new(coordinates[0], coordinates[1])
geokit_hoods.each do |hood|
if hood[1].contains?(point)
hood[0][model].create(coordinates: coordinates, name: station["properties"]["NAME"], address: station["properties"]["ADDRESS"])
break
end
end
end
end
Which I called via the following:
create_assets("metro-stations-district", "metros", geokit_hoods)
hood[0] refers to an existing neighborhood model, and hood[1] is the polygon associated with that neighborhood. The code works when referring to hood[0].metros.create(...), but I am looking for a way to make this method useful across many models.
Any ideas would be appreciated!
For now I'm going to assume that what you have in the variable is a String that is the name of the class in table-name format. eg in your example you have metros in the variable... from that I assume you have a Metro class which you are trying to create.
If so... you first need to convert your lowercase table-name style variable ("metros") into a class name-style eg "Metro"
Note: this is title cased and singular (rather than plural).
Rails has a method to do this to strings for exactly the purpose you want: classify eg you could use it thus:
model_name = hood[0][model] # 'metros'
model_name.classify # 'Metro'
Note that it's still just a string, and you can't call create on a string.. so how do you make it the real class? constantize
Use this to turn the string into the actual model-class you're trying to find... which you can then call create on eg:
model_name = hood[0][model] # 'metros'
the_klass = model_name.classify.constantize # Metro
your_instance = the_klass.create(...)

How to update table row from values determined within controller

ruby_on_rails rails 4 assignment non-screen data to insert record
Rather than using screen values (e.g. simple_form_for #user_evaluation_result) to populate the columns to insert a row I need to calculate some of the values in controller.
For example if I have these statements in the controller
….
# which if I had simple_form_for user_evaluation_result would get populated by the screen
#user_evaluation_result = UserEvaluationResult.new(user_evaluation_result_params)
….
# standard stuff I use for screen derived updates
def user_evaluation_result_params
params.require(:user_evaluation_result).
permit(:evaluation_assumption_id,
:company_listing_id,
:target_share_price_dollars )
end
How do I assign values to :user_assumption_id etc so that insert works. I have tried all sorts of statements. Alternatively do I use another format instead of calling "def user_evaluation_result_params".
Thanks in advance - Pierre
I'm hoping I've interpreted the question properly...
First, to make sure we're on the same page... The code inside of your user_evaluation_result_params method is using Strong Parameters to create an ActionController::Parameters object for the purpose of protecting your model from unpermitted mass-assignments. So, in general, when you're creating or updating an ActiveRecord object from a form in a view template, you want to use Strong Parameters so that users can't manipulate your form to set attributes that you're not expecting.
That said, if you want to set attributes on an object you don't have to use mass assignment. Here is an example of using one-at-a-time assignment (the opposite of mass-assignment):
obj = MyObject.new
obj.attr_one = "One"
obj.attr_two = "Two"
obj.save
There is nothing wrong with this approach other than that it's kind of a lot of work for the general case. So mass-assignment just saves us from having to do this all the time. But it sounds like this one-at-a-time assignment is what you're wanting in this case. So try something like this:
def create
#user_evaluation_result = UserEvaluationResult.new
# assuming you have a UserAssumption object instance in `my_user_assumption`
#user_evaluation_result.user_assumption = my_user_assumption
#user_evaluation_result.some_other_attr = "some value"
#user_evaluation_result.save
end
Note, instead of setting #user_evaluation_result.user_assumption_id directly, as you asked about, it is preferred to set the actual object association as I did above. Try to keep associations outside of mass-assignment and use object relationships to build up your object graphs.
Or, if you have some attributes coming from a form you can mix and match the two approaches:
def create
#user_evaluation_result = UserEvaluationResult.new(user_evaluation_result_params)
# assuming you have a UserAssumption object instance in `my_user_assumption`
#user_evaluation_result.user_assumption = my_user_assumption
#user_evaluation_result.some_other_attr = params[:user_evaluation_result][:some_other_attr]
#user_evaluation_result.save
end
private
def user_evaluation_result_params
params.require(:user_evaluation_result)
.permit(:evaluation_assumption_id,
:company_listing_id,
:target_share_price_dollars)
end

correct way of modifying a passed argument in the same way for many objects

I have some rails services which (depending on the service) accept a specific type of object, an Array of that object, or an ActiveRecord::Relation of that object.
So:
FooService accepts Foo objects, BarService accepts Bar objects, etc.
But..all the service objects want to massage what is passed in into an array of their given objects.
within the scope of the called service method I could do this pretty easily.
I was trying to DRY things up and make a method to do this for all services, because each of them had the same chunk of code. Unfortunately, I run into issues with Ruby.
For example:
def change_obj(element)
element = [element]
end
blah = "hello"
change_obj(blah)
puts blah.class.to_s #I want it to be an array with the string in it now.
I understand why it doesn't work. But, I don't have a clear idea of what the correct way to do this is.
Ideally I want something like:
class BarService < Service
def initialize(foo)
#foo = foo
end
def do_something(bars)
massage_to_array(bars) #method inherited from Service. if bars is an ActiveRecordRelation, convert it to array..if bars is a singular object, flip it to an array with that object as the sole element.
process_array_of_bars(bars)
end
end
bars = massage_to_array(bars)
process_array_of_bars(bars)
as long as you are modifying passed object - ruby will keep the reference and update contents of that object and not create new object. For example if you pass an array as argument and then add element to an array inside the method - ruby will add element to the object that was passed in, but if you re-assign value to the object, then ruby will remove the reference to passed object and create new object in the local scope and add reference to it to the variable name.
That being said, you can still achieve similar result by creating class that will acts as a proxy. Remember ruby will maintain reference to original object as long as you dont re-assign the variable name to new variable. For example
class ArgProxy
attr_accessor :arg_object
def initialize(arg=nil)
self.arg_object = arg
end
end
def some_method(arg_proxy)
arg_proxy.arg_object = [arg_proxy.arg_object]
end
arg_proxy = ArgProxy.new("qwerty")
some_method(arg_proxy)
arg_proxy.arg_object #=> ["qwerty"]

Ruby on Rails Class method scoped objects references

I was wondering If I could modify objects using a class method.
For example, users = User.scoped # This will select all the objects
And, suppose, I want to assign a variable for each of the object there is. Let's say, I want them to share a single value. So, when I try to access, for example, users.first.my_variable it would produce the value, I want.
My naive implementation:
def self.set_my_variable(variable_value)
scoped.tap do |obj|
obj.my_variable = variable_value
end
end
So, ideally, when I wan't this variable to be set, I should call the class method like this: users.set_my_variable("hello, stackoverflow")
But, when I try accessing the variable through arbitrary object of the set, like this:
users.first.my_variable
I get nil. Comparing .object_id's in both: obj.object_id and users.first.object_id shows that they are different. Why so ? I thought that they share the same reference (pointing to the same objects)
Any way of fixing it (preferrably without passing a collection to this class method) ?
you need to save object to database:
obj.my_variable = variable_value
obj.save

Trouble on finding a class object in a array of classes

I am using Ruby on Rails 3.0.7 and I would like to understand how to handle the following code in order to retrieve a class objects with a specified id.
In my view file I have:
#records = Users.all # This returns an array (class)
In another file, a partial template, I would like to retrieve, for example, the user with id 1, but if I make this:
#records.find(1)
I get an enumerator (class) of all records:
<Enumerator: [<Users id: 1, ... ] >
How can I find the user with id 1 (or other ids) "a là Ruby on Rails Way"?
UPDATE
I use #records = Users.all in a view file because I aim to minimize calls to the database since I need to iterate almost over all records and check them existence. If I do for example:
some_hash.each { |key, value|
put User.find(value)
}
and I go in the log file, I will see a lot of database requests.
Even though this is probably quite slow, and I suspect there are some less than optimal designs in the app you're working on (not judging, we've all been there), Array#index seems to be what you're looking for:
#records[#records.index{|user| user.id == 1}]
Edit
Although if you need to do something for every user, and you need to access them by id quickly, I'd probably do something like this in your controller. Even if it's not really faster, it's much more readable (to me anyways):
#users_hash = {}
User.all.each{|user| #users_hash[user.id] = user}
Then in your views you can do:
#users_hash[id].username
Use User.scoped instead of User.all. #all will immediately query the database and return an array, whereas #scoped will return an ActiveRecord::Relation object which you can chain further queries. In this case, the database won't be hit until you try and somehow inspect or enumerate the result
Actually you're mistaken. #records.find(1) is returning an object of the class Enumerator (which is not the same as the class Enumerator itself).
The problem here is that, as you've noted, #records is an Array, not an ActiveRecord object, and Array#find (inherited from Enumerable#find--which, when not given a block, returns an object of class Enumerable) is not the same method as ActiveRecord::Base#find (i.e. User#find).
What you should do is, in your controller, pick out the one user record you want:
#user = User.find 1
...and then use #user directly in your template. Generally you should avoid doing ActiveRecord lookups (e.g. find) in your templates. That kind of logic should happen in your controller.
Last time for such case I ended up doing like this:
#assignments = Assignment.find_by_sql(' ... ')
#assignments.find(id: 1).first

Resources