Enhancement - annotations

- Added support for "annotate" command to annotate existing tasks.
- Bumped file format to version 3, due to the annotations.
- Added unit tests to verify that annotations work.
- Changed 'description' column everywhere to include annotations.
- Added 'description_only' column to exclude the annotations.
- Fixed bug in Table.cpp that calculated the width of multi-line
  columns by using the cell length, instead of the length of the
  longest individual line.
- Updated documentation with new feature.
- Updated documentation with new column.
- Enhanced t.t unit tests to cover format 43
This commit is contained in:
Paul Beckingham 2009-03-24 01:57:12 -04:00
parent ca795ea281
commit 3979c3283e
16 changed files with 446 additions and 54 deletions

View file

@ -12,6 +12,10 @@
+ Added support for the "echo.command" configuration variable that displays + Added support for the "echo.command" configuration variable that displays
the task affected by the start, stop, do, undo, delete and undelete the task affected by the start, stop, do, undo, delete and undelete
commands (thanks to Bruce Dillahunty). commands (thanks to Bruce Dillahunty).
+ Added support for task annotations, with each annotation comprising a
timestamp and a description.
+ Added support for a 'description_only' column that can be used in custom
reports which excludes annotations.
------ old releases ------------------------------ ------ old releases ------------------------------

View file

@ -102,6 +102,25 @@ Car 2 2 wks 25% XXXXXXXXX</code></pre>
Appends the additional description to an existing task. Appends the additional description to an existing task.
</p> </p>
<strong>% task annotate &lt;id&gt; additional note...</strong>
<p>
Allows an annotation to be attached to an existing task. Each
annotation has a time stamp, and when displayed, the annotations
are shown under the task description. For example:
</p>
<pre><code>% task add Go to the supermarket
% task annotate 1 need milk
% task ls
ID Project Pri Due Active Age Description
1 Go to the supermarket
3/23/2009 need milk</code></pre>
<p>
The date of the annotation uses the "dateformat" configuration
variable.
</p>
<strong>% task delete &lt;id&gt;</strong> <strong>% task delete &lt;id&gt;</strong>
<p> <p>
There are two ways of getting rid of tasks - mark them as done, or There are two ways of getting rid of tasks - mark them as done, or

View file

@ -100,6 +100,7 @@ report.mine.sort=priority-,project+</pre></code>
<li>active <li>active
<li>tags <li>tags
<li>recur <li>recur
<li>description_only
<li>description <li>description
</ul> </ul>

View file

@ -108,6 +108,10 @@
<li>Added support for the "echo.command" configuration variable that displays <li>Added support for the "echo.command" configuration variable that displays
the task affected by the start, stop, do, undo, delete and undelete the task affected by the start, stop, do, undo, delete and undelete
commands (thanks to Bruce Dillahunty). commands (thanks to Bruce Dillahunty).
<li>Added support for task annotations, with each annotation comprising a
timestamp and a description.
<li>Added support for a 'description_only' column that can be used in custom
reports which excludes annotations.
</ul> </ul>
<p> <p>

View file

@ -37,6 +37,7 @@
<pre><code>Usage: task <pre><code>Usage: task
task add [tags] [attrs] desc... task add [tags] [attrs] desc...
task append [tags] [attrs] desc... task append [tags] [attrs] desc...
task annotate ID desc...
task completed [tags] [attrs] desc... task completed [tags] [attrs] desc...
task ID [tags] [attrs] [desc...] task ID [tags] [attrs] [desc...]
task ID /from/to/ task ID /from/to/

View file

@ -171,6 +171,7 @@ void Config::createDefault (const std::string& home)
// Custom reports. // Custom reports.
fprintf (out, "# Fields: id,uuid,project,priority,entry,start,due,recur,age,active,tags,description\n"); fprintf (out, "# Fields: id,uuid,project,priority,entry,start,due,recur,age,active,tags,description\n");
fprintf (out, "# description_only\n");
fprintf (out, "# Description: This report is ...\n"); fprintf (out, "# Description: This report is ...\n");
fprintf (out, "# Sort: due+,priority-,project+\n"); fprintf (out, "# Sort: due+,priority-,project+\n");
fprintf (out, "# Filter: pro:x pri:H +bug\n"); fprintf (out, "# Filter: pro:x pri:H +bug\n");

