Code Coverage
 
Lines
Covered
73.44% covered (warning)
73.44%
329 / 448
1
module TheFox
1
module Timr
1
module Model
1
class Task < BasicModel
1
include TheFox::Timr::Error
1
attr_reader :foreign_id
1
attr_reader :description
1
attr_reader :current_track
1
attr_reader :hourly_rate
1
attr_reader :has_flat_rate
1
def initialize
67
super()
# Meta
67
@foreign_id = nil # --id
67
@name = nil
67
@description = nil
67
@current_track = nil
67
@estimation = nil
67
@hourly_rate = nil
67
@has_flat_rate = false
# Data
67
@tracks = Hash.new
end
# Get Foreign ID or Short ID.
1
def id_foreign_or_short
0
@foreign_id ? @foreign_id : short_id
end
1
def foreign_id=(foreign_id)
6
@foreign_id = foreign_id
# Mark Task as changed.
6
changed
end
# Set name.
1
def name=(name)
8
@name = name
# Mark Task as changed.
8
changed
end
# Get name.
1
def name(max_length = nil)
61
name = @name
61
if name && max_length && name.length > max_length + 2
3
name = name[0, max_length] << '...'
end
61
name
end
# Get name or `---` if name is not set.
1
def name_s(max_length = nil)
3
s = name(max_length)
3
if s.nil?
1
'---'
else
2
s
end
end
# Set description.
1
def description=(description)
7
@description = description
# Mark Task as changed.
7
changed
end
# Add a Track.
1
def add_track(track, set_as_current_track = false)
53
track.task = self
53
@tracks[track.id] = track
53
if set_as_current_track
1
@current_track = track
end
# Mark Task as changed.
53
changed
end
# Remove a Track.
1
def remove_track(track)
5
track.task = nil
5
if @tracks.delete(track.id)
# Mark Task as changed.
3
changed
else
# Track is not assiged to this Task.
2
false
end
end
# Move a Track to another Task.
1
def move_track(track, target_task)
3
if eql?(target_task)
1
return false
end
2
unless remove_track(track)
1
return false
end
1
set_as_current_track = false
1
if @current_track && @current_track.eql?(track)
0
@current_track = nil
0
set_as_current_track = true
end
1
target_task.add_track(track, set_as_current_track)
1
true
end
1
def reset
0
if @current_track
0
@current_track = nil
# Mark Task as changed.
0
changed
end
end
# Select Track by Time Range and/or Status.
#
# Options:
#
# - `:from`, `:to` limit the begin and end datetimes to a specific range.
# - `:status` filter Tracks by Short Status.
# - `:billed` filter Tracks by is_billed flag.
# - `true` filter billed Tracks.
# - `false` filter unbilled Tracks.
# - `nil` filter off.
#
# Fixed Start and End (`from != nil && to != nil`)
#
# ```
# Selected Range |----------|
# Track A +-----------------+
# Track B +------+
# Track C +------------+
# Track D +----+
# Track E +---+
# Track F +---+
# ```
#
# * Track A is bigger then the Options range. Take it.
# * Track B ends in the Options range. Take it.
# * Track C starts in the Options range. Take it.
# * Track D starts and ends within the Options range. Definitely take this.
# * Track E is out-of-score. Ignore it.
# * Track F is out-of-score. Ignore it.
#
# ---
#
# Open End (`to == nil`)
# Take all except Track E.
#
# ```
# Selected Range |---------->
# Track A +-----------------+
# Track B +------+
# Track C +------------+
# Track D +----+
# Track E +---+
# Track F +---+
# ```
#
# ---
#
# Open Start (`from == nil`)
# Take all except Track F.
#
# ```
# Selected Range <----------|
# Track A +-----------------+
# Track B +------+
# Track C +------------+
# Track D +----+
# Track E +---+
# Track F +---+
# ```
1
def tracks(options = Hash.new)
39
from_opt = options.fetch(:from, nil)
39
to_opt = options.fetch(:to, nil)
39
status_opt = options.fetch(:status, nil)
39
sort_opt = options.fetch(:sort, true)
39
billed_opt = options.fetch(:billed, nil)
39
if status_opt
1
case status_opt
when String
0
status_opt = [status_opt]
when Array
# OK
else
1
raise TaskError, ":status needs to be an instance of String or Array, #{status_opt.class} given."
end
end
38
if from_opt && to_opt && from_opt > to_opt
1
raise TaskError, 'From cannot be bigger than To.'
end
37
filtered_tracks = Hash.new
37
if from_opt.nil? && to_opt.nil?
# Take all Tracks.
16
filtered_tracks = @tracks.select{ |track_id, track|
# Filter Tracks with no Begin DateTime.
# This can happen when 'timr track add' without any DateTime.
18
!track.begin_datetime.nil?
}
21
elsif !from_opt.nil? && to_opt.nil?
# Open End (to_opt == nil)
7
filtered_tracks = @tracks.select{ |track_id, track|
21
bdt = track.begin_datetime
21
edt = track.end_datetime || Time.now
bdt && ( \
# Track A, B
bdt < from_opt && edt > from_opt || \
# Track C, D, F
21
bdt >= from_opt && edt >= from_opt
21
)
}
14
elsif from_opt.nil? && !to_opt.nil?
# Open Start (from_opt == nil)
7
filtered_tracks = @tracks.select{ |track_id, track|
21
bdt = track.begin_datetime
21
edt = track.end_datetime || Time.now
bdt && ( \
# Track B, D, E
bdt < to_opt && edt <= to_opt || \
# Track A, C
21
bdt < to_opt && edt > to_opt
21
)
}
7
elsif !from_opt.nil? && !to_opt.nil?
# Fixed Start and End (from_opt != nil && to_opt != nil)
7
filtered_tracks = @tracks.select{ |track_id, track|
20
bdt = track.begin_datetime
20
edt = track.end_datetime || Time.now
bdt && ( \
# Track D
bdt >= from_opt && edt <= to_opt || \
# Track A
20
bdt < from_opt && edt > to_opt || \
# Track B
bdt < from_opt && edt <= to_opt && edt > from_opt || \
# Track C
bdt >= from_opt && edt > to_opt && bdt < to_opt
20
)
}
else
0
raise ThisShouldNeverHappenError, 'Should never happen, bug shit happens.'
end
37
if status_opt
0
filtered_tracks.select!{ |track_id, track|
0
status_opt.include?(track.status.short_status)
}
end
37
unless billed_opt.nil?
10
if billed_opt
5
filtered_tracks.select!{ |track_id, track|
5
track.is_billed
}
else
5
filtered_tracks.select!{ |track_id, track|
5
!track.is_billed
}
end
end
37
if sort_opt
filtered_tracks.sort{ |t1, t2|
26
t1 = t1.last
26
t2 = t2.last
26
cmp1 = t1.begin_datetime <=> t2.begin_datetime
26
if cmp1.nil? || cmp1 == 0
8
t1.end_datetime <=> t2.end_datetime
else
18
cmp1
end
31
}.to_h
else
6
filtered_tracks
end
end
# Uses `tracks()` with `options` to filter.
#
# Options:
#
# - `:from`
1
def begin_datetime(options = Hash.new)
3
from_opt = options.fetch(:from, nil)
# Cache
# if @begin_datetime
# return @begin_datetime
# end
# Cannot use this cache because of :from :to range limitation.
# It needs always to be direct from child Tracks, because the
# cache does not know when the begin and end datetimes of the
# child Tracks change.
# Do not sort. We only need to sort the tracks
# by begin_datetime and take the first.
3
options[:sort] = false
3
first_track = tracks(options)
7
.select{ |track_id, track| track.begin_datetime } # filter nil
7
.sort_by{ |track_id, track| track.begin_datetime }
.to_h # sort_by makes [[]]
.values # no keys to take the first
.first
3
if first_track
3
bdt = first_track.begin_datetime(options)
end
3
if from_opt && bdt && from_opt > bdt
0
bdt = from_opt
end
3
bdt
end
# Options:
#
# - `:format`
1
def begin_datetime_s(options = Hash.new)
3
format_opt = options.fetch(:format, HUMAN_DATETIME_FOMRAT)
3
bdt = begin_datetime(options)
3
if bdt
3
bdt.strftime(format_opt)
else
0
'---'
end
end
# Uses `tracks()` with `options` to filter.
#
# Options:
#
# - `:to`
1
def end_datetime(options = Hash.new)
3
to_opt = options.fetch(:to, nil)
# Cache
# if @end_datetime
# return @end_datetime
# end
# Cannot use this cache because of :from :to range limitation.
# It needs always to be direct from child Tracks, because the
# cache does not know when the begin and end datetimes of the
# child Tracks change.
# Do not sort. We only need to sort the tracks
# by end_datetime and take the last.
3
options[:sort] = false
3
last_track = tracks(options)
7
.select{ |track_id, track| track.end_datetime } # filter nil
6
.sort_by{ |track_id, track| track.end_datetime }
.to_h # sort_by makes [[]]
.values # no keys to take the last
.last
3
if last_track
2
edt = last_track.end_datetime(options)
end
3
if to_opt && edt && to_opt < edt
0
edt = to_opt
end
3
edt
end
# Options:
#
# - `:format`
1
def end_datetime_s(options = Hash.new)
3
format_opt = options.fetch(:format, HUMAN_DATETIME_FOMRAT)
3
edt = end_datetime(options)
3
if edt
2
edt.strftime(format_opt)
else
1
'---'
end
end
# Set estimation.
#
# Either using a Duration instance, Integer or a String like `2h 30m`.
# Estimation is parsed by [chronic_duration](https://github.com/henrypoydar/chronic_duration).
#
# Examples:
#
# - `-e 2:10:5`
# Sets Estimation to 2h 10m 5s.
#
# - `-e '2h 10m 5s'`
# Sets Estimation to 2h 10m 5s.
#
# Use `+` or `-` to calculate with Estimation Times:
#
# - `-e '-45m'`
# Subtracts 45 minutes from the original Estimation.
# - `-e '+1h 30m'`
# Adds 1 hour 30 minutes to the original Estimation.
#
# See [chronic_duration](https://github.com/henrypoydar/chronic_duration) for more examples.
1
def estimation=(estimation)
16
case estimation
when String
# Cannot use estimation.strip! because frozen.
13
estimation = estimation.strip
13
if estimation[0] == '+'
1
estimation = estimation[1..-1]
1
@estimation += Duration.parse(estimation)
12
elsif estimation[0] == '-'
2
estimation = estimation[1..-1]
2
@estimation -= Duration.parse(estimation)
else
10
@estimation = Duration.parse(estimation)
end
when Integer
1
@estimation = Duration.new(estimation)
when Duration
1
@estimation = estimation
when nil
0
@estimation = estimation
else
1
raise TaskError, "estimation needs to be an instance of String, Integer, Duration or nil, #{estimation.class} given."
end
# Mark Task as changed.
15
changed
end
# Get estimation.
1
def estimation
45
@estimation
end
# Get estimation as String.
1
def estimation_s
7
if @estimation
6
@estimation.to_human_s
else
1
'---'
end
end
# Set hourly_rate.
1
def hourly_rate=(new_hourly_rate)
6
if new_hourly_rate.nil?
2
@hourly_rate = nil
else
4
@hourly_rate = new_hourly_rate.to_f
end
# Mark Task as changed.
6
changed
end
# Set has_flat_rate.
1
def has_flat_rate=(has_flat_rate)
3
@has_flat_rate = has_flat_rate
# Mark Task as changed.
3
changed
end
# Get the actual consumed budge.
1
def consumed_budge
3
if @hourly_rate
2
duration.to_i.to_f / 3600.0 * @hourly_rate
else
1
0.0
end
end
# Calculate the budge based on estimation.
1
def estimated_budge
3
if @hourly_rate
2
estimation.to_i.to_f / 3600.0 * @hourly_rate
else
1
0.0
end
end
# Calculates the budge loss when a Flat Rate is used and the consumed duration is greater than the estimation.
1
def loss_budge
5
if @has_flat_rate && @hourly_rate
2
if duration > estimation
1
(duration - estimation).to_i.to_f / 3600.0 * @hourly_rate
else
1
0.0
end
else
3
0.0
end
end
# Start a new Track by given `options`.
#
# Options:
#
# - `:foreign_id` (String)
# - `:track_id` (String)
# - `:no_stop` (Boolean)
1
def start(options = Hash.new)
0
foreign_id_opt = options.fetch(:foreign_id, nil)
0
track_id_opt = options.fetch(:track_id, nil)
# Used by Push.
0
no_stop_opt = options.fetch(:no_stop, false)
0
unless no_stop_opt
# End current Track before starting a new one.
# Leave options empty here for stop().
0
stop
end
0
if foreign_id_opt && @foreign_id.nil?
0
@foreign_id = foreign_id_opt
end
0
if track_id_opt
0
found_track = find_track_by_id(track_id_opt)
0
if found_track
0
@current_track = found_track.dup
else
0
raise TrackError, "No Track found for Track ID '#{track_id_opt}'."
end
else
0
@current_track = Track.new
0
@current_track.task = self
end
0
@tracks[@current_track.id] = @current_track
# Mark Task as changed.
0
changed
0
@current_track.start(options)
0
@current_track
end
# Stops a current running Track.
1
def stop(options = Hash.new)
0
if @current_track
0
@current_track.stop(options)
# Reset current Track variable.
0
@current_track = nil
# Mark Task as changed.
0
changed
end
nil
end
# Pauses a current running Track.
1
def pause(options = Hash.new)
0
if @current_track
0
@current_track.stop(options)
# Mark Task as changed.
0
changed
0
@current_track
end
end
# Continues the current Track.
# Only if it isn't already running.
1
def continue(options = Hash.new)
0
track_opt = options.fetch(:track, nil)
0
if @current_track
0
if @current_track.stopped?
# Duplicate and start.
0
@current_track = @current_track.dup
0
@current_track.start(options)
0
add_track(@current_track)
else
0
raise TrackError, "Cannot continue Track #{@current_track.short_id}, is already running."
end
else
0
unless track_opt
0
raise TaskError, 'No Track given.'
end
# Duplicate and start.
0
@current_track = track_opt.dup
0
@current_track.start(options)
0
add_track(@current_track)
end
0
@current_track
end
# Consumed duration.
#
# Options:
#
# - `:billed` (Boolean)
1
def duration(options = Hash.new)
132
billed_opt = options.fetch(:billed, nil)
132
duration = Duration.new
132
@tracks.each do |track_id, track|
81
if billed_opt.nil? || (billed_opt && track.is_billed) || (!billed_opt && !track.is_billed)
64
duration += track.duration(options)
end
end
132
duration
end
# Alias for `duration` method.
#
# Options:
#
# - `:billed` (Boolean)
1
def billed_duration(options = Hash.new)
5
duration(options.merge({:billed => true}))
end
# Alias for `duration` method.
#
# Options:
#
# - `:billed` (Boolean)
1
def unbilled_duration(options = Hash.new)
5
duration(options.merge({:billed => false}))
end
# Get the remaining Time of estimation.
#
# Returns a Duration instance.
1
def remaining_time
20
if @estimation
16
estimation - duration
end
end
# Get the remaining Time as Human String.
#
# - Like `2h 30m`.
# - Or `---` when `@estimation` is `nil`.
1
def remaining_time_s
7
rmt = remaining_time
7
if rmt
6
rmt.to_human_s
else
1
'---'
end
end
# Get the remaining Time as percent.
1
def remaining_time_percent
10
rmt = remaining_time
10
if rmt && @estimation
8
(rmt.to_i.to_f / @estimation.to_i.to_f) * 100.0
end
end
# Get the remaining Time Percent as String.
1
def remaining_time_percent_s
7
rmtp = remaining_time_percent
7
if rmtp
6
'%.1f %%' % [rmtp]
else
1
'---'
end
end
# Get Task status as Status instance.
1
def status
13
stati = @tracks.map{ |track_id, track| track.status.short_status }.to_set
8
if @tracks.count == 0
5
status = ?-
3
elsif stati.include?(?R)
1
status = ?R
2
elsif stati.include?(?S)
1
status = ?S
else
1
status = ?U
end
8
Status.new(status)
end
# Set is_billed.
1
def is_billed=(is_billed)
2
@tracks.each do |track_id, track|
10
track.is_billed = is_billed
end
end
# Find a Track by ID even if the ID is not 40 characters long.
# When the ID is 40 characters long `@tracks[id]` is faster. ;)
1
def find_track_by_id(track_id)
2
track_id_len = track_id.length
# puts "search track id '#{track_id}'"
2
if track_id_len < 40
2
found_track_id = nil
2
@tracks.keys.each do |key|
5
if track_id == key[0, track_id_len]
3
if found_track_id
1
raise TrackError, "Track ID '#{track_id}' is not a unique identifier."
else
2
found_track_id = key
# Do not break the loop here.
# Iterate all keys to make sure the ID is unique.
end
end
end
1
track_id = found_track_id
end
1
@tracks[track_id]
end
# Are two Tasks equal?
#
# Uses ID for comparision.
1
def eql?(task)
6
unless task.is_a?(Task)
1
raise TaskError, "task variable must be a Task instance. #{task.class} given."
end
5
self.id == task.id
end
# To String
1
def to_s
1
"Task_#{short_id}"
end
# Use to print informations for Track.
#
# Options:
#
# - `:full_id` (Boolean) Show full Task ID.
1
def to_track_array(options = Hash.new)
0
full_id_opt = options.fetch(:full_id, false) # @TODO full_id unit test
0
full_id = full_id_opt ? self.id : ( self.foreign_id ? self.foreign_id : self.short_id )
0
name_a = ['Task:', full_id]
0
if self.name
0
name_a << self.name
end
0
to_ax = Array.new
0
to_ax << name_a.join(' ')
0
to_ax
end
# Used to print informations to STDOUT.
1
def to_compact_str
0
to_compact_array.join("\n")
end
# Used to print informations to STDOUT.
1
def to_compact_array
2
full_id = self.foreign_id ? self.foreign_id : self.short_id
2
to_ax = Array.new
2
to_ax << 'Task: %s %s' % [full_id, self.name]
2
if self.description
2
to_ax << 'Description: %s' % [self.description]
end
2
if self.estimation
2
to_ax << 'Estimation: %s' % [self.estimation.to_human_s]
end
2
to_ax
end
# Used to print informations to STDOUT.
1
def to_detailed_str
2
to_detailed_array.join("\n")
end
# Used to print informations to STDOUT.
#
# Options:
#
# - `:full_id` (Boolean) Show full Task ID.
1
def to_detailed_array(options = Hash.new)
4
full_id_opt = options.fetch(:full_id, false)
4
full_id = full_id_opt ? self.id : self.short_id
4
to_ax = Array.new
4
to_ax << 'Task: %s' % [full_id]
4
if self.foreign_id
2
to_ax << 'Foreign ID: %s' % [self.foreign_id]
end
4
to_ax << 'Name: %s' % [self.name]
4
if self.description
4
to_ax << 'Description: %s' % [self.description]
end
# Duration
4
duration_human = self.duration.to_human_s
4
to_ax << 'Duration: %s' % [duration_human]
4
duration_man_days = self.duration.to_man_days
4
if duration_human != duration_man_days
4
to_ax << 'Man Unit: %s' % [duration_man_days]
end
# Billed Duration
4
billed_duration_human = self.billed_duration.to_human
4
to_ax << 'Billed Duration: %s' % [billed_duration_human]
# Unbilled Duration
4
unbilled_duration_human = self.unbilled_duration.to_human
4
to_ax << 'Unbilled Duration: %s' % [unbilled_duration_human]
4
if self.estimation
4
to_ax << 'Estimation: %s' % [self.estimation.to_human]
4
to_ax << 'Time Remaining: %s (%s)' % [self.remaining_time_s, self.remaining_time_percent_s]
4
bar_options = {
:total => self.estimation.to_i,
:progress => self.duration.to_i,
:length => 50,
:progress_mark => '#',
:remainder_mark => '-',
}
4
bar = ProgressBar.new(bar_options)
4
to_ax << ' |%s|' % [bar.render]
end
4
if self.hourly_rate
0
to_ax << 'Hourly Rate: %.2f' % [self.hourly_rate]
0
to_ax << 'Flat Rate: %s' % [@has_flat_rate ? 'Yes' : 'No']
0
to_ax << 'Consumed Budge: %.2f' % [self.consumed_budge]
0
if self.estimation
0
to_ax << 'Estimated Budge: %.2f' % [self.estimated_budge]
end
0
if @has_flat_rate
0
to_ax << 'Loss Budge: %.2f' % [self.loss_budge]
end
end
4
tracks = self.tracks
4
first_track = tracks
0
.select{ |track_id, track| track.begin_datetime }
0
.sort_by{ |track_id, track| track.begin_datetime }
.to_h
.values
.first
4
if first_track
0
to_ax << 'Begin Track: %s %s' % [first_track.short_id, first_track.begin_datetime_s]
end
4
last_track = tracks
0
.select{ |track_id, track| track.end_datetime }
0
.sort_by{ |track_id, track| track.end_datetime }
.to_h
.values
.last
4
if last_track
0
to_ax << 'End Track: %s %s' % [last_track.short_id, last_track.end_datetime_s]
end
4
status = self.status.colorized
4
to_ax << 'Status: %s' % [status]
4
if @current_track
0
to_ax << 'Current Track: %s %s' % [@current_track.short_id, @current_track.title]
end
4
tracks_count = tracks.count
4
to_ax << 'Tracks: %d' % [tracks_count]
4
billed_tracks_count = tracks({:billed => true}).count
4
to_ax << 'Billed Tracks: %d' % [billed_tracks_count]
4
unbilled_tracks_count = tracks({:billed => false}).count
4
to_ax << 'Unbilled Tracks: %d' % [unbilled_tracks_count]
4
if tracks_count > 0 && @tracks_opt # --tracks
0
to_ax << 'Track IDs: %s' % [tracks.map{ |track_id, track| track.short_id }.join(' ')]
end
4
if self.file_path
0
to_ax << 'File path: %s' % [self.file_path]
end
4
to_ax
end
# Return formatted String.
#
# Options:
#
# - `:format`
# - `:prefix` - Default: `%`
#
# Format:
#
# - `%id` - ID
# - `%sid` - Short ID
# - `%fid` - Foreign ID
# - `%n` - Name
# - `%d` - Description
# - `%ds` - Duration Seconds
# - `%dh` - Duration Human Format
1
def formatted(options = Hash.new)
39
format = options.fetch(:format, '')
39
prefix = options.fetch(:prefix, '%')
39
formatted_s = format
.gsub("#{prefix}id", self.id)
39
.gsub("#{prefix}sid", self.short_id ? self.short_id : '')
39
.gsub("#{prefix}fid", self.foreign_id ? self.foreign_id : '')
39
.gsub("#{prefix}n", self.name ? self.name : '')
.gsub("#{prefix}ds", self.duration(options).to_s)
39
duration_human = self.duration(options).to_human
39
if duration_human
2
formatted_s.gsub!('%dh', duration_human)
else
37
formatted_s.gsub!('%dh', '')
end
# Must not before %dh and %ds.
39
formatted_s.gsub!("#{prefix}d", self.description ? self.description : '')
39
formatted_s
end
1
def inspect
0
"#<Task_#{short_id} tracks=#{@tracks.count}>"
end
# All methods in this block are static.
1
class << self
# Load a Task from a file into a Task instance.
1
def load_task_from_file(path)
0
task = Task.new
0
task.load_from_file(path)
0
task
end
# Search a Task in a base path for a Track by ID.
# If found a file load it into a Task instance.
1
def load_task_from_file_with_id(base_path, task_id)
0
task_file_path = BasicModel.find_file_by_id(base_path, task_id)
0
if task_file_path
0
load_task_from_file(task_file_path)
end
end
# Create a new Task using a Hash.
#
# Options:
#
# - `:name` (String)
# - `:description` (String)
# - `:estimation` (String|Integer|Duration)
# - `:hourly_rate` (Integer)
1
def create_task_from_hash(options)
1
task = Task.new
1
task.name = options.fetch(:name, nil)
1
task.description = options.fetch(:description, nil)
1
task.estimation = options.fetch(:estimation, nil)
1
task.hourly_rate = options.fetch(:hourly_rate, nil)
1
task.has_flat_rate = options.fetch(:has_flat_rate, false)
1
task
end
end
1
private
# BasicModel Hook
1
def pre_save_to_file
# Meta
0
@meta['foreign_id'] = @foreign_id
0
@meta['name'] = @name
0
@meta['description'] = @description
0
@meta['current_track_id'] = nil
0
if @current_track
0
@meta['current_track_id'] = @current_track.id
end
0
if @estimation
0
@meta['estimation'] = @estimation.to_i
else
0
@meta['estimation'] = nil
end
0
if @hourly_rate
0
@meta['hourly_rate'] = @hourly_rate.to_f
else
0
@meta['hourly_rate'] = nil
end
0
if @has_flat_rate
0
@meta['has_flat_rate'] = @has_flat_rate
else
0
@meta['has_flat_rate'] = false
end
# Tracks
0
@data = @tracks.map{ |track_id, track|
0
[track_id, track.to_h]
}.to_h
0
super()
end
# BasicModel Hook
1
def post_load_from_file
0
@tracks = @data.map{ |track_id, track_h|
0
track = Track.create_track_from_hash(track_h)
0
track.task = self
0
[track_id, track]
}.to_h
0
current_track_id = @meta['current_track_id']
0
if current_track_id
0
@current_track = @tracks[current_track_id]
end
0
@foreign_id = @meta['foreign_id']
0
@name = @meta['name']
0
@description = @meta['description']
0
if @meta['estimation']
0
@estimation = Duration.new(@meta['estimation'])
end
0
if @meta['hourly_rate']
0
@hourly_rate = @meta['hourly_rate'].to_f
end
0
if @meta['has_flat_rate']
0
@has_flat_rate = @meta['has_flat_rate']
end
end
end # class Task
end # module Model
end # module Timr
end #module TheFox