Code Coverage
 
Lines
Covered
18.94% covered (danger)
18.94%
57 / 301
1
require 'csv'
1
module TheFox
1
module Timr
1
module Command
# This Command is very similar to LogCommand. By default it prints all [Tasks](rdoc-ref:TheFox::Timr::Model::Task) of the current month.
#
# Man page: [timr-report(1)](../../../../man/timr-report.1.html)
1
class ReportCommand < BasicCommand
1
include TheFox::Timr::Helper
1
include TheFox::Timr::Error
# Path to man page.
1
MAN_PATH = 'man/timr-report.1'
1
def initialize(argv = Array.new)
9
super()
9
@help_opt = false
9
@tasks_opt = false
9
@tracks_opt = false
9
@from_opt = nil
9
@to_opt = nil
9
@billed_opt = false
9
@unbilled_opt = false
9
@format_opt = nil
9
@csv_opt = nil
9
@force_opt = false
9
loop_c = 0 # Limit the loop.
9
while loop_c < 1024 && argv.length > 0
8
loop_c += 1
8
arg = argv.shift
8
case arg
when '-h', '--help'
1
@help_opt = true
when '-d', '--day'
0
@from_opt, @to_opt = DateTimeHelper.parse_day_argv(argv)
when '-m', '--month'
0
@from_opt, @to_opt = DateTimeHelper.parse_month_argv(argv)
when '-y', '--year'
0
@from_opt, @to_opt = DateTimeHelper.parse_year_argv(argv)
when '-a', '--all'
1
@from_opt = Time.parse('1970-01-01 00:00:00')
1
@to_opt = Time.parse('2099-12-31 23:59:59')
when '--tasks'
1
@tasks_opt = true
when '-t', '--tracks'
2
@tracks_opt = true
when '--billed'
0
@billed_opt = true
when '--unbilled'
0
@unbilled_opt = true
when '--format'
0
@format_opt = argv.shift
when '--csv'
1
@csv_opt = argv.shift
1
if !@csv_opt
1
raise ReportCommandError, 'Invalid value for --csv option.'
end
when '-f', '--force' # -f inofficial, maybe used for --file?
1
@force_opt = true
else
1
raise ReportCommandError, "Unknown argument '#{arg}'. See 'timr report --help'."
end
end
7
today = Date.today
7
unless @from_opt
6
@from_opt = Time.new(today.year, today.month, 1, 0, 0, 0)
end
7
unless @to_opt
6
month_end = Date.new(today.year, today.month, -1)
6
@to_opt = Time.new(today.year, today.month, month_end.day, 23, 59, 59)
end
7
@billed_resolved_opt = nil
7
if @billed_opt || @unbilled_opt
0
if @billed_opt
0
@billed_resolved_opt = true
0
elsif @unbilled_opt
0
@billed_resolved_opt = false
end
end
7
@filter_options = {
:format => '%y-%m-%d %H:%M',
:from => @from_opt,
:to => @to_opt,
:billed => @billed_resolved_opt,
}
7
@csv_filter_options = {
:format => '%F %T %z',
:from => @from_opt,
:to => @to_opt,
}
# Used by
# print_formatted_task_list
# print_formatted_track_list
7
@format_options = {
:format => @format_opt,
:billed => @billed_resolved_opt,
}
7
if @csv_opt
0
if @csv_opt == '-'
# OK
else
0
@csv_opt = Pathname.new(@csv_opt).expand_path
end
end
end
# See BasicCommand#run.
1
def run
0
if @help_opt
0
help
0
return
end
0
@timr = Timr.new(@cwd)
0
if @csv_opt
0
if @tasks_opt
0
export_tasks_csv
0
elsif @tracks_opt
0
export_tracks_csv
else
0
export_tasks_csv
end
0
elsif @format_opt
0
if @tasks_opt
0
print_formatted_task_list
0
elsif @tracks_opt
0
print_formatted_track_list
else
0
print_formatted_task_list
end
else
0
if @tasks_opt
0
print_task_table
0
elsif @tracks_opt
0
print_track_table
else
0
print_task_table
end
end
end
1
private
1
def print_task_table
0
puts "From #{@from_opt.strftime('%F %T %z')}"
0
puts " To #{@to_opt.strftime('%F %T %z')}"
0
puts
0
table_options = {
:headings => [
{:format => '%3s', :label => '###'},
{:format => '%-14s', :label => 'START', :padding_left => ' ', :padding_right => ' '},
{:format => '%-14s', :label => 'END', :padding_left => ' ', :padding_right => ' '},
{:format => '%7s', :label => 'DUR', :padding_left => ' ', :padding_right => ' '},
{:format => '%7s', :label => 'UNB', :padding_left => ' ', :padding_right => ' '},
#{:format => '%3s', :label => 'TRC'},
{:format => '%-6s', :label => 'TASK', :padding_right => ' '},
],
}
0
table = Table.new(table_options)
0
totals = {
:duration => Duration.new,
:unbilled_duration => Duration.new,
:task_c => 0,
:tracks_c => 0,
:begin_datetime => nil,
:end_datetime => nil,
}
0
table_has_rows = false
0
filtered_tasks.each do |task|
0
table_has_rows = true
0
totals[:task_c] += 1
0
tracks = task.tracks
# Task Tracks Count
0
tracks_c = tracks.count
# Global Tracks Count
0
totals[:tracks_c] += tracks_c
# Task Duration
0
duration = task.duration(@filter_options)
0
unbilled_duration = task.unbilled_duration(@filter_options)
# Global Duration Sum
0
totals[:duration] += duration
0
totals[:unbilled_duration] += unbilled_duration
# Task Begin DateTime
0
bdt = task.begin_datetime(@filter_options)
# Task End DateTime
0
edt = task.end_datetime(@filter_options)
# Determine First Begin DateTime of the table.
0
if bdt && (!totals[:begin_datetime] || bdt < totals[:begin_datetime])
0
totals[:begin_datetime] = bdt
end
# Determine Last End DateTime of the table.
0
if edt && (!totals[:end_datetime] || edt > totals[:end_datetime])
0
totals[:end_datetime] = edt
end
table << [
totals[:task_c],
task.begin_datetime_s(@filter_options),
task.end_datetime_s(@filter_options),
duration.to_human_s,
unbilled_duration.to_human_s,
'%s %s' % [task.id_foreign_or_short, task.name(15)]
0
]
end
0
table << []
0
totals[:begin_datetime_s] = totals[:begin_datetime] ? totals[:begin_datetime].localtime.strftime(@filter_options[:format]) : '---'
0
totals[:end_datetime_s] = totals[:end_datetime] ? totals[:end_datetime].localtime.strftime(@filter_options[:format]) : '---'
# Add totals to the bottom.
table << [
nil, # task_c
totals[:begin_datetime_s],
totals[:end_datetime_s],
totals[:duration].to_human_s, # duration
totals[:unbilled_duration].to_human_s, # duration
'TOTAL', # task
0
]
0
if table_has_rows
0
puts table
else
0
puts 'No tasks found.'
end
end
1
def print_formatted_task_list
0
filtered_tasks.each do |task|
0
puts task.formatted(@format_options)
end
end
1
def print_track_table
0
puts "From #{@from_opt.strftime('%F %T %z')}"
0
puts " To #{@to_opt.strftime('%F %T %z')}"
0
puts
0
table_options = {
:headings => [
{:format => '%3s', :label => '###'},
{:format => '%-14s', :label => 'START', :padding_left => ' ', :padding_right => ' '},
{:format => '%-14s', :label => 'END', :padding_left => ' ', :padding_right => ' '},
{:format => '%7s', :label => 'DUR', :padding_left => ' ', :padding_right => ' '},
{:format => '%-6s', :label => 'TASK', :padding_right => ' '},
{:format => '%-6s', :label => 'TRACK', :padding_right => ' '},
],
}
0
table = Table.new(table_options)
0
totals = {
:duration => Duration.new,
:tracks_c => 0,
:begin_datetime => nil,
:end_datetime => nil,
}
0
table_has_rows = false
0
@timr.tracks(@filter_options).each do |track_id, track|
0
table_has_rows = true
0
totals[:tracks_c] += 1
# Track Duration
0
duration = track.duration(@filter_options)
# Global Duration Sum
0
totals[:duration] += duration
# Track Begin DateTime
0
bdt = track.begin_datetime(@filter_options)
# Track End DateTime
0
edt = track.end_datetime(@filter_options)
# Determine First Begin DateTime of the table.
0
if bdt && (!totals[:begin_datetime] || bdt < totals[:begin_datetime])
0
totals[:begin_datetime] = bdt
end
# Determine Last End DateTime of the table.
0
if edt && (!totals[:end_datetime] || edt > totals[:end_datetime])
0
totals[:end_datetime] = edt
end
# Get Task from Track.
0
task = track.task
table << [
totals[:tracks_c],
track.begin_datetime_s(@filter_options),
track.end_datetime_s(@filter_options),
duration.to_human_s,
'%s' % [task.id_foreign_or_short],
'%s %s' % [track.short_id, track.title(15)],
0
]
end
0
table << []
0
totals[:begin_datetime_s] = totals[:begin_datetime] ? totals[:begin_datetime].localtime.strftime(@filter_options[:format]) : '---'
0
totals[:end_datetime_s] = totals[:end_datetime] ? totals[:end_datetime].localtime.strftime(@filter_options[:format]) : '---'
# Add totals to the bottom.
table << [
nil, # task_c
totals[:begin_datetime_s],
totals[:end_datetime_s],
totals[:duration].to_human_s, # duration
'TOTAL', # task
nil, # track
0
]
0
if table_has_rows
0
puts table
else
0
puts 'No tracks found.'
end
end
1
def print_formatted_track_list
0
@timr.tracks(@filter_options).each do |track_id, track|
0
puts track.formatted(@format_options)
end
end
1
def export_tasks_csv
0
if @csv_opt == '-'
0
csv_file_handle = STDOUT
else
0
if @csv_opt.exist? && !@force_opt
0
raise ReportCommandError, "File '#{@csv_opt}' already exist. Use --force to overwrite it."
end
#csv_file_handle = @csv_opt.to_s
0
csv_file_handle = File.open(@csv_opt, 'wb')
end
0
totals = {
:row_c => 0,
:duration => Duration.new,
:estimation => Duration.new,
:remaining_time => Duration.new,
:billed_duration => Duration.new,
:unbilled_duration => Duration.new,
:tracks_c => 0,
:billed_tracks_c => 0,
:unbilled_tracks_c => 0,
:begin_datetime => nil,
:end_datetime => nil,
}
0
csv_options = {
:headers => [
'ROW_NO',
'TASK_ID',
'TASK_FOREIGN_ID',
'TASK_NAME',
'TASK_BEGIN_DATETIME',
'TASK_END_DATETIME',
'TASK_DURATION_HUMAN',
'TASK_DURATION_SECONDS',
'TASK_ESTIMATION_HUMAN',
'TASK_ESTIMATION_SECONDS',
'TASK_REMAINING_TIME_HUMAN',
'TASK_REMAINING_TIME_SECONDS',
'TASK_BILLED_DURATION_HUMAN',
'TASK_BILLED_DURATION_SECONDS',
'TASK_UNBILLED_DURATION_HUMAN',
'TASK_UNBILLED_DURATION_SECONDS',
'TASK_TRACK_COUNT',
'TASK_BILLED_TRACK_COUNT',
'TASK_UNBILLED_TRACK_COUNT',
# 'TASK_SHORTEST_TRACK_DURATION_HUMAN', # @TODO shortest longest track duration
# 'TASK_SHORTEST_TRACK_DURATION_SECONDS',
# 'TASK_LONGEST_TRACK_DURATION_HUMAN',
# 'TASK_LONGEST_TRACK_DURATION_SECONDS',
],
:write_headers => true,
:skip_blanks => true,
#:force_quotes => true,
}
0
csv = CSV.new(csv_file_handle, csv_options)
0
filtered_tasks.each do |task|
0
totals[:row_c] += 1
0
tracks = task.tracks
# Task Tracks Count
0
tracks_c = tracks.count
0
billed_tracks_c = task.tracks({:billed => true}).count
0
unbilled_tracks_c = task.tracks({:billed => false}).count
# Global Tracks Count
0
totals[:tracks_c] += tracks_c
0
totals[:billed_tracks_c] += billed_tracks_c
0
totals[:unbilled_tracks_c] += unbilled_tracks_c
# Task Duration
0
duration = task.duration(@csv_filter_options)
0
estimation = task.estimation
0
remaining_time = task.remaining_time
0
billed_duration = task.billed_duration(@csv_filter_options)
0
unbilled_duration = task.unbilled_duration(@csv_filter_options)
# Global Duration Sum
0
if duration
0
totals[:duration] += duration
end
0
if estimation
0
totals[:estimation] += estimation
end
0
if remaining_time
0
totals[:remaining_time] += remaining_time
end
0
if billed_duration
0
totals[:billed_duration] += billed_duration
end
0
if unbilled_duration
0
totals[:unbilled_duration] += unbilled_duration
end
# Task Begin DateTime
0
bdt = task.begin_datetime(@csv_filter_options)
# Task End DateTime
0
edt = task.end_datetime(@csv_filter_options)
# Determine First Begin DateTime of the table.
0
if bdt && (!totals[:begin_datetime] || bdt < totals[:begin_datetime])
0
totals[:begin_datetime] = bdt
end
# Determine Last End DateTime of the table.
0
if edt && (!totals[:end_datetime] || edt > totals[:end_datetime])
0
totals[:end_datetime] = edt
end
0
row = [
totals[:row_c],
task.id,
task.foreign_id,
task.name,
task.begin_datetime_s(@csv_filter_options),
task.end_datetime_s(@csv_filter_options),
duration.to_human_s,
duration.to_i,
]
0
if estimation
0
row << estimation.to_human_s
0
row << estimation.to_i
else
0
row << '---'
0
row << 0
end
0
if remaining_time
0
row << remaining_time.to_human_s
0
row << remaining_time.to_i
else
0
row << '---'
0
row << 0
end
0
row << billed_duration.to_human_s
0
row << billed_duration.to_i
0
row << unbilled_duration.to_human_s
0
row << unbilled_duration.to_i
0
row << tracks_c
0
row << billed_tracks_c
0
row << unbilled_tracks_c
0
csv << row
end
0
totals[:begin_datetime_s] = totals[:begin_datetime] ? totals[:begin_datetime].localtime.strftime(@csv_filter_options[:format]) : '---'
0
totals[:end_datetime_s] = totals[:end_datetime] ? totals[:end_datetime].localtime.strftime(@csv_filter_options[:format]) : '---'
0
totals[:row_c] += 1
csv << [
totals[:row_c],
'TOTAL',
nil,
nil,
totals[:begin_datetime_s],
totals[:end_datetime_s],
totals[:duration].to_human_s,
totals[:duration].to_i,
totals[:estimation].to_human_s,
totals[:estimation].to_i,
totals[:remaining_time].to_human_s,
totals[:remaining_time].to_i,
totals[:billed_duration].to_human_s,
totals[:billed_duration].to_i,
totals[:unbilled_duration].to_human_s,
totals[:unbilled_duration].to_i,
totals[:tracks_c],
totals[:billed_tracks_c],
totals[:unbilled_tracks_c],
0
]
0
csv.close
end
1
def export_tracks_csv
0
if @csv_opt == '-'
0
csv_file_handle = STDOUT
else
0
if @csv_opt.exist? && !@force_opt
0
raise ReportCommandError, "File '#{@csv_opt}' already exist. Use --force to overwrite it."
end
#csv_file_handle = @csv_opt.to_s
0
csv_file_handle = File.open(@csv_opt, 'wb')
end
0
totals = {
:duration => Duration.new,
:billed_duration => Duration.new,
:unbilled_duration => Duration.new,
:row_c => 0,
:begin_datetime => nil,
:end_datetime => nil,
}
0
csv_options = {
:headers => [
'ROW_NO',
'TASK_ID',
'TASK_FOREIGN_ID',
'TASK_NAME',
'TRACK_ID',
'TRACK_TITLE',
'TRACK_BEGIN_DATETIME',
'TRACK_END_DATETIME',
'TRACK_DURATION_HUMAN',
'TRACK_DURATION_SECONDS',
'TRACK_BILLED_DURATION_HUMAN',
'TRACK_BILLED_DURATION_SECONDS',
'TRACK_UNBILLED_DURATION_HUMAN',
'TRACK_UNBILLED_DURATION_SECONDS',
'TRACK_IS_BILLED',
],
:write_headers => true,
:skip_blanks => true,
#:force_quotes => true,
}
0
csv = CSV.new(csv_file_handle, csv_options)
0
@timr.tracks(@csv_filter_options).each do |track_id, track|
0
totals[:row_c] += 1
# Track Duration
0
duration = track.duration(@csv_filter_options)
0
billed_duration = track.billed_duration(@csv_filter_options)
0
unbilled_duration = track.unbilled_duration(@csv_filter_options)
# Global Duration Sum
0
if duration
0
totals[:duration] += duration
end
0
if billed_duration
0
totals[:billed_duration] += billed_duration
end
0
if unbilled_duration
0
totals[:unbilled_duration] += unbilled_duration
end
# Get Task from Track.
0
task = track.task
# Track Begin DateTime
0
bdt = track.begin_datetime(@csv_filter_options)
# Track End DateTime
0
edt = track.end_datetime(@csv_filter_options)
# Determine First Begin DateTime of the table.
0
if bdt && (!totals[:begin_datetime] || bdt < totals[:begin_datetime])
0
totals[:begin_datetime] = bdt
end
# Determine Last End DateTime of the table.
0
if edt && (!totals[:end_datetime] || edt > totals[:end_datetime])
0
totals[:end_datetime] = edt
end
0
row = [
totals[:row_c],
task.id,
task.foreign_id,
task.name,
track.id,
track.title,
track.begin_datetime_s(@csv_filter_options),
track.end_datetime_s(@csv_filter_options),
duration.to_human_s,
duration.to_i,
billed_duration.to_human_s,
billed_duration.to_i,
unbilled_duration.to_human_s,
unbilled_duration.to_i,
track.is_billed.to_i,
]
0
csv << row
end
0
totals[:begin_datetime_s] = totals[:begin_datetime] ? totals[:begin_datetime].localtime.strftime(@csv_filter_options[:format]) : '---'
0
totals[:end_datetime_s] = totals[:end_datetime] ? totals[:end_datetime].localtime.strftime(@csv_filter_options[:format]) : '---'
0
totals[:row_c] += 1
0
row = [
totals[:row_c],
'TOTAL',
nil,
nil,
nil,
nil,
totals[:begin_datetime_s],
totals[:end_datetime_s],
totals[:duration].to_human_s,
totals[:duration].to_i,
totals[:billed_duration].to_human_s,
totals[:billed_duration].to_i,
totals[:unbilled_duration].to_human_s,
totals[:unbilled_duration].to_i,
]
0
csv << row
0
csv.close
end
1
def filtered_tasks
# Get Tracks.
0
tracks = @timr.tracks(@filter_options)
# Convert Tracks to Tasks. Convert the Array to a Set removes all duplicates.
0
tracks.map{ |track_id, track| track.task }.to_set
end
1
def help
0
puts 'usage: timr report [-d|--day <date>] [-m|--month <[YYYY-]MM>]'
0
puts ' [-y|--year [<YYYY>]] [-a|--all] [--tasks|--tracks]'
0
puts ' [--billed|--unbilled]'
0
puts ' [--csv <path>] [--force] [--format <str>]'
0
puts ' or: timr report [-h|--help]'
0
puts
0
puts 'Options'
0
puts ' -d, --day [<date>] A single day from 00:00 to 23:59.'
0
puts ' -m, --month <[YYYY-]MM> A single month from 01 to 31.'
0
puts ' -y, --year [<YYYY>] A single year from 01-01 to 12-31.'
0
puts ' -a, --all All.'
0
puts ' --tasks Export Tasks (default)'
0
puts ' --tracks Export Tracks'
0
puts ' --billed Filter only Tasks/Tracks which are billed.'
0
puts ' --unbilled Filter only Tasks/Tracks which are not billed.'
0
puts ' --format <str> Format Tasks and Tracks output.'
0
puts " --csv <path> Export as CSV file. Use '--csv -' to use STDOUT."
0
puts " --force Force overwrite file."
0
puts
0
puts 'For column descriptions see man page:'
0
puts "'timr help report'"
0
puts
0
HelpCommand.print_datetime_help
0
puts
0
puts 'Task Format'
0
puts ' %id ID'
0
puts ' %sid Short ID'
0
puts ' %fid Foreign ID'
0
puts ' %n Name'
0
puts ' %d Description'
0
puts ' %ds Duration Seconds'
0
puts ' %dh Duration Human Format'
0
puts
0
puts 'Track Format'
0
puts ' %id ID'
0
puts ' %sid Short ID'
0
puts ' %m Message'
0
puts ' %bdt Begin DateTime'
0
puts ' %bd Begin Date'
0
puts ' %bt Begin Time'
0
puts ' %edt End DateTime'
0
puts ' %ed End Date'
0
puts ' %et End Time'
0
puts ' %ds Duration Seconds'
0
puts ' %dh Duration Human Format'
0
puts ' %bi Billed Integer'
0
puts ' %bh Billed Human Format (YES, NO)'
0
puts
0
puts "Use '%T' prefix for each Task attribute for Track formatting."
0
puts "For example use '%Tid' to use the Task ID."
0
puts
end
end # class TrackCommand
end # module Command
end # module Timr
end # module TheFox