169
src/T.cpp
View file

@ -25,6 +25,7 @@
// //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
#include <iostream> #include <iostream>
#include <sstream>
#include <algorithm> #include <algorithm>
#include "task.h" #include "task.h"
#include "T.h" #include "T.h"
@ -39,6 +40,7 @@ T::T ()
mTags.clear (); mTags.clear ();
mAttributes.clear (); mAttributes.clear ();
mDescription = ""; mDescription = "";
mAnnotations.clear ();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -58,6 +60,7 @@ T::T (const T& other)
mTags = other.mTags; mTags = other.mTags;
mRemoveTags = other.mRemoveTags; mRemoveTags = other.mRemoveTags;
mAttributes = other.mAttributes; mAttributes = other.mAttributes;
mAnnotations = other.mAnnotations;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -72,6 +75,7 @@ T& T::operator= (const T& other)
mTags = other.mTags; mTags = other.mTags;
mRemoveTags = other.mRemoveTags; mRemoveTags = other.mRemoveTags;
mAttributes = other.mAttributes; mAttributes = other.mAttributes;
mAnnotations = other.mAnnotations;
} }
return *this; return *this;
@ -244,7 +248,23 @@ void T::setSubstitution (const std::string& from, const std::string& to)
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// uuid status [tags] [attributes] description void T::getAnnotations (std::map <time_t, std::string>& all)
{
all = mAnnotations;
}
////////////////////////////////////////////////////////////////////////////////
void T::addAnnotation (const std::string& description)
{
std::string sanitized = description;
std::replace (sanitized.begin (), sanitized.end (), '\'', '"');
std::replace (sanitized.begin (), sanitized.end (), '[', '(');
std::replace (sanitized.begin (), sanitized.end (), ']', ')');
mAnnotations[time (NULL)] = sanitized;
}
////////////////////////////////////////////////////////////////////////////////
// uuid status [tags] [attributes] [annotations] description
// //
// uuid \x{8}-\x{4}-\x{4}-\x{4}-\x{12} // uuid \x{8}-\x{4}-\x{4}-\x{4}-\x{12}
// status - + X r // status - + X r
@ -282,10 +302,26 @@ const std::string T::compose () const
++count; ++count;
} }
line += "] "; line += "] [";
// Annotations
std::stringstream annotation;
bool first = true;
foreach (note, mAnnotations)
{
if (first)
first = false;
else
annotation << " ";
annotation << note->first << ":'" << note->second << "'";
}
line += annotation.str () + "] ";
// Description // Description
line += mDescription; line += mDescription;
// EOL
line += "\n"; line += "\n";
if (line.length () > T_LINE_MAX) if (line.length () > T_LINE_MAX)
@ -421,10 +457,12 @@ void T::parse (const std::string& line)
} }
else else
throw std::string ("Line too short"); throw std::string ("Line too short");
mAnnotations.clear ();
} }
break; break;
// File format version 2, from 2008.1.1 // File format version 2, from 2008.1.1 - 2009.3.23
case 2: case 2:
{ {
if (line.length () > 46) // ^.{36} . \[\] \[\] \n if (line.length () > 46) // ^.{36} . \[\] \[\] \n
@ -473,6 +511,104 @@ void T::parse (const std::string& line)
} }
else else
throw std::string ("Line too short"); throw std::string ("Line too short");
mAnnotations.clear ();
}
break;
// File format version 3, from 2009.3.23
case 3:
{
if (line.length () > 49) // ^.{36} . \[\] \[\] \[\] \n
{
mUUID = line.substr (0, 36);
mStatus = line[37] == '+' ? completed
: line[37] == 'X' ? deleted
: line[37] == 'r' ? recurring
: pending;
size_t openTagBracket = line.find ("[");
size_t closeTagBracket = line.find ("]", openTagBracket);
if (openTagBracket != std::string::npos &&
closeTagBracket != std::string::npos)
{
size_t openAttrBracket = line.find ("[", closeTagBracket);
size_t closeAttrBracket = line.find ("]", openAttrBracket);
if (openAttrBracket != std::string::npos &&
closeAttrBracket != std::string::npos)
{
size_t openAnnoBracket = line.find ("[", closeAttrBracket);
size_t closeAnnoBracket = line.find ("]", openAnnoBracket);
if (openAnnoBracket != std::string::npos &&
closeAnnoBracket != std::string::npos)
{
std::string tags = line.substr (
openTagBracket + 1, closeTagBracket - openTagBracket - 1);
std::vector <std::string> rawTags;
split (mTags, tags, ' ');
std::string attributes = line.substr (
openAttrBracket + 1, closeAttrBracket - openAttrBracket - 1);
std::vector <std::string> pairs;
split (pairs, attributes, ' ');
for (size_t i = 0; i < pairs.size (); ++i)
{
std::vector <std::string> pair;
split (pair, pairs[i], ':');
if (pair.size () == 2)
mAttributes[pair[0]] = pair[1];
}
// Extract and split the annotations, which are of the form:
// 1234:'...' 5678:'...'
std::string annotations = line.substr (
openAnnoBracket + 1, closeAnnoBracket - openAnnoBracket - 1);
pairs.clear ();
std::string::size_type start = 0;
std::string::size_type end = 0;
do
{
end = annotations.find ('\'', start);
if (end != std::string::npos)
{
end = annotations.find ('\'', end + 1);
if (start != std::string::npos &&
end != std::string::npos)
{
pairs.push_back (annotations.substr (start, end - start + 1));
start = end + 2;
}
}
}
while (start != std::string::npos &&
end != std::string::npos);
for (size_t i = 0; i < pairs.size (); ++i)
{
std::string pair = pairs[i];
std::string::size_type colon = pair.find (":");
if (colon != std::string::npos)
{
std::string name = pair.substr (0, colon);
std::string value = pair.substr (colon + 2, pair.length () - colon - 3);
mAnnotations[::atoi (name.c_str ())] = value;
}
}
mDescription = line.substr (closeAnnoBracket + 2, std::string::npos);
}
}
else
throw std::string ("Missing attribute brackets");
}
else
throw std::string ("Missing tag brackets");
}
else
throw std::string ("Line too short");
} }
break; break;
@ -512,16 +648,31 @@ int T::determineVersion (const std::string& line)
line[23] == '-' && line[23] == '-' &&
line[36] == ' ' && line[36] == ' ' &&
(line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r')) (line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r'))
{
// Version 3 looks like:
//
// uuid status [tags] [attributes] [annotations] description\n
//
// Scan for the number of [] pairs.
std::string::size_type tagAtts = line.find ("] [", 0);
std::string::size_type attsAnno = line.find ("] [", tagAtts + 1);
std::string::size_type annoDesc = line.find ("] ", attsAnno + 1);
if (tagAtts != std::string::npos &&
attsAnno != std::string::npos &&
annoDesc != std::string::npos)
return 3;
else
return 2; return 2;
}
// Version 3? // Version 4?
// //
// Fortunately, with the hindsight that will come with version 3, the // Fortunately, with the hindsight that will come with version 4, the
// identifying characteristics of 1 and 2 may be modified such that if 3 has // identifying characteristics of 1, 2 and 3 may be modified such that if 4
// a UUID followed by a status, then there is still a way to differentiate // has a UUID followed by a status, then there is still a way to differentiate
// between 2 and 3. // between 2, 3 and 4.
// //
// The danger is that a version 2 binary reads and misinterprets a version 3 // The danger is that a version 3 binary reads and misinterprets a version 4
// file. This is why it is a good idea to rely on an explicit version // file. This is why it is a good idea to rely on an explicit version
// declaration rather than chance positioning. // declaration rather than chance positioning.

