Merge pull request #168 from kytrinyx/stats-actions

Encapsulate the stats for actions on the stats index page
This commit is contained in:
Reinier Balt 2013-03-02 13:58:32 -08:00
commit 1e3782ce67
7 changed files with 246 additions and 65 deletions

View file

@ -13,8 +13,8 @@ class StatsController < ApplicationController
@tags_count = tag_ids.size
@unique_tags_count = tag_ids.uniq.size
@hidden_contexts = current_user.contexts.hidden
@actions = Stats::Actions.new(current_user)
get_stats_actions
get_stats_contexts
get_stats_projects
get_stats_tags
@ -387,60 +387,6 @@ class StatsController < ApplicationController
@cut_off_3months = 3.months.ago.beginning_of_day
end
def get_stats_actions
# time to complete
@completed_actions = current_user.todos.completed.select("completed_at, created_at")
actions_sum, actions_max = 0,0
actions_min = @completed_actions.first ? @completed_actions.first.completed_at - @completed_actions.first.created_at : 0
@completed_actions.each do |r|
actions_sum += (r.completed_at - r.created_at)
actions_max = [(r.completed_at - r.created_at), actions_max].max
actions_min = [(r.completed_at - r.created_at), actions_min].min
end
sum_actions = @completed_actions.size
sum_actions = 1 if sum_actions==0 # to prevent dividing by zero
@actions_avg_ttc = (actions_sum/sum_actions)/SECONDS_PER_DAY
@actions_max_ttc = actions_max/SECONDS_PER_DAY
@actions_min_ttc = actions_min/SECONDS_PER_DAY
min_ttc_sec = Time.utc(2000,1,1,0,0)+actions_min # convert to a datetime
@actions_min_ttc_sec = (min_ttc_sec).strftime("%H:%M:%S")
@actions_min_ttc_sec = (actions_min / SECONDS_PER_DAY).round.to_s + " days " + @actions_min_ttc_sec if actions_min > SECONDS_PER_DAY
# get count of actions created and actions done in the past 30 days.
@sum_actions_done_last30days = current_user.todos.completed.completed_after(@cut_off_month).count
@sum_actions_created_last30days = current_user.todos.created_after(@cut_off_month).count
# get count of actions done in the past 12 months.
@sum_actions_done_last12months = current_user.todos.completed.completed_after(@cut_off_year).count
@sum_actions_created_last12months = current_user.todos.created_after(@cut_off_year).count
@completion_charts = %w{
actions_done_last30days_data
actions_done_last12months_data
actions_completion_time_data
}.map do |action|
Stats::Chart.new(action)
end
@timing_charts = %w{
actions_visible_running_time_data
actions_running_time_data
actions_open_per_week_data
actions_day_of_week_all_data
actions_day_of_week_30days_data
actions_time_of_day_all_data
actions_time_of_day_30days_data
}.map do |action|
Stats::Chart.new(action)
end
end
def get_stats_contexts
@actions_per_context = Stats::TopContextsQuery.new(current_user, :limit => 5).result
@running_actions_per_context = Stats::TopContextsQuery.new(current_user, :limit => 5, :running => true).result

View file

@ -0,0 +1,77 @@
module Stats
class Actions
SECONDS_PER_DAY = 86400;
attr_reader :user
def initialize(user)
@user = user
end
def ttc
@ttc ||= TimeToComplete.new(completed)
end
def done_last30days
@done_last30days ||= done_since(one_month)
end
def done_last12months
@done_last12months ||= done_since(one_year)
end
def created_last30days
@sum_actions_created_last30days ||= new_since(one_month)
end
def created_last12months
@sum_actions_created_last12months ||= new_since(one_year)
end
def completion_charts
@completion_charts ||= %w{
actions_done_last30days_data
actions_done_last12months_data
actions_completion_time_data
}.map do |action|
Stats::Chart.new(action)
end
end
def timing_charts
@timing_charts ||= %w{
actions_visible_running_time_data
actions_running_time_data
actions_open_per_week_data
actions_day_of_week_all_data
actions_day_of_week_30days_data
actions_time_of_day_all_data
actions_time_of_day_30days_data
}.map do |action|
Stats::Chart.new(action)
end
end
private
def one_year
@one_year ||= 12.months.ago.beginning_of_day
end
def one_month
@one_month ||= 1.month.ago.beginning_of_day
end
def new_since(cutoff)
user.todos.created_after(cutoff).count
end
def done_since(cutoff)
user.todos.completed.completed_after(cutoff).count
end
def completed
@completed ||= user.todos.completed.select("completed_at, created_at")
end
end
end

View file

@ -0,0 +1,63 @@
module Stats
class TimeToComplete
SECONDS_PER_DAY = 86400;
attr_reader :actions
def initialize(actions)
@actions = actions
end
def avg
@avg ||= to_days(sum / count)
end
def max
@max ||= to_days(max_in_seconds)
end
def min
@min ||= to_days(min_in_seconds)
end
def min_sec
min_sec = arbitrary_day + min_in_seconds # convert to a datetime
@min_sec = min_sec.strftime("%H:%M:%S")
@min_sec = min.floor.to_s + " days " + @min_sec if min >= 1
@min_sec
end
private
def to_days(value)
(value * 10 / SECONDS_PER_DAY).round / 10.0
end
def min_in_seconds
@min_in_seconds ||= durations.min || 0
end
def max_in_seconds
@max_in_seconds ||= durations.max || 0
end
def count
actions.empty? ? 1 : actions.size
end
def durations
@durations ||= actions.map do |r|
(r.completed_at - r.created_at)
end
end
def sum
@sum ||= durations.inject(0) {|sum, d| sum + d}
end
def arbitrary_day
@arbitrary_day ||= Time.utc(2000, 1, 1, 0, 0)
end
end
end

