CmdTimesheet: Rewrote the command

- Updated the 'timesheet' command with a more compact report that accepts a
  filter, and has a default filter showing the last four weeks of completed and
  started tasks.
This commit is contained in:
Paul Beckingham 2017-02-09 08:19:30 -05:00
parent 4c20ff04c2
commit 440cfb009e
10 changed files with 182 additions and 190 deletions

View file

@ -80,6 +80,9 @@
(thanks to Jelle van der Waa).
- Improved portability for SunOS-like OSes.
(thanks to Antonio Huete Jimenez).
- Updated the 'timesheet' command with a more compact report that accepts a
filter, and has a default filter showing the last four weeks of completed and
started tasks.
------ current release ---------------------------

9
NEWS
View file

@ -4,20 +4,25 @@ New Features in Taskwarrior 2.6.0
- The 'QUARTER' virutal tag was added.
- Improved compatibility with SmartOS, OmniOS and OpenIndiana.
- New DOM reference: annotations.count.
- Renovated 'timesheet' command with a more compact report that accepts a
filter, and has a default filter showing the last four weeks of completed
and started tasks.
New Commands in Taskwarrior 2.6.0
- The 'purge' command was introduced.
- The 'purge' command was added, which completely removes old tasks.
New Configuration Options in Taskwarrior 2.6.0
- The 'default.scheduled' date/duraiton works just like 'default.due'.
- The 'report.timesheet.filter' setting controls the tasks shown by the
'timesheet' command.
Newly Deprecated Features in Taskwarrior 2.6.0
- The 'DUETODAY' virtual tag is a synonym for the 'TODAY' virtual tag, and is
not needed.
- The 'new-uuid' verbosity option is to be removed due to being redundant, its
- The 'new-uuid' verbosity option is to be removed, as it is redundant, its
functionality will be merged with 'new-id' option.
- The use of alternate Boolean configuration settings is deprecated. Use values
"0" for off, and "1" for on. Avoid used of "on", "off", "true", "t",

View file

@ -1279,6 +1279,9 @@ will be presented in the order (if any) in which they are selected.
This adds a filter to the report X so that only tasks matching the filter
criteria are displayed in the generated report.
There is a special case for 'report.timesheet.filter', which may be specified
even though the 'timesheet' report is not very customizable.
.TP
.B report.X.dateformat
This adds a dateformat to the report X that will be used by the "due date"

View file

@ -251,6 +251,7 @@ std::string Config::_defaults =
"#default.due=eom # Default due date for 'add' command\n"
"#default.scheduled=eom # Default scheduled date for 'add' command\n"
"default.command=next # When no arguments are specified\n"
"default.timesheet.filter=( +PENDING and start.after:now-4wks ) or ( +COMPLETED and end.after:now-4wks )\n"
"\n"
"_forcecolor=0 # Forces color to be on, even for non TTY output\n"
"complete.all.tags=0 # Include old tag names in '_ags' command\n"
@ -383,7 +384,9 @@ std::string Config::_defaults =
"report.blocking.labels=ID,UUID,A,Deps,Project,Tags,R,W,Sch,Due,Until,Description,Urg\n"
"report.blocking.columns=id,uuid.short,start.active,depends,project,tags,recur,wait,scheduled.remaining,due.relative,until.remaining,description.count,urgency\n"
"report.blocking.sort=urgency-,due+,entry+\n"
"report.blocking.filter= status:pending +BLOCKING\n"
"report.blocking.filter=status:pending +BLOCKING\n"
"\n"
"report.timesheet.filter=(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)\n"
"\n";
extern Context context;

View file

@ -103,7 +103,7 @@ int CmdInfo::execute (std::string& output)
view.width (context.getWidth ());
if (context.config.getBoolean ("obfuscate"))
view.obfuscate ();
if (context.config.getBoolean ("color"))
if (context.color ())
view.forceColor ();
view.add (STRING_COLUMN_LABEL_NAME);
view.add (STRING_COLUMN_LABEL_VALUE);
@ -113,11 +113,11 @@ int CmdInfo::execute (std::string& output)
{
Color alternate (context.config.get ("color.alternate"));
view.colorOdd (alternate);
view.intraColorOdd (alternate);
Color label (context.config.get ("color.label"));
view.colorHeader (label);
view.colorHeader (Color ("underline " + context.config.get ("color.label")));
}
else
view.underlineHeaders ();
Datetime now;
@ -444,8 +444,7 @@ int CmdInfo::execute (std::string& output)
urgencyDetails.colorOdd (alternate);
urgencyDetails.intraColorOdd (alternate);
Color label (context.config.get ("color.label"));
urgencyDetails.colorHeader (label);
urgencyDetails.colorHeader (Color ("underline " + context.config.get ("color.label")));
}
if (context.config.getBoolean ("obfuscate"))
@ -548,9 +547,10 @@ int CmdInfo::execute (std::string& output)
journal.colorOdd (alternate);
journal.intraColorOdd (alternate);
Color label (context.config.get ("color.label"));
journal.colorHeader (label);
journal.colorHeader (Color ("underline " + context.config.get ("color.label")));
}
else
journal.underlineHeaders ();
if (context.config.getBoolean ("obfuscate"))
journal.obfuscate ();