View file

@ -56,6 +56,7 @@ public:
const std::string getDescription () const { return mDescription; } const std::string getDescription () const { return mDescription; }
void setDescription (const std::string& description) { mDescription = description; } void setDescription (const std::string& description) { mDescription = description; }
int getAnnotationCount () const { return mAnnotations.size (); }
void getSubstitution (std::string&, std::string&) const; void getSubstitution (std::string&, std::string&) const;
void setSubstitution (const std::string&, const std::string&); void setSubstitution (const std::string&, const std::string&);
@ -77,6 +78,9 @@ public:
void removeAttribute (const std::string&); void removeAttribute (const std::string&);
void removeAttributes (); void removeAttributes ();
void getAnnotations (std::map <time_t, std::string>&);
void addAnnotation (const std::string&);
const std::string compose () const; const std::string compose () const;
const std::string composeCSV (); const std::string composeCSV ();
void parse (const std::string&); void parse (const std::string&);
@ -93,9 +97,9 @@ private:
std::vector<std::string> mTags; std::vector<std::string> mTags;
std::vector<std::string> mRemoveTags; std::vector<std::string> mRemoveTags;
std::map<std::string, std::string> mAttributes; std::map<std::string, std::string> mAttributes;
std::string mFrom; std::string mFrom;
std::string mTo; std::string mTo;
std::map <time_t, std::string> mAnnotations;
}; };
#endif #endif

