assemblymade/meta

View on GitHub
app/models/product_metric.rb

Summary

Maintainability
C
1 day
Test Coverage
class ProductMetric < ActiveRecord::Base
  belongs_to :product

  serialize :response_times


  # Metrics methods
  def self.trailing_range_activity(product:, time: Time.now, range: 30.days)
    Activity.where(product: product)
            .where('created_at >= ?', time-range)
            .where('created_at < ?', time)
            .count
  end

  def self.response_times(product:, time: Time.now)

    response_times = { "core" => [], "noncore" => [], "comments_count" => 0 }

    # need to join on NFIs since comments are now attached to those
    tasks_with_comments = product.tasks
                                 .joins(:news_feed_items)
                                 .where('news_feed_items.comments_count > 0')
                                 .where('news_feed_items.updated_at <= ?', time)

    if (ppms = ProductMetric.where(product: product).where('updated_at <= ?', time).order(created_at: :desc)).exists?
      tasks_with_comments = tasks_with_comments.where('news_feed_items.updated_at > ?', ppms.first.created_at)
    end

    unless tasks_with_comments.blank?

      tasks_with_comments.each do |t|
        # ordered from least to most recent
        comments = t.news_feed_item.comments.order(created_at: :asc).where('created_at <= ?', time)

        if ppms.exists?
          comments = comments.where('created_at > ?', ppms.first.created_at)
        end

        response_times["comments_count"] = comments.count
        next if comments.count == 0

        # add penalty to core for unanswered comments
        # unless bounty is closed or last comment was from core
        unless ["awarded", "closed", "resolved"].include?(t.state) || product.core_team.include?(comments.last.user)
          response_times["core"] << (Time.now - comments.maximum(:created_at)).abs
        end

        if (product.core_team.include?(t.user) && !product.core_team.include?(comments.first.user))
          response_times["noncore"] << comments.first.created_at - t.created_at
        elsif (!product.core_team.include?(t.user) && product.core_team.include?(comments.first.user))
          response_times["core"] << comments.first.created_at - t.created_at
        end

        next if comments.count == 1

        comments.each do |c|
          # time from comment to most recent previous comment
          previous = t.news_feed_item.comments
                                     .where('created_at < ?', c.created_at)
                                     .where.not(id: c.id)
                                     .reorder(created_at: :desc)
                                     .limit(1)
                                     .first

          # unless no previous comment or current and previous comment are both made by core user
          unless previous.nil? || (product.core_team.include?(c.user) && product.core_team.include?(previous.user))

            # current comment made by core and previous is made by noncore
            if product.core_team.include?(c.user) && !product.core_team.include?(previous.user)
              times = response_times["core"]
            else
              times = response_times["noncore"]
            end

            time_diff = c.created_at - previous.created_at
            times << time_diff unless time_diff <= 0
          end
        end
      end
    end
    response_times.merge!(ppms.first.response_times){|k, x, y| x + y} if ppms.exists? && !ppms.first.response_times.nil?
    response_times
  end

  def self.record_new(product:, time: Time.now)
    response_times = response_times(product: product)
    activity = trailing_range_activity(product: product, range: 30.days)

    create(
      product: product,
      comments_count: response_times.delete("comments_count"){0},
      noncore_responsiveness: response_times["noncore"].empty? ? -1 : response_times["noncore"].sum / response_times["noncore"].length,
      core_responsiveness: response_times["core"].empty? ? -1 : response_times["core"].sum / response_times["core"].length,
      response_times: response_times,
      trailing_month_activity: activity,
      created_at: time
      )
  end

  # Math methods

  # note that queries exclude products that have no comments

  # weighted arithmetic mean comment responsiveness of all products
  # uses the most recent ProductMetric record
  # associated with each product
  def self.overflow_safe_weighted_average(attr_name)
    raise "that attribute name is invalid!" unless valid_numerical_attribute_type?(attr_name)

    records = all_most_recent(attr_name).where('comments_count > 0')
    denom = records.sum(:comments_count)

    records.map{|x| x.public_send(attr_name).to_i * x.public_send(:comments_count).to_f / denom}.sum.to_i
  end

  def self.calc_stddev(attr_name)
    raise "that attribute name is invalid!" unless valid_numerical_attribute_type?(attr_name)
    sql = "select stddev(product_metrics.#{attr_name}) as stddev_#{attr_name} FROM product_metrics where (product_id, created_at) in (select product_id, max(created_at) from product_metrics group by product_id) AND #{attr_name} > 0"
    ActiveRecord::Base.connection.execute(sql).values.flatten.first.try(:to_i)
  end

  def self.calc_minimum(attr_name)
    raise "that attribute name is invalid!" unless valid_numerical_attribute_type?(attr_name)
    where("#{attr_name} > 0").minimum(attr_name)
  end

  def self.median(attr_name)
    raise "that attribute name is invalid!" unless valid_numerical_attribute_type?(attr_name)
    r = all_most_recent(attr_name).order(attr_name => :asc)
    total = r.count
    return "error" if total < 1
    return r.first if total == 1

    midpoint = total / 2

    total % 2 == 0 ? (r.offset(midpoint-1).limit(2).pluck(attr_name).sum / 2.0) : r.offset(midpoint-1).limit(1).first.read_attribute(attr_name)
  end

  # only returns records where attr_name is positive
  def self.all_most_recent(attr_name=nil)
    raise "that attribute name is invalid!" unless valid_numerical_attribute_type?(attr_name)
    clause = "(product_id, created_at) in (select product_id, max(created_at) from product_metrics group by product_id)"
    query = where(clause)
    attr_name.present? ? query.where("#{attr_name} > 0") : query
  end

  private

    def self.get_attribute_type(attr_name)
      columns_hash[attr_name.to_s].try(:type)
    end

    def self.valid_numerical_attribute_type?(attr_name)
      [:float, :integer].include? get_attribute_type(attr_name)
    end

end