Rails: Finding records that overlap - ruby-on-rails

I have a job model that has_many :fonctions and in my job model I'm creating a method to give me the number of similar fonction in all job compared to a given job.
ex: I want to compare all my job to Job1 job1 has this fonction("strategy", "management", "marketing", entrepreneurship")
another job job2 has this fonction( "strategy", "management", "data science")
So this must give me when doing (job1 & job2).size 2
for this i have this method in my job model that must do a similar job but the problem that i get this error undefined local variable or method for job'
def fonctions_score
(job.fonctions.collect(&:id) & self.fonctions.collect(&:id)).size
end
Update
This is the code that I'm trying now but still getting this error wrong number of arguments (0 for 1)
def fonctions_score(other_job)
these = other_job.fonctions.collect {|f| f.id }
those = self.fonctions.collect {|f| f.id }
logger.debug these logger.debug those # should just have lists of ids
common = (these & those)
logger.debug common # should be common ids
common.size
end
in my controller I'm ordering jobs like this
#related_jobs = Job.all
#related_jobs.sort_by do |related_job|
related_job.final_score
end

Try this
def calculate(j1, j2)
(j1.fonctions.pluck(:id) & j2.fonctions.pluck(:id)).size
end
Or
def calculate
self.fonctions.count
end

Related

Rails Query a List for a CRON Job

I'm a complete novice with CRON jobs but I think I have that set up correctly.
Ultimately what I'm trying to do is send an email every day at 8:00 am to users (and a couple others) that have not logged in within the last 3 days, have not received the email, AND are marked as active OR temp as a status.
So from querying the db in console I know that I can do:
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
That's not certainly clean but I was struggling with finding a contained query that grabbed all that data. Is there a cleaner way to do this query?
I believe I have my cron job set up correctly using a runner. I have whenever installed and in my schedule.rb I have:
every 1.day, at: '8:00 am' do
runner 'ReminderMailer.agent_mailer.deliver'
end
So under app > mailer I created ReminderMailer
class ReminderMailer < ApplicationMailer
helper ReminderHelper
def agent_reminder(user)
#user = user
mail(to: email_recipients(user), subject: 'This is your reminder')
end
def email_recipients(agent)
email_address = ''
email_addresses += agent.notification_emails + ',' if agent.notification_emails
email_addresses += agent.manager
email_address += agent.email
end
end
Where I'm actually struggling is where I should put my queries to send to the mailer, which is why I built a ReminderHelper.
module ReminderHelper
def applicable_agents(user)
agent = []
first = User.where(status: 'active').or(User.where(status: 'temp'))
second = first.where("last_login_at < ? ", Time.now-3.days)
third = second.where(notified: false)
agent << third
return agent
end
end
EDIT: So I know I could in theory do a chain of where queries. There's gotta be a better way right?
So what I need help on is: do I have the right structure in place? Is there a cleaner way to query this data in ActiveRecord for the CRON job? Is there a way to test this?
Try combining them together as if understand the conditions correct
Have not logged in within the last 3 days,
Have not received the email
Are marked as active OR temp as a status
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
module ReminderHelper
def applicable_agents(user)
User.where("last_login_at < ? ", 3.days.ago).
where(notified: false).
where(status: ['active', temp])
end
end
You don't need to add/ assign them to array. Because this relation is already like an array. You can use .to_a if you need array. If you just want to iterate over them then users.each should work fine.
Update
class User
scope :not_notified, -> { where(notified: false) }
scope :active_or_temp, -> { where(status: ['active', 'temmp']) }
scope :last_login_in, -> (default_days = 3) { where("last_login_at < ?", default_days.days.ago) }
end
and then use
User.not_notified.active_or_temp.last_login_in(3)
Instead of Time.now-3.days it's better to use 3.days.ago because it keeps time zone also in consideration and avoids unnecessary troubles and failing test cases.
Additionally you can create small small scopes and combine them. More read on scopes https://guides.rubyonrails.org/active_record_querying.html

Run Rails jobs sequentially

I have three Rails jobs to process a player yellow/red cards in a soccer tournament, and the penalties these players will have due to getting this cards.
The idea is that the first job collects all Incidences (an Incidence is when a Player gets a yellow card, to give an example), and counts all the cards a Player got.
class ProcessBookedPlayersJob < ApplicationJob
queue_as :default
#cards = []
def perform(*args)
#cards = []
yellows = calculate_cards(1)
reds = calculate_cards(2)
#cards << yellows << reds
end
def after_perform(*match)
#ProcessPenaltiesJob.perform_later #cards
ProcessPenalties.perform_later #cards
#PenaltiesFinalizerJob.perform_later match
PenaltiesFinalizer.perform_later match
end
def calculate_cards(card_type)
cards = Hash.new
players = Player.fetch_players_with_active_incidences
players.each { |p|
# 1 is yellow, 2 is red
counted_cards = Incidence.incidences_for_player_id_and_incidence_type(p.id, card_type).size
cards[p] = counted_cards
}
return cards
end
end
This first job is executed when an Instance is created.
class Incidence < ApplicationRecord
belongs_to :player
belongs_to :match
after_save :process_incidences, on: :create
def self.incidences_for_player_id_and_incidence_type(player_id, card_type)
return Incidence.where(status: 1).where(incidence_type: card_type).where(player_id: player_id)
end
protected
def process_incidences
ProcessBookedPlayers.perform_later
end
end
After this, another job runs and creates the necessary Penalties (a Penalty is a ban for the next Match, for example) according to the Hash output that the previous job created.
class ProcessPenaltiesJob < ApplicationJob
queue_as :default
def perform(*cards)
yellows = cards[0]
reds = cards[1]
create_penalties_for_yellow_cards(yellows)
create_penalties_for_red_cards(reds)
end
# rest of the job...
And also there's another job, that sets these bans as disabled, once they have expired.
class PenaltiesFinalizerJob < ApplicationJob
queue_as :default
def perform(match)
active_penalties = Penalty.find_by(status: 1)
active_penalties.each do |p|
#penalty.starting_match.order + penalty.length == el_match_que_inserte.order (ver si seria >=)
if p.match.order + p.length >= match.order
p.status = 2 # Inactivate
p.save!
end
end
end
end
As you can see in ProcessBookedPlayersJob's after_perform method
def after_perform(*match)
ProcessPenalties.perform_later #cards
PenaltiesFinalizer.perform_later match
end
I'm trying to get those two other jobs executed (ProcessPenaltiesJob and PenaltiesFinalizerJob) with no luck. The job ProcessBookedPlayersJob is being executed (because I can see this in the log)
[ActiveJob] [ProcessBookedPlayersJob] [dbb8445e-a706-4443-9cb8-2c45f49a4f8f] Performed ProcessBookedPlayersJob (Job ID: dbb8445e-a706-4443-9cb8-2c45f49a4f8f) from Async(default) in 38.81ms
But the other two jobs aren't executed. So, how can I get both ProcessPenaltiesJob and PenaltiesFinalizerJob run after ProcessBookedPlayersJob has finalized its execution? I don't mind if they run in parallel, but they need to be run after the first one finishes, since they need its output as their input.
I have searched for this, and the closest match I found was this answer. Quoting it:
If the sequential jobs you are talking about however are of different
jobs / class, then you can just call the other job once the first job
has finished.
That's exactly the behaviour I'm trying to have... but how can I get my jobs to run sequentially?
For now, I'm thinking in adding the first job's logic into Incidences's after_savehook, but that doesn't sound too natural. Is there any other way to pipeline the execution of my jobs?
Many thanks in advance

testing behaviour of method that scopes outputs into a hash

I have a method which creates a key value pair of delivery costs, the key being the type, and the value being the cost.
def calculate_scoped_job_delivery_costs
delivery_hash = {}
['install', 'fuel', 'breakdown'].each do |scope|
delivery_hash[scope.humanize] = job_delivery_costs.send(scope).inject(0) { |total, item| total + (item.cost_per_unit * item.hour_count) * item.quantity }
end
delivery_hash.delete_if {|key, value| value <= 0 }
end
the key is a scope in the job delivery costs model, which retrieves all associated costs with that scope and adds them up. It works, but I want to test its behaviour, albeit retrospectively.
So its core expected behaviour is:
it should output a hash
it should calculate each scope value
it should remove blank values from the hash
So I have written this test (factories posted below)
let(:jdc1){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
let(:jdc2){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
let(:jdc3){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
describe "calculate_scoped_job_delivery_costs" do
before do
allow(jdc1).to receive(:timing).and_return('fuel')
jdc2.update_attributes(quantity: 4)
jdc2.delivery_cost.update_attributes(timing: 'breakdown')
allow(job).to receive(:job_delivery_costs).and_return(JobDeliveryCost.where(id: [jdc1,jdc2,jdc3].map{|jdc| jdc.id}))
end
it "should retrieve a hash with jdc scopes" do
expect(job.calculate_scoped_job_delivery_costs.is_a?(Hash)).to be_truthy
end
it "should calculate each hash value" do
expect(job.calculate_scoped_job_delivery_costs).to eq "Fuel"=>15.0
end
it "should remove blank values from hash" do
expect(job.calculate_scoped_job_delivery_costs).to_not include "Breakdown"=>0
end
end
So in the last test, it passes, why? I have purposefully tried to make it break by updating the attributes in the before block on jdc2 so that breakdown is another scoped value.
Secondly, by changing the state of jdc2 and its values, this should break test 2 as fuel is no longer calculated against the same values.
Here are my factories...
FactoryGirl.define do
factory :job_delivery_cost do
job
delivery_cost
cost_per_unit 1.5
quantity 3
hour_count 1.0
end
end
FactoryGirl.define do
factory :delivery_cost do
title
timing "Fuel"
cost_per_unit 1.5
end
end
FactoryGirl.define do
factory :job do
job_type
initial_contact_id_placeholder {FactoryGirl.create(:contact).id}
title "random Title"
start "2013-10-04 11:21:24"
finish "2013-10-05 11:21:24"
delivery "2013-10-04 11:21:24"
collection "2013-10-05 11:21:24"
delivery_required false
collection_required false
client { Client.first || FactoryGirl.create(:client) }
workflow_state "offer"
admin
end
end
job has_many :job_delivery_costs.
job_delivery_cost belongs_to :delivery_cost
has_many :job_delivery_costs
has_many :jobs, through: :job_delivery_costs
I am really struggling with the logic of these tests, I am sure there are more holes than what I have laid out above. I welcome criticism in that regard.
thanks
A couple of suggestions:
Remember that let is lazy-evaluated; factories within the block are not created until the symbol defined by let is encountered in the code. This can have unexpected consequences for things like scopes, where you might think that the database already includes your factory-generated rows. You can get around this by using let!, which is evaluated immediately, or by restructuring the spec to ensure things get created in the right order.
I prefer not to do partial stubbing where it can be avoided. For scopes, you are probably better off using factories and just letting the scopes retrieve the rows instead of stubbing the relations. In your case this means getting rid of the code in the before block and setting up each example with factories, so that the scopes are retrieving the expected values.

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)

Ruby on Rails - weird behaviour logic by delayed job

I am doing the delayed_job by tobi and when I run the delayed_job but the fbLikes count is all wrong and it seems to increment each time I add one more company. Not sure wheres the logic wrong. The fbLikes method I tested before and it work(before I changed to delayed_job)
not sure where the "1" come from...
[output]
coca-cola
http://www.cocacola.com
Likes: 1 <--- Not sure why the fbLikes is 1 and it increment with second company fbLikes is 2 and so on...
.
[Worker(host:aname.local pid:1400)] Starting job worker
[Worker(host:aname.local pid:1400)] CountJob completed after 0.7893
[Worker(host:aname.local pid:1400)] 1 jobs processed at 1.1885 j/s, 0 failed ...
I am running the delayed_job in Model and trying to run the job of
counting the facebook likes
here is my code.
[lib/count_rb.job]
require 'net/http'
class CountJob< Struct.new(:fbid)
def perform
uri = URI("http://graph.facebook.com/#{fbid}")
data = Net::HTTP.get(uri)
return JSON.parse(data)['likes']
end
end
[Company model]
before_save :fb_likes
def fb_likes
self.fbLikes = Delayed::Job.enqueue(CountJob.new(self.fbId))
end
the issue is coming from
before_save :fb_likes
def fb_likes
self.fbLikes = Delayed::Job.enqueue(CountJob.new(self.fbId))
end
the enqueue method will not return the results of running the CountJob. I believe it will return whether the job successfully enqueued or not and when you are saving this to the fb_likes value it will evaluate to 1 when the job is enqueued successfully.
You should be setting fbLikes inside the job that is being run by delayed_job not as a result of the enqueue call.
before_save :enqueue_fb_likes
def fb_likes
Delayed::Job.enqueue(CountJob.new(self.fbId))
end
Your perform method in the CountJob class should probably take the model id for you to look up and have access to the fbId and the fbLikes attributes instead of just taking the fbId.
class CountJob< Struct.new(:id)
def perform
company = Company.find(id)
uri = URI("http://graph.facebook.com/#{company.fbid}")
data = Net::HTTP.get(uri)
company.fbLikes = JSON.parse(data)['likes']
company.save
end

Resources