Custom Reports - oldest, newest

- Added support for the "report.X.limit" configuration variable, to
  restrict the number of rows a report generates.
- Added support for Table::render (limit) to limit the number of rows
  that are rendered.
- Removed "oldest" and "newest" report code.
- Added "oldest" and "newest" custom report details to Config.cpp
- Updated various documentation.
This commit is contained in:
Paul Beckingham 2009-03-12 22:34:45 -04:00
parent 8c95e82a63
commit c35a764019
14 changed files with 101 additions and 369 deletions

View file

@ -17,12 +17,13 @@
+ Added support for custom reports, comprised of a set of column names and
sort order, with optional filtering in the configuration file. This
means user-defined reports can be written, and the reports currently
in the configuration file can be renamed.
in the configuration file can be renamed. Several of task's built in
reports have been converted to user-defined reports.
+ New online documentation for custom reports.
+ New algorithm for determining when the "nag" message is displayed.
+ Fixed bug where task hangs with a certain combination of recurring tasks
and shadow files.
+ Fixed bug with the task sort alogrithm, which led to an unstable sequence
+ Fixed bug with the task sort algorithm, which led to an unstable sequence
when there were only a handful of tasks.
+ Performance enhanced by eliminating unnecessary sorting.
+ Task now has a large (and growing) test suite and bug regression tests

View file

@ -179,18 +179,6 @@
only show as many that will fit.
</dd>
<dt>oldest</dt>
<dd>
Determines how many tasks the "task oldest" command displays.
Defaults to 10.
</dd>
<dt>newest</dt>
<dd>
Determines how many tasks the "task newest" command displays.
Defaults to 10.
</dd>
<dt>defaultwidth</dt>
<dd>
The width of tables used when ncurses support is not available.

View file

@ -36,9 +36,10 @@
<div class="content">
<p>
Task allows you to customize reports, to a limited degree.
The "list", "long", and "ls" reports are all now custom
reports, whereas in previous releases of task they were not
mutable. This means they can be modified, renamed, or deleted.
The "list", "long", "ls", "oldest" and "newest" reports are
all now custom reports, whereas in previous releases of task
they were not mutable. This means they can be modified,
renamed, or deleted.
</p>
<p>
@ -74,6 +75,14 @@ report.mine.sort=priority-,project+</pre></code>
definition is optional.
</p>
<p>
An optional limit can also be specified, which limits the
number of tasks shown in the report. If a limit is not
specified, then the number of tasks is not limited.
</p>
<code><pre>report.mine.limit=10</pre></code>
<p>
Here is a list of all the possible columns that may be included
in a report:

View file

@ -111,7 +111,8 @@
<li>Added support for custom reports, comprised of a set of column names and
sort order, with optional filtering in the configuration file. This
means user-defined reports can be written, and the reports currently
in the configuration file can be renamed.
in the configuration file can be renamed. Several of task's built in
reports have been converted to user-defined reports.
<li>New online documentation for custom reports.
<li>New algorithm for determining when the "nag" message is displayed.
<li>Fixed bug where task hangs with a certain combination of recurring tasks

View file

@ -34,12 +34,10 @@
<br />
<h2 class="title"><a name="usage">Command Usage<a></h2>
<div class="content">
<pre><code>task add [tags] [attrs] desc...
task list [tags] [attrs] desc...
task long [tags] [attrs] desc...
task ls [tags] [attrs] desc...
<pre><code>Usage: task
task add [tags] [attrs] desc...
task completed [tags] [attrs] desc...
task ID [tags] [attrs] ["desc..."]
task ID [tags] [attrs] [desc...]
task ID /from/to/
task delete ID
task undelete ID
@ -57,12 +55,18 @@
task calendar
task active
task overdue
task oldest
task newest
task stats
task export
task color
task version
task help
task list [tags] [attrs] desc...
task long [tags] [attrs] desc...
task ls [tags] [attrs] desc...
task newest [tags] [attrs] desc...
task oldest [tags] [attrs] desc...
See http://www.beckingham.net/task.html for the latest releases and a full tutorial.
ID is the numeric identifier displayed by the 'task list' command
@ -74,8 +78,11 @@ Attributes are:
project: Project name
priority: Priority
due: Due date
recur: Recurrence frequency
until: Recurrence end date
fg: Foreground color
bg: Background color
rc: Alternate .taskrc file
Any command or attribute name may be abbreviated if still unique:
task list project:Home
@ -86,7 +93,7 @@ Some task descriptions need to be escaped because of the shell:
task add escaped \' quote
Many characters have special meaning to the shell, including:
$ ! ' " ( ) ; \ ` * ? { } [ ] < > | &amp; % # ~</code></pre>
$ ! ' " ( ) ; \ ` * ? { } [ ] < > | & % # ~</code></pre>
<div>
<br />

