ruby-on-railsruby-on-rails-4scopeclass-methodnamed-scope

Rails class method used as scope with complex logic


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
]

Solution

  • 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.