timestream 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/bin/timestream +58 -1
- data/lib/timestream/employee_activity_report.rb +79 -19
- data/lib/timestream/hamster.rb +101 -0
- data/lib/timestream/version.rb +1 -1
- data/timestream.gemspec +2 -0
- metadata +35 -6
data/.gitignore
CHANGED
data/bin/timestream
CHANGED
@@ -1,8 +1,65 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
|
+
|
5
|
+
require 'chronic'
|
6
|
+
require 'optparse'
|
4
7
|
require 'timestream/employee_activity_report'
|
8
|
+
require 'timestream/hamster'
|
9
|
+
require 'timestream/version'
|
10
|
+
|
11
|
+
options = {}
|
12
|
+
|
13
|
+
option_parser = OptionParser.new do |option_parser|
|
14
|
+
option_parser.banner = "Usage: timestream [--date-format=DATE_FORMAT] [--week YYYY-MM-DD]"
|
15
|
+
|
16
|
+
options[:date_format] = :day_of_week
|
17
|
+
option_parser.on('-d',
|
18
|
+
'--date-format DATE_FORMAT',
|
19
|
+
"Format dates according to DATE_FORMAT. Supports DATE_FORMAT of 'day_of_week', or 'erp'.") do |date_format|
|
20
|
+
options[:date_format] = date_format.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
option_parser.on_tail('-h', '--help', 'Show this message') do
|
24
|
+
puts option_parser
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
option_parser.on_tail('--version', 'Show version') do
|
29
|
+
puts Timestream::VERSION
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
option_parser.on('--week WEEK',
|
34
|
+
"Read in data from Hamster and write it out to EAR for WEEK " \
|
35
|
+
"beginning on Saturday. Use Chronic gem for parsing WEEK, " \
|
36
|
+
"so you do strings like 'last saturday'") do |week|
|
37
|
+
time = Chronic.parse(week, :context => :past)
|
38
|
+
options[:saturday] = Date.new(time.year, time.month, time.day)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
option_parser.parse!
|
5
43
|
|
6
44
|
employee_activity_report = Timestream::EmployeeActivityReport.new ARGV[0]
|
7
45
|
|
8
|
-
|
46
|
+
DAYS_PER_WEEK = 7
|
47
|
+
HOURS_PER_DAY = 24
|
48
|
+
MINUTES_PER_HOUR = 60
|
49
|
+
SECONDS_PER_MINUTE = 60
|
50
|
+
HOURS_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY
|
51
|
+
MINUTES_PER_WEEK = HOURS_PER_WEEK * MINUTES_PER_HOUR
|
52
|
+
SECONDS_PER_WEEK = MINUTES_PER_WEEK * SECONDS_PER_MINUTE
|
53
|
+
|
54
|
+
if options[:saturday]
|
55
|
+
saturday_date = options[:saturday]
|
56
|
+
saturday_time = Time.local(saturday_date.year, saturday_date.month, saturday_date.day)
|
57
|
+
# next saturday - 1 second for 23:59 on Friday
|
58
|
+
friday_time = saturday_time + (SECONDS_PER_WEEK) - 1
|
59
|
+
hamster = Timestream::Hamster.new(saturday_time, friday_time)
|
60
|
+
|
61
|
+
employee_activity_report.updateFromHamster!(hamster)
|
62
|
+
end
|
63
|
+
|
64
|
+
puts employee_activity_report.to_text_table(options[:date_format])
|
65
|
+
|
@@ -4,25 +4,33 @@ require 'terminal-table/import'
|
|
4
4
|
|
5
5
|
module Timestream
|
6
6
|
class EmployeeActivityReport
|
7
|
-
|
8
|
-
attr_reader :column
|
9
|
-
|
10
|
-
def initialize(row, column)
|
11
|
-
@column = column
|
12
|
-
@row = row
|
13
|
-
end
|
14
|
-
|
15
|
-
attr_reader :row
|
16
|
-
end
|
7
|
+
ACTIVITY_COLUMN = 2
|
17
8
|
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
DATE_CELL = Cell.new(2, 1)
|
9
|
+
DATE_COLUMN = 1
|
10
|
+
DATE_ROW = 2
|
23
11
|
|
24
12
|
DAY_COLUMN_SPAN = 3
|
25
13
|
|
14
|
+
def date_column(date)
|
15
|
+
slot = date_slot(date)
|
16
|
+
slot_column(slot)
|
17
|
+
end
|
18
|
+
|
19
|
+
def date_slot(date)
|
20
|
+
day_delta = (date - saturday).to_i
|
21
|
+
|
22
|
+
case day_delta
|
23
|
+
# Saturday and Sunday map to weekend
|
24
|
+
when 0..1
|
25
|
+
0
|
26
|
+
# All others map to their own day
|
27
|
+
when 2..6
|
28
|
+
day_delta - 1
|
29
|
+
else
|
30
|
+
raise ArgumentError, "#{date} is not in the same week as #{saturday}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
26
34
|
def initialize(path)
|
27
35
|
@path = path
|
28
36
|
end
|
@@ -33,6 +41,14 @@ module Timestream
|
|
33
41
|
worksheet.row(index)
|
34
42
|
end
|
35
43
|
|
44
|
+
def saturday
|
45
|
+
@saturday ||= worksheet.row(DATE_ROW)[DATE_COLUMN]
|
46
|
+
end
|
47
|
+
|
48
|
+
def slot_column(slot)
|
49
|
+
9 + (slot * DAY_COLUMN_SPAN)
|
50
|
+
end
|
51
|
+
|
36
52
|
def spreadsheet
|
37
53
|
@spreadsheet ||= Spreadsheet.open path
|
38
54
|
end
|
@@ -79,13 +95,26 @@ module Timestream
|
|
79
95
|
@timestream_by_project_number
|
80
96
|
end
|
81
97
|
|
82
|
-
def to_text_table
|
98
|
+
def to_text_table(date_format=:day_of_week)
|
83
99
|
employee_activity_report = self
|
100
|
+
|
84
101
|
table {
|
85
102
|
self.headings = ['Project Number']
|
86
|
-
|
87
|
-
|
88
|
-
|
103
|
+
|
104
|
+
case date_format
|
105
|
+
when :day_of_week
|
106
|
+
date_format = "%A"
|
107
|
+
when :erp
|
108
|
+
date_format = "%a %m/%d"
|
109
|
+
end
|
110
|
+
|
111
|
+
Timestream::SLOT_NAMES.each_index do |sunday_offset|
|
112
|
+
# XXX assign weekend activity to sunday to simplify math
|
113
|
+
sunday = employee_activity_report.saturday + 1
|
114
|
+
slot_date = sunday + sunday_offset
|
115
|
+
|
116
|
+
self.headings << slot_date.strftime(date_format)
|
117
|
+
end
|
89
118
|
|
90
119
|
employee_activity_report.timestream_by_project_number.each do |project_number, timestream|
|
91
120
|
row = [project_number]
|
@@ -98,6 +127,37 @@ module Timestream
|
|
98
127
|
}
|
99
128
|
end
|
100
129
|
|
130
|
+
def updateFromHamster(hamster)
|
131
|
+
# Categories in Hamster are mapped to Activities in EAR
|
132
|
+
|
133
|
+
hamster.timestream_by_category.each do |category, timestream|
|
134
|
+
# XXX should this been creating EAR Timestreams instead of updating rows directly?
|
135
|
+
(12 .. 36).each do |row_index|
|
136
|
+
row = self.row(row_index)
|
137
|
+
activity = row[ACTIVITY_COLUMN]
|
138
|
+
|
139
|
+
# XXX Category is assumed to be a substring of Activity since Activity is very wordy.
|
140
|
+
if activity and activity.include? category
|
141
|
+
timestream.each do |date, hours|
|
142
|
+
column = self.date_column(date)
|
143
|
+
row[column] ||= 0
|
144
|
+
row[column] += hours
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def updateFromHamster!(hamster)
|
152
|
+
updateFromHamster(hamster)
|
153
|
+
|
154
|
+
# spreadsheet library recommends not writing back to same file, write to
|
155
|
+
# temporary file and then rename that file to original.
|
156
|
+
temporary_path = "#{path}.timestream"
|
157
|
+
spreadsheet.write(temporary_path)
|
158
|
+
File.rename(temporary_path, path)
|
159
|
+
end
|
160
|
+
|
101
161
|
def worksheet
|
102
162
|
@worksheet ||= spreadsheet.worksheet "EAR - Current Week"
|
103
163
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'dbus'
|
5
|
+
require 'terminal-table/import'
|
6
|
+
require 'time'
|
7
|
+
|
8
|
+
module Timestream
|
9
|
+
class Hamster
|
10
|
+
class Fact
|
11
|
+
attr_reader :category_name
|
12
|
+
attr_reader :date
|
13
|
+
attr_reader :delta_in_seconds
|
14
|
+
|
15
|
+
def initialize(id, start_timestamp, end_timestamp, description, activity_name, activity_id, category_name, tag_list, date_timestamp, delta_in_seconds)
|
16
|
+
@id = id
|
17
|
+
|
18
|
+
# Convert start and end to Times
|
19
|
+
@start_time = Time.at(start_timestamp)
|
20
|
+
@end_time = Time.at(end_timestamp)
|
21
|
+
|
22
|
+
@description = description
|
23
|
+
@activity_name = activity_name
|
24
|
+
@activity_id = activity_id
|
25
|
+
@category_name = category_name
|
26
|
+
@tag_list = tag_list
|
27
|
+
|
28
|
+
# Convert date to a Date
|
29
|
+
date_time = Time.at(date_timestamp)
|
30
|
+
date = Date.new(date_time.year, date_time.month, date_time.day)
|
31
|
+
@date = date
|
32
|
+
|
33
|
+
@delta_in_seconds = delta_in_seconds
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def facts
|
38
|
+
dbus_facts = proxy.GetFacts(@start_time.to_i, @end_time.to_i, '')
|
39
|
+
# XXX for some reason GetFacts returns facts in a one-element array
|
40
|
+
dbus_facts = dbus_facts[0]
|
41
|
+
|
42
|
+
# convert dbus facts into instances
|
43
|
+
dbus_facts.collect { |dbus_fact|
|
44
|
+
Fact.new(*dbus_fact)
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(start_time, end_time)
|
49
|
+
@start_time = start_time
|
50
|
+
@end_time = end_time
|
51
|
+
end
|
52
|
+
|
53
|
+
def proxy
|
54
|
+
@proxy ||= begin
|
55
|
+
session_bus = DBus::SessionBus.instance
|
56
|
+
hamster_service = session_bus.service('org.gnome.Hamster')
|
57
|
+
hamster = hamster_service.object('/org/gnome/Hamster')
|
58
|
+
# interfaces and therefore methods won't work if not first introspected
|
59
|
+
hamster.introspect
|
60
|
+
# set interface so methods can be called directly on object
|
61
|
+
hamster.default_iface = 'org.gnome.Hamster'
|
62
|
+
|
63
|
+
hamster
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Timestream
|
68
|
+
def [](date)
|
69
|
+
@hours_by_date[date]
|
70
|
+
end
|
71
|
+
|
72
|
+
def []=(date, hours)
|
73
|
+
@hours_by_date[date] = hours
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize
|
77
|
+
@hours_by_date = Hash.new(0)
|
78
|
+
end
|
79
|
+
|
80
|
+
def each(&block)
|
81
|
+
@hours_by_date.sort.each(&block)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def timestream_by_category
|
86
|
+
if @timestream_by_category.nil?
|
87
|
+
@timestream_by_category = Hash.new { |hash, category|
|
88
|
+
hash[category] = Timestream.new
|
89
|
+
}
|
90
|
+
|
91
|
+
facts.each do |fact|
|
92
|
+
timestream = @timestream_by_category[fact.category_name]
|
93
|
+
# convert seconds to fractional hours
|
94
|
+
timestream[fact.date] += fact.delta_in_seconds.fdiv(60 * 60)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
@timestream_by_category
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/timestream/version.rb
CHANGED
data/timestream.gemspec
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timestream
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.3
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Luke Imhoff
|
@@ -15,11 +15,11 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-05-
|
18
|
+
date: 2011-05-16 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
name:
|
22
|
+
name: chronic
|
23
23
|
prerelease: false
|
24
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
@@ -33,7 +33,7 @@ dependencies:
|
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
|
-
name:
|
36
|
+
name: ruby-dbus
|
37
37
|
prerelease: false
|
38
38
|
requirement: &id002 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
@@ -46,6 +46,34 @@ dependencies:
|
|
46
46
|
version: "0"
|
47
47
|
type: :runtime
|
48
48
|
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: spreadsheet
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: terminal-table
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
type: :runtime
|
76
|
+
version_requirements: *id004
|
49
77
|
description: Converts Hamster report to EAR to JDE Timecard
|
50
78
|
email:
|
51
79
|
- luke@cray.com
|
@@ -62,6 +90,7 @@ files:
|
|
62
90
|
- bin/timestream
|
63
91
|
- lib/timestream.rb
|
64
92
|
- lib/timestream/employee_activity_report.rb
|
93
|
+
- lib/timestream/hamster.rb
|
65
94
|
- lib/timestream/version.rb
|
66
95
|
- timestream.gemspec
|
67
96
|
has_rdoc: true
|