gitcoinco/code_fund_ads

View on GitHub
app/models/job_posting.rb

Summary

Maintainability
C
1 day
Test Coverage
# == Schema Information
#
# Table name: job_postings
#
#  id                         :bigint           not null, primary key
#  auto_renew                 :boolean          default(TRUE), not null
#  city                       :string
#  company_email              :string
#  company_logo_url           :string
#  company_name               :string
#  company_url                :string
#  country_code               :string
#  description                :text             not null
#  detail_view_count          :integer          default(0), not null
#  display_salary             :boolean          default(TRUE)
#  end_date                   :date             not null
#  full_text_search           :tsvector
#  how_to_apply               :text
#  job_type                   :string           not null
#  keywords                   :string           default([]), not null, is an Array
#  list_view_count            :integer          default(0), not null
#  max_annual_salary_cents    :integer          default(0), not null
#  max_annual_salary_currency :string           default("USD"), not null
#  min_annual_salary_cents    :integer          default(0), not null
#  min_annual_salary_currency :string           default("USD"), not null
#  offers                     :string           default([]), not null, is an Array
#  plan                       :string
#  province_code              :string
#  province_name              :string
#  remote                     :boolean          default(FALSE), not null
#  remote_country_codes       :string           default([]), not null, is an Array
#  source                     :string           default("internal"), not null
#  source_identifier          :string
#  start_date                 :date             not null
#  status                     :string           default("pending"), not null
#  title                      :string           not null
#  url                        :text
#  created_at                 :datetime         not null
#  updated_at                 :datetime         not null
#  campaign_id                :bigint
#  coupon_id                  :bigint
#  organization_id            :bigint
#  session_id                 :string
#  stripe_charge_id           :string
#  user_id                    :bigint
#
# Indexes
#
#  index_job_postings_on_auto_renew                    (auto_renew)
#  index_job_postings_on_campaign_id                   (campaign_id)
#  index_job_postings_on_city                          (city)
#  index_job_postings_on_company_name                  (company_name)
#  index_job_postings_on_country_code                  (country_code)
#  index_job_postings_on_coupon_id                     (coupon_id)
#  index_job_postings_on_detail_view_count             (detail_view_count)
#  index_job_postings_on_end_date                      (end_date)
#  index_job_postings_on_full_text_search              (full_text_search) USING gin
#  index_job_postings_on_job_type                      (job_type)
#  index_job_postings_on_keywords                      (keywords) USING gin
#  index_job_postings_on_list_view_count               (list_view_count)
#  index_job_postings_on_max_annual_salary_cents       (max_annual_salary_cents)
#  index_job_postings_on_min_annual_salary_cents       (min_annual_salary_cents)
#  index_job_postings_on_offers                        (offers) USING gin
#  index_job_postings_on_organization_id               (organization_id)
#  index_job_postings_on_plan                          (plan)
#  index_job_postings_on_province_code                 (province_code)
#  index_job_postings_on_province_name                 (province_name)
#  index_job_postings_on_remote                        (remote)
#  index_job_postings_on_remote_country_codes          (remote_country_codes) USING gin
#  index_job_postings_on_session_id                    (session_id)
#  index_job_postings_on_source_and_source_identifier  (source,source_identifier) UNIQUE
#  index_job_postings_on_start_date                    (start_date)
#  index_job_postings_on_title                         (title)
#  index_job_postings_on_user_id                       (user_id)
#

