mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
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:
parent
4c20ff04c2
commit
440cfb009e
10 changed files with 182 additions and 190 deletions
|
@ -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
9
NEWS
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ();
|
||||
|
|
|
@ -147,6 +147,7 @@ int CmdShow::execute (std::string& output)
|
|||
" default.scheduled"
|
||||
" defaultheight"
|
||||
" defaultwidth"
|
||||
" default.timesheet.filter"
|
||||
" dependency.confirmation"
|
||||
" dependency.indicator"
|
||||
" dependency.reminder"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue