gpuzzletime 0.4.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da04f6e2158de538ed579e152023e1577f4743eaf869148398cad63cc4602e7e
4
- data.tar.gz: 1cbf496d462a395a0466e7fe0d94ef54ef3bd59c64caccfa7c3a77e5ca013fe7
3
+ metadata.gz: fc1eef3b25f4f3015781febe8555a216bf698792c48156bd329b4141320afc73
4
+ data.tar.gz: 3ed5f21efea39a9ed1d22dc0f2d88e5f0f84f52354aef61ea796c622c1e1fea8
5
5
  SHA512:
6
- metadata.gz: e35352a95c6bc0bda0ce852f7b95c9681ca508d094239b754d5081fdb625c0d8e0eafd50983206b70ef2fbd7656164f3965d4d730612eec786c6dd1fe580ae4d
7
- data.tar.gz: 8ca1ea92a1036ce3f9a791d2d565728dcc7ecd2afe510cb04f60579e57d0ce3eaf78d7e5c7fb9f3208ec58cbac01c8117e5b3361befc8b4c2b8599869d14d860
6
+ metadata.gz: 23645f4ee698fc07c15037cf42a90675239d562be0ba88335215877db47a28942886e5f2d3db0cb1e30837636f4b11cf3d2be1ff17405fc6470def9c52f736f6
7
+ data.tar.gz: bec498910a2e0ff180fc3bba9b7a226ffcf1f7244a770c9b61186196d1f3b0c742fba92875bab91816e59ff1b08c86ed267a1b38a17fed44f1ba28e0c2b43f43
data/.rubocop.yml CHANGED
@@ -17,3 +17,17 @@ Layout/AlignHash:
17
17
  EnforcedHashRocketStyle: table
18
18
  EnforcedColonStyle: table
19
19
  EnforcedLastArgumentHashStyle: always_inspect # default
20
+
21
+ Naming/UncommunicativeMethodParamName:
22
+ AllowedNames:
23
+ - fn
24
+ # default allowed names
25
+ - io
26
+ - id
27
+ - to
28
+ - by
29
+ - on
30
+ - in
31
+ - at
32
+ - ip
33
+ - db
data/README.md CHANGED
@@ -6,7 +6,7 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
6
6
 
7
7
  - [x] read timelog.txt
8
8
  - [x] from known location
9
- - [ ] later: configure location?
9
+ - [x] later: configure location?
10
10
  - [ ] later: auto-detect location?
11
11
  - [x] parse out last day
12
12
  - [x] especially start/end-times for each entry
@@ -19,8 +19,9 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
19
19
  - [x] get ticket-parser from tags
20
20
  - [ ] merge equal adjacent entries into one
21
21
  - [ ] complete login/entry automation
22
- - [ ] login
23
- - [ ] store cookie
22
+ - [ ] handle authentication
23
+ - [ ] login and store cookie
24
+ - [ ] send user and pwd with every request
24
25
  - [ ] make entries
25
26
  - [ ] open day in browser for review
26
27
  - [ ] avoid duplicate entries
@@ -37,7 +38,7 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
37
38
  - [ ] allow to have a list of "favourite" time-accounts
38
39
  - [ ] select best-matching time-account according to tags, possibly limited to the favourites
39
40
  - [ ] add cli-help
40
-
41
+ - [ ] use commander for CLI?
41
42
 
42
43
  ## Installation
43
44
 
data/lib/gpuzzletime.rb CHANGED
@@ -5,6 +5,17 @@ $LOAD_PATH.unshift File.dirname(__FILE__)
5
5
  # Autoloading and such
6
6
  module Gpuzzletime
7
7
  autoload :App, 'gpuzzletime/app'
8
+ autoload :Configuration, 'gpuzzletime/configuration'
9
+ autoload :Entry, 'gpuzzletime/entry'
10
+ autoload :NamedDate, 'gpuzzletime/named_date'
11
+ autoload :Script, 'gpuzzletime/script'
8
12
  autoload :Timelog, 'gpuzzletime/timelog'
9
13
  autoload :VERSION, 'gpuzzletime/version'
14
+
15
+ # Collection of commands available at the CLI
16
+ module Command
17
+ autoload :Edit, 'gpuzzletime/command/edit'
18
+ autoload :Show, 'gpuzzletime/command/show'
19
+ autoload :Upload, 'gpuzzletime/command/upload'
20
+ end
10
21
  end
@@ -7,195 +7,60 @@ require 'pathname'
7
7
  module Gpuzzletime
8
8
  # Wrapper for everything
9
9
  class App
