AgileVentures/MetPlus_PETS

View on GitHub
app/models/job.rb

Summary

Maintainability
A
2 hrs
Test Coverage
class Job < ActiveRecord::Base
  after_save :save_job_to_cruncher
  belongs_to :company
  belongs_to :company_person
  belongs_to :education

  # force address validation in controller upon job create and job update when
  # new address is being created.  If valid, new address is saved when job is saved.
  belongs_to :address, autosave: true

  # Instance var "new_address" is not persisted, and is defined in order to support
  # adding a new company address (aka job location) by the user when creating
  # or editing a company job.
  attr_accessor :new_address
  belongs_to :new_address
  accepts_nested_attributes_for :new_address

  belongs_to :job_category
  has_and_belongs_to_many :job_types
  has_and_belongs_to_many :job_shifts

  has_many   :job_skills, inverse_of: :job
  has_many   :skills, through: :job_skills, dependent: :destroy
  accepts_nested_attributes_for :job_skills, allow_destroy: true, reject_if: :all_blank

  has_many   :required_skills, -> { where job_skills: { required: true } },
             through: :job_skills, class_name: 'Skill', source: :skill
  has_many   :nice_to_have_skills, -> { where job_skills: { required: false } },
             through: :job_skills, class_name: 'Skill', source: :skill
  has_many   :job_applications
  has_many   :job_seekers, through: :job_applications, dependent: :destroy

  has_many :job_licenses, inverse_of: :job
  has_many :licenses, through: :job_licenses, dependent: :destroy
  # ^^ Using "has_many, through" instead of HABTM becuase the latter does not
  #    work well with "accepts_nested_attributes_for"
  accepts_nested_attributes_for :job_licenses, allow_destroy: true, reject_if: :all_blank

  has_many :job_questions, inverse_of: :job
  has_many :questions, through: :job_questions, dependent: :destroy
  accepts_nested_attributes_for :job_questions, allow_destroy: true,
                                reject_if: :all_blank

  YEARS_OF_EXPERIENCE_OPTIONS = (0..20).to_a.freeze
  validates_presence_of :title
  validates_presence_of :company_job_id
  validates_length_of   :title, maximum: 100
  validates_presence_of :description
  validates_length_of   :description, maximum: 10_000
  validates_presence_of :company_id
  validates_presence_of :company_person_id, allow_nil: true
  validates_numericality_of :years_of_experience,
                            allow_blank: true,
                            greater_than_or_equal_to: 0,
                            less_than_or_equal_to: 20
  validates_numericality_of :available_positions, greater_than: 0
  validates_presence_of :pay_period, message: 'must be specified',
    if: Proc.new { |j| j.min_salary.present? }

  validates_numericality_of :min_salary, :max_salary, allow_blank: true,
    less_than_or_equal_to: 999999.99

  validates_format_of :min_salary, :max_salary, allow_blank: true,
    with: /\A\d{0,6}(\.\d{0,2})?\z/,
    message: 'must match format NNNNNN.NN (up to 6 digits, optional decimal ' +
             'point, optional digits for cents)'

  validate :max_salary_consistent_with_min_salary

  def max_salary_consistent_with_min_salary
    if max_salary.present?

      if min_salary.present?
        errors.add(:max_salary, 'cannot be less than minimum salary') if
          max_salary < min_salary
      else
        errors.add(:min_salary,
                   'must be specified if maximum salary is specified')
      end
    end
  end

  scope :new_jobs, ->(given_time) { where('jobs.created_at > ?', given_time) }
  scope :find_by_company, ->(company) { where(company: company) }

  enum status: [:active, :filled, :revoked]
  has_many :status_changes, as: :entity, dependent: :destroy

  # `status.to_sym` is necessary because sometimes we would need to create
  #  a job with a status other than `active`(in tests) and we do not want
  #  to have a mismatch in statuses across `jobs` and `status_changes` tables.

  after_create do
    StatusChange.update_status_history(self, status.to_sym || :active)
  end

  def number_applicants
    job_applications.size
  end

  def apply(job_seeker, questions_answers = nil)
    job_application = job_applications.build(job_seeker_id: job_seeker.id)

    # If job has questions to be answered by applicant:
    questions_answers&.each do |k, v|
      job_application.application_questions
        .build(question_id: k, answer: (v == 'true'))
    end

    if job_application.save!
      send_application_email_to_company job_seeker, job_application
      yield(job_application, self, job_seeker) if block_given?
      job_application
    end
  end

  def status_change_time(status, which = :latest)
    StatusChange.status_change_time(self, status, which)
  end

  def filled
    update_attribute(:status, :filled)
    StatusChange.update_status_history(self, :filled)
  end

  def revoked
    if update_attribute(:status, :revoked)
      StatusChange.update_status_history(self, :revoked)
      return true
    end
    false
  end

  def last_application_by_job_seeker(job_seeker)
    job_applications.where(job_seeker: job_seeker).order(:created_at).last
  end

  def recent_for?(user)
    created_at > user.last_sign_in_at
  end

  private

  def save_job_to_cruncher
    cruncher_posted = true

    unless (changed & %w(title description)).empty?
      begin
        if id_changed?
          cruncher_posted = JobCruncher.create_job(id, title, description)
          # If we fail on create it may be because the cruncher DB and
          # front-end DB are out of sync (which will almost certainly be
          # the case when the front-end is in testing).
          # In that case, try to update the job instead.
          unless cruncher_posted
            cruncher_posted = JobCruncher.update_job(id, title, description)
          end
        else
          cruncher_posted = JobCruncher.update_job(id, title, description)
        end
      rescue
        # Exception occured and save/update TX has been rolled back
        cruncher_posted = false
      end
    end
    unless cruncher_posted
      errors.add(:job, 'could not be posted to Cruncher, please try again.')

      # Here raising ActiveRecord::RecordInvalid in order to
      # 1) force a rollback of the save (update) transaction (if not already),
      # and, 2) force a return value of 'false' from save (update)
      # See: http://tech.taskrabbit.com/blog/2013/05/23/rollback-after-save/

      raise ActiveRecord::RecordInvalid, self
    end
    true
  end

  def send_application_email_to_company(job_seeker, job_application)
    resume = nil
    resume = job_seeker.resumes[0].id unless job_seeker.resumes.empty?
    # Send mail to the company with the attached resume
    CompanyMailerJob.set(wait: Event.delay_seconds.seconds)
                    .perform_later(Event::EVT_TYPE[:JS_APPLY],
                                   company,
                                   nil,
                                   application: job_application,
                                   resume_id: resume)
  end
end