View file

@ -227,7 +227,7 @@ void Table::setRowBg (const int row, const Text::color c)
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void Table::addCell (const int row, const int col, const std::string& data) void Table::addCell (const int row, const int col, const std::string& data)
{ {
int length = 0; unsigned int length = 0;
if (mSuppressWS) if (mSuppressWS)
{ {
@ -238,7 +238,19 @@ void Table::addCell (const int row, const int col, const std::string& data)
data2 = data; data2 = data;
clean (data2); clean (data2);
// For multi-line cells, find the longest line.
if (data2.find ("\n") != std::string::npos)
{
length = 0;
std::vector <std::string> lines;
split (lines, data2, "\n");
for (unsigned int i = 0; i < lines.size (); ++i)
if (lines[i].length () > length)
length = lines[i].length ();
}
else
length = data2.length (); length = data2.length ();
mData.add (row, col, data2); mData.add (row, col, data2);
} }
else else
@ -248,11 +260,22 @@ void Table::addCell (const int row, const int col, const std::string& data)
else else
mData.add (row, col, data); mData.add (row, col, data);
// For multi-line cells, find the longest line.
if (data.find ("\n") != std::string::npos)
{
length = 0;
std::vector <std::string> lines;
split (lines, data, "\n");
for (unsigned int i = 0; i < lines.size (); ++i)
if (lines[i].length () > length)
length = lines[i].length ();
}
else
length = data.length (); length = data.length ();
} }
// Automatically maintain max width. // Automatically maintain max width.
mMaxDataWidth[col] = max (mMaxDataWidth[col], length); mMaxDataWidth[col] = max (mMaxDataWidth[col], (int)length);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -508,9 +531,7 @@ void Table::calculateColumnWidths ()
} }
else else
{ {
// std::cout << "# insufficient room, considering only flexible columns." << std::endl; // The fallback position is to assume no width was specified, and just
// The fallback position is to assume no width was specificed, and just
// calculate widths accordingly. // calculate widths accordingly.
mTableWidth = 0; mTableWidth = 0;
calculateColumnWidths (); calculateColumnWidths ();

View file

@ -845,6 +845,14 @@ std::string handleAppend (TDB& tdb, T& task, Config& conf)
{ {
original.setId (task.getId ()); original.setId (task.getId ());
tdb.modifyT (original); tdb.modifyT (original);
if (conf.get ("echo.command", true))
out << "Appended '"
<< task.getDescription ()
<< "' to task "
<< task.getId ()
<< std::endl;
} }
return out.str (); return out.str ();
@ -944,3 +952,34 @@ std::string handleColor (Config& conf)
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::string handleAnnotate (TDB& tdb, T& task, Config& conf)
{
std::stringstream out;
std::vector <T> all;
tdb.pendingT (all);
std::vector <T>::iterator it;
for (it = all.begin (); it != all.end (); ++it)
{
if (it->getId () == task.getId ())
{
it->addAnnotation (task.getDescription ());
tdb.modifyT (*it);
if (conf.get ("echo.command", true))
out << "Annotated "
<< task.getId ()
<< " with '"
<< task.getDescription ()
<< "'"
<< std::endl;
return out.str ();
}
}
throw std::string ("Task not found.");
return out.str ();
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -121,6 +121,7 @@ static const char* commands[] =
"active", "active",
"add", "add",
"append", "append",
"annotate",
"calendar", "calendar",
"colors", "colors",
"completed", "completed",

View file

@ -207,7 +207,18 @@ std::string handleCompleted (TDB& tdb, T& task, Config& conf)
table.addCell (row, 0, end.toString (conf.get ("dateformat", "m/d/Y"))); table.addCell (row, 0, end.toString (conf.get ("dateformat", "m/d/Y")));
table.addCell (row, 1, refTask.getAttribute ("project")); table.addCell (row, 1, refTask.getAttribute ("project"));
table.addCell (row, 2, refTask.getDescription ());
std::string description = refTask.getDescription ();
std::string when;
std::map <time_t, std::string> annotations;
refTask.getAnnotations (annotations);
foreach (anno, annotations)
{
Date dt (anno->first);
when = dt.toString (conf.get ("dateformat", "m/d/Y"));
description += "\n" + when + " " + anno->second;
}
table.addCell (row, 2, description);
if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false)) if (conf.get ("color", true) || conf.get (std::string ("_forcecolor"), false))
{ {
@ -270,7 +281,7 @@ std::string handleInfo (TDB& tdb, T& task, Config& conf)
table.setTableDashedUnderline (); table.setTableDashedUnderline ();
table.setColumnWidth (0, Table::minimum); table.setColumnWidth (0, Table::minimum);
table.setColumnWidth (1, Table::minimum); table.setColumnWidth (1, Table::flexible);
table.setColumnJustification (0, Table::left); table.setColumnJustification (0, Table::left);
table.setColumnJustification (1, Table::left); table.setColumnJustification (1, Table::left);
@ -296,9 +307,20 @@ std::string handleInfo (TDB& tdb, T& task, Config& conf)
: refTask.getStatus () == T::recurring ? "Recurring" : refTask.getStatus () == T::recurring ? "Recurring"
: "")); : ""));
std::string description = refTask.getDescription ();
std::string when;
std::map <time_t, std::string> annotations;
refTask.getAnnotations (annotations);
foreach (anno, annotations)
{
Date dt (anno->first);
when = dt.toString (conf.get ("dateformat", "m/d/Y"));
description += "\n" + when + " " + anno->second;
}
row = table.addRow (); row = table.addRow ();
table.addCell (row, 0, "Description"); table.addCell (row, 0, "Description");
table.addCell (row, 1, refTask.getDescription ()); table.addCell (row, 1, description);
if (refTask.getAttribute ("project") != "") if (refTask.getAttribute ("project") != "")
{ {
@ -1683,6 +1705,7 @@ std::string handleReportStats (TDB& tdb, T& task, Config& conf)
int pendingT = 0; int pendingT = 0;
int completedT = 0; int completedT = 0;
int taggedT = 0; int taggedT = 0;
int annotationsT = 0;
int recurringT = 0; int recurringT = 0;
float daysPending = 0.0; float daysPending = 0.0;
int descLength = 0; int descLength = 0;
@ -1713,6 +1736,8 @@ std::string handleReportStats (TDB& tdb, T& task, Config& conf)
descLength += it->getDescription ().length (); descLength += it->getDescription ().length ();
annotationsT += it->getAnnotationCount ();
std::vector <std::string> tags; std::vector <std::string> tags;
it->getTags (tags); it->getTags (tags);
if (tags.size ()) ++taggedT; if (tags.size ()) ++taggedT;
@ -1760,6 +1785,7 @@ std::string handleReportStats (TDB& tdb, T& task, Config& conf)
out << "Tasks tagged " << std::setprecision (3) << (100.0 * taggedT / totalT) << "%" << std::endl; out << "Tasks tagged " << std::setprecision (3) << (100.0 * taggedT / totalT) << "%" << std::endl;
} }
out << "Annotations " << annotationsT << std::endl;
out << "Unique tags " << allTags.size () << std::endl; out << "Unique tags " << allTags.size () << std::endl;
out << "Projects " << allProjects.size () << std::endl; out << "Projects " << allProjects.size () << std::endl;
@ -2164,7 +2190,7 @@ std::string handleCustomReport (
} }
} }
else if (*col == "description") else if (*col == "description_only")
{ {
table.addColumn ("Description"); table.addColumn ("Description");
table.setColumnWidth (columnCount, Table::flexible); table.setColumnWidth (columnCount, Table::flexible);
@ -2174,6 +2200,30 @@ std::string handleCustomReport (
table.addCell (row, columnCount, tasks[row].getDescription ()); table.addCell (row, columnCount, tasks[row].getDescription ());
} }
else if (*col == "description")
{
table.addColumn ("Description");
table.setColumnWidth (columnCount, Table::flexible);
table.setColumnJustification (columnCount, Table::left);
std::string description;
std::string when;
for (unsigned int row = 0; row < tasks.size(); ++row)
{
description = tasks[row].getDescription ();
std::map <time_t, std::string> annotations;
tasks[row].getAnnotations (annotations);
foreach (anno, annotations)
{
Date dt (anno->first);
when = dt.toString (conf.get ("dateformat", "m/d/Y"));
description += "\n" + when + " " + anno->second;
}
table.addCell (row, columnCount, description);
}
}
else if (*col == "recur") else if (*col == "recur")
{ {
table.addColumn ("Recur"); table.addColumn ("Recur");
@ -2313,6 +2363,7 @@ void validReportColumns (const std::vector <std::string>& columns)
*it != "active" && *it != "active" &&
*it != "tags" && *it != "tags" &&
*it != "recur" && *it != "recur" &&
*it != "description_only" &&
*it != "description") *it != "description")
bad.push_back (*it); bad.push_back (*it);

View file

@ -88,6 +88,10 @@ static std::string shortUsage (Config& conf)
table.addCell (row, 1, "task append [tags] [attrs] desc..."); table.addCell (row, 1, "task append [tags] [attrs] desc...");
table.addCell (row, 2, "Appends more description to an existing task"); table.addCell (row, 2, "Appends more description to an existing task");
row = table.addRow ();
table.addCell (row, 1, "task annotate ID desc...");
table.addCell (row, 2, "Adds an annotation to an existing task");
row = table.addRow (); row = table.addRow ();
table.addCell (row, 1, "task completed [tags] [attrs] desc..."); table.addCell (row, 1, "task completed [tags] [attrs] desc...");
table.addCell (row, 2, "Chronological listing of all completed tasks matching the specified criteria"); table.addCell (row, 2, "Chronological listing of all completed tasks matching the specified criteria");
@ -759,12 +763,12 @@ void updateShadowFile (TDB& tdb, Config& conf)
catch (std::string& error) catch (std::string& error)
{ {
std::cout << error << std::endl; std::cerr << error << std::endl;
} }
catch (...) catch (...)
{ {
std::cout << "Unknown error." << std::endl; std::cerr << "Unknown error." << std::endl;
} }
} }
@ -833,6 +837,7 @@ std::string runTaskCommand (
else if (command == "" && task.getId ()) { cmdMod = true; out = handleModify (tdb, task, conf); } else if (command == "" && task.getId ()) { cmdMod = true; out = handleModify (tdb, task, conf); }
else if (command == "add") { cmdMod = true; out = handleAdd (tdb, task, conf); } else if (command == "add") { cmdMod = true; out = handleAdd (tdb, task, conf); }
else if (command == "append") { cmdMod = true; out = handleAppend (tdb, task, conf); } else if (command == "append") { cmdMod = true; out = handleAppend (tdb, task, conf); }
else if (command == "annotate") { cmdMod = true; out = handleAnnotate (tdb, task, conf); }
else if (command == "done") { cmdMod = true; out = handleDone (tdb, task, conf); } else if (command == "done") { cmdMod = true; out = handleDone (tdb, task, conf); }
else if (command == "undelete") { cmdMod = true; out = handleUndelete (tdb, task, conf); } else if (command == "undelete") { cmdMod = true; out = handleUndelete (tdb, task, conf); }
else if (command == "delete") { cmdMod = true; out = handleDelete (tdb, task, conf); } else if (command == "delete") { cmdMod = true; out = handleDelete (tdb, task, conf); }

View file

@ -88,6 +88,7 @@ std::string handleStart (TDB&, T&, Config&);
std::string handleStop (TDB&, T&, Config&); std::string handleStop (TDB&, T&, Config&);
std::string handleUndo (TDB&, T&, Config&); std::string handleUndo (TDB&, T&, Config&);
std::string handleColor (Config&); std::string handleColor (Config&);
std::string handleAnnotate (TDB&, T&, Config&);
// report.cpp // report.cpp
void filter (std::vector<T>&, T&); void filter (std::vector<T>&, T&);

74
src/tests/annotate.t Executable file
View file

@ -0,0 +1,74 @@
#! /usr/bin/perl
################################################################################
## task - a command line task list manager.
##
## Copyright 2006 - 2009, Paul Beckingham.
## All rights reserved.
##
## This program is free software; you can redistribute it and/or modify it under
## the terms of the GNU General Public License as published by the Free Software
## Foundation; either version 2 of the License, or (at your option) any later
## version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
## FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## this program; if not, write to the
##
## Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor,
## Boston, MA
## 02110-1301
## USA
##
################################################################################
use strict;
use warnings;
use Test::More tests => 8;
# Create the rc file.
if (open my $fh, '>', 'annotate.rc')
{
print $fh "data.location=.\n",
"report.r.description=r\n",
"report.r.columns=id,description\n",
"report.r.sort=id+\n";
close $fh;
ok (-r 'annotate.rc', 'Created annotate.rc');
}
# Add two tasks, annotate one twice.
qx{../task rc:annotate.rc add one};
qx{../task rc:annotate.rc add two};
qx{../task rc:annotate.rc annotate 1 foo};
sleep 2;
qx{../task rc:annotate.rc annotate 1 bar};
my $output = qx{../task rc:annotate.rc r};
# ID Description
# -- -------------------------------
# 1 one
# 3/24/2009 foo
# 3/24/2009 bar
# 2 two
#
# 2 tasks
like ($output, qr/1 one/, 'task 1');
like ($output, qr/2 two/, 'task 2');
like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} foo/ms, 'first annotation');
like ($output, qr/foo.+\d{1,2}\/\d{1,2}\/\d{4} bar/ms, 'second annotation');
like ($output, qr/2 tasks/, 'count');
# Cleanup.
unlink 'pending.data';
ok (!-r 'pending.data', 'Removed pending.data');
unlink 'annotate.rc';
ok (!-r 'annotate.rc', 'Removed annotate.rc');
exit 0;