View file

@ -147,6 +147,7 @@ int CmdShow::execute (std::string& output)
" default.scheduled"
" defaultheight"
" defaultwidth"
" default.timesheet.filter"
" dependency.confirmation"
" dependency.indicator"
" dependency.reminder"

View file

@ -26,6 +26,7 @@
#include <cmake.h>
#include <CmdTimesheet.h>
#include <algorithm>
#include <sstream>
#include <stdlib.h>
#include <Context.h>
@ -33,6 +34,7 @@
#include <Table.h>
#include <Datetime.h>
#include <main.h>
#include <util.h>
#include <i18n.h>
#include <format.h>
@ -42,16 +44,16 @@ extern Context context;
CmdTimesheet::CmdTimesheet ()
{
_keyword = "timesheet";
_usage = "task timesheet [weeks]";
_usage = "task [filter] timesheet";
_description = STRING_CMD_TIMESHEET_USAGE;
_read_only = true;
_displays_id = false;
_needs_gc = true;
_uses_context = false;
_accepts_filter = false;
_accepts_filter = true;
_accepts_modifications = false;
_accepts_miscellaneous = true;
_category = Command::Category::graphs;
_accepts_miscellaneous = false;
_category = Command::Category::report;
}
////////////////////////////////////////////////////////////////////////////////
@ -59,165 +61,145 @@ int CmdTimesheet::execute (std::string& output)
{
int rc = 0;
// Scan the pending tasks.
handleRecurrence ();
std::vector <Task> all = context.tdb2.all_tasks ();
// What day of the week does the user consider the first?
int weekStart = Datetime::dayOfWeek (context.config.get ("weekstart"));
if (weekStart != 0 && weekStart != 1)
throw std::string (STRING_DATE_BAD_WEEKSTART);
// Determine the date of the first day of the most recent report.
Datetime today;
Datetime start;
start -= (((today.dayOfWeek () - weekStart) + 7) % 7) * 86400;
// Roll back to midnight.
start = Datetime (start.year (), start.month (), start.day ());
Datetime end = start + (7 * 86400);
// Determine how many reports to run.
int quantity = 1;
std::vector <std::string> words = context.cli2.getWords ();
if (words.size () == 1)
quantity = strtol (words[0].c_str (), NULL, 10);;
std::stringstream out;
for (int week = 0; week < quantity; ++week)
// Detect a filter.
bool hasFilter {false};
for (auto& a : context.cli2._args)
{
Datetime endString (end);
endString -= 86400;
std::string title = start.toString (context.config.get ("dateformat"))
+ " - "
+ endString.toString (context.config.get ("dateformat"));
Color bold;
if (context.color ())
bold = Color ("bold");
out << '\n'
<< bold.colorize (title)
<< '\n';
// Render the completed table.
Table completed;
completed.width (context.getWidth ());
completed.add (" ");
completed.add (STRING_COLUMN_LABEL_PROJECT);
completed.add (STRING_COLUMN_LABEL_DUE, false);
completed.add (STRING_COLUMN_LABEL_DESC);
Color label;
if (context.color ())
if (a.hasTag ("FILTER"))
{
label = Color (context.config.get ("color.label"));
completed.colorHeader (label);
hasFilter = true;
break;
}
}
for (auto& task : all)
if (! hasFilter)
{
auto defaultFilter = context.config.get ("report.timesheet.filter");
if (defaultFilter == "")
defaultFilter = "(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)";
context.cli2.addFilter (defaultFilter);
}
// Apply filter to get a set of tasks.
handleRecurrence ();
Filter filter;
std::vector <Task> filtered;
filter.subset (filtered);
// Subset the tasks to only those that are either completed or started.
// The _key attribute is represents either the 'start' or 'end' date.
int num_completed = 0;
int num_started = 0;
std::vector <Task> shown;
for (auto& task : filtered)
{
// If task completed within range.
if (task.getStatus () == Task::completed)
{
Datetime compDate (task.get_date ("end"));
if (compDate >= start && compDate < end)
task.set ("_key", task.get ("end"));
++num_completed;
}
if (task.getStatus () == Task::pending && task.has ("start"))
{
Color c;
autoColorize (task, c);
task.set ("_key", task.get ("start"));
++num_started;
}
int row = completed.addRow ();
std::string format = context.config.get ("dateformat.report");
if (format == "")
format = context.config.get ("dateformat");
completed.set (row, 1, task.get ("project"), c);
shown.push_back (task);
}
if(task.has ("due"))
// Sort tasks by _key.
std::sort (shown.begin (),
shown.end (),
[](const Task& a, const Task& b) { return a.get ("_key") < b.get ("_key"); });
// Render the completed table.
Table table;
table.width (context.getWidth ());
if (context.config.getBoolean ("obfuscate"))
table.obfuscate ();
table.add ("Wk");
table.add ("Date");
table.add ("Day");
table.add ("Action");
table.add ("Project");
table.add ("Due");
table.add ("Task");
if (context.color ())
{
Datetime dt (task.get_date ("due"));
completed.set (row, 2, dt.toString (format));
table.forceColor ();
table.colorHeader (Color ("underline " + context.config.get ("color.label")));
table.colorOdd (Color (context.config.get ("color.alternate")));
}
else
table.underlineHeaders ();
auto dateformat = context.config.get ("dateformat");
int previous_week = -1;
std::string previous_date = "";
std::string previous_day = "";
int weekCounter = 0;
Color week_color;
for (auto& task : shown)
{
Datetime key (task.get_date ("_key"));
std::string label = task.has ("end") ? "Completed"
: task.has ("start") ? "Started"
: "";
auto week = key.week ();
auto date = key.toString (dateformat);
auto due = task.has ("due") ? Datetime (task.get ("due")).toString (dateformat) : "";
auto day = Datetime::dayNameShort (key.dayOfWeek ());
Color task_color;
autoColorize (task, task_color);
// Add a blank line between weeks.
if (week != previous_week && previous_week != -1)
{
auto row = table.addRowEven ();
table.set (row, 0, " ");
}
std::string description = task.get ("description");
int indent = context.config.getInteger ("indent.annotation");
// Keep track of unique week numbers.
if (week != previous_week)
++weekCounter;
for (auto& ann : task.getAnnotations ())
description += '\n'
+ std::string (indent, ' ')
+ Datetime (ann.first.substr (11)).toString (context.config.get ("dateformat"))
+ ' '
+ ann.second;
// User-defined oddness.
int row;
if (weekCounter % 2)
row = table.addRowOdd ();
else
row = table.addRowEven ();
completed.set (row, 3, description, c);
}
}
// If the data doesn't change, it doesn't get shown.
table.set (row, 0, (week != previous_week ? format ("W{1}", week) : ""));
table.set (row, 1, (date != previous_date ? date : ""));
table.set (row, 2, (day != previous_day ? day : ""));
table.set (row, 3, label);
table.set (row, 4, task.get ("project"));
table.set (row, 5, due);
table.set (row, 6, task.get ("description"), task_color);
previous_week = week;
previous_date = date;
previous_day = day;
}
out << " " << format (STRING_CMD_TIMESHEET_DONE, completed.rows ()) << '\n';
if (completed.rows ())
out << completed.render ()
// Render the table.
std::stringstream out;
if (table.rows ())
out << optionalBlankLine ()
<< table.render ()
<< '\n';
// Now render the started table.
Table started;
started.width (context.getWidth ());
started.add (" ");
started.add (STRING_COLUMN_LABEL_PROJECT);
started.add (STRING_COLUMN_LABEL_DUE, false);
started.add (STRING_COLUMN_LABEL_DESC);
started.colorHeader (label);
for (auto& task : all)
{
// If task started within range, but not completed withing range.
if (task.getStatus () == Task::pending &&
task.has ("start"))
{
Datetime startDate (task.get_date ("start"));
if (startDate >= start && startDate < end)
{
Color c;
autoColorize (task, c);
int row = started.addRow ();
std::string format = context.config.get ("dateformat.report");
if (format == "")
format = context.config.get ("dateformat");
started.set (row, 1, task.get ("project"), c);
if (task.has ("due"))
{
Datetime dt (task.get_date ("due"));
started.set (row, 2, dt.toString (format));
}
std::string description = task.get ("description");
int indent = context.config.getInteger ("indent.annotation");
for (auto& ann : task.getAnnotations ())
description += '\n'
+ std::string (indent, ' ')
+ Datetime (ann.first.substr (11)).toString (context.config.get ("dateformat"))
+ ' '
+ ann.second;
started.set (row, 3, description, c);
}
}
}
out << " " << format (STRING_CMD_TIMESHEET_STARTED, started.rows ()) << '\n';
if (started.rows ())
out << started.render ()
<< "\n\n";
// Prior week.
start -= 7 * 86400;
end -= 7 * 86400;
}
if (context.verbose ("affected"))
out << format ("{1} completed, {2} started.", num_completed, num_started)
<< '\n';
output = out.str ();
return rc;

