time-sheet 0.7.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 790973ca70322810e9000108c07cc90d32fe466c1e34eb5afeb0120afb438296
4
- data.tar.gz: 1ee803b2a25bd8d30c43987bf332fbfa398043ac6a8485d8e3733eb9a46b4dc8
3
+ metadata.gz: c1f3f57a31269c965756a1eb5f00eccd8c831ab69bcd2f71cc65ca9242e54dcc
4
+ data.tar.gz: b5e121a0c379450019c7606ee994c5ea5ebdb5e5cef8afcb3f33f5fe864b8a6f
5
5
  SHA512:
6
- metadata.gz: f9eaa5dd328e9e952d394cadd895a8151f567a4ee28a7885f53287ec8b2beae8203c97f315fdc45da5d066bc81283ff2b691324c4f02f7fd8b872b296aa75d40
7
- data.tar.gz: 50da6fcc3e30a29269aa52347eba0cc320dc3efe1140222aca130179f77f99e85df2293c56eb60ba93305a35297486e9036fa5581e7bcd675801b26a24f06759
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
@@ -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[0].to_time)
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
@@ -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 directory)',
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
- (@data['tags'] || '').split(/\s*,\s*/)
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
- [date, start, self.end, duration.to_i, project, activity, description]
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
- results = []
56
- Spreadsheet.open(f).worksheets.each do |sheet|
57
- headers = sheet.rows.first.to_a
58
- sheet.rows[1..-1].each do |row|
59
- # TODO find a way to guard against xls sheets with 65535 (empty)
60
- # lines, perhaps:
61
- # break if row[1].nil?
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
- record = {}
64
- row.each_with_index do |value, i|
65
- record[headers[i]] = value
66
- end
67
- results << record
68
- end
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
@@ -1,3 +1,3 @@
1
1
  module TimeSheet
2
- VERSION = "0.7.0"
2
+ VERSION = "0.11.0"
3
3
  end
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", "~> 1.15"
31
- spec.add_development_dependency "rake", "~> 10.0"
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.7.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: 2019-07-13 00:00:00.000000000 Z
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: '1.15'
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: '1.15'
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: '10.0'
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: '10.0'
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
- rubyforge_project:
145
- rubygems_version: 2.7.6
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: []