View file

@ -31,11 +31,11 @@
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int main (int argc, char** argv) int main (int argc, char** argv)
{ {
UnitTest test (5); UnitTest test (8);
T t; T t;
std::string s = t.compose (); std::string s = t.compose ();
test.is ((int)s.length (), 46, "T::T (); T::compose ()"); test.is ((int)s.length (), 49, "T::T (); T::compose ()");
test.diag (s); test.diag (s);
t.setStatus (T::completed); t.setStatus (T::completed);
@ -54,11 +54,26 @@ int main (int argc, char** argv)
test.diag (s); test.diag (s);
// Round trip test. // Round trip test.
std::string sample = "00000000-0000-0000-0000-000000000000 - [] [] Sample"; std::string sample = "00000000-0000-0000-0000-000000000000 - [] [] [] Sample";
T t2; T t2;
t2.parse (sample); t2.parse (sample);
sample += "\n"; sample += "\n";
test.is (t2.compose (), sample, "T::parse -> T::compose round trip"); test.is (t2.compose (), sample, "T::parse -> T::compose round trip");
// b10b3236-70d8-47bb-840a-b4c430758fb6 - [foo] [bar:baz] [1237865996:'woof'] sample\n
// ....:....|....:....|....:....|....:....|....:....|....:....|....:....|....:....|....:....|
// ^ ^ ^
// 0 36 66
t.setStatus (T::pending);
t.addTag ("foo");
t.setAttribute ("bar", "baz");
t.addAnnotation ("woof");
t.setDescription ("sample");
std::string format = t.compose ();
test.is (format.substr (36, 20), " - [foo] [bar:baz] [", "compose tag, attribute");
test.is (format.substr (66, 16), ":'woof'] sample\n", "compose annotation");
test.is (t.getAnnotationCount (), 1, "annotation count");
return 0; return 0;
} }