10
- CONFIGURATION_DEFAULTS = {
11
- base_url: 'https://time.puzzle.ch',
12
- rounding: 15,
13
- dir: Pathname.new('~/.config/gpuzzletime').expand_path,
14
- }.freeze
15
-
16
10
  def initialize(args)
17
- @config = load_config(CONFIGURATION_DEFAULTS[:dir].join('config'))
18
- @command = (args[0] || :show).to_sym
19
-
20
- case @command
21
- when :show, :upload
22
- @date = named_dates(args[1] || 'last') || :all
23
- when :edit
24
- @file = args[1]
25
- else
26
- raise ArgumentError, "Unsupported Command #{@command}"
27
- end
11
+ @config = Configuration.instance
12
+ command = (args[0] || :show).to_sym
13
+
14
+ @command = case command
15
+ when :show
16
+ @date = NamedDate.new.date(args[1])
17
+ Gpuzzletime::Command::Show.new(@config)
18
+ when :upload
19
+ @date = NamedDate.new.date(args[1])
20
+ Gpuzzletime::Command::Upload.new(@config)
21
+ when :edit
22
+ Gpuzzletime::Command::Edit.new(@config, args[1])
23
+ else
24
+ raise ArgumentError, "Unsupported Command #{@command}"
25
+ end
28
26
  end
29
27
 
30
28
  def run
31
- case @command
32
- when :show
33
- fill_entries(@command)
34
- entries.each do |date, entries|
35
- puts date, '----------'
36
- entries.each do |entry|
37
- puts entry
38
- end
39
- puts nil
40
- end
41
- when :upload
42
- fill_entries(@command)
43
- entries.each do |date, entries|
44
- puts "Uploading #{date}"
45
- entries.each do |start, entry|
46
- open_browser(start, entry)
47
- end
48
- end
49
- when :edit
50
- launch_editor
29
+ if @command.needs_entries?
30
+ fill_entries
31
+ @command.entries = entries
51
32
  end
33
+
34
+ @command.run
52
35
  end
53
36
 
54
37
  private
55
38
 
56
- def load_config(config_fn)
57
- user_config = config_fn.exist? ? YAML.load_file(config_fn) : {}
58
-
59
- CONFIGURATION_DEFAULTS.merge(user_config)
60
- end
61
-
62
39
  def entries
63
40
  @entries ||= {}
64
41
  end
65
42
 
66
- def timelog
43
+ def timeload
67
44
  Timelog.load
68
45
  end
69
46
 
70
- def fill_entries(purpose)
47
+ def fill_entries
71
48
  timelog.each do |date, lines|
72
- # this is mixing preparation, assembly and output, but gets the job done
73
49
  next unless date # guard against the machine
74
50
  next unless @date == :all || @date == date # limit to one day if passed
75
51
 
76
52
  entries[date] = []
77
-
78
53
  start = nil # at the start of the day, we have no previous end
79
54
 
80
- lines.each do |entry|
81
- finish = round_time(entry[:time], @config[:rounding]) # we use that twice
82
- hidden = entry[:description].match(/\*\*$/) # hide lunch and breaks
55
+ lines.each do |line|
56
+ entry = Entry.from_timelog(line)
57
+ entry.start_time = start
83
58
 
84
- if start && !hidden
85
- case purpose # assemble data according to command
86
- when :show
87
- entries[date] << [
88
- start, '-', finish,
89
- [
90
- entry[:ticket],
91
- entry[:description],
92
- entry[:tags],
93
- infer_account(entry),
94
- ].compact.join(' ∴ '),
95
- ].compact.join(' ')
96
- when :upload
97
- entries[date] << [start, entry]
98
- end
99
- end
59
+ entries[date] << entry if entry.valid?
100
60
 
101
- start = finish # store previous ending for nice display of next entry
61
+ start = entry.finish_time # store previous ending for nice display of next entry
102
62
  end
103
63
  end
104
64
  end