class JobPosting < ApplicationRecord
  # extends ...................................................................
  # includes ..................................................................
  include Sanitizable
  include Taggable
  include FullTextSearchable
  include JobPostings::Presentable
  include ActionView::Helpers::TextHelper

  # relationships .............................................................
  belongs_to :campaign, optional: true
  belongs_to :organization, optional: true
  belongs_to :user, optional: true
  belongs_to :coupon, optional: true

  # validations ...............................................................
  validates :title, length: {within: 2..130}
  validates :description, presence: true
  validates :job_type, inclusion: {in: ENUMS::JOB_TYPES.keys}
  validates :max_annual_salary_cents, presence: true
  validates :max_annual_salary_currency, presence: true
  validates :min_annual_salary_cents, presence: true
  validates :min_annual_salary_currency, presence: true
  validates :source, inclusion: {in: ENUMS::JOB_SOURCES.keys}
  validates :source_identifier, presence: true, if: -> { external? }
  validates :start_date, presence: true
  validates :end_date, presence: true

  with_options if: :internal? do |job_posting|
    job_posting.validates :company_name, presence: true
    job_posting.validates :company_url, presence: true
    job_posting.validates :company_logo_url, presence: true
    job_posting.validates :city, presence: true
    job_posting.validates :province_name, presence: true
    job_posting.validates :province_code, presence: true
    job_posting.validates :country_code, presence: true
  end

  # callbacks .................................................................
  before_validation :set_currency
  before_validation :sanitize_plan
  before_validation :sanitize_offers

  # scopes ....................................................................
  scope :active, -> { where status: ENUMS::JOB_STATUSES::ACTIVE }
  scope :internal, -> { where source: ENUMS::JOB_SOURCES::INTERNAL }
  scope :remoteok, -> { where source: ENUMS::JOB_SOURCES::REMOTEOK }
  scope :github, -> { where source: ENUMS::JOB_SOURCES::GITHUB }
  scope :search_company_name, ->(value) { value.blank? ? all : search_column(:company_name, value) }
  scope :search_country_codes, ->(*values) { values.blank? ? all : where(country_code: values) }
  scope :search_description, ->(value) { value.blank? ? all : search_column(:description, value) }
  scope :search_job_types, ->(*values) { values.blank? ? all : where(job_type: values) }
  scope :search_keywords, ->(*values) { values.blank? ? all : with_any_keywords(*values) }
  scope :search_organization, ->(value) { value.blank? ? all : where(organization_id: value) }
  scope :search_province_codes, ->(*values) { values.blank? ? all : where(province_code: values) }
  scope :search_remote, ->(value) { value.nil? ? all : where(remote: value) }
  scope :search_title, ->(value) { value.blank? ? all : search_column(:title, value) }
  scope :similar_to, ->(job_posting) { search_keywords(job_posting.keywords).search_remote(job_posting.remote) }
  scope :ranked_by_source, -> {
    order Arel::Nodes::Case.new.when(arel_table[:source].eq(ENUMS::JOB_SOURCES::INTERNAL), 1).else(2)
  }

  # additional config (i.e. accepts_nested_attribute_for etc...) ..............
  tag_columns :keywords
  tag_columns :remote_country_codes
  tag_columns :offers
  sanitize :title, :description, :how_to_apply
  monetize :min_annual_salary_cents, numericality: {greater_than_or_equal_to: 0}
  monetize :max_annual_salary_cents, numericality: {greater_than_or_equal_to: 0}

  # class methods .............................................................

  class << self
    def recent_sample(count)
      active
        .ranked_by_source
        .where.not(company_logo_url: "")
        .where.not(company_logo_url: nil)
        .where(arel_table[:start_date].gt(1.month.ago))
        .group(:id, :company_name)
        .sample(count + 18)
        .to_a
        .uniq { |job| job.company_name }
        .sample(count)
    end
  end

  # public instance methods ...................................................

  def paid?
    stripe_charge_id.present?
  end

  def internal?
    source == ENUMS::JOB_SOURCES::INTERNAL
  end

  def external?
    !internal?
  end

  def pending?
    status == ENUMS::JOB_STATUSES::PENDING
  end

  def active?
    status == ENUMS::JOB_STATUSES::ACTIVE
  end

  def recent?
    start_date >= Date.current - 5.days
  end

  def premium_placement?
    has_offer? ENUMS::JOB_OFFERS::PREMIUM_PLACEMENT
  end

  def code_fund_ads?
    has_offer? ENUMS::JOB_OFFERS::CODE_FUND_ADS
  end

  def read_the_docs_ads?
    has_offer? ENUMS::JOB_OFFERS::READ_THE_DOCS_ADS
  end

  def province
    Province.find_by_iso_code(province_code)
  end

  def to_meta_tags
    {
      title: "#{company_name} is hiring: #{title}",
      keywords: ["Jobs"] + keywords,
      description: "Job Description: #{meta_description}",
      image_src: company_logo_url
    }
  end

  def to_tsvectors
    []
      .then { |result| remote? ? result << make_tsvector("remote", weight: "A") : result }
      .then { |result| keywords.blank? ? result : keywords.each_with_object(result) { |tag, memo| memo << make_tsvector(tag, weight: "A") } }
      .then { |result| job_type.blank? ? result : result << make_tsvector(job_type, weight: "B") }
      .then { |result| title.blank? ? result : result << make_tsvector(title, weight: "B") }
      .then { |result| company_name.blank? ? result : result << make_tsvector(company_name, weight: "B") }
      .then { |result| city.blank? ? result : result << make_tsvector(city, weight: "C") }
      .then { |result| province_name.blank? ? result : result << make_tsvector(province_name, weight: "C") }
      .then { |result| country_code.blank? ? result : result << make_tsvector(country_code, weight: "C") }
      .then { |result| Country.find(country_code).blank? ? result : result << make_tsvector(Country.find(country_code).name, weight: "C") }
      .then { |result| description.blank? ? result : result << make_tsvector(description, weight: "D") }
  end

  # protected instance methods ................................................

  # private instance methods ..................................................

  private

  def set_currency
    if min_annual_salary_currency.present?
      self.max_annual_salary_currency = min_annual_salary_currency
    end
  end

  def meta_description
    truncate(ActionView::Base.full_sanitizer.sanitize(description), length: 290)
  end

  def sanitize_plan
    self.plan = nil unless ENUMS::JOB_PLANS[plan]
  end

  def sanitize_offers
    self.offers = offers & ENUMS::JOB_OFFERS.keys
  end
end