Rails more idiomatic way of adding up values - ruby-on-rails

I am working on a survey app, and an Organisation has 0 or more SurveyGroups which have 0 or more Members who take the survey
For a SurveyGroup I need to know how many surveys still need to be completed so I have this method:
SurveyGroup#surveys_outstanding
def surveys_outstanding
respondents_count - surveys_completed
end
But I also need to know how many surveys are outstanding at an organisational level, so I have a method like below, but is there a more idiomatic way to do this with Array#inject or Array#reduce or similar?
Organisation#surveys_pending
def surveys_pending
result = 0
survey_groups.each do |survey_group|
result += survey_group.surveys_outstanding
end
result
end

Try this:
def surveys_pending
#surveys_pending ||= survey_groups.map(&:surveys_outstanding).sum
end
I'm using memoization in case it is slow to calculate

def surveys_pending
survey_groups.inject(0) do |result, survey_group|
result + survey_group.surveys_outstanding
end
end

Related

Getting all the pages from an API

This is something I struggle with, or whenever I do it it seems to be messy.
I'm going to ask the question in a very generic way as it's not a single problem I'm really trying to solve.
I have an API that I want to consume some data from, e.g. via:
def get_api_results(page)
results = HTTParty.get("api.api.com?page=#{page}")
end
When I call it I can retrieve a total.
results["total"] = 237
The API limits the number of records I can retrieve in one call, say 20. So I need to call it a few more times.
I want to do something like the following, ideally breaking it into pieces so I can use things like delayed_job..etc
def get_all_api_pages
results = get_api_results(1)
total = get_api_results(1)["total"]
until page*20 > total do |p|
results += get_api_results(p)
end
end
I always feel like I'm writing rubbish whenever I try and solve this (and I've tried to solve it in a number of ways).
The above, for example, leaves me at the mercy of an error with the API, which knocks out all my collected results if I hit an error at any point.
Wondering if there is just a generally good, clean way of dealing with this situation.
I don't think you can have that much cleaner...because you only receive the total once you called the API.
Have you tried to build your own enum for this. It encapsulates the ugly part. Here is a bit of sample code with a "mocked" API:
class AllRecords
PER_PAGE = 50
def each
return enum_for(:each) unless block_given?
current_page = 0
total = nil
while total.nil? || current_page * PER_PAGE < total
current_page += 1
page = load_page(current_page)
total = page[:total]
page[:items].each do |item|
yield(item)
end
end
end
private
def load_page(page)
if page == 5
{items: Array.new(37) { rand(100) }, total: 237}
else
{items: Array.new(50) { rand(100) }, total: 237}
end
end
end
AllRecords.new.each.each_with_index do |item, index|
p index
end
You can surely clean that out a bit but i think that this is nice because it does not collect all the items first.

Is it possible to define two methods in a Rails model that require different initialization?

