From 440cfb009e9f05b2ba4ded57cf318aacaf64cda0 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Thu, 9 Feb 2017 08:19:30 -0500 Subject: [PATCH] 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. --- ChangeLog | 3 + NEWS | 9 +- doc/man/taskrc.5.in | 3 + src/Config.cpp | 5 +- src/commands/CmdInfo.cpp | 16 +- src/commands/CmdShow.cpp | 1 + src/commands/CmdTimesheet.cpp | 296 ++++++++++++++++------------------ src/l10n/eng-USA.h | 2 +- src/l10n/jpn-JPN.h | 2 +- test/timesheet.t | 35 ++-- 10 files changed, 182 insertions(+), 190 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8c72f41ab..e7f0638d0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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 --------------------------- diff --git a/NEWS b/NEWS index 417426875..b805ab9ca 100644 --- a/NEWS +++ b/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", diff --git a/doc/man/taskrc.5.in b/doc/man/taskrc.5.in index eae6c4ce0..096860210 100644 --- a/doc/man/taskrc.5.in +++ b/doc/man/taskrc.5.in @@ -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" diff --git a/src/Config.cpp b/src/Config.cpp index 84682b8b6..5fa2ab79c 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -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; diff --git a/src/commands/CmdInfo.cpp b/src/commands/CmdInfo.cpp index e4159c36c..4caf327e3 100644 --- a/src/commands/CmdInfo.cpp +++ b/src/commands/CmdInfo.cpp @@ -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 (); diff --git a/src/commands/CmdShow.cpp b/src/commands/CmdShow.cpp index b617fc95a..9a286be5b 100644 --- a/src/commands/CmdShow.cpp +++ b/src/commands/CmdShow.cpp @@ -147,6 +147,7 @@ int CmdShow::execute (std::string& output) " default.scheduled" " defaultheight" " defaultwidth" + " default.timesheet.filter" " dependency.confirmation" " dependency.indicator" " dependency.reminder" diff --git a/src/commands/CmdTimesheet.cpp b/src/commands/CmdTimesheet.cpp index 4f26ed506..a15c882b1 100644 --- a/src/commands/CmdTimesheet.cpp +++ b/src/commands/CmdTimesheet.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -33,6 +34,7 @@ #include #include #include +#include #include #include @@ -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 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 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; + if (a.hasTag ("FILTER")) + { + hasFilter = true; + break; + } + } - std::string title = start.toString (context.config.get ("dateformat")) - + " - " - + endString.toString (context.config.get ("dateformat")); + 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); + } - Color bold; - if (context.color ()) - bold = Color ("bold"); + // Apply filter to get a set of tasks. + handleRecurrence (); + Filter filter; + std::vector filtered; + filter.subset (filtered); - out << '\n' - << bold.colorize (title) + // 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 shown; + for (auto& task : filtered) + { + if (task.getStatus () == Task::completed) + { + task.set ("_key", task.get ("end")); + ++num_completed; + } + + if (task.getStatus () == Task::pending && task.has ("start")) + { + task.set ("_key", task.get ("start")); + ++num_started; + } + + shown.push_back (task); + } + + // 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 ()) + { + 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, " "); + } + + // Keep track of unique week numbers. + if (week != previous_week) + ++weekCounter; + + // User-defined oddness. + int row; + if (weekCounter % 2) + row = table.addRowOdd (); + else + row = table.addRowEven (); + + // 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; + } + + // Render the table. + std::stringstream out; + if (table.rows ()) + out << optionalBlankLine () + << table.render () << '\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 ()) - { - label = Color (context.config.get ("color.label")); - completed.colorHeader (label); - } - - for (auto& task : all) - { - // If task completed within range. - if (task.getStatus () == Task::completed) - { - Datetime compDate (task.get_date ("end")); - if (compDate >= start && compDate < end) - { - Color c; - autoColorize (task, c); - - 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); - - if(task.has ("due")) - { - Datetime dt (task.get_date ("due")); - completed.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; - - completed.set (row, 3, description, c); - } - } - } - - out << " " << format (STRING_CMD_TIMESHEET_DONE, completed.rows ()) << '\n'; - - if (completed.rows ()) - out << completed.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; diff --git a/src/l10n/eng-USA.h b/src/l10n/eng-USA.h index b9445c104..325e625b9 100644 --- a/src/l10n/eng-USA.h +++ b/src/l10n/eng-USA.h @@ -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" diff --git a/src/l10n/jpn-JPN.h b/src/l10n/jpn-JPN.h index 32ae621e2..d89159997 100644 --- a/src/l10n/jpn-JPN.h +++ b/src/l10n/jpn-JPN.h @@ -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" diff --git a/test/timesheet.t b/test/timesheet.t index 016cb93f1..28796fdf5 100755 --- a/test/timesheet.t +++ b/test/timesheet.t @@ -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