gpuzzletime 0.4.0 → 0.4.1

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