assemblymade/coderwall

View on GitHub
app/models/team.rb

Summary

Maintainability
F
4 days
Test Coverage
# == Schema Information
#
# Table name: teams
#
#  id                       :integer          not null, primary key
#  created_at               :datetime         not null
#  updated_at               :datetime         not null
#  website                  :string(255)
#  about                    :text
#  total                    :decimal(40, 30)  default(0.0)
#  size                     :integer          default(0)
#  mean                     :decimal(40, 30)  default(0.0)
#  median                   :decimal(40, 30)  default(0.0)
#  score                    :decimal(40, 30)  default(0.0)
#  twitter                  :string(255)
#  facebook                 :string(255)
#  slug                     :citext           not null
#  premium                  :boolean          default(FALSE)
#  analytics                :boolean          default(FALSE)
#  valid_jobs               :boolean          default(FALSE)
#  hide_from_featured       :boolean          default(FALSE)
#  preview_code             :string(255)
#  youtube_url              :string(255)
#  github                   :string(255)
#  highlight_tags           :string(255)
#  branding                 :text
#  headline                 :text
#  big_quote                :text
#  big_image                :string(255)
#  featured_banner_image    :string(255)
#  benefit_name_1           :text
#  benefit_description_1    :text
#  benefit_name_2           :text
#  benefit_description_2    :text
#  benefit_name_3           :text
#  benefit_description_3    :text
#  reason_name_1            :text
#  reason_description_1     :text
#  reason_name_2            :text
#  reason_description_2     :text
#  reason_name_3            :text
#  reason_description_3     :text
#  why_work_image           :text
#  organization_way         :text
#  organization_way_name    :text
#  organization_way_photo   :text
#  blog_feed                :text
#  our_challenge            :text
#  your_impact              :text
#  hiring_tagline           :text
#  link_to_careers_page     :text
#  avatar                   :string(255)
#  achievement_count        :integer          default(0)
#  endorsement_count        :integer          default(0)
#  upgraded_at              :datetime
#  paid_job_posts           :integer          default(0)
#  monthly_subscription     :boolean          default(FALSE)
#  stack_list               :text             default("")
#  number_of_jobs_to_show   :integer          default(2)
#  location                 :string(255)
#  country_id               :integer
#  name                     :string(255)
#  github_organization_name :string(255)
#  team_size                :integer
#  mongo_id                 :string(255)
#  office_photos            :string(255)      default([]), is an Array
#  upcoming_events          :text             default([]), is an Array
#  interview_steps          :text             default([]), is an Array
#  invited_emails           :string(255)      default([]), is an Array
#  pending_join_requests    :string(255)      default([]), is an Array
#  state                    :string(255)      default("active")
#

# encoding utf-8
require 'search'

