logstats 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/.rvmrc +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +21 -0
- data/README +71 -0
- data/Rakefile +2 -0
- data/bin/logstats +6 -0
- data/examples/example.haml +128 -0
- data/lib/logstats/haml/helpers.rb +46 -0
- data/lib/logstats/template.haml +123 -0
- data/lib/logstats/version.rb +3 -0
- data/lib/logstats/worklog.rb +154 -0
- data/lib/logstats.rb +40 -0
- data/logstats.gemspec +24 -0
- data/watchr/all.watchr +1 -0
- metadata +113 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
GIT
|
|
2
|
+
remote: git://github.com/jstirk/tail_from_sentinel.git
|
|
3
|
+
revision: def3003aa61a46dfd239cad938700d8ad26c2988
|
|
4
|
+
specs:
|
|
5
|
+
tail_from_sentinel (0.0.1)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: http://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
haml (3.0.25)
|
|
11
|
+
ruby-fsevent (0.2.1)
|
|
12
|
+
watchr (0.7)
|
|
13
|
+
|
|
14
|
+
PLATFORMS
|
|
15
|
+
ruby
|
|
16
|
+
|
|
17
|
+
DEPENDENCIES
|
|
18
|
+
haml
|
|
19
|
+
ruby-fsevent
|
|
20
|
+
tail_from_sentinel!
|
|
21
|
+
watchr
|
data/README
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
LogStats
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
Generates a simple HTML file based upon my custom timesheet format.
|
|
5
|
+
|
|
6
|
+
I can then embed this on my OSX Dashboard to be able to see how I'm
|
|
7
|
+
progressing throughout the day/week/month.
|
|
8
|
+
|
|
9
|
+
It's probably not so useful to you, unless you happen to like my
|
|
10
|
+
worklog format.
|
|
11
|
+
|
|
12
|
+
I run the script every 5 minutes with cron so as that the HTML stays
|
|
13
|
+
up-to-date. The page is set to auto-refresh every 2.5 minutes.
|
|
14
|
+
|
|
15
|
+
The colours change when I have done at least 5 hours of work per day,
|
|
16
|
+
and 25 hours per week.
|
|
17
|
+
|
|
18
|
+
Requirements
|
|
19
|
+
============
|
|
20
|
+
* HAML
|
|
21
|
+
* tail_from_sentinel
|
|
22
|
+
|
|
23
|
+
Usage
|
|
24
|
+
=====
|
|
25
|
+
|
|
26
|
+
logstats INPUTFILE.txt OUTPUTFILE.html
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Worklog Format
|
|
30
|
+
==============
|
|
31
|
+
|
|
32
|
+
I use a text based worklog format for all my timekeeping.
|
|
33
|
+
|
|
34
|
+
This is a simple text file that sits in my Dropbox, and it symlinked to
|
|
35
|
+
my home. I keep it open in an editor all day so as that I can quickly jot
|
|
36
|
+
down when my task changes.
|
|
37
|
+
|
|
38
|
+
It works great for unexpected interruptions, as I can quickly update it
|
|
39
|
+
when I am back, or even while I am on the phone (only need to type the time
|
|
40
|
+
I answer the call).
|
|
41
|
+
|
|
42
|
+
The format looks like this :
|
|
43
|
+
|
|
44
|
+
01Jan2011
|
|
45
|
+
0900 ABC 1234567 1012
|
|
46
|
+
1015 ABC 9876543 1100
|
|
47
|
+
1100 ABC Deploy 1110
|
|
48
|
+
|
|
49
|
+
03Jan
|
|
50
|
+
1200 XY1 2345678 1230
|
|
51
|
+
|
|
52
|
+
Each day has a simple header in DDMMMYYYY format, where YYYY is
|
|
53
|
+
optional where it is the same as the record before.
|
|
54
|
+
|
|
55
|
+
Within the day, each line follows a simple format :
|
|
56
|
+
* the start time (in 24hr)
|
|
57
|
+
|
|
58
|
+
* a message, which starts with a 3-character project code (if billable).
|
|
59
|
+
I usually follow this with a Pivotal Tracker Story ID, or a note
|
|
60
|
+
about what I was doing.
|
|
61
|
+
If it's not billable work, I might use this space to log an
|
|
62
|
+
unexpected call for instance.
|
|
63
|
+
This field is freeform, with the project picked up if it's there.
|
|
64
|
+
|
|
65
|
+
* the end time (in 24hr). It's fine for this to be in the next day.
|
|
66
|
+
Eg: 2330 ABC allnighter 0230
|
|
67
|
+
|
|
68
|
+
TODO
|
|
69
|
+
====
|
|
70
|
+
* Tests!
|
|
71
|
+
* Make it more customizable. Not everyone has the same thresholds that I keep.
|
data/Rakefile
ADDED
data/bin/logstats
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
!!!
|
|
2
|
+
%html
|
|
3
|
+
%head
|
|
4
|
+
%title Log Stats @ 2011-01-18 07:09
|
|
5
|
+
%style
|
|
6
|
+
:sass
|
|
7
|
+
$background_color_light: #454342
|
|
8
|
+
$background_color_dark: #333335
|
|
9
|
+
$text_color: white
|
|
10
|
+
$link_color: #a9c743
|
|
11
|
+
$border_color: #272b3b
|
|
12
|
+
|
|
13
|
+
body
|
|
14
|
+
:font-size 62.5%
|
|
15
|
+
:font-family Helvetica, sans-serif
|
|
16
|
+
:background $background_color_light
|
|
17
|
+
:color $text_color
|
|
18
|
+
|
|
19
|
+
#logstats
|
|
20
|
+
:width 400px
|
|
21
|
+
:height 241px
|
|
22
|
+
:background $background_color_dark
|
|
23
|
+
:border 1px solid $border_color
|
|
24
|
+
:text-align center
|
|
25
|
+
|
|
26
|
+
h1
|
|
27
|
+
:font-size 120%
|
|
28
|
+
:text-decoration underline
|
|
29
|
+
:font-variant small-caps
|
|
30
|
+
:text-align center
|
|
31
|
+
:margin 0 0 10px 0
|
|
32
|
+
:padding 0
|
|
33
|
+
|
|
34
|
+
ul
|
|
35
|
+
:list-style-type none
|
|
36
|
+
:margin 0
|
|
37
|
+
:padding 0
|
|
38
|
+
:text-align left
|
|
39
|
+
|
|
40
|
+
li
|
|
41
|
+
:margin 0
|
|
42
|
+
:padding 0
|
|
43
|
+
|
|
44
|
+
.duration
|
|
45
|
+
:font-size 200%
|
|
46
|
+
:color #cacaca
|
|
47
|
+
|
|
48
|
+
span
|
|
49
|
+
:color $text_color
|
|
50
|
+
|
|
51
|
+
.remaining, .average
|
|
52
|
+
:margin-top 5px
|
|
53
|
+
|
|
54
|
+
.recent, .history
|
|
55
|
+
:width 200px
|
|
56
|
+
:float left
|
|
57
|
+
|
|
58
|
+
.recent
|
|
59
|
+
.current, .today
|
|
60
|
+
:padding 5px
|
|
61
|
+
|
|
62
|
+
.current
|
|
63
|
+
:height 50px
|
|
64
|
+
:border-bottom 1px solid $border-color
|
|
65
|
+
|
|
66
|
+
.history
|
|
67
|
+
.today, .week, .month
|
|
68
|
+
:height 70px
|
|
69
|
+
:padding 5px
|
|
70
|
+
:border-left 1px solid $border_color
|
|
71
|
+
|
|
72
|
+
.today, .week
|
|
73
|
+
:border-bottom 1px solid $border-color
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
%body
|
|
77
|
+
#logstats
|
|
78
|
+
.recent
|
|
79
|
+
.current
|
|
80
|
+
%h1 Current
|
|
81
|
+
%span.duration
|
|
82
|
+
%span.minute 45
|
|
83
|
+
min
|
|
84
|
+
.today
|
|
85
|
+
%h1 Projects
|
|
86
|
+
%ul.projects
|
|
87
|
+
%li
|
|
88
|
+
%span.project MB1
|
|
89
|
+
%span.duration
|
|
90
|
+
%span.hour 1
|
|
91
|
+
hr
|
|
92
|
+
%span.minute 57
|
|
93
|
+
min
|
|
94
|
+
%li
|
|
95
|
+
%span.project MY1
|
|
96
|
+
%span.duration
|
|
97
|
+
%span.hour 2
|
|
98
|
+
hr
|
|
99
|
+
%span.minute 30
|
|
100
|
+
min
|
|
101
|
+
.history
|
|
102
|
+
.today
|
|
103
|
+
%h1 Today
|
|
104
|
+
%span.duration
|
|
105
|
+
%span.hour 5
|
|
106
|
+
hr
|
|
107
|
+
%span.minute 57
|
|
108
|
+
min
|
|
109
|
+
.remaining
|
|
110
|
+
(target met!)
|
|
111
|
+
.week
|
|
112
|
+
%h1 Week
|
|
113
|
+
%span.duration
|
|
114
|
+
%span.hour 5
|
|
115
|
+
hr
|
|
116
|
+
%span.minute 57
|
|
117
|
+
min
|
|
118
|
+
.remaining
|
|
119
|
+
(20 hr 3 min remaining)
|
|
120
|
+
.month
|
|
121
|
+
%h1 Month
|
|
122
|
+
%span.duration
|
|
123
|
+
%span.hour 35
|
|
124
|
+
hr
|
|
125
|
+
%span.minute 57
|
|
126
|
+
min
|
|
127
|
+
.average
|
|
128
|
+
(6 hr 12 min average)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module LogStats
|
|
2
|
+
module Helpers
|
|
3
|
+
# NOTE: Because of the way I'm calling this (as the HAML context) these all need to be class methods
|
|
4
|
+
|
|
5
|
+
# Turns a number of seconds into a pretty HTML string
|
|
6
|
+
def self.time_to_html(seconds, options={})
|
|
7
|
+
hrs=(seconds / 3600).floor
|
|
8
|
+
min=(seconds % 3600).floor / 60
|
|
9
|
+
o=[]
|
|
10
|
+
o << "<span class=\"hour\">#{hrs}</span> hr" if hrs.to_i > 0
|
|
11
|
+
o << "<span class=\"minute\">#{min}</span> min" if min.to_i > 0
|
|
12
|
+
o.join(' ')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Options:
|
|
16
|
+
# :class : The CSS class to apply to the top element (default: duration)
|
|
17
|
+
def self.duration_tag(seconds, options={})
|
|
18
|
+
options[:class]='duration' if options[:class].nil?
|
|
19
|
+
"<span class=\"#{options[:class]}\">#{self.time_to_html(seconds)}</span>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.remaining_tag(seconds_so_far, period)
|
|
23
|
+
seconds_required=case period
|
|
24
|
+
when :day
|
|
25
|
+
# 5 hrs per day
|
|
26
|
+
5 * 3600
|
|
27
|
+
when :week
|
|
28
|
+
# 5 hrs per day, 5 days per week
|
|
29
|
+
5 * 5 * 3600
|
|
30
|
+
else
|
|
31
|
+
raise "Unknown period: #{period}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
seconds=seconds_required - seconds_so_far
|
|
35
|
+
|
|
36
|
+
css_class=[ 'remaining ']
|
|
37
|
+
if seconds.nil? || seconds < 0 then
|
|
38
|
+
content=self.time_to_html(seconds.abs) + ' over!'
|
|
39
|
+
css_class << 'met'
|
|
40
|
+
else
|
|
41
|
+
content=self.time_to_html(seconds) + ' remaining'
|
|
42
|
+
end
|
|
43
|
+
"<div class=\"#{css_class.join(' ')}\">(#{content})</div>"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
!!!
|
|
2
|
+
%html
|
|
3
|
+
%head
|
|
4
|
+
%title Log Stats @ #{Time.now.strftime('%a, %e %b %Y %H:%M')}
|
|
5
|
+
%meta{ "http-equiv" => "refresh", :content => "150" }
|
|
6
|
+
%style
|
|
7
|
+
:sass
|
|
8
|
+
$background_color_light: #454342
|
|
9
|
+
$background_color_dark: #333335
|
|
10
|
+
$text_color: white
|
|
11
|
+
$text_faded_color: #cacaca
|
|
12
|
+
$link_color: #a9c743
|
|
13
|
+
$border_color: #272b3b
|
|
14
|
+
$success_color: #a9c743
|
|
15
|
+
$success_faded_color: #7D9231
|
|
16
|
+
|
|
17
|
+
body
|
|
18
|
+
:font-size 62.5%
|
|
19
|
+
:font-family Helvetica, sans-serif
|
|
20
|
+
:background $background_color_light
|
|
21
|
+
:color $text_color
|
|
22
|
+
|
|
23
|
+
#logstats
|
|
24
|
+
:width 400px
|
|
25
|
+
:height 241px
|
|
26
|
+
:background $background_color_dark
|
|
27
|
+
:border 1px solid $border_color
|
|
28
|
+
:text-align center
|
|
29
|
+
|
|
30
|
+
h1
|
|
31
|
+
:font-size 120%
|
|
32
|
+
:text-decoration underline
|
|
33
|
+
:font-variant small-caps
|
|
34
|
+
:text-align center
|
|
35
|
+
:margin 0
|
|
36
|
+
:padding 0
|
|
37
|
+
|
|
38
|
+
ul
|
|
39
|
+
:list-style-type none
|
|
40
|
+
:margin 0
|
|
41
|
+
:padding 0
|
|
42
|
+
:text-align left
|
|
43
|
+
|
|
44
|
+
li
|
|
45
|
+
:margin 0
|
|
46
|
+
:padding 0
|
|
47
|
+
|
|
48
|
+
.duration
|
|
49
|
+
:font-size 200%
|
|
50
|
+
:color $text_faded_color
|
|
51
|
+
:line-height 40px
|
|
52
|
+
|
|
53
|
+
span
|
|
54
|
+
:color $text_color
|
|
55
|
+
|
|
56
|
+
.met
|
|
57
|
+
:color $success_color
|
|
58
|
+
|
|
59
|
+
.duration
|
|
60
|
+
:color $success_faded_color
|
|
61
|
+
|
|
62
|
+
span
|
|
63
|
+
:color $success_color
|
|
64
|
+
|
|
65
|
+
//.remaining, .average
|
|
66
|
+
|
|
67
|
+
.recent, .history
|
|
68
|
+
:width 200px
|
|
69
|
+
:float left
|
|
70
|
+
|
|
71
|
+
.recent
|
|
72
|
+
.current, .today
|
|
73
|
+
:padding 5px
|
|
74
|
+
|
|
75
|
+
.current
|
|
76
|
+
:height 50px
|
|
77
|
+
:border-bottom 1px solid $border-color
|
|
78
|
+
|
|
79
|
+
.today ul
|
|
80
|
+
.duration
|
|
81
|
+
:line-height 30px
|
|
82
|
+
|
|
83
|
+
.history
|
|
84
|
+
.today, .week, .month
|
|
85
|
+
:height 70px
|
|
86
|
+
:padding 5px
|
|
87
|
+
:border-left 1px solid $border_color
|
|
88
|
+
|
|
89
|
+
.today, .week
|
|
90
|
+
:border-bottom 1px solid $border-color
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
%body
|
|
94
|
+
#logstats
|
|
95
|
+
.recent
|
|
96
|
+
.current
|
|
97
|
+
%h1 Current Task
|
|
98
|
+
- if current then
|
|
99
|
+
= duration_tag(current)
|
|
100
|
+
- else
|
|
101
|
+
N/A
|
|
102
|
+
.today
|
|
103
|
+
%h1 Projects
|
|
104
|
+
%ul.projects
|
|
105
|
+
- today[:projects].each do |project, time|
|
|
106
|
+
%li
|
|
107
|
+
%span.project= project
|
|
108
|
+
= duration_tag(time)
|
|
109
|
+
.history
|
|
110
|
+
.today{ :class => (today[:remaining].nil? ? 'met' : nil )}
|
|
111
|
+
%h1 Today
|
|
112
|
+
= duration_tag(today[:total])
|
|
113
|
+
= remaining_tag(today[:total], :day)
|
|
114
|
+
|
|
115
|
+
.week{ :class => (week[:remaining].nil? ? 'met' : nil )}
|
|
116
|
+
%h1 Week
|
|
117
|
+
= duration_tag(week[:total])
|
|
118
|
+
= remaining_tag(week[:total], :week)
|
|
119
|
+
.month
|
|
120
|
+
%h1 Month
|
|
121
|
+
= duration_tag(month[:total])
|
|
122
|
+
.average
|
|
123
|
+
(#{time_to_html(month[:average])} average)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require 'tail_from_sentinel'
|
|
2
|
+
require 'time'
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
# Parses my own WorkLog file format and extracts the stats for the past month
|
|
6
|
+
module LogStats
|
|
7
|
+
class WorkLog < TailFromSentinel::Base
|
|
8
|
+
DATE_SENTINEL_REGEX=/(\d+)([A-Z]+)(\d{4})?$/i
|
|
9
|
+
WORKLOG_RECORD_REGEX=/(\d{2})(\d{2}) (.*) (\d{2})(\d{2})$/i
|
|
10
|
+
OPEN_WORKLOG_RECORD_REGEX=/(\d{2})(\d{2}) (.*)$/i
|
|
11
|
+
|
|
12
|
+
DATE_SENTINEL_PROC=Proc.new do |line|
|
|
13
|
+
if line =~ DATE_SENTINEL_REGEX then
|
|
14
|
+
# It's a date marker - parse it to see whether it's close enough to when we want
|
|
15
|
+
day,month,year=self.parse_sentinel($1, $2, $3)
|
|
16
|
+
(month.downcase == self.now.strftime('%b').downcase) && (year.to_i == self.now.year)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.now
|
|
21
|
+
@@now ||= Time.now
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.last_seen_year
|
|
25
|
+
@@last_seen_year
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.last_seen_year=(year)
|
|
29
|
+
@@last_seen_year=year
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.parse_sentinel(d,m,y)
|
|
33
|
+
year=y || self.last_seen_year
|
|
34
|
+
self.last_seen_year=year
|
|
35
|
+
[ d, m, year ]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(filename)
|
|
39
|
+
@last_seen_year=nil
|
|
40
|
+
@stas=nil
|
|
41
|
+
super(File.open(filename, 'r'), &DATE_SENTINEL_PROC)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# TODO: Document this
|
|
45
|
+
def stats
|
|
46
|
+
return @stats unless @stats.nil?
|
|
47
|
+
|
|
48
|
+
days={}
|
|
49
|
+
now=WorkLog.now
|
|
50
|
+
current_time=nil
|
|
51
|
+
|
|
52
|
+
data.each do |line|
|
|
53
|
+
line=line.strip
|
|
54
|
+
case line
|
|
55
|
+
when DATE_SENTINEL_REGEX
|
|
56
|
+
# It's a date marker - set current_time
|
|
57
|
+
current_time=Time.parse(WorkLog.parse_sentinel($1,$2,$3).reverse.join('-'))
|
|
58
|
+
days[current_time]={ :total => 0, :projects => {} }
|
|
59
|
+
|
|
60
|
+
when WORKLOG_RECORD_REGEX
|
|
61
|
+
# It's a worklog record - parse it, and add it to the relevant buckets
|
|
62
|
+
duration, project=parse_worklog_record(current_time, $1, $2, $3, $4, $5)
|
|
63
|
+
|
|
64
|
+
days[current_time][:total] += duration
|
|
65
|
+
days[current_time][:projects][project] ||= 0
|
|
66
|
+
days[current_time][:projects][project] += duration
|
|
67
|
+
|
|
68
|
+
when OPEN_WORKLOG_RECORD_REGEX
|
|
69
|
+
duration, project=parse_worklog_record(current_time, $1, $2, $3)
|
|
70
|
+
days[:current]=duration
|
|
71
|
+
|
|
72
|
+
when /^\s*$/
|
|
73
|
+
# Blank line - ignore
|
|
74
|
+
else
|
|
75
|
+
puts "Warning: Unknown format of \"#{line}\""
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
@stats=compile_day_data(days)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_worklog_record(current_time, sh,sm,msg,eh=nil,em=nil)
|
|
82
|
+
start_time=current_time + (sh.to_i * 3600) + (sm.to_i * 60)
|
|
83
|
+
|
|
84
|
+
if eh && em then
|
|
85
|
+
end_time=current_time + (eh.to_i * 3600) + (em.to_i * 60)
|
|
86
|
+
end_time += (24 * 3600) if end_time < start_time
|
|
87
|
+
else
|
|
88
|
+
end_time=Time.now
|
|
89
|
+
end
|
|
90
|
+
duration=end_time - start_time
|
|
91
|
+
|
|
92
|
+
if msg.match(/^([A-Z0-9]{3})/) then
|
|
93
|
+
project=$1
|
|
94
|
+
else
|
|
95
|
+
project='MISC'
|
|
96
|
+
end
|
|
97
|
+
[ duration, project ]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def compile_day_data(days)
|
|
101
|
+
stats={ :current => days[:current],
|
|
102
|
+
:today => { :total => 0,
|
|
103
|
+
:projects => { }
|
|
104
|
+
},
|
|
105
|
+
:week => { :total => 0,
|
|
106
|
+
:average => 0,
|
|
107
|
+
:projects => { }
|
|
108
|
+
},
|
|
109
|
+
:month => { :total => 0,
|
|
110
|
+
:average => 0,
|
|
111
|
+
:days_logged => 0,
|
|
112
|
+
:projects => { }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
now=WorkLog.now
|
|
117
|
+
today=Date.today
|
|
118
|
+
|
|
119
|
+
days.each do |time, data|
|
|
120
|
+
next if time == :current
|
|
121
|
+
|
|
122
|
+
# It's data for today - use it
|
|
123
|
+
stats[:today]=data if time.day == now.day
|
|
124
|
+
|
|
125
|
+
if time_to_date(time).cweek == today.cweek then
|
|
126
|
+
stats[:week][:total] += data[:total]
|
|
127
|
+
stats[:week][:average] = (stats[:week][:total] / time_to_date(time).wday.to_f)
|
|
128
|
+
data[:projects].each do |project, duration|
|
|
129
|
+
stats[:week][:projects][project] ||= 0
|
|
130
|
+
stats[:week][:projects][project] += duration
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Everything is included in this month
|
|
135
|
+
stats[:month][:total] += data[:total]
|
|
136
|
+
stats[:month][:days_logged] += 1
|
|
137
|
+
stats[:month][:average] = (stats[:month][:total] / stats[:month][:days_logged])
|
|
138
|
+
data[:projects].each do |project, duration|
|
|
139
|
+
stats[:month][:projects][project] ||= 0
|
|
140
|
+
stats[:month][:projects][project] += duration
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
stats[:today][:total] += days[:current]
|
|
145
|
+
#stats[:today][:projects]['WIP'] = days[:current]
|
|
146
|
+
|
|
147
|
+
return stats
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def time_to_date(time)
|
|
151
|
+
Date.new(time.year, time.month, time.day)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
data/lib/logstats.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'haml'
|
|
2
|
+
require 'logstats/haml/helpers'
|
|
3
|
+
require 'logstats/worklog'
|
|
4
|
+
module LogStats
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :worklog
|
|
7
|
+
|
|
8
|
+
def initialize(source_file, output_path)
|
|
9
|
+
@worklog=WorkLog.new(source_file)
|
|
10
|
+
@output_path=output_path
|
|
11
|
+
@base_path=File.dirname(__FILE__)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate!
|
|
15
|
+
# Calculate the stats from the file
|
|
16
|
+
locals={}
|
|
17
|
+
@worklog.stats.each { |key, data| locals[key]=data }
|
|
18
|
+
process_haml(locals)
|
|
19
|
+
return true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def process_haml(locals)
|
|
25
|
+
# Inject them into the HAML layout
|
|
26
|
+
haml=nil
|
|
27
|
+
File.open(File.join(@base_path, 'logstats', 'template.haml'), 'r') do |f|
|
|
28
|
+
haml=f.read
|
|
29
|
+
end
|
|
30
|
+
engine = Haml::Engine.new(haml)
|
|
31
|
+
html=engine.render(LogStats::Helpers, locals)
|
|
32
|
+
|
|
33
|
+
# Save the HAML to a file
|
|
34
|
+
File.open(@output_path, 'w') do |f|
|
|
35
|
+
f << html
|
|
36
|
+
end
|
|
37
|
+
return true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/logstats.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "logstats/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "logstats"
|
|
7
|
+
s.version = Logstats::VERSION
|
|
8
|
+
s.platform = Gem::Platform::RUBY
|
|
9
|
+
s.authors = ["Jason Stirk"]
|
|
10
|
+
s.email = ["jstirk@oobleyboo.com"]
|
|
11
|
+
s.homepage = "http://github.com/jstirk/logstats"
|
|
12
|
+
s.summary = "Generates a simple HTML file based upon my custom timesheet format."
|
|
13
|
+
s.description = "Generates a simple HTML file based upon my custom timesheet format."
|
|
14
|
+
|
|
15
|
+
s.rubyforge_project = "logstats"
|
|
16
|
+
|
|
17
|
+
s.files = `git ls-files`.split("\n")
|
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
20
|
+
s.require_paths = ["lib"]
|
|
21
|
+
|
|
22
|
+
s.add_dependency('tail_from_sentinel', '>= 0.0.1')
|
|
23
|
+
s.add_dependency('haml', '~> 3.0.25')
|
|
24
|
+
end
|
data/watchr/all.watchr
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
watch( 'examples/.*\.haml' ) {|md| system("haml #{md[0]} #{md[0].gsub(/haml$/,'html')}") }
|
metadata
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: logstats
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
hash: 29
|
|
5
|
+
prerelease: false
|
|
6
|
+
segments:
|
|
7
|
+
- 0
|
|
8
|
+
- 0
|
|
9
|
+
- 1
|
|
10
|
+
version: 0.0.1
|
|
11
|
+
platform: ruby
|
|
12
|
+
authors:
|
|
13
|
+
- Jason Stirk
|
|
14
|
+
autorequire:
|
|
15
|
+
bindir: bin
|
|
16
|
+
cert_chain: []
|
|
17
|
+
|
|
18
|
+
date: 2011-01-08 00:00:00 +11:00
|
|
19
|
+
default_executable:
|
|
20
|
+
dependencies:
|
|
21
|
+
- !ruby/object:Gem::Dependency
|
|
22
|
+
name: tail_from_sentinel
|
|
23
|
+
prerelease: false
|
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
hash: 29
|
|
30
|
+
segments:
|
|
31
|
+
- 0
|
|
32
|
+
- 0
|
|
33
|
+
- 1
|
|
34
|
+
version: 0.0.1
|
|
35
|
+
type: :runtime
|
|
36
|
+
version_requirements: *id001
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: haml
|
|
39
|
+
prerelease: false
|
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ~>
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
hash: 53
|
|
46
|
+
segments:
|
|
47
|
+
- 3
|
|
48
|
+
- 0
|
|
49
|
+
- 25
|
|
50
|
+
version: 3.0.25
|
|
51
|
+
type: :runtime
|
|
52
|
+
version_requirements: *id002
|
|
53
|
+
description: Generates a simple HTML file based upon my custom timesheet format.
|
|
54
|
+
email:
|
|
55
|
+
- jstirk@oobleyboo.com
|
|
56
|
+
executables:
|
|
57
|
+
- logstats
|
|
58
|
+
extensions: []
|
|
59
|
+
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
|
|
62
|
+
files:
|
|
63
|
+
- .gitignore
|
|
64
|
+
- .rvmrc
|
|
65
|
+
- Gemfile
|
|
66
|
+
- Gemfile.lock
|
|
67
|
+
- README
|
|
68
|
+
- Rakefile
|
|
69
|
+
- bin/logstats
|
|
70
|
+
- examples/example.haml
|
|
71
|
+
- lib/logstats.rb
|
|
72
|
+
- lib/logstats/haml/helpers.rb
|
|
73
|
+
- lib/logstats/template.haml
|
|
74
|
+
- lib/logstats/version.rb
|
|
75
|
+
- lib/logstats/worklog.rb
|
|
76
|
+
- logstats.gemspec
|
|
77
|
+
- watchr/all.watchr
|
|
78
|
+
has_rdoc: true
|
|
79
|
+
homepage: http://github.com/jstirk/logstats
|
|
80
|
+
licenses: []
|
|
81
|
+
|
|
82
|
+
post_install_message:
|
|
83
|
+
rdoc_options: []
|
|
84
|
+
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
none: false
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
hash: 3
|
|
93
|
+
segments:
|
|
94
|
+
- 0
|
|
95
|
+
version: "0"
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
none: false
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
hash: 3
|
|
102
|
+
segments:
|
|
103
|
+
- 0
|
|
104
|
+
version: "0"
|
|
105
|
+
requirements: []
|
|
106
|
+
|
|
107
|
+
rubyforge_project: logstats
|
|
108
|
+
rubygems_version: 1.3.7
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 3
|
|
111
|
+
summary: Generates a simple HTML file based upon my custom timesheet format.
|
|
112
|
+
test_files: []
|
|
113
|
+
|