thomasklemm/pluginGeek

View on GitHub
app/models/repo.rb

Summary

Maintainability
A
3 hrs
Test Coverage
class Repo < ActiveRecord::Base
  # Friendly ID
  extend FriendlyId
  friendly_id :full_name

  # Validations
  validates :full_name, presence: true, uniqueness: true
  validates :description, length: {maximum: 360}

  # Named scopes
  def self.order_by_score
    order('repos.score DESC')
  end

  def self.order_by_name
    order('repos.full_name ASC')
  end

  def self.ids_and_full_names
    select([:id, :full_name]).
      order_by_score
  end

  def self.ids_and_full_names_without(repo)
    where('id != ?', repo.id).
      ids_and_full_names
  end

  # Defaults
  def stars
    self[:stars] || 0
  end

  def score
    self[:score] || 0
  end

  # Parents
  has_many :parent_child_relationships,
    class_name:   'RepoRelationship',
    foreign_key:  :child_id,
    dependent:    :destroy
  has_many :parents,
    through:      :parent_child_relationships,
    source:       :parent

  # Children
  has_many :child_parent_relationships,
    class_name:   'RepoRelationship',
    foreign_key:  :parent_id,
    dependent:    :destroy
  has_many :children,
    through:      :child_parent_relationships,
    source:       :child

  def parents_and_children
    parents | children
  end

  # Categories
  # FIXME: Console shows this setup leaves orphan join records behind. Why? What's better to do?
  has_many :categorizations,
    dependent: :destroy
  has_many :categories,
    -> { order(score: :desc) },
    through: :categorizations

  # Languages
  has_many :language_classifications,
    as: :classifier,
    dependent: :destroy
  has_many :languages,
    through: :language_classifications

  # Links
  has_many :link_relationships,
    as: :linkable,
    dependent: :destroy
  has_many :links,
    -> { uniq },
    through: :link_relationships

 # Lists for tag inputs
  def child_list
    children.map(&:full_name).join(', ')
  end

  def language_list
    languages.map(&:name).join(', ')
  end

  def category_list
    categories.map(&:full_name).join(', ')
  end

  # Handle tag input changes
  def category_list=(new_list)
    # Return early if category assignments don't change
    return if new_list == category_list

    expire_categories # expire old categories

    full_category_names = prepare_category_list(new_list)
    assign_categories(full_category_names)

    expire_categories # expire new categories
  end

  # Times
  def last_updated
    Time.current - github_updated_at
  end

  def github_updated_at
    time = self[:github_updated_at].present? ? self[:github_updated_at] : 2.years.ago
    time.utc
  end

  # Assign updated fields from Git
  def update_repo_from_github(github)
    assign_fields_from_github(github)
    self.update_success = true
    self.save
  end

  # Flag repo as successfully retrieved
  def update_succeeded!
    update_success? or self.update_column(:update_success, true)
    puts "Repo #{ full_name } has been updated successfully."
    true
  end

  # Flag repo as failing to update
  def update_failed!(opts = {})
    if persisted?
      update_success? and self.update_column(:update_success, false)
    else
      update_success? and self.update_success = false
    end

    message = case opts[:reason]
      when :not_found_on_github
        "Repo #{ full_name } could not be found on Github."
      when :not_saved
        "Repo #{ full_name } could not be saved while updating from Github."
      else
        "Repo #{ full_name } could not be updated."
      end

    puts message
    false
  end

  # Update this very record from Github, live and in color
  def retrieve_from_github
    updater = RepoUpdater.new
    updater.update(full_name)
  end

  # Callbacks
  # Assign a repo's languages from its' categories' languages
  # on every save
  before_save :assign_languages

  # Calculate a repo's score on each save
  before_save :assign_score

  # Update category caches and more
  after_commit :update_and_expire_categories

  private

  # Assign categories from a list of category names
  def assign_categories(full_category_names)
    self.categories = full_category_names.map do |full_name|
      Category.where(full_name: full_name).first_or_create!
    end
  end

  # Deduce languages from categories' languages
  def assign_languages
    self.languages = categories.flat_map(&:languages).uniq
  end

  def assign_fields_from_github(github)
    self.name                = github['name']
    self.owner               = github['owner']['login']
    self.github_description  = github['description']
    self.stars               = github['watchers']
    self.homepage_url        = github['homepage']
    self.github_updated_at   = github['pushed_at']
  end

  # NOTE: Select2 sends strings separated by ',' in some versions by default
  def prepare_category_list(list)
    list.gsub(', ', ',').split(',').select(&:present?).map(&:strip)
  end

  def assign_score
    self.score = ((stars + 1) * activity_bonus * staff_pick_bonus).ceil
    self.score = 0 if score < 0
  end

  def activity_bonus
    2.0 - (last_updated / 12.months)
  end

  def staff_pick_bonus
    staff_pick? ? 1.25 : 1
  end

  def expire_categories
    categories.update_all(updated_at: Time.current)
  end

  # Update categories' caches
  def update_and_expire_categories
    categories.each(&:save)
  end
end