View file

@ -39,17 +39,32 @@
// These are default (but overridable) reports. These entries are necessary
// because these three reports were converted from hard-coded reports to custom
// reports, and therefore need these config file entries. However, users are
// already used to seeing these three reports, but do not have these entries.
// already used to seeing these five reports, but do not have these entries.
// The choice was a) make users edit their .taskrc files, b) write a .taskrc
// upgrade program to make the change, or c) this.
Config::Config ()
{
(*this)["report.long.columns"] = "id,project,priority,entry,start,due,age,tags,description";
(*this)["report.long.description"] = "Lists all task, all data, matching the specified criteria";
(*this)["report.long.columns"] = "id,project,priority,entry,start,due,recur,age,tags,description";
(*this)["report.long.sort"] = "due+,priority-,project+";
(*this)["report.list.description"] = "Lists all tasks matching the specified criteria";
(*this)["report.list.columns"] = "id,project,priority,due,active,age,description";
(*this)["report.list.sort"] = "due+,priority-,project+";
(*this)["report.ls.description"] = "Minimal listing of all tasks matching the specified criteria";
(*this)["report.ls.columns"] = "id,project,priority,description";
(*this)["report.ls.sort"] = "priority-,project+";
(*this)["report.newest.description"] = "Shows the newest tasks";
(*this)["report.newest.columns"] = "id,project,priority,due,active,age,description";
(*this)["report.newest.sort"] = "id-";
(*this)["report.newest.limit"] = "10";
(*this)["report.oldest.description"] = "Shows the oldest tasks";
(*this)["report.oldest.columns"] = "id,project,priority,due,active,age,description";
(*this)["report.oldest.sort"] = "id+";
(*this)["report.oldest.limit"] = "10";
}
////////////////////////////////////////////////////////////////////////////////
@ -59,9 +74,9 @@ Config::Config (const std::string& file)
}
////////////////////////////////////////////////////////////////////////////////
// Read the Configuration filee and populate the *this map. The file format
// is simply lines with name=value pairs. Whitespace between name, = and value
// is not tolerated, but blank lines and comments starting with # are allowed.
// Read the Configuration file and populate the *this map. The file format is
// simply lines with name=value pairs. Whitespace between name, = and value is
// not tolerated, but blank lines and comments starting with # are allowed.
bool Config::load (const std::string& file)
{
std::ifstream in;
@ -154,17 +169,33 @@ void Config::createDefault (const std::string& home)
fprintf (out, "default.command=list\n");
// Custom reports.
fprintf (out, "# Fields: id,uuid,project,priority,entry,start,due,age,active,tags,description\n");
fprintf (out, "# Fields: id,uuid,project,priority,entry,start,due,recur,age,active,tags,description\n");
fprintf (out, "# Description: This report is ...\n");
fprintf (out, "# Sort: due+,priority-,project+\n");
fprintf (out, "# Filter: pro:x pri:H +bug\n");
fprintf (out, "report.large.columns=id,uuid,project,priority,entry,start,due,age,active,tags,description\n");
fprintf (out, "report.large.sort=due+,priority-,project+\n");
fprintf (out, "report.long.columns=id,project,priority,entry,start,due,age,tags,description\n");
fprintf (out, "report.long.sort=due+,priority-,project+\n");
fprintf (out, "report.list.columns=id,project,priority,due,active,age,description\n");
fprintf (out, "report.list.sort=due+,priority-,project+\n");
fprintf (out, "report.ls.columns=id,project,priority,description\n");
fprintf (out, "report.ls.sort=priority-,project+\n");
fprintf (out, "# Limit: 10\n");
fprintf (out, "report.long.description=Lists all task, all data, matching the specified criteria");
fprintf (out, "report.long.columns=id,project,priority,entry,start,due,recur,age,tags,description");
fprintf (out, "report.long.sort=due+,priority-,project+");
fprintf (out, "report.list.description=Lists all tasks matching the specified criteria");
fprintf (out, "report.list.columns=id,project,priority,due,active,age,description");
fprintf (out, "report.list.sort=due+,priority-,project+");
fprintf (out, "report.ls.description=Minimal listing of all tasks matching the specified criteria");
fprintf (out, "report.ls.columns=id,project,priority,description");
fprintf (out, "report.ls.sort=priority-,project+");
fprintf (out, "report.newest.description=Shows the newest tasks");
fprintf (out, "report.newest.columns=id,project,priority,due,active,age,description");
fprintf (out, "report.newest.sort=id-");
fprintf (out, "report.newest.limit=10");
fprintf (out, "report.oldest.description=Shows the oldest tasks");
fprintf (out, "report.oldest.columns=id,project,priority,due,active,age,description");
fprintf (out, "report.oldest.sort=id+");
fprintf (out, "report.oldest.limit=10");
fclose (out);

View file

@ -325,7 +325,7 @@ Grid::Cell::operator int () const
case CELL_INT: return mInt;
case CELL_FLOAT: return (int) mFloat;
case CELL_DOUBLE: return (int) mDouble;
case CELL_STRING: return mString.length ();
case CELL_STRING: return ::atoi (mString.c_str ());
}
return 0;
@ -340,7 +340,7 @@ Grid::Cell::operator float () const
case CELL_INT: return (float) mInt;
case CELL_FLOAT: return mFloat;
case CELL_DOUBLE: return (float) mDouble;
case CELL_STRING: return (float) mString.length ();
case CELL_STRING: return (float) ::atof (mString.c_str ());
}
return 0.0;
@ -355,7 +355,7 @@ Grid::Cell::operator double () const
case CELL_INT: return (double) mInt;
case CELL_FLOAT: return (double) mFloat;
case CELL_DOUBLE: return mDouble;
case CELL_STRING: return (double) mString.length ();
case CELL_STRING: return (double) ::atof (mString.c_str ());
}
return 0.0;

