app/models/opportunity.rb
# == Schema Information
#
# Table name: opportunities
#
# id :integer not null, primary key
# name :string(255)
# description :text
# designation :string(255)
# location :string(255)
# cached_tags :string(255)
# link :string(255)
# salary :integer
# options :float
# deleted :boolean default(FALSE)
# deleted_at :datetime
# created_at :datetime
# updated_at :datetime
# expires_at :datetime default(1970-01-01 00:00:00 UTC)
# opportunity_type :string(255) default("full-time")
# location_city :string(255)
# apply :boolean default(FALSE)
# public_id :string(255)
# team_id :integer
# remote :boolean
#
require 'search'
class Opportunity < ActiveRecord::Base
include Tire::Model::Search
include Tire::Model::Callbacks
include SearchModule
include OpportunityMapping
acts_as_taggable
OPPORTUNITY_TYPES = %w(full-time part-time contract internship)
has_many :seized_opportunities, dependent: :delete_all
has_many :applicants, through: :seized_opportunities, source: :user
# Order here dictates the order of validation error messages displayed in views.
validates :name, presence: true, allow_blank: false
validates :opportunity_type, inclusion: { in: OPPORTUNITY_TYPES }
validates :description, length: { minimum: 100, maximum: 2000 }
validates :tag_list, with: :tags_within_length
validates :location, presence: true, allow_blank: false
validates :location_city, presence: true, allow_blank: false, unless: lambda { location && anywhere?(location) }
validates :salary, presence: true, numericality: {only_integer: true, greater_than: 0, less_than_or_equal_to: 800000}, allow_blank: true
before_validation :set_location_city
before_save :update_cached_tags
before_create :ensure_can_afford, :set_expiration, :assign_random_id
after_save :save_team
after_save :remove_from_index, unless: :alive?
after_create :pay_for_it!
#this scope should be renamed.
scope :valid, -> { where(deleted: false).where('expires_at > ?', Time.now).order('created_at DESC') }
scope :by_city, ->(city) { where('LOWER(location_city) LIKE ?', "%#{city.try(:downcase)}%") }
scope :by_tag, ->(tag) { where('LOWER(cached_tags) LIKE ?', "%#{tag}%") unless tag.nil? }
scope :by_query, ->(query) { where("name ~* ? OR description ~* ? OR cached_tags ~* ?", query, query, query) }
#remove default scope
default_scope { valid }
HUMANIZED_ATTRIBUTES = { name: 'Title' }
belongs_to :team, class_name: 'Team', touch: true
def self.human_attribute_name(attr,options={})
HUMANIZED_ATTRIBUTES[attr.to_sym] || super
end
def self.based_on(tags)
query_string = "tags:#{tags.join(' OR ')}"
failover_scope = Opportunity.joins('inner join taggings on taggings.taggable_id = opportunities.id').joins('inner join tags on taggings.tag_id = tags.id').where("taggings.taggable_type = 'Opportunity' AND taggings.context = 'tags'").where('lower(tags.name) in (?)', tags.map(&:downcase)).group('opportunities.id').order('count(opportunities.id) desc')
Opportunity::Search.new(Opportunity, Opportunity::Search::Query.new(query_string), nil, nil, nil, failover: failover_scope).execute
end
def self.random
uncached do
order('RANDOM()')
end
end
def tags_within_length
tags_string = tag_list.join(',')
errors.add(:skill_tags, 'are too long(Maximum is 250 characters)') if tags_string.length > 250
errors.add(:base, 'You need to specify at least one skill tag') if tags_string.length == 0
end
def update_cached_tags
self.cached_tags = tag_list.join(',')
end
def seize_by(user)
seized_opportunities.create(user_id: user.id)
end
def seized_by?(user)
seized_opportunities.exists?(user_id: user.id)
end
def active?
!deleted
end
def activate!
self.deleted = false
self.deleted_at = nil
save
end
def deactivate!
destroy
end
def destroy(force = false)
if force
super()
else
self.deleted = true
self.deleted_at = Time.now.utc
save
end
end
def set_expiration
self.expires_at = team.has_monthly_subscription? ? 1.year.from_now : 1.month.from_now
end
def title
name
end
def title=(new_title)
self.name = new_title
end
def accepts_applications?
apply
end
def apply_for(user)
unless user.already_applied_for?(self)
seize_by(user)
end
end
def has_application_from?(user)
seized_by?(user)
end
def viewed_by(viewer)
epoch_now = Time.now.to_i
Redis.current.incr(impressions_key)
if viewer.is_a?(User)
Redis.current.zadd(user_views_key, epoch_now, viewer.id)
else
Redis.current.zadd(user_anon_views_key, epoch_now, viewer)
end
end
def impressions_key
"opportunity:#{id}:impressions"
end
def user_views_key
"opportunity:#{id}:views"
end
def user_anon_views_key
"opportunity:#{id}:views:anon"
end
def viewers(since = 0)
epoch_now = Time.now.to_i
viewer_ids = Redis.current.zrevrange(user_views_key, since, epoch_now)
User.where(id: viewer_ids).all
end
def total_views(epoch_since = 0)
epoch_now = Time.now.to_i
Redis.current.zcount(user_views_key, epoch_since, epoch_now) + Redis.current.zcount(user_anon_views_key, epoch_since, epoch_now)
end
def ensure_can_afford
team.can_post_job?
end
def pay_for_it!
team.paid_job_posts -= 1
team.save
end
def locations
location_city.try(:split, '|') || ['Worldwide']
end
def alive?
expires_at.nil? && deleted_at.nil?
end
def to_html
CFM::Markdown.render(self.description)
end
def to_indexed_json
to_public_hash.deep_merge(
public_id: public_id,
name: name,
description: description,
designation: designation,
opportunity_type: opportunity_type,
tags: cached_tags,
link: link,
salary: salary,
created_at: created_at,
updated_at: updated_at,
expires_at: expires_at,
apply: apply,
team: {
slug: team.slug,
id: team.id.to_s,
featured_banner_image: team.featured_banner_image,
big_image: team.big_image,
avatar_url: team.avatar_url,
name: team.name
}
).to_json(methods: [:to_param])
end
def to_public_hash
{
title: title,
type: opportunity_type,
locations: locations,
description: description,
company: team.name,
url: url
}
end
def url
Rails.application.routes.url_helpers.job_path(slug: team.slug, job_id: public_id, host: Rails.application.config.host, only_path: false) + '#open-positions'
end
def assign_random_id
self.public_id = title.gsub(/[^a-z0-9]+/i, '-').chomp('-') + '-' + SecureRandom.urlsafe_base64(4).downcase
assign_random_id unless self.class.where(public_id: public_id).blank? # retry if not unique
end
protected
def set_location_city
add_opportunity_locations_to_team
locations = team.cities.compact.select { |city| location.include?(city) }
return if locations.blank? && anywhere?(location)
self.location_city = locations.join('|')
end
def add_opportunity_locations_to_team
geocoded_all = true
location.split('|').each do |location_string|
# skip if location is anywhere or already exists
if anywhere?(location_string) || team.locations.select{|v| v.address.include?(location_string)}.count > 0
geocoded_all = false
next
end
geocoded_all &&= team.locations.build(address: location_string, name: location_string).geocode
end
geocoded_all || nil
end
def valid_location_city
location_city || anywhere?(location)
end
def anywhere?(location)
location.downcase.include?('anywhere')
end
def save_team
team.save
end
def remove_from_index
self.class.tire.index.remove self
end
end