How can I refactor this Rails 3 code? - ruby-on-rails

Can anybody figure a way to refactor this further?
#hourly_pay = {}
HourlyPay.all.each { |hp| #hourly_pay[t("hourly_pay.#{hp.amount}")] = hp.amount }
Thanks!
Edit: based on the answers I've received, here's how I refactored
HourlyPay.all.map(&:amount).index_by { |hp| t("hourly_pay.#{hp.amount}") }
Placing this into my model directly, this becomes
def self.get_options
all.map(&:amount).index_by { |hp| I18n.t("hourly_pay.#{hp.amount}") }
end
However, I'm not sure if this is more calculation intensive, since I'm calling map on the values returned from the database, and then calling index_by on that.
Since my HourlyPay model only includes an id and an amount, I'm not worried about selecting everything. However, if I had a lot more fields, I would do the following instead:
def self.get_options
select(:amount).map(&:amount).index_by { |hp| I18n.t("hourly_pay.#{hp.amount}") }
end
So only the amount field is selected
Thanks for the responses!

The intention is not very clear here but I suggest that you look at using the index_by method in enumerable class. You'll get the complete HourlyPay object as the value in the hash though and can get the hash in one line.
Consider putting this code in the model if not already there.

Related

Rails - Returning all records for who a method returns true

I have a method:
class Role
def currently_active
klass = roleable_type.constantize
actor = Person.find(role_actor_id)
parent = klass.find(roleable_id)
return true if parent.current_membership?
actor.current_membership?
end
end
I would like to return all instances of Role for who this method is true, however can't iterate through them with all.each as this takes around 20 seconds. I'm trying to use where statements, however they rely on an attribute of the model rather than a method:
Role.where(currently_active: true)
This obviously throws an error as there is no attribute called currently_active. How can I perform this query the most efficient way possible, and if possible using Active Records rather than arrays?
Thanks in advance
It seems impossible, in your case you have to do iterations. I think the best solution is to add a Boolean column in your table, so you can filter by query and this will be much faster.
After seeing your method after edit, it seems that it's not slow because of the loop, it is slow because Person.find and klass.find , you are doing alot of queries and database read here. (You better use associations and do some kind of eager loading, it will be much faster)
Another work-around is you can use ActiveModelSerializers , in the serializer you can get the attributes on the object based on condition. and after that you can work your logic to neglect the objects that have some kind of flag or attribute.
See here the documentation of active model serializer
Conditional attributes in Active Model Serializers
Wherever possible you better delegate your methods to SQL through activerecord when you're seeking better efficiency and speed and avoid iterating through objects in ruby to apply the method. I understand this is an old question but still many might get the wrong idea.
There is not enough information on current_membership? methods on associations but here's an example based on some guess-work from me:
roleables = roleable_type.pluralize
roleable_type_sym = roleable_type.to_sym
Role.joins(roleables_sym).where(" ? BETWEEN #{roleables}.membership_start_date AND #{roleables}.membership_end_date", DateTime.current).or(Role.joins(:person).where(" ? BETWEEN persons.membership_start_date AND persons.membership_end_date", DateTime.current))
so you might have to re-implement the method you have written in the model in SQL to improve efficiency and speed.
Try the select method: https://www.rubyguides.com/2019/04/ruby-select-method/
Role.all.select { |r| r.currently_active? }
The above can be shortened to Role.select(&:currently_active?)

Where should method go (model?, somewhere else)

I've got a method in one of my models which returns data which will be fed into a charting gem.
class MyModel < ActiveRecord::Base
def ownership_data
format_data(item_ownerships.group(:owned).count)
end
end
I need to guarantee that the data return always has 2 values in the result. Something like this:
{ "yes" => 4, "no" => 2 }
In order to do this, I've written another method which is used in the first method:
def format_data(values)
values[false].nil? ? values = values.merge({ "no" => 0 }) : true
values[true].nil? ? values = values.merge({ "yes" => 0 }) : true
return values
end
My question is, where should this method go and how can I unit test it using rspec? I've currently got it in the model, however in trying to test it with rspec, my specs look like this:
let(:values) { { "yes" =>2 } }
it "it will return 2 values" do
result = MyModel.new.format_data(values)
expect(result.keys.count).to eq(2)
end
I'm not too happy about having to instantiate an instance of the model to test this. Any guidance is appreciated.
As AJ mentioned in the comment, we probably need more information to go on to give more specific advice. I'll give some anyway...
If you have a object that isn't necessarily depending on the model itself, you should consider moving it to a plain old ruby object or a service object. Those can live in a separate folder (lib/services) or in your models folder without inheriting from ActiveRecord::Base.
Your method could also be a class method def self.method_name(values, values_2) instead of a instance method (if you don't need specific values from the model).
When it comes to data manipulation for reporting for my projects, I've built specific folder of ruby service objects for those under 'lib/reports' and they take raw data (usually in init method) and return formatted data from a method call (allowing me to have multiple calls if the same data can be formatted in different output options). This makes them decoupled from the model. Also, this makes testing easy (pass in known values in Class.new expect specific values in method outputs.

How to assign to Rails ActiveRecord conditionally?

Hey I wasn't quite sure what to call this but here's the deal.
I'm trying to only assign things to my database value if
There isn't a value in the database already, and
The value I'm assigning isn't blank.
The rudimentary version of this code is:
venue.address = venue_json['address'] if venue.address.blank? && !venue_json['address'].blank?
where venue is my ActiveRecord result.
This is what I have now (a little better). With the init_value in the Venue.rb class.
Venue.init_value(venue.address, venue_json['address'])
def self.init_value(record, value)
if record.blank? && !value.blank?
record = value
end
end
I'd like to get to this point, but really have no idea how.
venue.address.init_value(venue_json['address'])
especially since I'd like it it work with any attribute of the ActiveRecord class not just the address value.
Separating it into a method sounds like a good idea, but in this case it makes more sense to use an instance method rather than a class method.
def init_attribute(attribute, value)
self.update(attribute => value) if self.send(attribute).blank? && value.present?
end
venue.init_attribute(:address, venue_json['address'])
Some quick comments on the snippet above:
Using direct assignment won't persist the database value. You could go with something else like update or update_column. Or you can use assignment and then call #save on the object.
Whenever you need something not to be blank, you can use the more readable Object#present? which is part of ActiveSupport.
You'll need to call the method with the same name as the attribute on the database object. For this you'll want to use Object#send from Ruby.

Setting ActiveRecord model attribute

I'm trying to override a initialize in a ActiveRecord model, I just saw one thing that I could not understand what was happening there. I wrote this initialize method:
def initialize params={}
super params
#data = Date.strptime(params[:data], '%d/%m/%Y') if not params[:data].nil?
self.number = generate_contract_number(params[:unit]) if not params[:unit].nil?
self
end
Given the generate_contract_number works and Date.strptime works as well. My question is: Why when I do self.number= the number is set and when I do #number= the number is not set. But when I do just the same with #contract_date= it works, and the self.contract_date= is set automatically?
Thanks
Don't override ActiveRecord's initialize; use an after_initialize callback.
For more details, see this SO post.
I don't see anything about #contract_date, so I'm not sure what you mean. Without the model definition it's tough to say more, we don't know what #data or #contract_date is, or what "working" means. Bear in mind that an ActiveRecord's DB attributes aren't simple #-style attributes.

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