View file

@ -994,7 +994,7 @@ void Table::clean (std::string& value)
}
////////////////////////////////////////////////////////////////////////////////
const std::string Table::render ()
const std::string Table::render (int maximum /* = 0 */)
{
calculateColumnWidths ();
@ -1028,8 +1028,14 @@ const std::string Table::render ()
if (mSortColumns.size ())
sort (order);
// If a non-zero maximum is specified, then it limits the number of rows of
// the table that are rendered.
int limit = mRows;
if (maximum != 0)
limit = maximum;
// Print all rows.
for (int row = 0; row < mRows; ++row)
for (int row = 0; row < limit; ++row)
{
std::vector <std::vector <std::string> > columns;
std::vector <std::string> blanks;

View file

@ -91,7 +91,7 @@ public:
int rowCount ();
int columnCount ();
const std::string render ();
const std::string render (int maximum = 0);
private:
std::string getCell (const int, const int);

View file

@ -367,8 +367,8 @@ std::string handleVersion (Config& conf)
"blanklines color color.active color.due color.overdue color.pri.H "
"color.pri.L color.pri.M color.pri.none color.recurring color.tagged "
"confirmation curses data.location dateformat default.command "
"default.priority defaultwidth due locking monthsperline nag newest next "
"oldest project shadow.command shadow.file shadow.notify";
"default.priority defaultwidth due locking monthsperline nag next project "
"shadow.command shadow.file shadow.notify";
// This configuration variable is supported, but not documented. It exists
// so that unit tests can force color to be on even when the output from task

View file

@ -130,12 +130,7 @@ static const char* commands[] =
"history",
"ghistory",
"info",
"list",
"long",
"ls",
"newest",
"next",
"oldest",
"overdue",
"projects",
"start",

View file

@ -1678,302 +1678,6 @@ std::string handleReportOverdue (TDB& tdb, T& task, Config& conf)
return out.str ();
}
////////////////////////////////////////////////////////////////////////////////
// Successively apply filters based on the task object built from the command
// line. Tasks that match all the specified criteria are listed.
std::string handleReportOldest (TDB& tdb, T& task, Config& conf)
{
std::stringstream out;
// Determine window size, and set table accordingly.
int width = conf.get ("defaultwidth", 80);
#ifdef HAVE_LIBNCURSES
if (conf.get ("curses", true))
{
WINDOW* w = initscr ();
width = w->_maxx + 1;
endwin ();
}
#endif
// Get the pending tasks.
std::vector <T> tasks;
tdb.allPendingT (tasks);
handleRecurrence (tdb, tasks);
filter (tasks, task);
initializeColorRules (conf);
unsigned int quantity = conf.get ("oldest", 10);
// Create a table for output.
Table table;
table.setTableWidth (width);
table.addColumn ("ID");
table.addColumn ("Project");
table.addColumn ("Pri");
table.addColumn ("Due");
table.addColumn ("Active");
table.addColumn ("Age");
table.addColumn ("Description");
if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false))
{
table.setColumnUnderline (0);
table.setColumnUnderline (1);
table.setColumnUnderline (2);
table.setColumnUnderline (3);
table.setColumnUnderline (4);
table.setColumnUnderline (5);
table.setColumnUnderline (6);
}
else
table.setTableDashedUnderline ();
table.setColumnWidth (0, Table::minimum);
table.setColumnWidth (1, Table::minimum);
table.setColumnWidth (2, Table::minimum);
table.setColumnWidth (3, Table::minimum);
table.setColumnWidth (4, Table::minimum);
table.setColumnWidth (5, Table::minimum);
table.setColumnWidth (6, Table::flexible);
table.setColumnJustification (0, Table::right);
table.setColumnJustification (3, Table::right);
table.setColumnJustification (5, Table::right);
table.sortOn (3, Table::ascendingDate);
table.sortOn (2, Table::descendingPriority);
table.sortOn (1, Table::ascendingCharacter);
table.setDateFormat (conf.get ("dateformat", "m/d/Y"));
for (unsigned int i = 0; i < min (quantity, tasks.size ()); ++i)
{
T refTask (tasks[i]);
Date now;
// Now format the matching task.
bool imminent = false;
bool overdue = false;
std::string due = refTask.getAttribute ("due");
if (due.length ())
{
switch (getDueState (due))
{
case 2: overdue = true; break;
case 1: imminent = true; break;
case 0:
default: break;
}
Date dt (::atoi (due.c_str ()));
due = dt.toString (conf.get ("dateformat", "m/d/Y"));
}
std::string active;
if (refTask.getAttribute ("start") != "")
active = "*";
std::string age;
std::string created = refTask.getAttribute ("entry");
if (created.length ())
{
Date dt (::atoi (created.c_str ()));
formatTimeDeltaDays (age, (time_t) (now - dt));
}
// All criteria match, so add refTask to the output table.
int row = table.addRow ();
table.addCell (row, 0, refTask.getId ());
table.addCell (row, 1, refTask.getAttribute ("project"));
table.addCell (row, 2, refTask.getAttribute ("priority"));
table.addCell (row, 3, due);
table.addCell (row, 4, active);
table.addCell (row, 5, age);
table.addCell (row, 6, refTask.getDescription ());
if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false))
{
Text::color fg = Text::colorCode (refTask.getAttribute ("fg"));
Text::color bg = Text::colorCode (refTask.getAttribute ("bg"));
autoColorize (refTask, fg, bg, conf);
table.setRowFg (row, fg);
table.setRowBg (row, bg);
if (fg == Text::nocolor)
{
if (overdue)
table.setCellFg (row, 3, Text::colorCode (conf.get ("color.overdue", "red")));
else if (imminent)
table.setCellFg (row, 3, Text::colorCode (conf.get ("color.due", "yellow")));
}
}
}
if (table.rowCount ())
out << optionalBlankLine (conf)
<< table.render ()
<< optionalBlankLine (conf)
<< table.rowCount ()
<< (table.rowCount () == 1 ? " task" : " tasks")
<< std::endl;
else
out << "No matches."
<< std::endl;
return out.str ();
}
////////////////////////////////////////////////////////////////////////////////
// Successively apply filters based on the task object built from the command
// line. Tasks that match all the specified criteria are listed.
std::string handleReportNewest (TDB& tdb, T& task, Config& conf)
{
std::stringstream out;
// Determine window size, and set table accordingly.
int width = conf.get ("defaultwidth", 80);
#ifdef HAVE_LIBNCURSES
if (conf.get ("curses", true))
{
WINDOW* w = initscr ();
width = w->_maxx + 1;
endwin ();
}
#endif
// Get the pending tasks.
std::vector <T> tasks;
tdb.allPendingT (tasks);
handleRecurrence (tdb, tasks);
filter (tasks, task);
initializeColorRules (conf);
int quantity = conf.get ("newest", 10);
// Create a table for output.
Table table;
table.setTableWidth (width);
table.addColumn ("ID");
table.addColumn ("Project");
table.addColumn ("Pri");
table.addColumn ("Due");
table.addColumn ("Active");
table.addColumn ("Age");
table.addColumn ("Description");
if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false))
{
table.setColumnUnderline (0);
table.setColumnUnderline (1);
table.setColumnUnderline (2);
table.setColumnUnderline (3);
table.setColumnUnderline (4);
table.setColumnUnderline (5);
table.setColumnUnderline (6);
}
else
table.setTableDashedUnderline ();
table.setColumnWidth (0, Table::minimum);
table.setColumnWidth (1, Table::minimum);
table.setColumnWidth (2, Table::minimum);
table.setColumnWidth (3, Table::minimum);
table.setColumnWidth (4, Table::minimum);
table.setColumnWidth (5, Table::minimum);
table.setColumnWidth (6, Table::flexible);
table.setColumnJustification (0, Table::right);
table.setColumnJustification (3, Table::right);
table.setColumnJustification (5, Table::right);
table.sortOn (3, Table::ascendingDate);
table.sortOn (2, Table::descendingPriority);
table.sortOn (1, Table::ascendingCharacter);
table.setDateFormat (conf.get ("dateformat", "m/d/Y"));
int total = tasks.size ();
for (int i = total - 1; i >= max (0, total - quantity); --i)
{
T refTask (tasks[i]);
Date now;
// Now format the matching task.
bool imminent = false;
bool overdue = false;
std::string due = refTask.getAttribute ("due");
if (due.length ())
{
switch (getDueState (due))
{
case 2: overdue = true; break;
case 1: imminent = true; break;
case 0:
default: break;
}
Date dt (::atoi (due.c_str ()));
due = dt.toString (conf.get ("dateformat", "m/d/Y"));
}
std::string active;
if (refTask.getAttribute ("start") != "")
active = "*";
std::string age;
std::string created = refTask.getAttribute ("entry");
if (created.length ())
{
Date dt (::atoi (created.c_str ()));
formatTimeDeltaDays (age, (time_t) (now - dt));
}
// All criteria match, so add refTask to the output table.
int row = table.addRow ();
table.addCell (row, 0, refTask.getId ());
table.addCell (row, 1, refTask.getAttribute ("project"));
table.addCell (row, 2, refTask.getAttribute ("priority"));
table.addCell (row, 3, due);
table.addCell (row, 4, active);
table.addCell (row, 5, age);
table.addCell (row, 6, refTask.getDescription ());
if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false))
{
Text::color fg = Text::colorCode (refTask.getAttribute ("fg"));
Text::color bg = Text::colorCode (refTask.getAttribute ("bg"));
autoColorize (refTask, fg, bg, conf);
table.setRowFg (row, fg);
table.setRowBg (row, bg);
if (fg == Text::nocolor)
{
if (overdue)
table.setCellFg (row, 3, Text::colorCode (conf.get ("color.overdue", "red")));
else if (imminent)
table.setCellFg (row, 3, Text::colorCode (conf.get ("color.due", "yellow")));
}
}
}
if (table.rowCount ())
out << optionalBlankLine (conf)
<< table.render ()
<< optionalBlankLine (conf)
<< table.rowCount ()
<< (table.rowCount () == 1 ? " task" : " tasks")
<< std::endl;
else
out << "No matches."
<< std::endl;
return out.str ();
}
////////////////////////////////////////////////////////////////////////////////
std::string handleReportStats (TDB& tdb, T& task, Config& conf)
{
@ -2575,10 +2279,12 @@ std::string handleCustomReport (
}
}
int maximum = conf.get (std::string ("report.") + report + ".limit", (int)0);
std::stringstream out;
if (table.rowCount ())
out << optionalBlankLine (conf)
<< table.render ()
<< table.render (maximum)
<< optionalBlankLine (conf)
<< table.rowCount ()
<< (table.rowCount () == 1 ? " task" : " tasks")

