Rails class method used as scope with complex logic - ruby-on-rails

In the system there are employees with login information in the User model and
other information about them in the Profile model.
We want to be able to display a list of employees who have an anniversary
this month (the month of hire is the same as the current one) and it is
their 1st, 2nd, or a multiple of 5 years on the job.
We want to use it like a scope, but since the logic is complex, we are making
a Class method. Trying to split the logic into small chunks is becoming messy.
I am sure that the code can be simplified.
The biggest issue is that instead of getting a list of only the employees with
an anniversary as a scope would do, I am getting a list of all the employees
as nil or their user info if it is their anniversary month.
An example:
irb_001 >> Profile.anniversary?
[
[0] nil,
[1] nil,
[2] #<User:0x007fd17c883740> {
:id => 3,
:first_name => "Sally",
:last_name => "Brown",
:email => "sally#peanuts.com",
:password_digest => "[redacted]",
:created_at => Tue, 21 Feb 2018 11:12:42 EST -05:00,
:updated_at => Sat, 25 Feb 2018 12:28:45 EST -05:00,
},
[3] nil,
[4] nil,
[5] #<User:0x007fd17a2eaf38> {
:id => 6,
:first_name => "Lucy",
:last_name => "Van Pelt",
:email => "lucy#peanuts.com",
:password_digest => "[redacted]",
:created_at => Tue, 20 Nov 2018 21:01:04 EST -05:00,
:updated_at => Tue, 20 Nov 2018 21:02:36 EST -05:00,
},
[6] nil
]
irb_002 >>
What is the best way to achieve the desired result and clean up this code?
class User < ActiveRecord::Base
has_one :profile, dependent: :destroy
accepts_nested_attributes_for :profile, allow_destroy: true
after_create :create_matching_profile
delegate :active, to: :profile, prefix: true
private
def create_matching_profile
profile = build_profile
profile.save
end
end
class Profile < ActiveRecord::Base
belongs_to :user
def self.years_employed(profile)
# calculate how many years employed
#profile = profile
if #profile.employed_since?
(( Date.today.to_time - #profile.employed_since.to_time )/1.year.second).to_i
else
0
end
end
def self.anniversary_month(profile)
# get the month of hire
#profile = profile
#profile.employed_since? ? #profile.employed_since.month : 0
end
def self.anniversary?
# first, second, or multiple of five year anniversary month
#profiles = Profile.where("employed_since is not null")
#profiles.map do |profile|
if ( Date.today.month == anniversary_month(profile) )
#years_working = years_employed(profile)
if ( #years_working> 0 &&
( #years_working == 1 || #years_working == 2 || ( #years_working % 5 == 0 )))
result = true
else
result = false
end
else
result = false
end
profile.user if result
end
end
end
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# first_name :string
# last_name :string
# email :string
# password_digest :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Table name: profiles
#
# id :integer not null, primary key
# user_id :integer
# active :boolean
# employed_since :date
# ...other attributes...
# created_at :datetime not null
# updated_at :datetime not null
#
employed since data from Profiles
[
[0] Sun, 01 Dec 1991,
[1] Thu, 01 May 2018,
[2] Wed, 01 Nov 2017,
[3] Wed, 01 Feb 2017,
[4] Thu, 01 Aug 2018,
[5] Fri, 01 Nov 2013,
[6] Fri, 01 Nov 1991
]

This can be done in a much simpler and more efficient way by using the date functions in the database and doing the comparison there.
class User < ApplicationRecord
has_one :profile
def self.anniversary
self.joins(:profile)
.where("EXTRACT(MONTH FROM profiles.employed_since) = EXTRACT(MONTH FROM now())")
.where("profiles.employed_since < ?", 1.year.ago)
.where(%q{
EXTRACT(year FROM now()) - EXTRACT(year FROM profiles.employed_since BETWEEN 1 AND 2
OR
CAST(EXTRACT(year FROM now()) - EXTRACT(year FROM profiles.employed_since) AS INTEGER) % 5 = 0
})
end
end
This example is written for Postgres and you might need to adapt it to your RDBMS.

Using SQLITE the where clause looks like:
where "strftime('%m',employed_since) = strftime('%m', date('now'))
AND employed_since < date('now','-1 year','+1 day')
AND ( (strftime('%Y','now') - strftime('%Y', employed_since)) BETWEEN 1 AND 2
OR (strftime('%Y','now') - strftime('%Y', employed_since)) % 5 = 0 )"
This actually works as a scope, no need for a class method as I originally thought.

Related

factory_bot build_stubbed strategy

The factory_bot documentation for build strategies says:
factory_bot supports several different build strategies: build, create, attributes_for and build_stubbed
And continues with some examples of usage. However, it doesn't clearly state what the result of each one is. I've been using create and build for a while now. attributes_for seems straightforward from the description and I see some uses for it. However, what is build_stubbed? The description says
Returns an object with all defined attributes stubbed out
What does "stubbed out" mean? How is this different from either create or build?
Let's consider the difference on the example of these factories:
FactoryBot.define do
factory :post do
user
title { 'Post title' }
body { 'Post body' }
end
end
FactoryBot.define do
factory :user do
first_name { 'John' }
last_name { 'Doe' }
end
end
build
With build method everything is easy. It returns a Post instance that's not saved
# initialization
post = FactoryBot.build(:post)
# call
p post
p post.user
# output
#<Post:0x00007fd10f824168> {
:id => nil,
:user_id => nil,
:title => "Post title",
:body => "Post body",
:created_at => nil,
:updated_at => nil
}
#<User:0x00007f8792ed9290> {
:id => nil,
:first_name => "Post title",
:last_name => "Post body",
:created_at => nil,
:updated_at => nil
}
Post.all # => []
User.all # => []
create
With create everything is also quite obvious. It saves and returns a Post instance. But it calls all validations and callbacks and also creates associated instance of User
# initialization
post = FactoryBot.create(:post)
# call
p post
p post.user
# output
#<Post:0x00007fd10f824168> {
:id => 1,
:user_id => 1,
:title => "Post title",
:body => "Post body",
:created_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00,
:updated_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00
}
#<User:0x00007f8792ed9290> {
:id => 1,
:first_name => "John",
:last_name => "Joe",
:created_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00,
:updated_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00
}
Post record and associated user record were created in the database:
Post.all # => [<Post:0x00007fd10f824168> {...}]
# User also created in the database
User.all # => [<User:0x00007f91af405b30> {...}]
build_stubbed
build_stubbed imitates creating. It slubs id, created_at, updated_at and user_id attributes. Also it skips all validations and callbacks.
Stubs means that FactoryBot just initialize object and assigns values to the id created_at and updated_at attributes so that it just looks like created. For id it assign integer number 1001 (1001 is just default number what FactoryBot uses to assign to id), for created_at and updated_at assigns current datetime. And for every other record created with build_stubbed is will increment number to be assigned to id by 1.
First FactoryBot initialize user record and assign 1001 to id attribute but not save it to the database than it initialize post record and assing 1002 to the id attribute and 1001 to user_id attribute to make association, but also doesn't save record to the database.
See example below.
#initialization
post = FactoryBot.build_stubbed(:post)
# call
p post
p post.user
# output
# It looks like persisted instance
#<Post:0x00007fd10f824168> {
:id => 1002,
:user_id => 1001,
:title => "Post title",
:body => "Post body",
:created_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00,
:updated_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00
}
#<User:0x00007f8792ed9290> {
:id => 1001,
:first_name => "John",
:last_name => "Joe",
:created_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00,
:updated_at => Sat, 18 Jun 2022 05:32:17.122906000 UTC +00:00
}
Post and user records were not created in the database!!!
# it is not persisted in the database
Post.all # => []
# Association was also just stubbed(initialized) and there are no users in the database.
User.all # => []

