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 +4 -4
- data/.rubocop.yml +14 -0
- data/README.md +5 -4
- data/lib/gpuzzletime.rb +11 -0
- data/lib/gpuzzletime/app.rb +27 -162
- data/lib/gpuzzletime/command/edit.rb +33 -0
- data/lib/gpuzzletime/command/show.rb +45 -0
- data/lib/gpuzzletime/command/upload.rb +67 -0
- data/lib/gpuzzletime/configuration.rb +52 -0
- data/lib/gpuzzletime/entry.rb +109 -0
- data/lib/gpuzzletime/named_date.rb +25 -0
- data/lib/gpuzzletime/script.rb +20 -0
- data/lib/gpuzzletime/timelog.rb +5 -3
- data/lib/gpuzzletime/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc1eef3b25f4f3015781febe8555a216bf698792c48156bd329b4141320afc73
|
4
|
+
data.tar.gz: 3ed5f21efea39a9ed1d22dc0f2d88e5f0f84f52354aef61ea796c622c1e1fea8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
- [
|
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
|
-
- [ ]
|
23
|
-
|
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
|
data/lib/gpuzzletime/app.rb
CHANGED
@@ -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
|
18
|
-
|
19
|
-
|
20
|
-
case
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
43
|
+
def timeload
|
67
44
|
Timelog.load
|
68
45
|
end
|
69
46
|
|
70
|
-
def fill_entries
|
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 |
|
81
|
-
|
82
|
-
|
55
|
+
lines.each do |line|
|
56
|
+
entry = Entry.from_timelog(line)
|
57
|
+
entry.start_time = start
|
83
58
|
|
84
|
-
|
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 =
|
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
|
data/lib/gpuzzletime/timelog.rb
CHANGED
@@ -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
|
-
|
12
|
+
instance.load
|
11
13
|
end
|
12
14
|
|
13
15
|
def timelog_txt
|
14
|
-
Pathname.new(
|
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
|
data/lib/gpuzzletime/version.rb
CHANGED
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.
|
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-
|
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
|