View file

@ -158,14 +158,6 @@ static void shortUsage (Config& conf)
table.addCell (row, 1, "task overdue");
table.addCell (row, 2, "Shows all incomplete tasks that are beyond their due date");
row = table.addRow ();
table.addCell (row, 1, "task oldest");
table.addCell (row, 2, "Shows the oldest tasks");
row = table.addRow ();
table.addCell (row, 1, "task newest");
table.addCell (row, 2, "Shows the newest tasks");
row = table.addRow ();
table.addCell (row, 1, "task stats");
table.addCell (row, 2, "Shows task database statistics");
@ -817,8 +809,6 @@ std::string runTaskCommand (
else if (command == "calendar") { if (gc) tdb.gc (); out = handleReportCalendar (tdb, task, conf ); if (shadow) updateShadowFile (tdb, conf); }
else if (command == "active") { if (gc) tdb.gc (); out = handleReportActive (tdb, task, conf ); if (shadow) updateShadowFile (tdb, conf); } // TODO replace with Custom
else if (command == "overdue") { if (gc) tdb.gc (); out = handleReportOverdue (tdb, task, conf ); if (shadow) updateShadowFile (tdb, conf); } // TODO replace with Custom
else if (command == "oldest") { if (gc) tdb.gc (); out = handleReportOldest (tdb, task, conf ); if (shadow) updateShadowFile (tdb, conf); } // TODO replace with Custom
else if (command == "newest") { if (gc) tdb.gc (); out = handleReportNewest (tdb, task, conf ); if (shadow) updateShadowFile (tdb, conf); } // TODO replace with Custom
else if (command == "colors") { out = handleColor ( conf ); }
else if (command == "version") { out = handleVersion ( conf ); }
else if (command == "help") { longUsage ( conf ); }

View file

@ -100,8 +100,6 @@ std::string handleReportCalendar (TDB&, T&, Config&);
std::string handleReportActive (TDB&, T&, Config&);
std::string handleReportOverdue (TDB&, T&, Config&);
std::string handleReportStats (TDB&, T&, Config&);
std::string handleReportOldest (TDB&, T&, Config&);
std::string handleReportNewest (TDB&, T&, Config&);
std::string handleCustomReport (TDB&, T&, Config&, const std::string&);
void validReportColumns (const std::vector <std::string>&);