105
-
106
- def round_time(time, interval)
107
- return time unless interval
108
-
109
- hour, minute = time.split(':')
110
- minute = (minute.to_i / interval.to_f).round * interval.to_i
111
-
112
- if minute == 60
113
- [hour.succ, 0]
114
- else
115
- [hour, minute]
116
- end.map { |part| part.to_s.rjust(2, '0') }.join(':')
117
- end
118
-
119
- def open_browser(start, entry)
120
- xdg_open "'#{@config[:base_url]}/ordertimes/new?#{url_options(start, entry)}'", silent: true
121
- end
122
-
123
- def xdg_open(args, silent: false)
124
- opener = 'xdg-open' # could be configurable, but is already a proxy
125
- silencer = '> /dev/null 2> /dev/null'
126
-
127
- if system("which #{opener} #{silencer}")
128
- system "#{opener} #{args} #{silencer if silent}"
129
- else
130
- abort <<~ERRORMESSAGE
131
- #{opener} not found
132
-
133
- This binary is needed to launch a webbrowser and open the page
134
- to enter the worktime-entry into puzzletime.
135
-
136
- If this needs to be configurable, please open an issue at
137
- https://github.com/kronn/gpuzzletime/issues/new
138
- ERRORMESSAGE
139
- end
140
- end
141
-
142
- def launch_editor
143
- editor = `which $EDITOR`.chomp
144
-
145
- file = @file.nil? ? Timelog.timelog_txt : parser_file(@file)
146
-
147
- exec "#{editor} #{file}"
148
- end
149
-
150
- def url_options(start, entry)
151
- account = infer_account(entry)
152
- {
153
- work_date: entry[:date],
154
- 'ordertime[ticket]': entry[:ticket],
155
- 'ordertime[description]': entry[:description],
156
- 'ordertime[from_start_time]': start,
157
- 'ordertime[to_end_time]': round_time(entry[:time], @config[:rounding]),
158
- 'ordertime[account_id]': account,
159
- 'ordertime[billable]': infer_billable(account),
160
- }
161
- .map { |key, value| [key, ERB::Util.url_encode(value)].join('=') }
162
- .join('&')
163
- end
164
-
165
- def named_dates(date)
166
- case date
167
- when 'yesterday' then Date.today.prev_day.to_s
168
- when 'today' then Date.today.to_s
169
- when 'last' then timelog.to_h.keys.compact.sort[-2] || Date.today.prev_day.to_s
170
- when /\d{4}(-\d{2}){2}/ then date
171
- end
172
- end
173
-
174
- def parser_file(parser_name)
175
- @config[:dir].join("parsers/#{parser_name}") # FIXME: security-hole, prevent relative paths!
176
- .expand_path
177
- end
178
-
179
- def infer_account(entry)
180
- return unless entry[:tags]
181
-
182
- tags = entry[:tags].split
183
- parser_name = tags.shift
184
-
185
- parser = parser_file(parser_name)
186
-
187
- return unless parser.exist?
188
-
189
- cmd = %(#{parser} "#{entry[:ticket]}" "#{entry[:description]}" #{tags.map(&:inspect).join(' ')})
190
- `#{cmd}`.chomp # maybe only execute if parser is in correct dir?
191
- end
192
-
193
- def infer_billable(account)
194
- script = @config[:dir].join('billable')
195
-
196
- return 1 unless script.exist?
197
-
198
- `#{script} #{account}`.chomp == 'true' ? 1 : 0
199
- end
200
65
  end
201
66
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gpuzzletime
4
+ module Command
5
+ # edit one file. without argument, it will edit the timelog, otherwise a
6
+ # parser-script is loaded
7
+ class Edit
8
+ def initalize(config, file)
9
+ @config = config
10
+ @script = Script.new(@config[:dir])
11
+ @file = file
12
+ end
13
+
14
+ def needs_entries?
15
+ false
16
+ end
17
+
18
+ def run
19
+ launch_editor(@file)
20
+ end
21
+
22
+ private
23
+
24
+ def launch_editor(file)
25
+ editor = `which $EDITOR`.chomp
26
+
27
+ file = file.nil? ? Timelog.timelog_txt : @script.parser(@file)
28
+
29
+ exec "#{editor} #{file}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gpuzzletime
4
+ module Command
5
+ # show entries of one day or all of them
6
+ class Show
7
+ def initialize(config)
8
+ @config = config
9
+ @entries = {}
10
+ end
11
+
12
+ def needs_entries?
13
+ true
14
+ end
15
+
16
+ def run
17
+ @entries.each do |date, list|
18
+ puts date, '----------'
19
+ list.each do |entry|
20
+ puts entry
21
+ end
22
+ puts nil
23
+ end
24
+ end
25
+
26
+ def entries=(entries)
27
+ entries.each do |date, list|
28
+ @entries[date] = []
29
+
30
+ list.each do |entry|
31
+ @entries[date] << [
32
+ entry.start_time, '-', entry.finish_time,
33
+ [
34
+ entry.ticket,
35
+ entry.description,
36
+ entry.tags,
37
+ entry.account,
38
+ ].compact.join(' ∴ '),
39
+ ].compact.join(' ')
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gpuzzletime
4
+ module Command
5
+ # Upload entries to puzzletime
6
+ class Upload
7
+ attr_writer :entries
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @entries = {}
12
+ end
13
+
14
+ def needs_entries?
15
+ true
16
+ end
17
+
18
+ def run
19
+ @entries.each do |date, list|
20
+ puts "Uploading #{date}"
21
+ list.each do |entry|
22
+ open_browser(entry)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def open_browser(entry)
30
+ xdg_open "'#{@config[:base_url]}/ordertimes/new?#{url_options(entry)}'", silent: true
31
+ end
32
+
33
+ def xdg_open(args, silent: false)
34
+ opener = 'xdg-open' # could be configurable, but is already a proxy
35
+ silencer = '> /dev/null 2> /dev/null'
36
+
37
+ if system("which #{opener} #{silencer}")
38
+ system "#{opener} #{args} #{silencer if silent}"
39
+ else
40
+ abort <<~ERRORMESSAGE
41
+ #{opener} not found
42
+
43
+ This binary is needed to launch a webbrowser and open the page
44
+ to enter the worktime-entry into puzzletime.
45
+
46
+ If this needs to be configurable, please open an issue at
47
+ https://github.com/kronn/gpuzzletime/issues/new
48
+ ERRORMESSAGE
49
+ end
50
+ end
51
+
52
+ def url_options(entry)
53
+ {
54
+ work_date: entry.date,
55
+ 'ordertime[ticket]': entry.ticket,
56
+ 'ordertime[description]': entry.description,
57
+ 'ordertime[from_start_time]': entry.start_time,
58
+ 'ordertime[to_end_time]': entry.finish_time,
59
+ 'ordertime[account_id]': entry.account,
60
+ 'ordertime[billable]': entry.billable,
61
+ }
62
+ .map { |key, value| [key, ERB::Util.url_encode(value)].join('=') }
63
+ .join('&')
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gpuzzletime
4
+ # Wrapper around configuration-options and -loading
5
+ class Configuration
6
+ include Singleton
7
+
8
+ CONFIGURATION_DEFAULTS = {
9
+ base_url: 'https://time.puzzle.ch',
10
+ rounding: 15,
11
+ dir: '~/.config/gpuzzletime',
12
+ timelog: '~/.local/share/gtimelog/timelog.txt',
13
+ }.freeze
14
+
15
+ def initialize
16
+ reset
17
+ end
18
+
19
+ def reset
20
+ @config = load_config(
21
+ Pathname.new(CONFIGURATION_DEFAULTS[:dir]).join('config')
22
+ )
23
+ wrap_with_pathname(:dir)
24
+ wrap_with_pathname(:timelog)
25
+ end
26
+
27
+ def load_config(fn)
28
+ user_config = fn.exist? ? YAML.load_file(fn) : {}
29
+
30
+ CONFIGURATION_DEFAULTS.merge(user_config)
31
+ end
32
+
33
+ def [](key)
34
+ @config[key.to_sym]
35
+ end
36
+
37
+ def []=(key, value)
38
+ @config[key.to_sym] = value
39
+
40
+ wrap_with_pathname(key.to_sym) if %w[dir timelog].include?(key.to_s)
41
+ end
42
+
43
+ private
44
+
45
+ def wrap_with_pathname(key)
46
+ return unless @config.key?(key)
47
+ return @config[key] if @config[key].is_a? Pathname
48
+
49
+ @config[key] = Pathname.new(@config[key]).expand_path
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gpuzzletime
4
+ # Dataclass to wrap an entry
5
+ class Entry
6
+ # allow to read everything
7
+ attr_reader :date, :start_time, :finish_time, :ticket, :description,
8
+ :tags, :billable, :account
9
+
10
+ # define only trivial writers, omit special and derived values
11
+ attr_writer :date, :start_time, :ticket, :description
12
+
13
+ def initialize(config = Configuration.instance)
14
+ @config = config
15
+ @script = Script.new(@config[:dir])
16
+ end
17
+
18
+ class << self
19
+ def from_timelog(matched_line)
20
+ entry = new
21
+ entry.from_timelog(matched_line)
22
+ entry
23
+ end
24
+ end
25
+
26
+ def from_timelog(matched_line)
27
+ self.date = matched_line[:date]
28
+ self.ticket = matched_line[:ticket]
29
+ self.description = matched_line[:description]
30
+ self.finish_time = matched_line[:time]
31
+ self.tags = matched_line[:tags]
32
+
33
+ infer_ptime_settings
34
+ end
35
+
36
+ def finish_time=(time)
37
+ @finish_time = round_time(time, @config[:rounding])
38
+ end
39
+
40
+ def tags=(tags)
41
+ return unless tags
42
+
43
+ @tags = tags.split
44
+ end
45
+
46
+ def valid?
47
+ @start_time && !hidden?
48
+ end
49
+
50
+ def hidden?
51
+ @description.match(/\*\*$/) # hide lunch and breaks
52
+ end
53
+
54
+ def infer_ptime_settings
55
+ @account = infer_account
56
+ @billable = infer_billable
57
+ end
58
+
59
+ def to_s
60
+ [
61
+ @start_time, '-', @finish_time,
62
+ [
63
+ @ticket,
64
+ @description,
65
+ @tags,
66
+ @account,
67
+ ].compact.join(' : '),
68
+ ].compact.join(' ')
69
+ end
70
+
71
+ # make sortable/def <=>
72
+ # duration if start and finish is set
73
+
74
+ private
75
+
76
+ def round_time(time, interval)
77
+ return time unless interval
78
+
79
+ hour, minute = time.split(':')
80
+ minute = (minute.to_i / interval.to_f).round * interval.to_i
81
+
82
+ if minute == 60
83
+ [hour.succ, 0]
84
+ else
85
+ [hour, minute]
86
+ end.map { |part| part.to_s.rjust(2, '0') }.join(':')
87
+ end
88
+
89
+ def infer_account
90
+ return unless @tags
91
+
92
+ parser_name = tags.shift
93
+ parser = @script.parser(parser_name)
94
+
95
+ return unless parser.exist?
96
+
97
+ cmd = %(#{parser} "#{@ticket}" "#{@description}" #{tags.map(&:inspect).join(' ')})
98
+ `#{cmd}`.chomp # maybe only execute if parser is in correct dir?
99
+ end
100
+
101
+ def infer_billable
102
+ script = @script.billable
103
+
104
+ return 1 unless script.exist?
105
+
106
+ `#{script} #{@account}`.chomp == 'true' ? 1 : 0
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gpuzzletime
4
+ # Mapping between semantic/relative names and absolute dates
5
+ class NamedDate
6
+ def date(arg = 'last')
7
+ named_date(arg) || :all
8
+ end
9
+
10
+ def named_date(date)
11
+ case date
12
+ when 'yesterday' then Date.today.prev_day.to_s
13
+ when 'today' then Date.today.to_s
14
+ when 'last' then timelog.to_h.keys.compact.sort[-2] || Date.today.prev_day.to_s
15
+ when /\d{4}(-\d{2}){2}/ then date
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def timelog
22
+ Timelog.load
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gpuzzletime
4
+ # Wrapper around all external scripts that might be called to get more
5
+ # information about the time-entries
6
+ class Script
7
+ def initialize(config_dir)
8
+ @config_dir = config_dir
9
+ end
10
+
11
+ def parser(parser_name)
12
+ @config_dir.join("parsers/#{parser_name}") # FIXME: security-hole, prevent relative paths!
13
+ .expand_path
14
+ end
15
+
16
+ def billable
17
+ @config_dir.join('billable').expand_path
18
+ end
19
+ end
20
+ end
@@ -5,18 +5,20 @@ require 'pathname'
5
5
  module Gpuzzletime
6
6
  # Load and tokenize the data from gtimelog
7
7
  class Timelog
8
+ include Singleton
9
+
8
10
  class << self
9
11
  def load
10
- new.load
12
+ instance.load
11
13
  end
12
14
 
13
15
  def timelog_txt
14
- Pathname.new('~/.local/share/gtimelog/timelog.txt').expand_path
16
+ Pathname.new(Configuration.instance[:timelog]).expand_path
15
17
  end
16
18
  end
17
19
 
18
20
  def load
19
- parse(read)
21
+ @load ||= parse(read)
20
22
  end
21
23
 
22
24
  def timelog_txt
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Version
4
4
  module Gpuzzletime
5
- VERSION = '0.4.0'
5
+ VERSION = '0.4.1'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gpuzzletime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Viehweger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-09 00:00:00.000000000 Z
11
+ date: 2019-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -134,6 +134,13 @@ files:
134
134
  - gpuzzletime.gemspec
135
135
  - lib/gpuzzletime.rb
136
136
  - lib/gpuzzletime/app.rb
137
+ - lib/gpuzzletime/command/edit.rb
138
+ - lib/gpuzzletime/command/show.rb
139
+ - lib/gpuzzletime/command/upload.rb
140
+ - lib/gpuzzletime/configuration.rb
141
+ - lib/gpuzzletime/entry.rb
142
+ - lib/gpuzzletime/named_date.rb
143
+ - lib/gpuzzletime/script.rb
137
144
  - lib/gpuzzletime/timelog.rb
138
145
  - lib/gpuzzletime/version.rb
139
146
  homepage: https://github.com/kronn/gpuzzletime