Code Coverage
 
Lines
Covered
80.51% covered (warning)
80.51%
219 / 272
1
require 'time'
1
module TheFox
1
module Timr
1
module Model
1
class Track < BasicModel
1
include TheFox::Timr::Helper
1
include TheFox::Timr::Error
# Parent Task instance
1
attr_accessor :task
# Track Message. What have you done?
1
attr_reader :message
# Is this even in use? ;D
1
attr_accessor :paused
1
attr_reader :is_billed
1
def initialize
86
super()
86
@task = nil
86
@begin_datetime = nil
86
@end_datetime = nil
86
@is_billed = false
86
@message = nil
86
@paused = false
end
# Set begin_datetime.
1
def begin_datetime=(begin_datetime)
57
case begin_datetime
when String
56
begin_datetime = Time.parse(begin_datetime)
when Time
# OK
else
1
raise TrackError, "begin_datetime needs to be a String or Time, #{begin_datetime.class} given."
end
56
if @end_datetime && begin_datetime >= @end_datetime
2
raise TrackError, 'begin_datetime must be lesser than end_datetime.'
end
54
@begin_datetime = begin_datetime
# Mark Track as changed.
54
changed
end
# Get begin_datetime.
#
# Options:
#
# - `:from` (Time)
# See documentation about `:to` on `end_datetime()`.
1
def begin_datetime(options = Hash.new)
282
from_opt = options.fetch(:from, nil)
282
if @begin_datetime
237
if from_opt && from_opt > @begin_datetime
2
bdt = from_opt
else
235
bdt = @begin_datetime
end
237
bdt.localtime
end
end
# Get begin_datetime String.
#
# Options:
#
# - `:format` (String)
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
2
bdt.strftime(format_opt)
else
1
'---'
end
end
# Set end_datetime.
1
def end_datetime=(end_datetime)
56
if !@begin_datetime
2
raise TrackError, 'end_datetime cannot be set until begin_datetime is set.'
end
54
case end_datetime
when String
53
end_datetime = Time.parse(end_datetime)
when Time
# OK
else
1
raise TrackError, "end_datetime needs to be a String or Time, #{end_datetime.class} given."
end
53
if end_datetime <= @begin_datetime
2
raise TrackError, 'end_datetime must be greater than begin_datetime.'
end
51
@end_datetime = end_datetime
# Mark Track as changed.
51
changed
end
# Get end_datetime.
#
# Options:
#
# - `:to` (Time)
# This limits `@end_datetime`. If `:to` > `@end_datetime` it returns the
# original `@end_datetime`. Otherwise it will return `:to`. The same applies for
# `:from` on `begin_datetime()` but just the other way round.
1
def end_datetime(options = Hash.new)
218
to_opt = options.fetch(:to, nil)
218
if @end_datetime
170
if to_opt && to_opt < @end_datetime
2
edt = to_opt
else
168
edt = @end_datetime
end
170
edt.localtime
end
end
# Get end_datetime String.
#
# Options:
#
# - `:format` (String)
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 message.
1
def message=(message)
8
@message = message
# Mark Track as changed.
8
changed
end
# Start this Track. A Track cannot be restarted because it's the smallest time unit.
1
def start(options = Hash.new)
8
message_opt = options.fetch(:message, nil)
8
if @begin_datetime
1
raise TrackError, 'Cannot restart Track. Use dup() on this instance or create a new instance by using Track.new().'
end
7
@begin_datetime = DateTimeHelper.get_datetime_from_options(options)
6
if message_opt
1
@message = message_opt
end
end
# Stop this Track.
#
# Options:
#
# - `:start_date`
# - `:start_time`
# - `:end_date`, `:date`
# - `:end_time`, `:time`
# - `:message` (String)
1
def stop(options = Hash.new)
1
start_date_opt = options.fetch(:start_date, nil)
1
start_time_opt = options.fetch(:start_time, nil)
1
end_date_opt = options.fetch(:end_date, options.fetch(:date, nil))
1
end_time_opt = options.fetch(:end_time, options.fetch(:time, nil))
1
message_opt = options.fetch(:message, nil)
# paused_opt = options.fetch(:paused, false)
# Set Start DateTime
1
if start_date_opt || start_time_opt
0
begin_options = {
:date => start_date_opt,
:time => start_time_opt,
}
0
@begin_datetime = DateTimeHelper.get_datetime_from_options(begin_options)
end
# Set End DateTime
1
end_options = {
:date => end_date_opt,
:time => end_time_opt,
}
1
@end_datetime = DateTimeHelper.get_datetime_from_options(end_options)
1
if message_opt
1
@message = message_opt
end
# @paused = paused_opt
# Mark Track as changed.
1
changed
end
# Cacluates the secondes between begin and end datetime and returns a new Duration instance.
#
# Options:
#
# - `:from` (Time), `:to` (Time)
# Limit the begin and end datetimes to a specific range.
# - `:billed` (Boolean)
1
def duration(options = Hash.new)
145
from_opt = options.fetch(:from, nil)
145
to_opt = options.fetch(:to, nil)
145
billed_opt = options.fetch(:billed, nil)
145
unless billed_opt.nil?
21
if @is_billed != billed_opt
2
return Duration.new(0)
end
end
143
if @begin_datetime
110
bdt = @begin_datetime.utc
end
143
if @end_datetime
110
edt = @end_datetime.utc
else
33
edt = Time.now.utc
end
# Cut Start
143
if from_opt && bdt && from_opt > bdt
5
bdt = from_opt.utc
end
# Cut End
143
if to_opt && edt && to_opt < edt
4
edt = to_opt.utc
end
143
seconds = 0
143
if bdt && edt
110
if bdt < edt
109
seconds = (edt - bdt).to_i
end
end
143
Duration.new(seconds)
end
# Alias method.
1
def billed_duration(options = Hash.new)
2
duration(options.merge({:billed => true}))
end
# Alias method.
1
def unbilled_duration(options = Hash.new)
2
duration(options.merge({:billed => false}))
end
# When begin_datetime is `2017-01-01 01:15`
# and end_datetime is `2017-01-03 02:17`
# this function returns
#
# ```
# [
# Date.new(2017, 1, 1),
# Date.new(2017, 1, 2),
# Date.new(2017, 1, 3),
# ]
# ```
1
def days
0
begin_date = @begin_datetime.to_date
0
end_date = @end_datetime.to_date
0
begin_date.upto(end_date)
end
# Evaluates the Short Status and returns a new Status instance.
1
def status
20
if @begin_datetime.nil?
7
short_status = '-' # not started
13
elsif @end_datetime.nil?
5
short_status = 'R' # running
8
elsif @end_datetime
8
if @paused
# It's actually stopped but with an additional flag.
2
short_status = 'P' # paused
else
6
short_status = 'S' # stopped
end
else
0
short_status = 'U' # unknown
end
20
Status.new(short_status)
end
# Is the Track running?
1
def running?
0
status.short_status == 'R' # running
end
# Is the Track stopped?
1
def stopped?
0
status.short_status == 'S' # stopped
end
# Title generated from message. If the message has multiple lines only the first
# line will be taken to create the title.
#
# `max_length` can be used to define a maximum length. Three dots `...` will be appended
# at the end if the title is longer than `max_length`.
1
def title(max_length = nil)
49
unless @message
15
return
end
34
msg = @message.split("\n\n").first.split("\n").first
34
if max_length && msg.length > max_length + 2
1
msg = msg[0, max_length] << '...'
end
34
msg
end
# Title Alias
1
def name(max_length = nil)
0
title(max_length)
end
# Set is_billed.
1
def is_billed=(is_billed)
20
@is_billed = is_billed
# Mark Track as changed.
20
changed
end
# When the Track is marked as changed it needs to mark the Task as changed.
#
# A single Track cannot be stored to a file. Tracks are assiged to a Task and are stored to the Task file.
1
def changed
156
super()
156
if @task
14
@task.changed
end
end
# Alias for Task. A Track cannot saved to a file. Only the whole Task.
1
def save_to_file(path = nil, force = false)
0
if @task
0
@task.save_to_file(path, force)
end
end
# Duplicate this Track using the same Message. This is used almost by every Command.
# Start, Continue, Push, etc.
1
def dup
1
track = Track.new
1
track.task = @task
1
track.message = @message.clone
1
track
end
# Removes itself from parent Task.
1
def remove
3
if @task
2
@task.remove_track(self)
else
1
false
end
end
# To String
1
def to_s
1
"Track_#{short_id}"
end
# To Hash
1
def to_h
1
h = {
'id' => @meta['id'],
'short_id' => short_id, # Not used.
'created' => @meta['created'],
'modified' => @meta['modified'],
'is_billed' => @is_billed,
'message' => @message,
}
1
if @begin_datetime
0
h['begin_datetime'] = @begin_datetime.utc.strftime(MODEL_DATETIME_FORMAT)
end
1
if @end_datetime
0
h['end_datetime'] = @end_datetime.utc.strftime(MODEL_DATETIME_FORMAT)
end
1
h
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
0
to_ax = Array.new
0
if @task
0
to_ax.concat(@task.to_track_array)
end
0
to_ax << 'Track: %s %s' % [self.short_id, self.title]
# if self.title
# to_ax << 'Title: %s' % [self.title]
# end
0
if self.begin_datetime
0
to_ax << 'Start: %s' % [self.begin_datetime_s]
end
0
if self.end_datetime
0
to_ax << 'End: %s' % [self.end_datetime_s]
end
0
if self.duration && self.duration.to_i > 0
0
to_ax << 'Duration: %s' % [self.duration.to_human_s]
end
0
to_ax << 'Status: %s' % [self.status.colorized]
# if self.message
# to_ax << 'Message: %s' % [self.message]
# end
0
to_ax
end
# Used to print informations to STDOUT.
1
def to_detailed_str(options = Hash.new)
1
to_detailed_array(options).join("\n")
end
# Used to print informations to STDOUT.
#
# Options:
#
# - `:full_id` (Boolean) Show full Task and Track IDs.
1
def to_detailed_array(options = Hash.new)
2
full_id_opt = options.fetch(:full_id, false) # @TODO full_id unit test
2
to_ax = Array.new
2
if @task
0
to_ax.concat(@task.to_track_array(options))
end
2
if full_id_opt
0
to_ax << 'Track: %s' % [self.id]
else
2
to_ax << 'Track: %s' % [self.short_id]
end
2
if self.begin_datetime
0
to_ax << 'Start: %s' % [self.begin_datetime_s]
end
2
if self.end_datetime
0
to_ax << 'End: %s' % [self.end_datetime_s]
end
2
if self.duration && self.duration.to_i > 0
0
duration_human = self.duration.to_human_s
0
to_ax << 'Duration: %s' % [duration_human]
0
duration_man_days = self.duration.to_man_days
0
if duration_human != duration_man_days
0
to_ax << 'Man Unit: %s' % [duration_man_days]
end
end
2
to_ax << 'Billed: %s' % [self.is_billed ? 'Yes' : 'No']
2
to_ax << 'Status: %s' % [self.status.colorized]
2
if self.message
0
to_ax << 'Message: %s' % [self.message]
end
2
to_ax
end
# Are two Tracks equal?
#
# Uses ID for comparision.
1
def eql?(track)
0
unless track.is_a?(Track)
0
raise TrackError, "track variable must be a Track instance. #{track.class} given."
end
0
self.id == track.id
end
# Return formatted String.
#
# Options:
#
# - `:format`
#
# Format:
#
# - `%id` - ID
# - `%sid` - Short ID
# - `%t` - Title generated from message.
# - `%m` - Message
# - `%bdt` - Begin DateTime
# - `%bd` - Begin Date
# - `%bt` - Begin Time
# - `%edt` - End DateTime
# - `%ed` - End Date
# - `%et` - End Time
# - `%ds` - Duration Seconds
# - `%dh` - Duration Human Format
# - `%bi` - Billed Integer
# - `%bh` - Billed Human Format (YES, NO)
1
def formatted(options = Hash.new)
27
format = options.fetch(:format, '')
27
formatted_s = format
.gsub('%id', self.id)
.gsub('%sid', self.short_id)
27
.gsub('%t', self.title ? self.title : '')
27
.gsub('%m', self.message ? self.message : '')
27
.gsub('%bdt', self.begin_datetime ? self.begin_datetime.strftime('%F %H:%M') : '')
27
.gsub('%bd', self.begin_datetime ? self.begin_datetime.strftime('%F') : '')
27
.gsub('%bt', self.begin_datetime ? self.begin_datetime.strftime('%H:%M') : '')
27
.gsub('%edt', self.end_datetime ? self.end_datetime.strftime('%F %H:%M') : '')
27
.gsub('%ed', self.end_datetime ? self.end_datetime.strftime('%F') : '')
27
.gsub('%et', self.end_datetime ? self.end_datetime.strftime('%H:%M') : '')
.gsub('%ds', self.duration.to_s)
.gsub('%bi', self.is_billed.to_i.to_s)
27
.gsub('%bh', self.is_billed ? 'YES' : 'NO')
27
duration_human = self.duration.to_human
27
if duration_human
13
formatted_s.gsub!('%dh', self.duration.to_human)
else
14
formatted_s.gsub!('%dh', '')
end
27
task_formating_options = {
:format => formatted_s,
:prefix => '%T',
}
27
if @task
3
formatted_s = @task.formatted(task_formating_options)
else
24
tmp_task = Task.new
24
tmp_task.id = ''
24
formatted_s = tmp_task.formatted(task_formating_options)
end
27
formatted_s
end
1
def inspect
0
"#<Track #{short_id}>"
end
# All methods in this block are static.
1
class << self
# Create a new Track instance from a Hash.
1
def create_track_from_hash(hash)
2
unless hash.is_a?(Hash)
1
raise TrackError, "hash variable must be a Hash instance. #{hash.class} given."
end
1
track = Track.new
1
if hash['id']
0
track.id = hash['id']
end
1
if hash['created']
0
track.created = hash['created']
end
1
if hash['modified']
0
track.modified = hash['modified']
end
1
if hash['is_billed']
0
track.is_billed = hash['is_billed']
end
1
if hash['message']
0
track.message = hash['message']
end
1
if hash['begin_datetime']
0
track.begin_datetime = hash['begin_datetime']
end
1
if hash['end_datetime']
0
track.end_datetime = hash['end_datetime']
end
1
track.has_changed = false
1
track
end
# This is really bad. Do not use this.
1
def find_track_by_id(base_path, track_id)
1
found_track = nil
# Iterate all files.
1
base_path.find.each do |file|
# Filter all directories.
4
unless file.file?
3
next
end
# Filter all non-yaml files.
1
unless file.basename.fnmatch('*.yml')
1
next
end
0
task = Task.load_task_from_file(file)
0
tmp_track = task.find_track_by_id(track_id)
0
if tmp_track
0
if found_track
0
raise TrackError, "Track ID '#{track_id}' is not a unique identifier."
else
0
found_track = tmp_track
# Do not break the loop here.
end
end
end
1
found_track
end
end
end # class Track
end # module Model
end # module Timr
end #module TheFox