Rspec let variable producing weird result

I am having a weird issue with RSpec that I don't quite understand.
This is my port_stock_spec.rb file:
# == Schema Information
#
# Table name: port_stocks
#
# id :bigint(8) not null, primary key
# portfolio_id :integer
# stock_id :integer
# volume :integer
# transaction_price :float
# current_price :float
# percent_change :float
# created_at :datetime not null
# updated_at :datetime not null
# current_value :float
# dollar_change :float
# total_spend :float
# transaction_date :datetime
# action :integer
# position :integer default("open")
# ticker :string
# slug :string
#
require 'rails_helper'
RSpec.describe PortStock, type: :model do
let(:stock) { create(:stock, price: 10.00) }
let(:portfolio) { create(:portfolio) }
let(:port_stock_1) { create(:port_stock, stock: stock, portfolio: portfolio, transaction_price: stock.price, action: :buy, volume: 100) }
context "associations" do
it { should belong_to(:portfolio) }
it { should belong_to (:stock) }
end
context "methods" do
it "should accurately calculate the positive percent_change of the current PortStock" do
port_stock_1.current_price = 20.00
expect(port_stock_1.calculate_percent_change).to eql 100.00
end
it "should accurately calculate the negative percent_change of the current PortStock" do
port_stock_1.current_price = 5.00
expect(port_stock_1.calculate_percent_change).to eql(-50.00)
end
it "should accurately calculate the negative dollar_change of the current PortStock" do
port_stock_1.current_price = 5.00
port_stock_1.volume = 1000
expect(port_stock_1.calculate_dollar_change).to eql (-5000.00)
end
# more specs that may or may no interact with the let variables.
it "should accurately calculate the portfolio's initial_dollar_value" do
expect(portfolio.initial_dollar_value).to eql 1000.00
end
end
Then I have the following method on my portfolio.rb model:
def calculate_portfolio_initial_dollar_value
if self.portfolio.initial_dollar_value.nil?
self.portfolio.initial_dollar_value = 0.0
end
self.portfolio.initial_dollar_value += (self.transaction_price * self.volume)
self.portfolio.save!
end
When I run my test suite, that last test keeps failing, when it shouldn't:
Failures:
1) PortStock methods should accurately calculate the portfolio's initial_dollar_value
Failure/Error: expect(portfolio.initial_dollar_value).to eql 1000.00
expected: 1000.0
got: 798229.0
(compared using eql?)
# ./spec/models/port_stock_spec.rb:77:in `block (3 levels) in <top (required)>'
Finished in 5.05 seconds (files took 3.68 seconds to load)
29 examples, 1 failure, 19 pending
So I put a binding.pry within the it blocks of the last few tests and when I check the portfolio.initial_dollar_value it repeatedly changes the value.
[1] pry(#<RSpec::ExampleGroups::PortStock::Methods>)> portfolio
=> #<Portfolio:0x00007fcdc5c5db28
id: 14,
user_id: 7,
current_dollar_value: 2864770.0,
percent_change: 75.02,
created_at: Sat, 13 Apr 2019 00:36:24 UTC +00:00,
updated_at: Sat, 13 Apr 2019 00:36:24 UTC +00:00,
num_winners: 2,
num_losers: 7,
initial_dollar_value: 860679.0,
dollar_change: 92865.0>
[2] pry(#<RSpec::ExampleGroups::PortStock::Methods>)> port_stock_1.portfolio
=> #<Portfolio:0x00007fcdc5c5db28
id: 14,
user_id: 7,
current_dollar_value: 150.0,
percent_change: -85.0,
created_at: Sat, 13 Apr 2019 00:36:24 UTC +00:00,
updated_at: Sat, 13 Apr 2019 00:36:42 UTC +00:00,
num_winners: 0,
num_losers: 1,
initial_dollar_value: 1000.0,
dollar_change: -850.0>
[3] pry(#<RSpec::ExampleGroups::PortStock::Methods>)> portfolio
=> #<Portfolio:0x00007fcdc5c5db28
id: 14,
user_id: 7,
current_dollar_value: 150.0,
percent_change: -85.0,
created_at: Sat, 13 Apr 2019 00:36:24 UTC +00:00,
updated_at: Sat, 13 Apr 2019 00:36:42 UTC +00:00,
num_winners: 0,
num_losers: 1,
initial_dollar_value: 1000.0,
dollar_change: -850.0>
[4] pry(#<RSpec::ExampleGroups::PortStock::Methods>)> portfolio
=> #<Portfolio:0x00007fcdc5c5db28
id: 14,
user_id: 7,
current_dollar_value: 150.0,
percent_change: -85.0,
created_at: Sat, 13 Apr 2019 00:36:24 UTC +00:00,
updated_at: Sat, 13 Apr 2019 00:36:42 UTC +00:00,
num_winners: 0,
num_losers: 1,
initial_dollar_value: 1000.0,
dollar_change: -850.0>
[5] pry(#<RSpec::ExampleGroups::PortStock::Methods>)>
I don't understand why.
Thoughts?
Edit 1
This is portfolio.rb Factory:
FactoryBot.define do
factory :portfolio do
user
current_dollar_value { Faker::Number.number(7) }
percent_change { Faker::Number.decimal(2) }
num_winners { Faker::Number.number(1) }
num_losers { Faker::Number.number(1) }
initial_dollar_value { Faker::Number.number(6) }
dollar_change { Faker::Number.number(5) }
end
end
Edit 2
There is a callback on my port_stock.rb model that triggers methods related to portfolio_initial_dollar_value:
after_save :calculate_portfolio_initial_dollar_value
Also other callbacks that impact other aspects of the portfolio:
after_save :update_portfolio_current_dollar_value
after_save :update_portfolio_initial_dollar_value, if: (:total_spend_previously_changed? || :volume_previously_changed?)
def update_portfolio_current_dollar_value
self.portfolio.current_dollar_value = self.portfolio.port_stocks.open.map(&:current_value).sum
self.portfolio.save!
end
def update_portfolio_initial_dollar_value
self.portfolio.initial_dollar_value = self.portfolio.port_stocks.open.map { |ps| ps.volume * ps.transaction_price }.sum
self.portfolio.save!
end
Edit 3
For the full version both the model (port_stock.rb) & spec (port_stock_spec.rb) files, check out this gist. I didn't want to pollute SO with that full dump.
As #grzekos point out you never call stock or port_stock_1 during the execution of it "should accurately calculate the portfolio's initial_dollar_value" test.
Why? Because you used let to setup/create the test objects.
If you want to always setup/create stock, portfolio and port_stock_1 you can either use let! (RSpec documentation) or use a before block like this:
let(:stock) { create(:stock, price: 10.00) }
let(:portfolio) { create(:portfolio) }
let(:port_stock_1) { create(:port_stock, stock: stock, portfolio: portfolio, transaction_price: stock.price, action: :buy, volume: 100) }
before do
stock
portfolio
port_stock_1
end
Why do you see different numbers during debuging with pry?
In your first test you called the portfolio object, which was created with FactoryBot. The Factory asinged a random 6 digit number to the initial_dollar_value atttribute via Faker::Number.number(6).
In your second test you called port_stock_1.portfolio. Now the block of let(:port_stock_1) gets evaluated. This creates a PortStock object, which in its after_save method updates the initial_dollar_value of portfolio.
All subsequet calls of portfolio or port_stock_1.portfolio do not change the value of initial_dollar_value anymore.
Ok, so the failing test is:
it "should accurately calculate the portfolio's initial_dollar_value" do
expect(portfolio.initial_dollar_value).to eql 1000.00
end
Here I can see that you create the portfolio object and the initial_dollar_value is set (in the factory) to Faker::Number.number(6). Why do you expect it to be equal to 1000.00?
The stock or the port_stock_1 objects are never created when you run this particular test. Quoting the Rspec let documentation
Use let to define a memoized helper method. The value will be cached across
multiple calls in the same example but not across examples.
Note that let is lazy-evaluated: it is not evaluated until the first time
the method it defines is invoked.

Can someone point out to me why ActiveRecord::Enum is not working as expected in this simple example?

I am implementing some code refactoring and for my purposes, I've determined that ActiveRecord::Enum would be a perfect fit. For those who have not used or heard of ActiveRecord::Enum here is the documentation link
ActiveRecord::Enum.
Here is code taken from the docs that I am having a problem with.
class Conversation < ActiveRecord::Base
enum status: [ :active, :archived ]
end
# conversation.update! status: 0
conversation.active!
conversation.active? # => true
conversation.status # => "active"
# conversation.update! status: 1
conversation.archived!
conversation.archived? # => true
conversation.status # => "archived"
# conversation.status = 1
conversation.status = "archived"
conversation.status = nil
conversation.status.nil? # => true
conversation.status # => nil
Here is my example code which tries to mimic this behavior:
class ReverificationTracer < ActiveRecord::Base
enum status: [ :verified, :unverified ]
end
rev = ReverificationTracker.create
rev.verified! ==> true
rev.unverified? ==> false
rev.status ==> nil
rev.unverified! ==> true
rev.unverified? ==> false
rev.status ==> nil
When I inspect the rev variable, I do see that the status has shifted to '0'
rev
id: 1,
account_id: nil,
status: "0",
created_at: Wed, 24 Feb 2016 16:24:55 UTC +00:00,
updated_at: Wed, 24 Feb 2016 16:25:21 UTC +00:00>
When it is archived I see the status increment to 1.
Why is this not working as I would expect from the documentation?
The issue is caused by your status column being string column, not the integer. Change it to integer and everything will work as expected.

How to validate :created_at in model (RAILS 3)

irb(main):044:0> i1.created_at
=> Thu, 24 Apr 2014 02:41:15 UTC +00:00
irb(main):045:0> i2.created_at
=> Thu, 24 Apr 2014 02:41:15 UTC +00:00
irb(main):046:0> i1.created_at == i2.created_at
=> false
irb(main):047:0> i1.created_at.to_time.to_i == i2.created_at.to_time.to_i
=> true
Seems not to work validates_uniqueness_of :created_at
because
irb(main):046:0> i1.created_at == i2.created_at
=> false
How to validate created_at? Don't want to save with the same date.
+++ UPDATE +++
irb(main):048:0> i1.created_at.class
=> ActiveSupport::TimeWithZone
irb(main):049:0> i2.created_at.class
=> ActiveSupport::TimeWithZone
Since they might have different precision milliseconds.
Refer to the post: Testing ActiveSupport::TimeWithZone objects for equality
Chances are the millisecond values would be unequal.
puts i1.created_at.usec
puts i2.created_at.usec
I think, if you are getting concurrent requests, there are chances that you may have multiple entries in the table which are created at same time and will have same time stamps.
As you said, if you don't want to save with the same date, you can put a lock while saving the entries, removing the possibility of creating two entries at same time. In that case validates_uniqueness_of :created_at should also work.
Just in case
class Item < ActiveRecord::Base
attr_accessible :asin, :domain, :formatted_price, :user_id, :created_at
validate :double_dates
private
def double_dates
if Item.where(:user_id => self.user_id, :asin => self.asin, :domain => self.domain).where("DATE(created_at) = ?", Date.today).length >= 1
errors.add(:created_at, "no double dates")
end
end
end

Wrong boolean result comparing Date objects

I'm trying to write test that compare some dates. So far i have 2 tests, one of them works as intended, but the other one fails because doesnt/not correctly compare dates.
Here is my code:
def self.has_expired?(card, start_month, start_year, annually)
card_date = Date.new(card.year, card.month, -1)
billing_date = Date.new(start_year, start_month, -1)
if !annually
p '--------'
p card_date
p billing_date
card_date > billing_date
else
#return false
end
end
creditcard object
creditcard = ActiveMerchant::Billing::CreditCard.new(
:number => 1234567890123456
:month => 01,
:year => 13,
:first_name => 'John',
:last_name => 'Doe',
:verification_value => 132,
:brand => 'visa'
)
Here is output of p's
First block works as intended.
"--------"
Tue, 31 Jan 0013
Thu, 28 Feb 2013
false
Second block fails, expecting true, but got false
."--------"
Tue, 31 Jan 0013
Fri, 30 Nov 2012
false
Here is my rspec code
describe CreditCard do
context 'card_expired' do
it 'should return false with args passed to method (02month, 13 year, annually==false)' do
CreditCard.has_expired?(creditcard, 02, 2013, false).should == false
end
it 'should return true with args passed to method (11month, 12 year, annually==false)' do
CreditCard.has_expired?(creditcard, 11, 2012, false).should == true
end
end
end
in irb it works as charm, returning correct value(true/false)
I think the problem is in your logic. A card is expired when the expiration date is before the billing date, thus when
card_date < billing_date # expired
and not when
card_date > billing_date # valid
Also try puting in the full 2013 and see if that helps if it keeps breaking
:year => 2013,
You're also missing a comma after this line (probably a copy/paste error) :number => 1234567890123456

Resources