timr 0.3.0 → 0.4.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 +4 -4
- data/.ackrc +9 -0
- data/.editorconfig +1 -0
- data/.env.example +7 -0
- data/.github/CONTRIBUTING.md +32 -0
- data/.github/ISSUE_TEMPLATE.md +13 -0
- data/.gitignore +8 -2
- data/.rdoc_options +21 -0
- data/.travis.yml +10 -7
- data/Gemfile +8 -0
- data/README.md +216 -3
- data/bin/.gitignore +2 -0
- data/bin/README.md +17 -0
- data/bin/build.sh +14 -0
- data/bin/build_api.sh +14 -0
- data/bin/build_coverage.sh +23 -0
- data/bin/build_info.sh +27 -0
- data/bin/build_man.sh +41 -0
- data/bin/clean.sh +14 -0
- data/bin/dev_setup.sh +19 -0
- data/bin/install.sh +49 -0
- data/bin/publish +38 -0
- data/bin/release.sh +35 -0
- data/bin/test.sh +19 -0
- data/bin/timr +20 -40
- data/bin/timr_bash_completion.sh +337 -0
- data/bin/uninstall.sh +24 -0
- data/lib/timr.rb +36 -8
- data/lib/timr/command/basic_command.rb +170 -0
- data/lib/timr/command/continue_command.rb +86 -0
- data/lib/timr/command/help_command.rb +137 -0
- data/lib/timr/command/log_command.rb +297 -0
- data/lib/timr/command/pause_command.rb +89 -0
- data/lib/timr/command/pop_command.rb +176 -0
- data/lib/timr/command/push_command.rb +141 -0
- data/lib/timr/command/report_command.rb +689 -0
- data/lib/timr/command/start_command.rb +172 -0
- data/lib/timr/command/status_command.rb +198 -0
- data/lib/timr/command/stop_command.rb +127 -0
- data/lib/timr/command/task_command.rb +318 -0
- data/lib/timr/command/track_command.rb +381 -0
- data/lib/timr/command/version_command.rb +18 -0
- data/lib/timr/duration.rb +159 -0
- data/lib/timr/exception/timr_error.rb +113 -0
- data/lib/timr/ext/time.rb +12 -0
- data/lib/timr/helper/datetime_helper.rb +128 -0
- data/lib/timr/helper/terminal_helper.rb +58 -0
- data/lib/timr/helper/translation_helper.rb +45 -0
- data/lib/timr/model/basic_model.rb +287 -0
- data/lib/timr/model/config.rb +48 -0
- data/lib/timr/model/foreign_id_db.rb +84 -0
- data/lib/timr/model/stack.rb +161 -0
- data/lib/timr/model/task.rb +1039 -0
- data/lib/timr/model/track.rb +589 -0
- data/lib/timr/progressbar.rb +41 -0
- data/lib/timr/simple_opt_parser.rb +230 -0
- data/lib/timr/status.rb +70 -0
- data/lib/timr/table.rb +88 -0
- data/lib/timr/timr.rb +500 -558
- data/lib/timr/version.rb +4 -15
- data/man/.gitignore +2 -0
- data/man/_footer +3 -0
- data/man/timr-continue.1 +48 -0
- data/man/timr-continue.1.ronn +39 -0
- data/man/timr-ftime.7 +77 -0
- data/man/timr-ftime.7.ronn +57 -0
- data/man/timr-log.1 +109 -0
- data/man/timr-log.1.ronn +87 -0
- data/man/timr-pause.1 +56 -0
- data/man/timr-pause.1.ronn +45 -0
- data/man/timr-pop.1 +66 -0
- data/man/timr-pop.1.ronn +53 -0
- data/man/timr-push.1 +25 -0
- data/man/timr-push.1.ronn +20 -0
- data/man/timr-report.1 +228 -0
- data/man/timr-report.1.ronn +193 -0
- data/man/timr-start.1 +100 -0
- data/man/timr-start.1.ronn +82 -0
- data/man/timr-status.1 +53 -0
- data/man/timr-status.1.ronn +42 -0
- data/man/timr-stop.1 +75 -0
- data/man/timr-stop.1.ronn +60 -0
- data/man/timr-task.1 +147 -0
- data/man/timr-task.1.ronn +115 -0
- data/man/timr-track.1 +109 -0
- data/man/timr-track.1.ronn +89 -0
- data/man/timr.1 +119 -0
- data/man/timr.1.ronn +68 -0
- data/timr.gemspec +18 -3
- data/timr.sublime-project +20 -1
- metadata +142 -23
- data/Makefile +0 -12
- data/Makefile.common +0 -56
- data/lib/timr/stack.rb +0 -81
- data/lib/timr/task.rb +0 -258
- data/lib/timr/track.rb +0 -167
- data/lib/timr/window.rb +0 -259
- data/lib/timr/window_help.rb +0 -41
- data/lib/timr/window_tasks.rb +0 -30
- data/lib/timr/window_test.rb +0 -20
- data/lib/timr/window_timeline.rb +0 -33
- data/tests/tc_stack.rb +0 -121
- data/tests/tc_task.rb +0 -190
- data/tests/tc_track.rb +0 -144
- data/tests/tc_window.rb +0 -428
- data/tests/ts_all.rb +0 -6
@@ -0,0 +1,161 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
module Model
|
5
|
+
|
6
|
+
# The Stack holds one or more [Tracks](rdoc-ref:TheFox::Timr::Model::Track). Only one Track can run at a time.
|
7
|
+
#
|
8
|
+
# When you push a new Track on the Stack the underlying running will be paused.
|
9
|
+
#
|
10
|
+
# Do not call Stack methods from extern. Only the Timr class is responsible to call Stack methods.
|
11
|
+
class Stack < BasicModel
|
12
|
+
|
13
|
+
include TheFox::Timr::Helper
|
14
|
+
include TheFox::Timr::Error
|
15
|
+
|
16
|
+
# Timr instance.
|
17
|
+
attr_accessor :timr
|
18
|
+
|
19
|
+
# Holds all Tracks.
|
20
|
+
attr_reader :tracks
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
super()
|
24
|
+
|
25
|
+
@timr = nil
|
26
|
+
|
27
|
+
# Data
|
28
|
+
@tracks = Array.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the current Track (Top Track).
|
32
|
+
def current_track
|
33
|
+
@tracks.last
|
34
|
+
end
|
35
|
+
|
36
|
+
# Start a Track.
|
37
|
+
def start(track)
|
38
|
+
unless track.is_a?(Track)
|
39
|
+
raise StackError, "track variable must be a Track instance. #{track.class} given."
|
40
|
+
end
|
41
|
+
|
42
|
+
stop
|
43
|
+
|
44
|
+
@tracks = Array.new
|
45
|
+
@tracks << track
|
46
|
+
|
47
|
+
# Mark Stack as changed.
|
48
|
+
changed
|
49
|
+
end
|
50
|
+
|
51
|
+
# Stop current running Track.
|
52
|
+
def stop
|
53
|
+
if @tracks.count > 0
|
54
|
+
@tracks.pop
|
55
|
+
|
56
|
+
# Mark Stack as changed.
|
57
|
+
changed
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Push a Track.
|
62
|
+
def push(track)
|
63
|
+
unless track.is_a?(Track)
|
64
|
+
raise StackError, "track variable must be a Track instance. #{track.class} given."
|
65
|
+
end
|
66
|
+
|
67
|
+
@tracks << track
|
68
|
+
|
69
|
+
# Mark Stack as changed.
|
70
|
+
changed
|
71
|
+
end
|
72
|
+
|
73
|
+
# Remove a Track.
|
74
|
+
def remove(track)
|
75
|
+
unless track.is_a?(Track)
|
76
|
+
raise StackError, "track variable must be a Track instance. #{track.class} given."
|
77
|
+
end
|
78
|
+
|
79
|
+
@tracks.delete(track)
|
80
|
+
|
81
|
+
# Mark Stack as changed.
|
82
|
+
changed
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check Track on Stack.
|
86
|
+
def on_stack?(track)
|
87
|
+
unless track.is_a?(Track)
|
88
|
+
raise StackError, "track variable must be a Track instance. #{track.class} given."
|
89
|
+
end
|
90
|
+
|
91
|
+
@tracks.include?(track)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Append a Track.
|
95
|
+
def <<(track)
|
96
|
+
@tracks << track
|
97
|
+
end
|
98
|
+
|
99
|
+
# To String
|
100
|
+
def to_s
|
101
|
+
tracks_s = TranslationHelper.pluralize(@tracks.count, 'track', 'tracks')
|
102
|
+
'Stack: %s' % [tracks_s]
|
103
|
+
end
|
104
|
+
|
105
|
+
def inspect
|
106
|
+
"#<Stack tracks=#{@tracks.count} current=#{@current_track.short_id}>"
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# BasicModel Hook
|
112
|
+
def pre_save_to_file
|
113
|
+
# Tracks
|
114
|
+
@data = @tracks.map{ |track| [track.task.id, track.id] }
|
115
|
+
|
116
|
+
super()
|
117
|
+
end
|
118
|
+
|
119
|
+
# BasicModel Hook
|
120
|
+
def post_load_from_file
|
121
|
+
unless @timr
|
122
|
+
raise StackError, 'Stack: @timr variable is not set.'
|
123
|
+
end
|
124
|
+
|
125
|
+
@tracks = @data.map{ |ids|
|
126
|
+
task_id, track_id = ids
|
127
|
+
|
128
|
+
begin
|
129
|
+
task = @timr.get_task_by_id(task_id)
|
130
|
+
if task
|
131
|
+
track = task.find_track_by_id(track_id)
|
132
|
+
|
133
|
+
if track.nil?
|
134
|
+
# Task file was found but no Track with ID from Stack.
|
135
|
+
|
136
|
+
# Mark Stack as changed.
|
137
|
+
changed
|
138
|
+
|
139
|
+
nil
|
140
|
+
else
|
141
|
+
track
|
142
|
+
end
|
143
|
+
end
|
144
|
+
rescue TimrError
|
145
|
+
# Task file for ID from Stack was not found.
|
146
|
+
|
147
|
+
# Mark Stack as changed.
|
148
|
+
changed
|
149
|
+
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
}.select{ |track|
|
153
|
+
!track.nil?
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
end # class Task
|
158
|
+
|
159
|
+
end # module Model
|
160
|
+
end # module Timr
|
161
|
+
end #module TheFox
|
@@ -0,0 +1,1039 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
module Model
|
5
|
+
|
6
|
+
class Task < BasicModel
|
7
|
+
|
8
|
+
include TheFox::Timr::Error
|
9
|
+
|
10
|
+
attr_reader :foreign_id
|
11
|
+
attr_reader :description
|
12
|
+
attr_reader :current_track
|
13
|
+
attr_reader :hourly_rate
|
14
|
+
attr_reader :has_flat_rate
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
super()
|
18
|
+
|
19
|
+
# Meta
|
20
|
+
@foreign_id = nil # --id
|
21
|
+
@name = nil
|
22
|
+
@description = nil
|
23
|
+
@current_track = nil
|
24
|
+
@estimation = nil
|
25
|
+
@hourly_rate = nil
|
26
|
+
@has_flat_rate = false
|
27
|
+
|
28
|
+
# Data
|
29
|
+
@tracks = Hash.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def foreign_id=(foreign_id)
|
33
|
+
@foreign_id = foreign_id
|
34
|
+
|
35
|
+
# Mark Task as changed.
|
36
|
+
changed
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set name.
|
40
|
+
def name=(name)
|
41
|
+
@name = name
|
42
|
+
|
43
|
+
# Mark Task as changed.
|
44
|
+
changed
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get name.
|
48
|
+
def name(max_length = nil)
|
49
|
+
name = @name
|
50
|
+
if name && max_length && name.length > max_length + 2
|
51
|
+
name = name[0, max_length] << '...'
|
52
|
+
end
|
53
|
+
name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get name or `---` if name is not set.
|
57
|
+
def name_s(max_length = nil)
|
58
|
+
s = name(max_length)
|
59
|
+
if s.nil?
|
60
|
+
'---'
|
61
|
+
else
|
62
|
+
s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Set description.
|
67
|
+
def description=(description)
|
68
|
+
@description = description
|
69
|
+
|
70
|
+
# Mark Task as changed.
|
71
|
+
changed
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add a Track.
|
75
|
+
def add_track(track, set_as_current_track = false)
|
76
|
+
track.task = self
|
77
|
+
|
78
|
+
@tracks[track.id] = track
|
79
|
+
|
80
|
+
if set_as_current_track
|
81
|
+
@current_track = track
|
82
|
+
end
|
83
|
+
|
84
|
+
# Mark Task as changed.
|
85
|
+
changed
|
86
|
+
end
|
87
|
+
|
88
|
+
# Remove a Track.
|
89
|
+
def remove_track(track)
|
90
|
+
track.task = nil
|
91
|
+
|
92
|
+
if @tracks.delete(track.id)
|
93
|
+
# Mark Task as changed.
|
94
|
+
changed
|
95
|
+
else
|
96
|
+
# Track is not assiged to this Task.
|
97
|
+
false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Move a Track to another Task.
|
102
|
+
def move_track(track, target_task)
|
103
|
+
if eql?(target_task)
|
104
|
+
return false
|
105
|
+
end
|
106
|
+
|
107
|
+
unless remove_track(track)
|
108
|
+
return false
|
109
|
+
end
|
110
|
+
|
111
|
+
set_as_current_track = false
|
112
|
+
if @current_track && @current_track.eql?(track)
|
113
|
+
@current_track = nil
|
114
|
+
set_as_current_track = true
|
115
|
+
end
|
116
|
+
|
117
|
+
target_task.add_track(track, set_as_current_track)
|
118
|
+
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
# Select Track by Time Range and/or Status.
|
123
|
+
#
|
124
|
+
# Options:
|
125
|
+
#
|
126
|
+
# - `:from`, `:to` limit the begin and end datetimes to a specific range.
|
127
|
+
# - `:status` filter Tracks by Short Status.
|
128
|
+
# - `:billed` filter Tracks by is_billed flag.
|
129
|
+
# - `true` filter billed Tracks.
|
130
|
+
# - `false` filter unbilled Tracks.
|
131
|
+
# - `nil` filter off.
|
132
|
+
#
|
133
|
+
# Fixed Start and End (`from != nil && to != nil`)
|
134
|
+
#
|
135
|
+
# ```
|
136
|
+
# Selected Range |----------|
|
137
|
+
# Track A +-----------------+
|
138
|
+
# Track B +------+
|
139
|
+
# Track C +------------+
|
140
|
+
# Track D +----+
|
141
|
+
# Track E +---+
|
142
|
+
# Track F +---+
|
143
|
+
# ```
|
144
|
+
#
|
145
|
+
# * Track A is bigger then the Options range. Take it.
|
146
|
+
# * Track B ends in the Options range. Take it.
|
147
|
+
# * Track C starts in the Options range. Take it.
|
148
|
+
# * Track D starts and ends within the Options range. Definitely take this.
|
149
|
+
# * Track E is out-of-score. Ignore it.
|
150
|
+
# * Track F is out-of-score. Ignore it.
|
151
|
+
#
|
152
|
+
# ---
|
153
|
+
#
|
154
|
+
# Open End (`to == nil`)
|
155
|
+
# Take all except Track E.
|
156
|
+
#
|
157
|
+
# ```
|
158
|
+
# Selected Range |---------->
|
159
|
+
# Track A +-----------------+
|
160
|
+
# Track B +------+
|
161
|
+
# Track C +------------+
|
162
|
+
# Track D +----+
|
163
|
+
# Track E +---+
|
164
|
+
# Track F +---+
|
165
|
+
# ```
|
166
|
+
#
|
167
|
+
# ---
|
168
|
+
#
|
169
|
+
# Open Start (`from == nil`)
|
170
|
+
# Take all except Track F.
|
171
|
+
#
|
172
|
+
# ```
|
173
|
+
# Selected Range <----------|
|
174
|
+
# Track A +-----------------+
|
175
|
+
# Track B +------+
|
176
|
+
# Track C +------------+
|
177
|
+
# Track D +----+
|
178
|
+
# Track E +---+
|
179
|
+
# Track F +---+
|
180
|
+
# ```
|
181
|
+
def tracks(options = Hash.new)
|
182
|
+
from_opt = options.fetch(:from, nil)
|
183
|
+
to_opt = options.fetch(:to, nil)
|
184
|
+
status_opt = options.fetch(:status, nil)
|
185
|
+
sort_opt = options.fetch(:sort, true)
|
186
|
+
billed_opt = options.fetch(:billed, nil)
|
187
|
+
|
188
|
+
if status_opt
|
189
|
+
case status_opt
|
190
|
+
when String
|
191
|
+
status_opt = [status_opt]
|
192
|
+
when Array
|
193
|
+
# OK
|
194
|
+
else
|
195
|
+
raise TaskError, ":status needs to be an instance of String or Array, #{status_opt.class} given."
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
if from_opt && to_opt && from_opt > to_opt
|
200
|
+
raise TaskError, 'From cannot be bigger than To.'
|
201
|
+
end
|
202
|
+
|
203
|
+
filtered_tracks = Hash.new
|
204
|
+
if from_opt.nil? && to_opt.nil?
|
205
|
+
# Take all Tracks.
|
206
|
+
filtered_tracks = @tracks.select{ |track_id, track|
|
207
|
+
# Filter Tracks with no Begin DateTime.
|
208
|
+
# This can happen when 'timr track add' without any DateTime.
|
209
|
+
!track.begin_datetime.nil?
|
210
|
+
}
|
211
|
+
elsif !from_opt.nil? && to_opt.nil?
|
212
|
+
# Open End (to_opt == nil)
|
213
|
+
filtered_tracks = @tracks.select{ |track_id, track|
|
214
|
+
bdt = track.begin_datetime
|
215
|
+
edt = track.end_datetime || Time.now
|
216
|
+
|
217
|
+
bdt && (
|
218
|
+
bdt < from_opt && edt > from_opt || # Track A, B
|
219
|
+
bdt >= from_opt && edt >= from_opt # Track C, D, F
|
220
|
+
)
|
221
|
+
}
|
222
|
+
elsif from_opt.nil? && !to_opt.nil?
|
223
|
+
# Open Start (from_opt == nil)
|
224
|
+
filtered_tracks = @tracks.select{ |track_id, track|
|
225
|
+
bdt = track.begin_datetime
|
226
|
+
edt = track.end_datetime || Time.now
|
227
|
+
|
228
|
+
bdt && (
|
229
|
+
bdt < to_opt && edt <= to_opt || # Track B, D, E
|
230
|
+
bdt < to_opt && edt > to_opt # Track A, C
|
231
|
+
)
|
232
|
+
}
|
233
|
+
elsif !from_opt.nil? && !to_opt.nil?
|
234
|
+
# Fixed Start and End (from_opt != nil && to_opt != nil)
|
235
|
+
filtered_tracks = @tracks.select{ |track_id, track|
|
236
|
+
bdt = track.begin_datetime
|
237
|
+
edt = track.end_datetime || Time.now
|
238
|
+
|
239
|
+
bdt && (
|
240
|
+
bdt >= from_opt && edt <= to_opt || # Track D
|
241
|
+
bdt < from_opt && edt > to_opt || # Track A
|
242
|
+
bdt < from_opt && edt <= to_opt && edt > from_opt || # Track B
|
243
|
+
bdt >= from_opt && edt > to_opt && bdt < to_opt # Track C
|
244
|
+
)
|
245
|
+
}
|
246
|
+
else
|
247
|
+
raise ThisShouldNeverHappenError, 'Should never happen, bug shit happens.'
|
248
|
+
end
|
249
|
+
|
250
|
+
if status_opt
|
251
|
+
filtered_tracks.select!{ |track_id, track|
|
252
|
+
status_opt.include?(track.status.short_status)
|
253
|
+
}
|
254
|
+
end
|
255
|
+
|
256
|
+
unless billed_opt.nil?
|
257
|
+
if billed_opt
|
258
|
+
filtered_tracks.select!{ |track_id, track|
|
259
|
+
track.is_billed
|
260
|
+
}
|
261
|
+
else
|
262
|
+
filtered_tracks.select!{ |track_id, track|
|
263
|
+
!track.is_billed
|
264
|
+
}
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
if sort_opt
|
269
|
+
filtered_tracks.sort{ |t1, t2|
|
270
|
+
t1 = t1.last
|
271
|
+
t2 = t2.last
|
272
|
+
|
273
|
+
cmp1 = t1.begin_datetime <=> t2.begin_datetime
|
274
|
+
if cmp1.nil? || cmp1 == 0
|
275
|
+
t1.end_datetime <=> t2.end_datetime
|
276
|
+
else
|
277
|
+
cmp1
|
278
|
+
end
|
279
|
+
}.to_h
|
280
|
+
else
|
281
|
+
filtered_tracks
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Uses `tracks()` with `options` to filter.
|
286
|
+
#
|
287
|
+
# Options:
|
288
|
+
#
|
289
|
+
# - `:from`
|
290
|
+
def begin_datetime(options = Hash.new)
|
291
|
+
from_opt = options.fetch(:from, nil)
|
292
|
+
|
293
|
+
# Cache
|
294
|
+
# if @begin_datetime
|
295
|
+
# return @begin_datetime
|
296
|
+
# end
|
297
|
+
# Cannot use this cache because of :from :to range limitation.
|
298
|
+
# It needs always to be direct from child Tracks, because the
|
299
|
+
# cache does not know when the begin and end datetimes of the
|
300
|
+
# child Tracks change.
|
301
|
+
|
302
|
+
# Do not sort. We only need to sort the tracks
|
303
|
+
# by begin_datetime and take the first.
|
304
|
+
options[:sort] = false
|
305
|
+
|
306
|
+
first_track = tracks(options)
|
307
|
+
.select{ |track_id, track| track.begin_datetime } # filter nil
|
308
|
+
.sort_by{ |track_id, track| track.begin_datetime }
|
309
|
+
.to_h # sort_by makes [[]]
|
310
|
+
.values # no keys to take the first
|
311
|
+
.first
|
312
|
+
|
313
|
+
if first_track
|
314
|
+
bdt = first_track.begin_datetime(options)
|
315
|
+
end
|
316
|
+
|
317
|
+
if from_opt && bdt && from_opt > bdt
|
318
|
+
bdt = from_opt
|
319
|
+
end
|
320
|
+
|
321
|
+
bdt
|
322
|
+
end
|
323
|
+
|
324
|
+
# Options:
|
325
|
+
#
|
326
|
+
# - `:format`
|
327
|
+
def begin_datetime_s(options = Hash.new)
|
328
|
+
format_opt = options.fetch(:format, HUMAN_DATETIME_FOMRAT)
|
329
|
+
|
330
|
+
bdt = begin_datetime(options)
|
331
|
+
if bdt
|
332
|
+
bdt.strftime(format_opt)
|
333
|
+
else
|
334
|
+
'---'
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# Uses `tracks()` with `options` to filter.
|
339
|
+
#
|
340
|
+
# Options:
|
341
|
+
#
|
342
|
+
# - `:to`
|
343
|
+
def end_datetime(options = Hash.new)
|
344
|
+
to_opt = options.fetch(:to, nil)
|
345
|
+
|
346
|
+
# Cache
|
347
|
+
# if @end_datetime
|
348
|
+
# return @end_datetime
|
349
|
+
# end
|
350
|
+
# Cannot use this cache because of :from :to range limitation.
|
351
|
+
# It needs always to be direct from child Tracks, because the
|
352
|
+
# cache does not know when the begin and end datetimes of the
|
353
|
+
# child Tracks change.
|
354
|
+
|
355
|
+
# Do not sort. We only need to sort the tracks
|
356
|
+
# by end_datetime and take the last.
|
357
|
+
options[:sort] = false
|
358
|
+
|
359
|
+
last_track = tracks(options)
|
360
|
+
.select{ |track_id, track| track.end_datetime } # filter nil
|
361
|
+
.sort_by{ |track_id, track| track.end_datetime }
|
362
|
+
.to_h # sort_by makes [[]]
|
363
|
+
.values # no keys to take the last
|
364
|
+
.last
|
365
|
+
|
366
|
+
if last_track
|
367
|
+
edt = last_track.end_datetime(options)
|
368
|
+
end
|
369
|
+
|
370
|
+
if to_opt && edt && to_opt < edt
|
371
|
+
edt = to_opt
|
372
|
+
end
|
373
|
+
|
374
|
+
edt
|
375
|
+
end
|
376
|
+
|
377
|
+
# Options:
|
378
|
+
#
|
379
|
+
# - `:format`
|
380
|
+
def end_datetime_s(options = Hash.new)
|
381
|
+
format_opt = options.fetch(:format, HUMAN_DATETIME_FOMRAT)
|
382
|
+
|
383
|
+
edt = end_datetime(options)
|
384
|
+
if edt
|
385
|
+
edt.strftime(format_opt)
|
386
|
+
else
|
387
|
+
'---'
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# Set estimation.
|
392
|
+
#
|
393
|
+
# Either using a Duration instance, Integer or a String like `2h 30m`.
|
394
|
+
# Estimation is parsed by [chronic_duration](https://github.com/henrypoydar/chronic_duration).
|
395
|
+
#
|
396
|
+
# Examples:
|
397
|
+
#
|
398
|
+
# - `-e 2:10:5`
|
399
|
+
# Sets Estimation to 2h 10m 5s.
|
400
|
+
#
|
401
|
+
# - `-e '2h 10m 5s'`
|
402
|
+
# Sets Estimation to 2h 10m 5s.
|
403
|
+
#
|
404
|
+
# Use `+` or `-` to calculate with Estimation Times:
|
405
|
+
#
|
406
|
+
# - `-e '-45m'`
|
407
|
+
# Subtracts 45 minutes from the original Estimation.
|
408
|
+
# - `-e '+1h 30m'`
|
409
|
+
# Adds 1 hour 30 minutes to the original Estimation.
|
410
|
+
#
|
411
|
+
# See [chronic_duration](https://github.com/henrypoydar/chronic_duration) for more examples.
|
412
|
+
def estimation=(estimation)
|
413
|
+
case estimation
|
414
|
+
when String
|
415
|
+
# Cannot use estimation.strip! because frozen.
|
416
|
+
estimation = estimation.strip
|
417
|
+
|
418
|
+
if estimation[0] == '+'
|
419
|
+
estimation = estimation[1..-1]
|
420
|
+
@estimation += Duration.parse(estimation)
|
421
|
+
elsif estimation[0] == '-'
|
422
|
+
estimation = estimation[1..-1]
|
423
|
+
@estimation -= Duration.parse(estimation)
|
424
|
+
else
|
425
|
+
@estimation = Duration.parse(estimation)
|
426
|
+
end
|
427
|
+
when Integer
|
428
|
+
@estimation = Duration.new(estimation)
|
429
|
+
when Duration
|
430
|
+
@estimation = estimation
|
431
|
+
when nil
|
432
|
+
@estimation = estimation
|
433
|
+
else
|
434
|
+
raise TaskError, "estimation needs to be an instance of String, Integer, Duration or nil, #{estimation.class} given."
|
435
|
+
end
|
436
|
+
|
437
|
+
# Mark Task as changed.
|
438
|
+
changed
|
439
|
+
end
|
440
|
+
|
441
|
+
# Get estimation.
|
442
|
+
def estimation
|
443
|
+
@estimation
|
444
|
+
end
|
445
|
+
|
446
|
+
# Get estimation as String.
|
447
|
+
def estimation_s
|
448
|
+
if @estimation
|
449
|
+
@estimation.to_human
|
450
|
+
else
|
451
|
+
'---'
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
# Set hourly_rate.
|
456
|
+
def hourly_rate=(new_hourly_rate)
|
457
|
+
if new_hourly_rate.nil?
|
458
|
+
@hourly_rate = nil
|
459
|
+
else
|
460
|
+
@hourly_rate = new_hourly_rate.to_f
|
461
|
+
end
|
462
|
+
|
463
|
+
# Mark Task as changed.
|
464
|
+
changed
|
465
|
+
end
|
466
|
+
|
467
|
+
# Set has_flat_rate.
|
468
|
+
def has_flat_rate=(has_flat_rate)
|
469
|
+
@has_flat_rate = has_flat_rate
|
470
|
+
|
471
|
+
# Mark Task as changed.
|
472
|
+
changed
|
473
|
+
end
|
474
|
+
|
475
|
+
# Get the actual consumed budge.
|
476
|
+
def consumed_budge
|
477
|
+
if @hourly_rate
|
478
|
+
duration.to_i.to_f / 3600.0 * @hourly_rate
|
479
|
+
else
|
480
|
+
0.0
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
# Calculate the budge based on estimation.
|
485
|
+
def estimated_budge
|
486
|
+
if @hourly_rate
|
487
|
+
estimation.to_i.to_f / 3600.0 * @hourly_rate
|
488
|
+
else
|
489
|
+
0.0
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# Calculates the budge loss when a Flat Rate is used and the consumed duration is greater than the estimation.
|
494
|
+
def loss_budge
|
495
|
+
if @has_flat_rate && @hourly_rate
|
496
|
+
if duration > estimation
|
497
|
+
(duration - estimation).to_i.to_f / 3600.0 * @hourly_rate
|
498
|
+
else
|
499
|
+
0.0
|
500
|
+
end
|
501
|
+
else
|
502
|
+
0.0
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
# Start a new Track by given `options`.
|
507
|
+
#
|
508
|
+
# Options:
|
509
|
+
#
|
510
|
+
# - `:foreign_id` (String)
|
511
|
+
# - `:track_id` (String)
|
512
|
+
# - `:no_stop` (Boolean)
|
513
|
+
def start(options = Hash.new)
|
514
|
+
foreign_id_opt = options.fetch(:foreign_id, nil)
|
515
|
+
track_id_opt = options.fetch(:track_id, nil)
|
516
|
+
|
517
|
+
# Used by Push.
|
518
|
+
no_stop_opt = options.fetch(:no_stop, false)
|
519
|
+
|
520
|
+
unless no_stop_opt
|
521
|
+
# End current Track before starting a new one.
|
522
|
+
# Leave options empty here for stop().
|
523
|
+
stop
|
524
|
+
end
|
525
|
+
|
526
|
+
if foreign_id_opt && @foreign_id.nil?
|
527
|
+
@foreign_id = foreign_id_opt
|
528
|
+
end
|
529
|
+
|
530
|
+
if track_id_opt
|
531
|
+
found_track = find_track_by_id(track_id_opt)
|
532
|
+
if found_track
|
533
|
+
|
534
|
+
@current_track = found_track.dup
|
535
|
+
else
|
536
|
+
raise TrackError, "No Track found for Track ID '#{track_id_opt}'."
|
537
|
+
end
|
538
|
+
else
|
539
|
+
@current_track = Track.new
|
540
|
+
@current_track.task = self
|
541
|
+
end
|
542
|
+
|
543
|
+
@tracks[@current_track.id] = @current_track
|
544
|
+
|
545
|
+
# Mark Task as changed.
|
546
|
+
changed
|
547
|
+
|
548
|
+
@current_track.start(options)
|
549
|
+
@current_track
|
550
|
+
end
|
551
|
+
|
552
|
+
# Stops a current running Track.
|
553
|
+
def stop(options = Hash.new)
|
554
|
+
if @current_track
|
555
|
+
@current_track.stop(options)
|
556
|
+
|
557
|
+
# Reset current Track variable.
|
558
|
+
@current_track = nil
|
559
|
+
|
560
|
+
# Mark Task as changed.
|
561
|
+
changed
|
562
|
+
end
|
563
|
+
|
564
|
+
nil
|
565
|
+
end
|
566
|
+
|
567
|
+
# Pauses a current running Track.
|
568
|
+
def pause(options = Hash.new)
|
569
|
+
if @current_track
|
570
|
+
@current_track.stop(options)
|
571
|
+
|
572
|
+
# Mark Task as changed.
|
573
|
+
changed
|
574
|
+
|
575
|
+
@current_track
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
# Continues the current Track.
|
580
|
+
# Only if it isn't already running.
|
581
|
+
def continue(options = Hash.new)
|
582
|
+
track_opt = options.fetch(:track, nil)
|
583
|
+
|
584
|
+
if @current_track
|
585
|
+
if @current_track.stopped?
|
586
|
+
|
587
|
+
# Duplicate and start.
|
588
|
+
@current_track = @current_track.dup
|
589
|
+
@current_track.start(options)
|
590
|
+
|
591
|
+
|
592
|
+
add_track(@current_track)
|
593
|
+
else
|
594
|
+
raise TrackError, "Cannot continue Track #{@current_track.short_id}, is already running."
|
595
|
+
end
|
596
|
+
else
|
597
|
+
unless track_opt
|
598
|
+
raise TaskError, 'No Track given.'
|
599
|
+
end
|
600
|
+
|
601
|
+
# Duplicate and start.
|
602
|
+
@current_track = track_opt.dup
|
603
|
+
@current_track.start(options)
|
604
|
+
|
605
|
+
add_track(@current_track)
|
606
|
+
end
|
607
|
+
|
608
|
+
@current_track
|
609
|
+
end
|
610
|
+
|
611
|
+
# Consumed duration.
|
612
|
+
#
|
613
|
+
# Options:
|
614
|
+
#
|
615
|
+
# - `:billed`
|
616
|
+
def duration(options = Hash.new)
|
617
|
+
|
618
|
+
billed_opt = options.fetch(:billed, nil)
|
619
|
+
|
620
|
+
duration = Duration.new
|
621
|
+
@tracks.each do |track_id, track|
|
622
|
+
if billed_opt.nil? || (billed_opt && track.is_billed) || (!billed_opt && !track.is_billed)
|
623
|
+
duration += track.duration(options)
|
624
|
+
end
|
625
|
+
end
|
626
|
+
duration
|
627
|
+
end
|
628
|
+
|
629
|
+
# Alias for `duration` method.
|
630
|
+
#
|
631
|
+
# Options:
|
632
|
+
#
|
633
|
+
# - `:billed` (Boolean)
|
634
|
+
def billed_duration(options = Hash.new)
|
635
|
+
duration(options.merge({:billed => true}))
|
636
|
+
end
|
637
|
+
|
638
|
+
# Alias for `duration` method.
|
639
|
+
#
|
640
|
+
# Options:
|
641
|
+
#
|
642
|
+
# - `:billed` (Boolean)
|
643
|
+
def unbilled_duration(options = Hash.new)
|
644
|
+
duration(options.merge({:billed => false}))
|
645
|
+
end
|
646
|
+
|
647
|
+
# Get the remaining Time of estimation.
|
648
|
+
#
|
649
|
+
# Returns a Duration instance.
|
650
|
+
def remaining_time
|
651
|
+
if @estimation
|
652
|
+
estimation - duration
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
# Get the remaining Time as Human String.
|
657
|
+
#
|
658
|
+
# - Like `2h 30m`.
|
659
|
+
# - Or `---` when `@estimation` is `nil`.
|
660
|
+
def remaining_time_s
|
661
|
+
rmt = remaining_time
|
662
|
+
if rmt
|
663
|
+
rmt.to_human
|
664
|
+
else
|
665
|
+
'---'
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
# Get the remaining Time as percent.
|
670
|
+
def remaining_time_percent
|
671
|
+
rmt = remaining_time
|
672
|
+
if rmt && @estimation
|
673
|
+
(rmt.to_i.to_f / @estimation.to_i.to_f) * 100.0
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
# Get the remaining Time Percent as String.
|
678
|
+
def remaining_time_percent_s
|
679
|
+
rmtp = remaining_time_percent
|
680
|
+
if rmtp
|
681
|
+
'%.1f %%' % [rmtp]
|
682
|
+
else
|
683
|
+
'---'
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
# Get Task status as Status instance.
|
688
|
+
def status
|
689
|
+
stati = @tracks.map{ |track_id, track| track.status.short_status }.to_set
|
690
|
+
|
691
|
+
if @tracks.count == 0
|
692
|
+
status = ?-
|
693
|
+
elsif stati.include?(?R)
|
694
|
+
status = ?R
|
695
|
+
elsif stati.include?(?S)
|
696
|
+
status = ?S
|
697
|
+
else
|
698
|
+
status = ?U
|
699
|
+
end
|
700
|
+
|
701
|
+
Status.new(status)
|
702
|
+
end
|
703
|
+
|
704
|
+
# Set is_billed.
|
705
|
+
def is_billed=(is_billed)
|
706
|
+
@tracks.each do |track_id, track|
|
707
|
+
track.is_billed = is_billed
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
# Find a Track by ID even if the ID is not 40 characters long.
|
712
|
+
# When the ID is 40 characters long `@tracks[id]` is faster. ;)
|
713
|
+
def find_track_by_id(track_id)
|
714
|
+
track_id_len = track_id.length
|
715
|
+
|
716
|
+
# puts "search track id '#{track_id}'"
|
717
|
+
|
718
|
+
if track_id_len < 40
|
719
|
+
found_track_id = nil
|
720
|
+
@tracks.keys.each do |key|
|
721
|
+
if track_id == key[0, track_id_len]
|
722
|
+
if found_track_id
|
723
|
+
raise TrackError, "Track ID '#{track_id}' is not a unique identifier."
|
724
|
+
else
|
725
|
+
found_track_id = key
|
726
|
+
|
727
|
+
# Do not break the loop here.
|
728
|
+
# Iterate all keys to make sure the ID is unique.
|
729
|
+
end
|
730
|
+
end
|
731
|
+
end
|
732
|
+
track_id = found_track_id
|
733
|
+
end
|
734
|
+
|
735
|
+
@tracks[track_id]
|
736
|
+
end
|
737
|
+
|
738
|
+
# Are two Tasks equal?
|
739
|
+
#
|
740
|
+
# Uses ID for comparision.
|
741
|
+
def eql?(task)
|
742
|
+
unless task.is_a?(Task)
|
743
|
+
raise TaskError, "task variable must be a Task instance. #{task.class} given."
|
744
|
+
end
|
745
|
+
|
746
|
+
self.id == task.id
|
747
|
+
end
|
748
|
+
|
749
|
+
# To String
|
750
|
+
def to_s
|
751
|
+
"Task_#{short_id}"
|
752
|
+
end
|
753
|
+
|
754
|
+
# Use to print informations for Track.
|
755
|
+
#
|
756
|
+
# Options:
|
757
|
+
#
|
758
|
+
# - `:full_id` (Boolean) Show full Task ID.
|
759
|
+
def to_track_array(options = Hash.new)
|
760
|
+
full_id_opt = options.fetch(:full_id, false) # @TODO full_id unit test
|
761
|
+
|
762
|
+
full_id = full_id_opt ? self.id : self.short_id
|
763
|
+
|
764
|
+
name_a = ["Task: #{full_id}"]
|
765
|
+
|
766
|
+
if self.foreign_id
|
767
|
+
name_a << self.foreign_id
|
768
|
+
end
|
769
|
+
if self.name
|
770
|
+
name_a << self.name
|
771
|
+
end
|
772
|
+
|
773
|
+
to_ax = Array.new
|
774
|
+
to_ax << name_a.join(' ')
|
775
|
+
to_ax
|
776
|
+
end
|
777
|
+
|
778
|
+
# Used to print informations to STDOUT.
|
779
|
+
def to_compact_str
|
780
|
+
to_compact_array.join("\n")
|
781
|
+
end
|
782
|
+
|
783
|
+
# Used to print informations to STDOUT.
|
784
|
+
def to_compact_array
|
785
|
+
to_ax = Array.new
|
786
|
+
if self.foreign_id
|
787
|
+
to_ax << 'Task: %s %s %s' % [self.short_id, self.foreign_id, self.name]
|
788
|
+
else
|
789
|
+
to_ax << 'Task: %s %s' % [self.short_id, self.name]
|
790
|
+
end
|
791
|
+
if self.description
|
792
|
+
to_ax << 'Description: %s' % [self.description]
|
793
|
+
end
|
794
|
+
if self.estimation
|
795
|
+
to_ax << 'Estimation: %s' % [self.estimation.to_human]
|
796
|
+
end
|
797
|
+
to_ax
|
798
|
+
end
|
799
|
+
|
800
|
+
# Used to print informations to STDOUT.
|
801
|
+
def to_detailed_str
|
802
|
+
to_detailed_array.join("\n")
|
803
|
+
end
|
804
|
+
|
805
|
+
# Used to print informations to STDOUT.
|
806
|
+
#
|
807
|
+
# Options:
|
808
|
+
#
|
809
|
+
# - `:full_id` (Boolean) Show full Task ID.
|
810
|
+
def to_detailed_array(options = Hash.new)
|
811
|
+
full_id_opt = options.fetch(:full_id, false)
|
812
|
+
|
813
|
+
to_ax = Array.new
|
814
|
+
|
815
|
+
if full_id_opt
|
816
|
+
to_ax << 'Task: %s' % [self.id]
|
817
|
+
else
|
818
|
+
to_ax << 'Task: %s' % [self.short_id]
|
819
|
+
end
|
820
|
+
|
821
|
+
if self.foreign_id
|
822
|
+
to_ax << 'Foreign ID: %s' % [self.foreign_id]
|
823
|
+
end
|
824
|
+
|
825
|
+
to_ax << 'Name: %s' % [self.name]
|
826
|
+
|
827
|
+
if self.description
|
828
|
+
to_ax << 'Description: %s' % [self.description]
|
829
|
+
end
|
830
|
+
|
831
|
+
# Duration
|
832
|
+
duration_human = self.duration.to_human
|
833
|
+
to_ax << 'Duration: %s' % [duration_human]
|
834
|
+
|
835
|
+
duration_man_days = self.duration.to_man_days
|
836
|
+
if duration_human != duration_man_days
|
837
|
+
to_ax << 'Man Unit: %s' % [duration_man_days]
|
838
|
+
end
|
839
|
+
|
840
|
+
# Billed Duration
|
841
|
+
billed_duration_human = self.billed_duration.to_human
|
842
|
+
to_ax << 'Billed Duration: %s' % [billed_duration_human]
|
843
|
+
|
844
|
+
# Unbilled Duration
|
845
|
+
unbilled_duration_human = self.unbilled_duration.to_human
|
846
|
+
to_ax << 'Unbilled Duration: %s' % [unbilled_duration_human]
|
847
|
+
|
848
|
+
if self.estimation
|
849
|
+
to_ax << 'Estimation: %s' % [self.estimation.to_human]
|
850
|
+
|
851
|
+
to_ax << 'Time Remaining: %s (%s)' % [self.remaining_time_s, self.remaining_time_percent_s]
|
852
|
+
|
853
|
+
bar_options = {
|
854
|
+
:total => self.estimation.to_i,
|
855
|
+
:progress => self.duration.to_i,
|
856
|
+
:length => 50,
|
857
|
+
:progress_mark => '#',
|
858
|
+
:remainder_mark => '-',
|
859
|
+
}
|
860
|
+
bar = ProgressBar.new(bar_options)
|
861
|
+
|
862
|
+
to_ax << ' |%s|' % [bar.render]
|
863
|
+
end
|
864
|
+
|
865
|
+
if self.hourly_rate
|
866
|
+
to_ax << 'Hourly Rate: %.2f' % [self.hourly_rate]
|
867
|
+
to_ax << 'Flat Rate: %s' % [@has_flat_rate ? 'Yes' : 'No']
|
868
|
+
|
869
|
+
to_ax << 'Consumed Budge: %.2f' % [self.consumed_budge]
|
870
|
+
|
871
|
+
if self.estimation
|
872
|
+
to_ax << 'Estimated Budge: %.2f' % [self.estimated_budge]
|
873
|
+
end
|
874
|
+
|
875
|
+
if @has_flat_rate
|
876
|
+
to_ax << 'Loss Budge: %.2f' % [self.loss_budge]
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
tracks = self.tracks
|
881
|
+
first_track = tracks
|
882
|
+
.select{ |track_id, track| track.begin_datetime }
|
883
|
+
.sort_by{ |track_id, track| track.begin_datetime }
|
884
|
+
.to_h
|
885
|
+
.values
|
886
|
+
.first
|
887
|
+
if first_track
|
888
|
+
to_ax << 'Begin Track: %s %s' % [first_track.short_id, first_track.begin_datetime_s]
|
889
|
+
end
|
890
|
+
|
891
|
+
last_track = tracks
|
892
|
+
.select{ |track_id, track| track.end_datetime }
|
893
|
+
.sort_by{ |track_id, track| track.end_datetime }
|
894
|
+
.to_h
|
895
|
+
.values
|
896
|
+
.last
|
897
|
+
if last_track
|
898
|
+
to_ax << 'End Track: %s %s' % [last_track.short_id, last_track.end_datetime_s]
|
899
|
+
end
|
900
|
+
|
901
|
+
status = self.status.colorized
|
902
|
+
to_ax << 'Status: %s' % [status]
|
903
|
+
|
904
|
+
if @current_track
|
905
|
+
to_ax << 'Current Track: %s %s' % [@current_track.short_id, @current_track.title]
|
906
|
+
end
|
907
|
+
|
908
|
+
tracks_count = tracks.count
|
909
|
+
to_ax << 'Tracks: %d' % [tracks_count]
|
910
|
+
|
911
|
+
billed_tracks_count = tracks({:billed => true}).count
|
912
|
+
to_ax << 'Billed Tracks: %d' % [billed_tracks_count]
|
913
|
+
|
914
|
+
unbilled_tracks_count = tracks({:billed => false}).count
|
915
|
+
to_ax << 'Unbilled Tracks: %d' % [unbilled_tracks_count]
|
916
|
+
|
917
|
+
if tracks_count > 0 && @tracks_opt # --tracks
|
918
|
+
to_ax << 'Track IDs: %s' % [tracks.map{ |track_id, track| track.short_id }.join(' ')]
|
919
|
+
end
|
920
|
+
|
921
|
+
if self.file_path
|
922
|
+
to_ax << 'File path: %s' % [self.file_path]
|
923
|
+
end
|
924
|
+
|
925
|
+
to_ax
|
926
|
+
end
|
927
|
+
|
928
|
+
def inspect
|
929
|
+
"#<Task_#{short_id} tracks=#{@tracks.count}>"
|
930
|
+
end
|
931
|
+
|
932
|
+
# All methods in this block are static.
|
933
|
+
class << self
|
934
|
+
|
935
|
+
# Load a Task from a file into a Task instance.
|
936
|
+
def load_task_from_file(path)
|
937
|
+
task = Task.new
|
938
|
+
task.load_from_file(path)
|
939
|
+
task
|
940
|
+
end
|
941
|
+
|
942
|
+
# Search a Task in a base path for a Track by ID.
|
943
|
+
# If found a file load it into a Task instance.
|
944
|
+
def load_task_from_file_with_id(base_path, task_id)
|
945
|
+
task_file_path = BasicModel.find_file_by_id(base_path, task_id)
|
946
|
+
if task_file_path
|
947
|
+
load_task_from_file(task_file_path)
|
948
|
+
end
|
949
|
+
end
|
950
|
+
|
951
|
+
# Create a new Task using a Hash.
|
952
|
+
#
|
953
|
+
# Options:
|
954
|
+
#
|
955
|
+
# - `:name` (String)
|
956
|
+
# - `:description` (String)
|
957
|
+
# - `:estimation` (String|Integer|Duration)
|
958
|
+
# - `:hourly_rate` (Integer)
|
959
|
+
def create_task_from_hash(options)
|
960
|
+
task = Task.new
|
961
|
+
task.name = options.fetch(:name, nil)
|
962
|
+
task.description = options.fetch(:description, nil)
|
963
|
+
task.estimation = options.fetch(:estimation, nil)
|
964
|
+
task.hourly_rate = options.fetch(:hourly_rate, nil)
|
965
|
+
task.has_flat_rate = options.fetch(:has_flat_rate, false)
|
966
|
+
task
|
967
|
+
end
|
968
|
+
|
969
|
+
end
|
970
|
+
|
971
|
+
private
|
972
|
+
|
973
|
+
# BasicModel Hook
|
974
|
+
def pre_save_to_file
|
975
|
+
# Meta
|
976
|
+
@meta['foreign_id'] = @foreign_id
|
977
|
+
@meta['name'] = @name
|
978
|
+
@meta['description'] = @description
|
979
|
+
|
980
|
+
@meta['current_track_id'] = nil
|
981
|
+
if @current_track
|
982
|
+
@meta['current_track_id'] = @current_track.id
|
983
|
+
end
|
984
|
+
|
985
|
+
if @estimation
|
986
|
+
@meta['estimation'] = @estimation.to_i
|
987
|
+
end
|
988
|
+
if @hourly_rate
|
989
|
+
@meta['hourly_rate'] = @hourly_rate.to_f
|
990
|
+
else
|
991
|
+
@meta['hourly_rate'] = nil
|
992
|
+
end
|
993
|
+
if @has_flat_rate
|
994
|
+
@meta['has_flat_rate'] = @has_flat_rate
|
995
|
+
else
|
996
|
+
@meta['has_flat_rate'] = false
|
997
|
+
end
|
998
|
+
|
999
|
+
# Tracks
|
1000
|
+
@data = @tracks.map{ |track_id, track|
|
1001
|
+
[track_id, track.to_h]
|
1002
|
+
}.to_h
|
1003
|
+
|
1004
|
+
super()
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
# BasicModel Hook
|
1008
|
+
def post_load_from_file
|
1009
|
+
@tracks = @data.map{ |track_id, track_h|
|
1010
|
+
track = Track.create_track_from_hash(track_h)
|
1011
|
+
track.task = self
|
1012
|
+
[track_id, track]
|
1013
|
+
}.to_h
|
1014
|
+
|
1015
|
+
current_track_id = @meta['current_track_id']
|
1016
|
+
if current_track_id
|
1017
|
+
@current_track = @tracks[current_track_id]
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
@foreign_id = @meta['foreign_id']
|
1021
|
+
@name = @meta['name']
|
1022
|
+
@description = @meta['description']
|
1023
|
+
|
1024
|
+
if @meta['estimation']
|
1025
|
+
@estimation = Duration.new(@meta['estimation'])
|
1026
|
+
end
|
1027
|
+
if @meta['hourly_rate']
|
1028
|
+
@hourly_rate = @meta['hourly_rate'].to_f
|
1029
|
+
end
|
1030
|
+
if @meta['has_flat_rate']
|
1031
|
+
@has_flat_rate = @meta['has_flat_rate']
|
1032
|
+
end
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
end # class Task
|
1036
|
+
|
1037
|
+
end # module Model
|
1038
|
+
end # module Timr
|
1039
|
+
end #module TheFox
|