Hi I'm attempting to create a model in Rails that can perform two calculations. This is my code:
class Calculator
def initialize(nair, cppy, interest_rate, payment, periods)
#nair = nair.to_f / 100
#cppy = cppy.to_f
#interest_rate = interest_rate
#payment = payment
#periods = periods
end
def effective
Refinance::Annuities.effective_interest_rate(#nair, #cppy)
end
def principal
Refinance::Annuities.principal(#interest_rate, #payment, #periods)
end
end
I have two forms that reside in different views that take input from the user including 'nair' and 'cppy' on one and 'interest_rate', 'payment' and 'periods' on the other.
The problem I've run into is that to use this model all five arguments need to be available.
Do I need to have separate models for each calculation?
I'm a complete beginning sorry if there is a really obvious answer.
Thanks!
There's probably a dozen different ways you could solve this, but one possible approach would be to use default arguments in your initialize method.
class Calculator
def initialize(nair=0, cppy=0, interest_rate=0, payment=0, periods=0)
#nair = nair.to_f / 100
#cppy = cppy.to_f
#interest_rate = interest_rate
#payment = payment
#periods = periods
end
def effective
Refinance::Annuities.effective_interest_rate(#nair, #cppy)
end
def principal
Refinance::Annuities.principal(#interest_rate, #payment, #periods)
end
end
Another possible solution is to make them class methods and not deal with instances or state:
class Calculator
def self.effective(nair, cppy)
nair = nair.to_f / 100
cppy = cppy.to_f
Refinance::Annuities.effective_interest_rate(nair, cppy)
end
def self.principal(interest_rate, payment, periods)
Refinance::Annuities.principal(interest_rate, payment, periods)
end
end
Calculator.effective(x, y)
Calculator.principal(x, y, z)

Refactor case/when using strings

I have a number of these in my controller:
def ups
#ups ||= Shipper::Ups.new(
ENV['UPS_ACCESS_KEY'],
ENV['UPS_PASSWORD'],
ENV['UPS_USERNAME'],
ENV['UPS_ACCOUNT']
)
end
And then I have this block that gets called:
def type(number, carrier)
case carrier.slug
when 'ups'
number_details = ups.track(number)
when 'fedex'
number_details = fedex.track(number)
when 'usps'
number_details = usps.track(number)
end
return number_details
end
But seems I could refactor that quite a bit if I could take the carrier.slug and prepend it to the lines like ups.track(number).
Is there a way to do that?
you can use send to do this but before that we need to make sure that you have the right carrier slug
if %w[ups fedex usps].include?(carrier.slug)
send(carrier.slug).track(number)
end

How to test the number of database calls in Rails

I am creating a REST API in rails. I'm using RSpec. I'd like to minimize the number of database calls, so I would like to add an automatic test that verifies the number of database calls being executed as part of a certain action.
Is there a simple way to add that to my test?
What I'm looking for is some way to monitor/record the calls that are being made to the database as a result of a single API call.
If this can't be done with RSpec but can be done with some other testing tool, that's also great.
The easiest thing in Rails 3 is probably to hook into the notifications api.
This subscriber
class SqlCounter< ActiveSupport::LogSubscriber
def self.count= value
Thread.current['query_count'] = value
end
def self.count
Thread.current['query_count'] || 0
end
def self.reset_count
result, self.count = self.count, 0
result
end
def sql(event)
self.class.count += 1
puts "logged #{event.payload[:sql]}"
end
end
SqlCounter.attach_to :active_record
will print every executed sql statement to the console and count them. You could then write specs such as
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)
You'll probably want to filter out some statements, such as ones starting/committing transactions or the ones active record emits to determine the structures of tables.
You may be interested in using explain. But that won't be automatic. You will need to analyse each action manually. But maybe that is a good thing, since the important thing is not the number of db calls, but their nature. For example: Are they using indexes?
Check this:
http://weblog.rubyonrails.org/2011/12/6/what-s-new-in-edge-rails-explain/
Use the db-query-matchers gem.
expect { subject.make_one_query }.to make_database_queries(count: 1)
Fredrick's answer worked great for me, but in my case, I also wanted to know the number of calls for each ActiveRecord class individually. I made some modifications and ended up with this in case it's useful for others.
class SqlCounter< ActiveSupport::LogSubscriber
# Returns the number of database "Loads" for a given ActiveRecord class.
def self.count(clazz)
name = clazz.name + ' Load'
Thread.current['log'] ||= {}
Thread.current['log'][name] || 0
end
# Returns a list of ActiveRecord classes that were counted.
def self.counted_classes
log = Thread.current['log']
loads = log.keys.select {|key| key =~ /Load$/ }
loads.map { |key| Object.const_get(key.split.first) }
end
def self.reset_count
Thread.current['log'] = {}
end
def sql(event)
name = event.payload[:name]
Thread.current['log'] ||= {}
Thread.current['log'][name] ||= 0
Thread.current['log'][name] += 1
end
end
SqlCounter.attach_to :active_record
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)

Rails 3 multiple parameter filtering using scopes

Trying to do a basic filter in rails 3 using the url params. I'd like to have a white list of params that can be filtered by, and return all the items that match. I've set up some scopes (with many more to come):
# in the model:
scope :budget_min, lambda {|min| where("budget > ?", min)}
scope :budget_max, lambda {|max| where("budget < ?", max)}
...but what's the best way to use some, none, or all of these scopes based on the present params[]? I've gotten this far, but it doesn't extend to multiple options. Looking for a sort of "chain if present" type operation.
#jobs = Job.all
#jobs = Job.budget_min(params[:budget_min]) if params[:budget_min]
I think you are close. Something like this won't extend to multiple options?
query = Job.scoped
query = query.budget_min(params[:budget_min]) if params[:budget_min]
query = query.budget_max(params[:budget_max]) if params[:budget_max]
#jobs = query.all
Generally, I'd prefer hand-made solutions but, for this kind of problem, a code base could become a mess very quickly. So I would go for a gem like meta_search.
One way would be to put your conditionals into the scopes:
scope :budget_max, lambda { |max| where("budget < ?", max) unless max.nil? }
That would still become rather cumbersome since you'd end up with:
Job.budget_min(params[:budget_min]).budget_max(params[:budget_max]) ...
A slightly different approach would be using something like the following inside your model (based on code from here:
class << self
def search(q)
whitelisted_params = {
:budget_max => "budget > ?",
:budget_min => "budget < ?"
}
whitelisted_params.keys.inject(scoped) do |combined_scope, param|
if q[param].nil?
combined_scope
else
combined_scope.where(whitelisted_params[param], q[param])
end
end
end
end
You can then use that method as follows and it should use the whitelisted filters if they're present in params:
MyModel.search(params)

Resources