app/models/measurement.rb
class Measurement < ActiveRecord::Base
belongs_to :subject, polymorphic: true, optional: true
validates :name, :value, :taken_at, presence: true
validates :name, length: { maximum: 50 }
default_scope -> { order(arel_table[:taken_at].desc) }
class << self
def take!(attributes)
required_keys = [:subject_type, :subject_id, :taken_at, :name].freeze
identifying_attributes = attributes.pick(required_keys)
subject = attributes[:subject]
identifying_attributes.merge!(subject_type: subject.class.name, subject_id: subject.id) if subject
identifying_attributes.reverse_merge!(subject_type: nil, subject_id: nil)
required_keys.each do |key|
raise ArgumentError, "#{key.inspect} is required to take a measurement" unless identifying_attributes.key?(key)
end
find_or_initialize_by(identifying_attributes).tap do |measurement|
measurement.value = attributes[:value]
measurement.save!
end
end
def taken_at(time)
where(taken_at: time)
end
def taken_on(date)
where(taken_on: date)
end
def taken_before(date)
where(arel_table[:taken_on].lteq(date))
end
def taken_after(date)
where(arel_table[:taken_on].gteq(date))
end
alias :taken_since :taken_after
def taken_between(date0, date1)
taken_after(date0).taken_before(date1)
end
def for(subject)
return where(subject_type: nil, subject_id: nil) if subject.nil?
return where(subject_type: subject.name) if subject.is_a?(Class)
where(subject_type: subject.class.name, subject_id: subject.id)
end
def global
self.for(nil)
end
# Valid identifies for names
# - weekly.hours.charged
# - weekly.hours.charged.*
# - weekly.hours.charged.{fix,chore}
# - weekly.hours.{worked,charged}.fix
# Invalid arguments
# - weekly.hours.*.fix
def named(*name_patterns)
name_patterns = name_patterns.flatten.map { |pattern| pattern
.gsub(/\{([\w\-,]+)\}/) { "(#{$~.captures[0].gsub(/,/, "|")})" }
.gsub(/\*$/, "%") }
where(["name SIMILAR TO ?", "(#{name_patterns.join("|")})"])
end
def total
pluck(:value).inject(0) { |sum, value| sum + value.to_d }
end
def value
limit(1).pluck(:value)[0]
end
def mean
denominator = count
return nil if denominator.zero?
total.to_f / denominator
end
def debug(colored: true)
format_subject = ->(s) { s.is_a?(User) ? s.first_name : s.is_a?(Project) ? s.slug : "" }
includes(:subject).reorder("taken_at ASC, subject_type ASC, subject_id ASC, name ASC").map { |m|
line = [ m.taken_on.strftime("%-m/%-d").rjust(5),
m.taken_at.strftime("%H:%M:%S"),
format_subject[m.subject].ljust(9),
m.name.ljust(50),
m.value.rjust(8) ].join(" ")
line = "\e[36m#{line}\e[0m" if colored && m.subject_type == "User"
line = "\e[35m#{line}\e[0m" if colored && m.subject_type == "Project"
line }.join("\n")
end
end
def taken_at=(value)
super
self.taken_on = value && value.to_date
end
def taken_on?(date)
taken_on == date
end
end