class Team < ActiveRecord::Base
  DEFAULT_HEX_BRAND        = '#343131'
  FEATURED_TEAMS_CACHE_KEY = 'featured_teams_results'
  MAX_TEAM_SCORE           = 400

  include TeamAnalytics

  include TeamSearch
  include Blog
  include SearchModule

  mount_uploader :avatar, TeamUploader

  has_many :invitations
  has_many :opportunities, dependent: :destroy
  has_many :followers, through: :follows, source: :team
  has_many :follows,   class_name: 'FollowedTeam',    foreign_key: 'team_id', dependent: :destroy
  has_many :jobs,      class_name: 'Opportunity',     foreign_key: 'team_id', dependent: :destroy
  has_many :locations, class_name: 'Teams::Location', foreign_key: 'team_id'
  has_many :members,   class_name: 'Teams::Member',   foreign_key: 'team_id'
  def admins
    members.admins
  end

  has_many :member_accounts, through: :members, source: :user, class_name: 'User'
  def admin_accounts
    member_accounts.where("teams_members.role = 'admin'")
  end

  has_one :account,    class_name: 'Teams::Account',  foreign_key: 'team_id'

  accepts_nested_attributes_for :locations, allow_destroy: true, reject_if: :all_blank

  before_validation :create_slug!
  before_validation :fix_website_url!
  before_save :clear_cache_if_premium_team
  after_create :generate_event
  after_save :reindex_search
  after_destroy :reindex_search

  validates :slug, uniqueness: true, presence: true
  validates :name, presence: true

  scope :featured, ->{ where(premium: true, valid_jobs: true, hide_from_featured: false) }

  def top_three_team_members
    members.first(3)
  end

  def sorted_team_members
    members.sorted
  end

  def all_jobs
    jobs.order(:created_at).reverse_order
  end

  def self.search(query_string, country, page, per_page, search_type = :query_and_fetch)
    country = query_string.gsub!(/country:(.+)/, '') && $1 if country.nil?
    query = ''

    if query_string.blank? or query_string =~ /:/
      query += query_string
    else
      query += "name:#{query_string}*"
    end

    begin
      tire.search(load: false, search_type: search_type, page: page, per_page: per_page) do
        query { string query, default_operator: 'AND' } if query_string.present?
        filter :term, country: country unless country.nil?
        sort { by [{ score: 'desc', total_member_count: 'desc', '_score' => {} }] }
      end
    rescue Tire::Search::SearchRequestFailed
      SearchResultsWrapper.new(nil, "Looks like our teams server is down. Try again soon.")
    end
  end

  def self.slugify(name)
    if !!(name =~ /\p{Latin}/)
      name.to_s.downcase.gsub(/[^a-z0-9]+/i, '-').chomp('-')
    else
      name.to_s.tr(' ', '-')
    end
  end

  def self.most_relevant_featured_for(user)
    Team.featured.sort_by { |team| -team.match_score_for(user) }
  end

  def self.completed_at_least(section_count = 6, page=1, per_page=Team.count, search_type = :query_and_fetch)
    Team.search("completed_sections:[ #{section_count} TO * ]", nil, page, per_page, search_type)
  end

  def self.with_similar_names(name)
    Team.where('name ilike ?', "%#{name}%").limit(3).to_a
  end

  def self.with_completed_section(section)
    empty = Team.new.send(section).is_a?(Array) ? [] : nil
    Team.where(section.to_sym.ne => empty)
  end

  def relevancy
    Protip.search_trending_by_team(slug, "created_at:[#{1.week.ago.strftime('%Y-%m-%dT%H:%M:%S')} TO *]", 1, 100).count
  end

  def match_score_for(user)
    team_skills = self.tokenized_stack.blank? ? self.tokenized_job_tags : self.tokenized_stack
    (user.skills.map(&:tokenized) & team_skills).count
  end

  def best_positions_for(user)
    user_skills = user.skills.map(&:tokenized)
    self.jobs.sort_by { |job| -(job.tags.map { |tag| Skill.tokenize(tag) } & user_skills).count }
  end

  def most_influential_members_for(user)
    influencers = user.following_by_type(User.name).where('follows.followable_id in (?)', self.members.map(&:id))
    (influencers + self.members.first(3)).uniq
  end

  def hiring_message
    (!self.hiring_tagline.blank? && self.hiring_tagline) || (!self.about.blank? && self.about) || (!self.big_quote.blank? && self.big_quote)
  end

  def tokenized_stack
    @tokenized_stack ||= self.stack.collect { |stack| Skill.tokenize(stack) }
  end

  def tokenized_job_tags
    @tokenized_job_tags ||= self.jobs.map(&:tags).flatten.collect { |tag| Skill.tokenize(tag) }
  end

  def tags_for_jobs
    (self.stack + self.jobs.map(&:tags).flatten)
  end

  def has_protips?
    trending_protips.size > 0
  end

  def trending_protips(limit=4)
    Protip.search_trending_by_team(slug, nil, 1, limit)
  end

  def company?
    true
  end

  def university?
    true
  end

  def locations_message
    if premium?
      locations.collect(&:name).join(', ')
    else
      locations.join(', ')
    end
  end

  def dominant_country_of_members
    members.map(&:user).map do |user|
      [user.country, 1]
    end.reduce(Hash.new(0)) do |memo, pair|
      memo[pair.first] += pair.last
      memo
    end.to_a.sort do |x, y|
      y[1] <=> x[1]
    end.map(&:first).compact.first
  end

  def reach
    team_member_ids = members.map(&:id)
    Follow.where(followable_type: 'User', followable_id: team_member_ids).count + Follow.where(follower_id: team_member_ids, follower_type: 'User').count
  end

  def on_team?(user)
    has_member?(user)
  end

  def has_member?(user)
    members.include?(user)
  end

  def branding_hex_color
    branding || DEFAULT_HEX_BRAND
  end

  def events
    @events ||= members.collect { |user| user.followed_repos }.flatten.sort { |x, y| y.date <=> x.date }
  end

  def achievements_with_counts
    @achievements_with_counts ||= begin
                                    achievements = {}
                                    members.each do |user|
                                      user.badges.each do |badge|
                                        achievements[badge.badge_class] = 0 if achievements[badge.badge_class].blank?
                                        achievements[badge.badge_class] += 1
                                      end
                                    end
                                    achievements.sort_by { |k, v| v }.reverse
                                  end
  end

  def top_members
    top_three_members.map do |member|
      {
        username:    member.username,
        profile_url: member.user.avatar_url,
        avatar:      ApplicationController.helpers.users_image_path(member)
      }
    end
  end

  def to_indexed_json
    summary.merge(
      score:              score.to_i,
      type:               self.class.name.downcase,
      url:                Rails.application.routes.url_helpers.team_path(self),
      follow_path:        Rails.application.routes.url_helpers.follow_team_path(self),
      members:       top_members,
      total_member_count: total_member_count,
      completed_sections: number_of_completed_sections,
      country:            dominant_country_of_members,
      hiring:             hiring?,
      locations:          locations_message.split(",").map(&:strip)
    ).to_json
  end

  def public_json
    public_hash.to_json
  end

  def public_hash
    summary.merge(
      members: member_accounts.collect { |user| {
        name:               user.display_name,
        username:           user.username,
        badges_count:       user.badges_count,
        endorsements_count: user.endorsements_count
      } }
    )
  end

  def summary
    {
      name:   name,
      about:  about,
      id:     id.to_s,
      size:   size,
      slug:   slug,
      avatar: avatar_url,
    }
  end

  def display_name
    name
  end

  def hiring?
    premium? && valid_jobs? && jobs.any?
  end

  alias_method :hiring, :hiring?

  def can_upgrade?
    !premium? && !valid_jobs?
  end

  def has_big_headline?
    !headline.blank?
  end

  def has_big_quote?
    !big_quote.blank? || !youtube_url.blank?
  end

  def has_challenges?
    !our_challenge.blank?
  end

  def has_favourite_benefits?
    !benefit_description_1.blank?
  end

  def has_organization_style?
    !organization_way.blank?
  end

  def has_office_images?
    !office_photos.blank?
  end

  def has_open_positions?
    !jobs.blank? && hiring?
  end

  def has_stack?
    !stack.blank?
  end

  def has_why_work?
    !reason_name_1.blank?
  end

  def has_interview_steps?
    !interview_steps.blank? && !interview_steps.first.blank?
  end

  def has_locations?
    !locations.blank?
  end

  def has_upcoming_events?
    false
  end

  def has_achievements?
    !achievements_with_counts.empty?
  end

  def has_specialties?
    !specialties_with_counts.empty?
  end

  def specialties_with_counts
    @specialties_with_counts ||= begin
                                   specialties = {}

                                   member_accounts.each do |user|
                                     user.speciality_tags.each do |tag|
                                       tag              = tag.downcase
                                       specialties[tag] = 0 if specialties[tag].blank?
                                       specialties[tag] += 1
                                     end
                                   end

                                   unless specialties.values.sum == specialties.values.length
                                     specialties.reject! { |k, v| v <= 1 }
                                   end

                                   specialties.sort_by { |k, v| v }.reverse[0..7]
                                 end
  end

  def empty?
    (members.size) <= 0
  end

  def pending_size
    members.size + invited_emails.size
  end

  def is_invited?(user)
    !pending_members.where(user_id: id_of(user)).first.nil?
  end

  def is_member?(user)
    members.include?(user)
  end

  def membership(user)
    members.where(user_id: id_of(user)).first
  end

  def top_team_member
    sorted_members.first
  end

  def top_two_members
    sorted_members[0...2] || []
  end

  def top_three_members
    sorted_members[0...3] || []
  end

  def sorted_members
    @sorted_members = members.order('score_cache DESC')
  end

  def add_member(user, state='pending', role='member')
    member = members.create(user_id: user.id)
    member.update_attributes(state: state, role: role)
    member
  end
  alias_method :add_user, :add_member

  def remove_member(user)
    members.destroy_all(user_id: user.id)
  end

  attr_accessor :skip_validations

  def touch!
    self.updated_at = Time.now.utc
    save!(validate: !skip_validations)
  end

  def total_member_count
    members.count
  end

  def team_size_threshold
    if size >= 3
      3
    else
      size
    end
  end

  def <=> y
    val = team_size_threshold <=> y.team_size_threshold
    return val unless val == 0

    val = score <=> y.score
    return val unless val == 0

    val = size <=> y.size
    return val unless val == 0

    id.to_s <=> y.id.to_s
  end

  def recalculate!
    return nil if members.size <= 0
    log_history!
    update_team_size!
    self.total             = members.collect(&:score).sum
    self.achievement_count = members.collect { |t| t.badges.count }.sum
    self.endorsement_count = members.collect { |t| t.endorsements.count }.sum
    self.mean              = members.empty? ? 0 : (total / members_with_scores.size).to_f
    self.median            = calculate_median
    self.score             = [real_score, MAX_TEAM_SCORE].min
    save!
  end

  def real_score
    ((median + mean) * multipler) + size_credit + members_with_score_above(mean) + leader_score + 100
  end

  def leader_score
    [leader.score, 50].min
  end

  def leader
    sorted_members.sort { |x, y| x.score <=> y.score }.reverse.first
  end

  def multipler
    team_score = members_with_scores.size
    if  team_score <= 3
      0.50
    elsif team_score <= 4
      0.75
    elsif team_score <= 5
      0.90
    else
      Math.log(members_with_scores.size - 2, 3)
    end
  end

  def members_with_score_above(score)
    members.select { |u| u.score >= score }.size
  end

  def size_credit
    if size < 20
      size / 2
    else
      20 / 2
    end
  end

  def calculate_median
    sorted = members.collect(&:score).sort
    return 0 if sorted.empty?
    lower = sorted[(sorted.size/2) - 1]
    upper = sorted[((sorted.size+1)/2) -1]
    (lower + upper) / 2
  end

  def members_with_scores
    @members_with_scores ||= members.collect { |t| t.score > 0 }
  end

  def log_history!
    Redis.current.rpush("team:#{id.to_s}:score", {
      date:  Date.today,
        score: self.score,
        size:  self.size
    }.to_json)
  end

  def predominant
    skill = {}
    members.each do |member|
      member.user.repositories.each do |repo|
        repo.tags.each do |tag|
          skill[tag] = (skill[tag] ||= 0) + 1
        end
      end
    end
    skill
  end

  def admin?(user)
    return false if user.nil?
    return true if user.admin?
    admins.pluck(:user_id).include?(user.id)
  end

  def timeline_key
    @timeline_key ||= "team:#{id.to_s}:timeline"
  end

  def has_user_with_referral_token?(token)
    member_accounts.exists?(referral_token: token)
  end

  def impressions_key
    "team:#{id}:impressions"
  end

  def user_views_key
    "team:#{id}:views"
  end

  def user_anon_views_key
    "team:#{id}:views:anon"
  end

  def user_detail_views_key
    "team:#{id}:views:detail"
  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
    Redis.current.get(impressions_key).to_i
  end

  def viewers(since=0)
    epoch_now  = Time.now.to_i
    viewer_ids = Redis.current.zrevrangebyscore(user_views_key, epoch_now, since)
    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 self.most_active_countries
    Country.where(name: User.select([:country, 'count(country) as count']).group(:country).order('count DESC').limit(10).map(&:country)).reverse
  end

  def primary_address
    locations.first.try(:address) || primary_address_name
  end

  def primary_address_name
    locations.first.try(:name)
  end

  def primary_address_description
    locations.first.try(:description)
  end

  def primary_points_of_interest
    locations.first.try(:points_of_interest).to_a
  end

  def cities
    locations.map(&:city).reject { |city| city.blank? }
  end

  def generate_event
    only_member_is_creator = members.first.try(:id)
    GenerateEventJob.perform_async(self.event_type, Audience.following_user(only_member_is_creator), self.to_event_hash, 1.minute) unless only_member_is_creator.nil?
  end

  def to_event_hash
    { user: { username: members.any? && members.first.username } }
  end

  def event_type
    :new_team
  end

  def fix_website_url!
    unless self.website.blank? or self.website =~ /^https?:\/\//
      self.website = "http://#{self.website}"
    end
  end

  def active_jobs
    jobs[0...4]
  end

  def active_job_titles
    active_jobs.collect(&:title).uniq
  end




  SECTION_FIELDS = %w(about headline big_quote our_challenge benefit_description_1 organization_way office_photos stack_list reason_name_1 interview_steps locations blog_feed)


  def visitors_interested_in_jobs
    aggregate_visitors.select { |visitor| visitor[:exit_target_type] == 'job-opportunity' }.collect { |visitor| visitor[:user_id] }
  end

  def members_interested_in_jobs
    User.where(id: aggregate_visitors.select { |visitor| visitor[:exit_target_type] == 'job-opportunity' || visitor[:exit_target_type] == 'all-job-opportunities' }.collect { |visitor| visitor[:user_id] }).compact
  end

  def click_through_rate
    self.visitors_interested_in_jobs.count/self.total_views(self.upgraded_at)
  end

  def coderwall?
    slug == 'coderwall'
  end

  def reindex_search
    if Rails.env.development? or Rails.env.test? or self.destroyed?
      self.tire.update_index
    else
      IndexTeamJob.perform_async(id)
    end
  end

  def can_post_job?
    has_monthly_subscription? || paid_job_posts > 0
  end

  def has_monthly_subscription?
    self.monthly_subscription
  end

  def has_specified_enough_info?
    number_of_completed_sections >= 6
  end

  def has_team_details?
    has_external_link? and !self.about.nil? and !self.avatar.nil?
  end

  def has_external_link?
    self.twitter.nil? or self.facebook.nil? or self.website.nil? or self.github.nil?
  end

  def has_members?
    members.count >= 2
  end

  def stack
    @stack_list ||= (self.stack_list || "").split(/,/)
  end

  def plan
    plan_id = self.account && self.account.plan_ids.first
    plan_id && Plan.find(plan_id)
  end

  def plan=(plan)
    self.build_account
    self.account.admin_id = self.admins.first || self.members.first.id
    self.account.subscribe_to!(plan, true)
  end

  def edited_by(user)
    self.editors.delete(user.id)
    self.editors << user.id
  end

  def latest_editors
    self.editors.collect { |editor| User.where(id: editor).first }.compact
  end

  def video_url
    youtube_pattern = /(youtube\.com|youtu\.be)\/(watch\?v=)?([\w\-_]{11})/i
    vimeo_pattern = /vimeo\.com\/(\d+)/

    if self.youtube_url =~ vimeo_pattern
      "https://player.vimeo.com/video/#{$1}"
    elsif self.youtube_url =~ youtube_pattern
      "https://www.youtube.com/embed/#{$3}"
    else
      self.youtube_url
    end
  end

  def request_to_join(user)
    self.pending_join_requests << user.id
  end

  def approve_join_request(user)
    self.add_member(user)
    self.pending_join_requests.delete user.id
  end

  def deny_join_request(user)
    self.pending_join_requests.delete user.id
  end

  private

  def identify_visitor(visitor_name)
    visitor_id = visitor_name.to_i
    if visitor_id != 0 and visitor_name =~ /^[0-9]+$/i
      User.where(id: visitor_id).first
    else
      nil
    end
  end

  def visitor_data(exit_url, exit_target_type, furthest_scrolled, time_spent, user_id, visited_at, user)
    { exit_url:          exit_url,
      exit_target_type:  exit_target_type,
      furthest_scrolled: furthest_scrolled,
      time_spent:        time_spent,
      user_id:           user_id,
      visited_at:        visited_at,
      user:              user }
  end

  def id_of(user)
    user.is_a?(User) ? user.id : user
  end

  def clear_cache_if_premium_team
    Rails.cache.delete(Team::FEATURED_TEAMS_CACHE_KEY) if premium?
  end

  def create_slug!
    self.slug ||= self.class.slugify(name)
  end
end