View file

@ -537,7 +537,7 @@
#define STRING_CMD_CUSTOM_COUNT "1 task"
#define STRING_CMD_CUSTOM_COUNTN "{1} tasks"
#define STRING_CMD_CUSTOM_TRUNCATED "truncated to {1} lines"
#define STRING_CMD_TIMESHEET_USAGE "Weekly summary of completed and started tasks"
#define STRING_CMD_TIMESHEET_USAGE "Summary of completed and started tasks"
#define STRING_CMD_TIMESHEET_STARTED "Started ({1} tasks)"
#define STRING_CMD_TIMESHEET_DONE "Completed ({1} tasks)"
#define STRING_CMD_BURN_USAGE_M "Shows a graphical burndown chart, by month"

View file

@ -539,7 +539,7 @@
#define STRING_CMD_CUSTOM_COUNT "1 task"
#define STRING_CMD_CUSTOM_COUNTN "{1} tasks"
#define STRING_CMD_CUSTOM_TRUNCATED "truncated to {1} lines"
#define STRING_CMD_TIMESHEET_USAGE "Weekly summary of completed and started tasks"
#define STRING_CMD_TIMESHEET_USAGE "Summary of completed and started tasks"
#define STRING_CMD_TIMESHEET_STARTED "Started ({1} tasks)"
#define STRING_CMD_TIMESHEET_DONE "Completed ({1} tasks)"
#define STRING_CMD_BURN_USAGE_M "Shows a graphical burndown chart, by month"