View file

@ -1,19 +1,17 @@
<p><%= t('stats.actions_avg_completion_time', :count => (@actions_avg_ttc*10).round/10.0) %>
<%= t('stats.actions_min_max_completion_days', :max => (@actions_max_ttc*10).round/10.0, :min => (@actions_min_ttc*10).round/10.0) %>
<%= t('stats.actions_min_completion_time', :time => @actions_min_ttc_sec) %></p>
<%= render :partial => 'time_to_complete', :locals => {:ttc => actions.ttc} -%>
<p><%= t('stats.actions_actions_avg_created_30days', :count => (@sum_actions_created_last30days*10.0/30.0).round/10.0 )%>
<%= t('stats.actions_avg_completed_30days', :count => (@sum_actions_done_last30days*10.0/30.0).round/10.0 )%>
<%= t('stats.actions_avg_created', :count => (@sum_actions_created_last12months*10.0/12.0).round/10.0 )%>
<%= t('stats.actions_avg_completed', :count => (@sum_actions_done_last12months*10.0/12.0).round/10.0 )%></p>
<p><%= t('stats.actions_actions_avg_created_30days', :count => (actions.created_last30days*10.0/30.0).round/10.0 )%>
<%= t('stats.actions_avg_completed_30days', :count => (actions.done_last30days*10.0/30.0).round/10.0 )%>
<%= t('stats.actions_avg_created', :count => (actions.created_last12months*10.0/12.0).round/10.0 )%>
<%= t('stats.actions_avg_completed', :count => (actions.done_last12months*10.0/12.0).round/10.0 )%></p>
<% @completion_charts.each do |chart| %><%=
<% actions.completion_charts.each do |chart| %><%=
render :partial => 'chart', :locals => {:chart => chart}
-%><% end %>
<br style="clear:both">
<% @timing_charts.each do |chart| %><%=
<% actions.timing_charts.each do |chart| %><%=
render :partial => 'chart', :locals => {:chart => chart}
-%><% end %>

View file

@ -0,0 +1,4 @@
<p><%= t('stats.actions_avg_completion_time', :count => ttc.avg) %>
<%= t('stats.actions_min_max_completion_days', :max => ttc.max, :min => ttc.min) %>
<%= t('stats.actions_min_completion_time', :time => ttc.min_sec) %></p>

View file

@ -6,7 +6,7 @@
<% unless current_user.todos.empty? -%>
<h2><%= t('stats.actions') %></h2>
<%= render :partial => 'actions' -%>
<%= render :partial => 'actions', :locals => {:actions => @actions} -%>
<h2><%= t('stats.contexts') %></h2>
<%= render :partial => 'contexts' -%>

View file

@ -0,0 +1,93 @@
require 'date'
require 'time'
require './test/minimal_test_helper'
require 'app/models/stats/time_to_complete'
FakeTask = Struct.new(:created_at, :completed_at)
class TimeToCompleteTest < Test::Unit::TestCase
def now
@now ||= Time.utc(2013, 1, 2, 3, 4, 5)
end
def day0
@day0 ||= Time.utc(2013, 1, 2, 0, 0, 0)
end
def day2
@day2 ||= Time.utc(2012, 12, 31, 0, 0, 0)
end
def day4
@day4 ||= Time.utc(2012, 12, 29, 0, 0, 0)
end
def day6
@day6 ||= Time.utc(2012, 12, 27, 0, 0, 0)
end
def fake_tasks
@fake_tasks ||= [
FakeTask.new(day2, now),
FakeTask.new(day4, now),
FakeTask.new(day6, now)
]
end
def test_empty_minimum
assert_equal 0, Stats::TimeToComplete.new([]).min
end
def test_empty_maximum
assert_equal 0, Stats::TimeToComplete.new([]).max
end
def test_empty_avg
assert_equal 0, Stats::TimeToComplete.new([]).avg
end
def test_empty_min_sec
assert_equal '00:00:00', Stats::TimeToComplete.new([]).min_sec
end
def test_minimum
assert_equal 2.1, Stats::TimeToComplete.new(fake_tasks).min
end
def test_maximum
assert_equal 6.1, Stats::TimeToComplete.new(fake_tasks).max
end
def test_avg
assert_equal 4.1, Stats::TimeToComplete.new(fake_tasks).avg
end
def test_min_sec
assert_equal '2 days 03:04:05', Stats::TimeToComplete.new(fake_tasks).min_sec
end
def test_min_sec_with_less_than_a_day
task = FakeTask.new(day0, now)
assert_equal '03:04:05', Stats::TimeToComplete.new([task]).min_sec
end
def test_rounding
start_time = Time.utc(2012, 12, 31, 8, 0, 0)
task = FakeTask.new(start_time, now)
ttc = Stats::TimeToComplete.new([task])
assert_equal 1.8, ttc.avg
assert_equal 1.8, ttc.min
assert_equal 1.8, ttc.max
end
def test_min_sec_with_almost_three_days
start_time = Time.utc(2012, 12, 30, 8, 0, 0)
task = FakeTask.new(start_time, now)
stats = Stats::TimeToComplete.new([task])
assert_equal 2.8, stats.min
assert_equal '2 days 19:04:05', stats.min_sec
end
end