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