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 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: []