View file

@ -70,32 +70,27 @@ class TestTimesheet(TestCase):
cls.t("log C1 entry:{0} end:{1}".format(fourteen, seven))
cls.t("log C2 entry:{0} end:{0}".format(fourteen))
def test_one_week(self):
"""One week of started and completed"""
code, out, err = self.t("timesheet")
expected = re.compile("Completed.+C0.+Started.+PS0", re.DOTALL)
self.assertRegexpMatches(out, expected)
def test_two_weeks(self):
"""Two weeks of started and completed"""
code, out, err = self.t("timesheet 2")
expected = re.compile(
"Completed.+C0.+Started.+PS0.+"
"Completed.+C1.+Started.+PS1", re.DOTALL)
self.assertRegexpMatches(out, expected)
def test_three_weeks(self):
"""Three weeks of started and completed"""
code, out, err = self.t("timesheet 3")
code, out, err = self.t("timesheet")
expected = re.compile(
"Completed.+C0.+Started.+PS0.+"
"Completed.+C1.+Started.+PS1.+"
"Completed.+C2.+Started.+PS2", re.DOTALL)
"Started.+PS2.+Completed.+C2.+"
"Started.+PS1.+Completed.+C1.+"
"Started.+PS0.+Completed.+C0", re.DOTALL)
self.assertRegexpMatches(out, expected)
def test_one_week(self):
"""One week of started and completed"""
# This is the default filter, reduced from 4 weeks to 1.
code, out, err = self.t("timesheet (+PENDING and start.after:now-1wk) or (+COMPLETED and end.after:now-1wk)")
expected = re.compile("Started.+PS0.+Completed.+C0", re.DOTALL)
self.assertRegexpMatches(out, expected)
self.assertNotIn("PS1", out)
self.assertNotIn("PS2", out)
self.assertNotIn("C1", out)
self.assertNotIn("C2", out)
if __name__ == "__main__":
from simpletap import TAPTestRunner