time-sheet 0.7.0 → 0.11.0
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.
- checksums.yaml +4 -4
- data/README.md +8 -0
- data/lib/time_sheet.rb +15 -0
- data/lib/time_sheet/time.rb +1 -1
- data/lib/time_sheet/time/cmd.rb +10 -1
- data/lib/time_sheet/time/entry.rb +36 -3
- data/lib/time_sheet/time/parser.rb +63 -14
- data/lib/time_sheet/version.rb +1 -1
- data/time_sheet.gemspec +3 -2
- metadata +26 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1f3f57a31269c965756a1eb5f00eccd8c831ab69bcd2f71cc65ca9242e54dcc
|
|
4
|
+
data.tar.gz: b5e121a0c379450019c7606ee994c5ea5ebdb5e5cef8afcb3f33f5fe864b8a6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c31004116f099381d80021a34d04c541bc13b221e0d980c9f073f28150dd8843fb837022670ff46015ddc1dd62c9d294d9847b9e4d5207d45249ca8ebf720389
|
|
7
|
+
data.tar.gz: a7f08110118e2aaacb4569e6d3835b4c40660a0561318490edef9b0023a6c33061bc2ff3c068062ce7d9bfb382926aaae44448e36715ab68988dc6b4d7aeb270
|
data/README.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
This gem allows you to parse a spreadsheet with time tracking information to
|
|
4
4
|
generate various statistics suitable for invoices and project effort estimates.
|
|
5
5
|
|
|
6
|
+
## Changelog
|
|
7
|
+
|
|
8
|
+
v0.9.0
|
|
9
|
+
|
|
10
|
+
* introduced employee data column: To use it, add a column "employee" to the
|
|
11
|
+
spreadsheets and use flag `-e` to filter by it. If employee is not set, it
|
|
12
|
+
falls back to "Me".
|
|
13
|
+
|
|
6
14
|
## Installation
|
|
7
15
|
|
|
8
16
|
```bash
|
data/lib/time_sheet.rb
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
require 'time_sheet/version'
|
|
2
2
|
|
|
3
|
+
# silence HTTPClient
|
|
4
|
+
module Warning
|
|
5
|
+
def warn(mgs)
|
|
6
|
+
# drop
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
3
10
|
if defined?(Bundler)
|
|
4
11
|
begin
|
|
5
12
|
require 'pry'
|
|
@@ -15,4 +22,12 @@ module TimeSheet
|
|
|
15
22
|
def self.root
|
|
16
23
|
@root ||= File.expand_path(File.dirname(__FILE__) + '/..')
|
|
17
24
|
end
|
|
25
|
+
|
|
26
|
+
def self.options=(options)
|
|
27
|
+
@options = options
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.options
|
|
31
|
+
@options
|
|
32
|
+
end
|
|
18
33
|
end
|
data/lib/time_sheet/time.rb
CHANGED
|
@@ -41,7 +41,7 @@ module TimeSheet::Time
|
|
|
41
41
|
unless results['entries'].empty?
|
|
42
42
|
time = (
|
|
43
43
|
(options[:to] || Util.day_end) -
|
|
44
|
-
(options[:from] || results['entries'].first[
|
|
44
|
+
(options[:from] || results['entries'].first[1].to_time)
|
|
45
45
|
).to_i
|
|
46
46
|
days = (time.to_f / 60 / 60 / 24).round
|
|
47
47
|
end
|
data/lib/time_sheet/time/cmd.rb
CHANGED
|
@@ -3,6 +3,8 @@ require 'time'
|
|
|
3
3
|
|
|
4
4
|
class TimeSheet::Time::Cmd
|
|
5
5
|
def run
|
|
6
|
+
TimeSheet.options = options
|
|
7
|
+
|
|
6
8
|
if d = options[:from]
|
|
7
9
|
if d.match(/^\d\d?-\d\d?$/)
|
|
8
10
|
d = "#{TimeSheet::Time::Util.now.year}-#{d}"
|
|
@@ -64,6 +66,10 @@ class TimeSheet::Time::Cmd
|
|
|
64
66
|
options[:to] = TimeSheet::Time::Util.month_end(-1)
|
|
65
67
|
options[:summary] = true
|
|
66
68
|
report
|
|
69
|
+
when 'year-to-day', 'year'
|
|
70
|
+
options[:from] = TimeSheet::Time::Util.year_start(-1)
|
|
71
|
+
options[:summary] = true
|
|
72
|
+
report
|
|
67
73
|
when 'overview'
|
|
68
74
|
overview
|
|
69
75
|
else
|
|
@@ -104,18 +110,21 @@ class TimeSheet::Time::Cmd
|
|
|
104
110
|
|
|
105
111
|
o.boolean '-h', '--help', 'show help'
|
|
106
112
|
o.boolean '--version', 'show the version'
|
|
107
|
-
o.array('-l', '--location', 'a location to gather data from (file or
|
|
113
|
+
o.array('-l', '--location', 'a location to gather data from (file, directory or google docs share-url)',
|
|
108
114
|
default: default_location
|
|
109
115
|
)
|
|
110
116
|
o.string '-f', '--from', 'ignore entries older than the date given'
|
|
111
117
|
o.string '-t', '--to', 'ignore entries more recent than the date given'
|
|
112
118
|
o.string '-p', '--project', 'take only entries of this project into account'
|
|
113
119
|
o.string '-a', '--activity', 'take only entries of this activity into account'
|
|
120
|
+
o.string '--tags', 'take only entries with these tags into account (comma separated, not case sensitive)'
|
|
114
121
|
o.string '-d', '--description', 'consider only entries matching this description'
|
|
122
|
+
o.string '-e', '--employee', 'consider only entries for this employee'
|
|
115
123
|
o.float '-r', '--rate', 'use an alternative hourly rate (default: 80.0)', default: 80.00
|
|
116
124
|
o.boolean '-s', '--summary', 'when reporting, add summary section'
|
|
117
125
|
o.boolean '--trim', 'compact the output for processing as CSV', default: false
|
|
118
126
|
o.boolean '-v', '--verbose', 'be more verbose'
|
|
127
|
+
o.boolean '--debug', 'drop into a REPL on errors'
|
|
119
128
|
o.separator "\n invoice options:"
|
|
120
129
|
o.integer '--package', 'for invoice output: build packages of this duration in hours', default: 0
|
|
121
130
|
o.integer '--petty', 'fold records under a certain threshold into a "misc" activity', default: 0
|
|
@@ -41,6 +41,10 @@ class TimeSheet::Time::Entry
|
|
|
41
41
|
)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def employee
|
|
45
|
+
@employee ||= @data['employee'] || (self.prev ? self.prev.employee : 'Me')
|
|
46
|
+
end
|
|
47
|
+
|
|
44
48
|
# Experiment to add timezone support. However, this would complicate every day
|
|
45
49
|
# handing because of daylight saving time changes.
|
|
46
50
|
# def start_zone
|
|
@@ -74,7 +78,8 @@ class TimeSheet::Time::Entry
|
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def tags
|
|
77
|
-
|
|
81
|
+
binding.pry if @data['tags'] == 86
|
|
82
|
+
self.class.parse_tags(@data['tags'])
|
|
78
83
|
end
|
|
79
84
|
|
|
80
85
|
def working_day?
|
|
@@ -86,7 +91,10 @@ class TimeSheet::Time::Entry
|
|
|
86
91
|
from = from.to_time if from.is_a?(Date)
|
|
87
92
|
to = (filters[:to] ? filters[:to] : nil)
|
|
88
93
|
to = (to + 1).to_time if to.is_a?(Date)
|
|
94
|
+
tags = self.class.parse_tags(filters[:tags])
|
|
89
95
|
|
|
96
|
+
has_tags?(tags) &&
|
|
97
|
+
self.class.attrib_matches_any?(employee, filters[:employee]) &&
|
|
90
98
|
self.class.attrib_matches_any?(description, filters[:description]) &&
|
|
91
99
|
self.class.attrib_matches_any?(project, filters[:project]) &&
|
|
92
100
|
self.class.attrib_matches_any?(activity, filters[:activity]) &&
|
|
@@ -94,12 +102,23 @@ class TimeSheet::Time::Entry
|
|
|
94
102
|
(!to || to >= self.end)
|
|
95
103
|
end
|
|
96
104
|
|
|
105
|
+
def has_tags?(tags)
|
|
106
|
+
return true if tags.empty?
|
|
107
|
+
|
|
108
|
+
tags.all? do |tag|
|
|
109
|
+
self.tags.include?(tag)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
97
113
|
def valid?
|
|
98
114
|
valid!
|
|
99
115
|
true
|
|
100
116
|
rescue TimeSheet::Time::Exception => e
|
|
101
117
|
self.exception = e
|
|
102
118
|
false
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
binding.pry if Timesheet.options[:debug]
|
|
121
|
+
false
|
|
103
122
|
end
|
|
104
123
|
|
|
105
124
|
def valid!
|
|
@@ -114,14 +133,22 @@ class TimeSheet::Time::Entry
|
|
|
114
133
|
if (self.start >= self.end) && self.next
|
|
115
134
|
raise TimeSheet::Time::Exception.new('time entry has no end')
|
|
116
135
|
end
|
|
136
|
+
|
|
137
|
+
if !employee
|
|
138
|
+
raise TimeSheet::Time::Exception.new('no employee set')
|
|
139
|
+
end
|
|
117
140
|
end
|
|
118
141
|
|
|
119
142
|
def to_row
|
|
120
|
-
[
|
|
143
|
+
[
|
|
144
|
+
employee, date, start, self.end, duration.to_i, project, activity,
|
|
145
|
+
description
|
|
146
|
+
]
|
|
121
147
|
end
|
|
122
148
|
|
|
123
149
|
def to_s
|
|
124
150
|
values = [
|
|
151
|
+
employee,
|
|
125
152
|
date.strftime('%Y-%m-%d'),
|
|
126
153
|
start.strftime('%H:%M'),
|
|
127
154
|
self.end.strftime('%H:%M'),
|
|
@@ -134,13 +161,15 @@ class TimeSheet::Time::Entry
|
|
|
134
161
|
|
|
135
162
|
def to_hash
|
|
136
163
|
return {
|
|
164
|
+
'employee' => employee,
|
|
137
165
|
'date' => date,
|
|
138
166
|
'start' => start,
|
|
139
167
|
'end' => self.end,
|
|
140
168
|
'duration' => duration,
|
|
141
169
|
'project' => project,
|
|
142
170
|
'activity' => activity,
|
|
143
|
-
'description' => description
|
|
171
|
+
'description' => description,
|
|
172
|
+
'tags' => tags
|
|
144
173
|
}
|
|
145
174
|
end
|
|
146
175
|
|
|
@@ -156,4 +185,8 @@ class TimeSheet::Time::Entry
|
|
|
156
185
|
end
|
|
157
186
|
end
|
|
158
187
|
|
|
188
|
+
def self.parse_tags(string)
|
|
189
|
+
(string || '').to_s.downcase.split(/\s*,\s*/).map{|t| t.strip}
|
|
190
|
+
end
|
|
191
|
+
|
|
159
192
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require 'spreadsheet'
|
|
2
|
+
require 'httpclient'
|
|
3
|
+
require 'csv'
|
|
2
4
|
|
|
3
5
|
class TimeSheet::Time::Parser
|
|
4
6
|
|
|
@@ -52,24 +54,71 @@ class TimeSheet::Time::Parser
|
|
|
52
54
|
def hashes_per_file
|
|
53
55
|
@hashes_per_file ||= begin
|
|
54
56
|
files.map do |f|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
if f.match(/https:\/\/docs\.google\.com/)
|
|
58
|
+
parse_google_doc(f)
|
|
59
|
+
else
|
|
60
|
+
parse_xls(f)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
def parse_xls(filename)
|
|
67
|
+
results = []
|
|
68
|
+
|
|
69
|
+
Spreadsheet.open(filename).worksheets.each do |sheet|
|
|
70
|
+
headers = sheet.rows.first.to_a
|
|
71
|
+
sheet.rows[1..-1].each.with_index do |row, i|
|
|
72
|
+
# TODO find a way to guard against xls sheets with 65535 (empty)
|
|
73
|
+
# lines, perhaps:
|
|
74
|
+
# break if row[1].nil?
|
|
75
|
+
next if row.all?{|cell| [nil, ''].include?(cell)}
|
|
76
|
+
|
|
77
|
+
record = {}
|
|
78
|
+
row.each_with_index do |value, i|
|
|
79
|
+
record[headers[i]] = value
|
|
69
80
|
end
|
|
70
|
-
results
|
|
81
|
+
results << record
|
|
71
82
|
end
|
|
72
83
|
end
|
|
84
|
+
|
|
85
|
+
results
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_google_doc(url)
|
|
89
|
+
# Chart Tools datasource protocol, see
|
|
90
|
+
# https://developers.google.com/chart/interactive/docs/querylanguage
|
|
91
|
+
response = HTTPClient.get(url)
|
|
92
|
+
|
|
93
|
+
if response.status == 200
|
|
94
|
+
data = CSV.parse(response.body, liberal_parsing: true)
|
|
95
|
+
headers = data.shift
|
|
96
|
+
data.map do |row|
|
|
97
|
+
record = nullify_empties(headers.zip(row).to_h)
|
|
98
|
+
parse_date_and_time(record)
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
raise "request to google docs failed (#{response.status}):\n#{response.body}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_date_and_time(record)
|
|
106
|
+
record.merge(
|
|
107
|
+
'date' => (record['date'] ? Date.parse(record['date']) : nil),
|
|
108
|
+
'start' => (record['start'] ? DateTime.parse(record['start']) : nil),
|
|
109
|
+
'end' => (record['end'] ? DateTime.parse(record['end']) : nil)
|
|
110
|
+
)
|
|
111
|
+
rescue ArgumentError => e
|
|
112
|
+
binding.pry if TimeSheet.options[:debug]
|
|
113
|
+
puts "current record: #{record.inspect}"
|
|
114
|
+
return {}
|
|
115
|
+
# raise e
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def nullify_empties(record)
|
|
119
|
+
record.transform_values do |v|
|
|
120
|
+
v == '' ? nil : v
|
|
121
|
+
end
|
|
73
122
|
end
|
|
74
123
|
|
|
75
124
|
end
|
data/lib/time_sheet/version.rb
CHANGED
data/time_sheet.gemspec
CHANGED
|
@@ -26,9 +26,10 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
|
|
27
27
|
spec.add_dependency 'spreadsheet'
|
|
28
28
|
spec.add_dependency 'slop'
|
|
29
|
+
spec.add_dependency 'httpclient'
|
|
29
30
|
|
|
30
|
-
spec.add_development_dependency "bundler", "~>
|
|
31
|
-
spec.add_development_dependency "rake", "~>
|
|
31
|
+
spec.add_development_dependency "bundler", "~> 2.2.16"
|
|
32
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
32
33
|
spec.add_development_dependency "rspec", "~> 3.0"
|
|
33
34
|
spec.add_development_dependency 'pry'
|
|
34
35
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: time-sheet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Moritz Schepp
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-06-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spreadsheet
|
|
@@ -38,34 +38,48 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: httpclient
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: bundler
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
44
58
|
requirements:
|
|
45
59
|
- - "~>"
|
|
46
60
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
61
|
+
version: 2.2.16
|
|
48
62
|
type: :development
|
|
49
63
|
prerelease: false
|
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
65
|
requirements:
|
|
52
66
|
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
68
|
+
version: 2.2.16
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
70
|
name: rake
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
58
72
|
requirements:
|
|
59
73
|
- - "~>"
|
|
60
74
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
75
|
+
version: '13.0'
|
|
62
76
|
type: :development
|
|
63
77
|
prerelease: false
|
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
79
|
requirements:
|
|
66
80
|
- - "~>"
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
82
|
+
version: '13.0'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: rspec
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,7 +108,7 @@ dependencies:
|
|
|
94
108
|
- - ">="
|
|
95
109
|
- !ruby/object:Gem::Version
|
|
96
110
|
version: '0'
|
|
97
|
-
description:
|
|
111
|
+
description:
|
|
98
112
|
email:
|
|
99
113
|
- moritz.schepp@gmail.com
|
|
100
114
|
executables:
|
|
@@ -120,13 +134,13 @@ files:
|
|
|
120
134
|
- lib/time_sheet/time/util.rb
|
|
121
135
|
- lib/time_sheet/version.rb
|
|
122
136
|
- time_sheet.gemspec
|
|
123
|
-
homepage:
|
|
137
|
+
homepage:
|
|
124
138
|
licenses:
|
|
125
139
|
- GPL-3.0-only
|
|
126
140
|
metadata:
|
|
127
141
|
bug_tracker_uri: https://github.com/moritzschepp/time-sheet/issues
|
|
128
142
|
documentation_uri: https://github.com/moritzschepp/time-sheet
|
|
129
|
-
post_install_message:
|
|
143
|
+
post_install_message:
|
|
130
144
|
rdoc_options: []
|
|
131
145
|
require_paths:
|
|
132
146
|
- lib
|
|
@@ -141,9 +155,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
141
155
|
- !ruby/object:Gem::Version
|
|
142
156
|
version: '0'
|
|
143
157
|
requirements: []
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
signing_key:
|
|
158
|
+
rubygems_version: 3.1.6
|
|
159
|
+
signing_key:
|
|
147
160
|
specification_version: 4
|
|
148
161
|
summary: a time tracking solution based on spreadsheets
|
|
149
162
|
test_files: []
|