From 5e53226eb8ba0767a714efa5489a11525a05d2ba Mon Sep 17 00:00:00 2001 From: Federico Hernandez Date: Sun, 20 Jun 2010 20:58:46 +0200 Subject: [PATCH] Feature #408 - Allow deletion of annotations. - Added new denotate command - Added unit tests in denotate.t - Change task.1 man page --- ChangeLog | 4 +- NEWS | 1 + doc/man/task.1 | 7 +++ src/Cmd.cpp | 2 + src/Context.cpp | 1 + src/Hooks.cpp | 1 + src/command.cpp | 93 +++++++++++++++++++++++++++++++++ src/i18n.h | 1 + src/main.h | 1 + src/report.cpp | 4 ++ src/tests/denotate.t | 119 +++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 233 insertions(+), 1 deletion(-) create mode 100755 src/tests/denotate.t diff --git a/ChangeLog b/ChangeLog index 5a7171704..96cd4d905 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,7 +15,9 @@ configuration settings or just the ones matching a search string. 'task config' is now only used to set new configuration values. + Added feature #298, supporting a configurable number of future recurring - tasks that are generated. + tasks that are generated. + + Added feature #408, making it possible to delete annotations with the new + denotate command and the provided description (thanks to Dirk Deimeke). + Fixed bug #406 so that task now includes command aliases in the _commands helper command used by shell completion scripts. + Fixed bug #211 - it was unclear which commands modify a task description. diff --git a/NEWS b/NEWS index e21171170..db52f511e 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,7 @@ New Features in task 1.9 - Alias support in shell completion scripts. - New iCalendar export format. - New 'show' command to display configuration settings. + - New 'denotate' command to delete annotations. Please refer to the ChangeLog file for full details. There are too many to list here. diff --git a/doc/man/task.1 b/doc/man/task.1 index eedb3e8eb..c05fa5720 100644 --- a/doc/man/task.1 +++ b/doc/man/task.1 @@ -25,6 +25,13 @@ Adds a new task that is already completed, to the task list. .B annotate ID description Adds an annotation to an existing task. +.TP +.B denotate ID description +Deletes an annotation for the specified task. If the provided description matches an +annotation exactly, the corresponding annotation is deleted. If the provided description +matches an annotation partly (from the beginning), the first partly matched annotation +is deleted. + .TP .B info ID Shows all data and metadata for the specified task. diff --git a/src/Cmd.cpp b/src/Cmd.cpp index e2f9cfc94..c55860890 100644 --- a/src/Cmd.cpp +++ b/src/Cmd.cpp @@ -123,6 +123,7 @@ void Cmd::load () commands.push_back (context.stringtable.get (CMD_ADD, "add")); commands.push_back (context.stringtable.get (CMD_APPEND, "append")); commands.push_back (context.stringtable.get (CMD_ANNOTATE, "annotate")); + commands.push_back (context.stringtable.get (CMD_DENOTATE, "denotate")); commands.push_back (context.stringtable.get (CMD_CALENDAR, "calendar")); commands.push_back (context.stringtable.get (CMD_COLORS, "colors")); commands.push_back (context.stringtable.get (CMD_CONFIG, "config")); @@ -235,6 +236,7 @@ bool Cmd::isWriteCommand () if (command == context.stringtable.get (CMD_ADD, "add") || command == context.stringtable.get (CMD_APPEND, "append") || command == context.stringtable.get (CMD_ANNOTATE, "annotate") || + command == context.stringtable.get (CMD_DENOTATE, "denotate") || command == context.stringtable.get (CMD_DELETE, "delete") || command == context.stringtable.get (CMD_DONE, "done") || command == context.stringtable.get (CMD_DUPLICATE, "duplicate") || diff --git a/src/Context.cpp b/src/Context.cpp index 6b8ddffc3..8cd22b6ba 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -226,6 +226,7 @@ int Context::dispatch (std::string &out) else if (cmd.command == "append") { rc = handleAppend (out); } else if (cmd.command == "prepend") { rc = handlePrepend (out); } else if (cmd.command == "annotate") { rc = handleAnnotate (out); } + else if (cmd.command == "denotate") { rc = handleDenotate (out); } else if (cmd.command == "done") { rc = handleDone (out); } else if (cmd.command == "delete") { rc = handleDelete (out); } else if (cmd.command == "start") { rc = handleStart (out); } diff --git a/src/Hooks.cpp b/src/Hooks.cpp index c405cdfe9..2d24205d3 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -280,6 +280,7 @@ bool Hooks::validProgramEvent (const std::string& event) event == "pre-shell-prompt" || event == "post-shell-prompt" || event == "pre-add-command" || event == "post-add-command" || event == "pre-annotate-command" || event == "post-annotate-command" || + event == "pre-denotate-command" || event == "post-denotate-command" || event == "pre-append-command" || event == "post-append-command" || event == "pre-calendar-command" || event == "post-calendar-command" || event == "pre-color-command" || event == "post-color-command" || diff --git a/src/command.cpp b/src/command.cpp index 172d05ed0..723027c1a 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -2016,6 +2016,99 @@ int handleAnnotate (std::string &outs) return rc; } +//////////////////////////////////////////////////////////////////////////////// +int handleDenotate (std::string &outs) +{ + int rc = 0; + + if (context.hooks.trigger ("pre-denotate-command")) + { + if (!context.task.has ("description")) + throw std::string ("Description needed to delete an annotation."); + + if (context.sequence.size () == 0) + throw std::string ("A task ID is needed to delete an annotation."); + + std::stringstream out; + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + context.filter.applySequence (tasks, context.sequence); + + Permission permission; + if (context.sequence.size () > (size_t) context.config.getInteger ("bulk")) + permission.bigSequence (); + + foreach (task, tasks) + { + Task before (*task); + std::string desc = context.task.get ("description"); + std::vector annotations; + task->getAnnotations (annotations); + + if (annotations.size () == 0) + throw std::string ("The specified task has no annotations that can be deleted."); + + std::vector ::iterator i; + std::string anno; + bool match = false;; + for (i = annotations.begin (); i != annotations.end (); ++i) + { + anno = i->value (); + if (anno == desc) + { + match = true; + annotations.erase (i); + task->setAnnotations (annotations); + break; + } + } + if (!match) + { + for (i = annotations.begin (); i != annotations.end (); ++i) + { + anno = i->value (); + std::string::size_type loc = anno.find (desc, 0); + if (loc != std::string::npos && loc == 0) + { + match = true; + annotations.erase (i); + task->setAnnotations (annotations); + break; + } + } + } + if (taskDiff (before, *task)) + { + if (permission.confirmed (before, taskDifferences (before, *task) + "Proceed with change?")) + { + context.tdb.update (*task); + if (context.config.getBoolean ("echo.command")) + out << "Found annotation '" + << anno + << "' and deleted it." + << std::endl; + } + } + else + out << "Did not find any matching annotation to be deleted for '" + << desc + << "'." + << std::endl; + } + + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-denotate-command"); + } + return rc; +} + //////////////////////////////////////////////////////////////////////////////// int deltaAppend (Task& task) { diff --git a/src/i18n.h b/src/i18n.h index 4d3816d15..0690e6c9f 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -77,6 +77,7 @@ #define CMD_DONE 208 #define CMD_DUPLICATE 209 #define CMD_EDIT 210 +#define CMD_DENOTATE 211 #define CMD_HELP 212 diff --git a/src/main.h b/src/main.h index 19e6c1b42..ade00a904 100644 --- a/src/main.h +++ b/src/main.h @@ -77,6 +77,7 @@ int handleStart (std::string &); int handleStop (std::string &); int handleColor (std::string &); int handleAnnotate (std::string &); +int handleDenotate (std::string &); int handleDuplicate (std::string &); void handleUndo (); #ifdef FEATURE_SHELL diff --git a/src/report.cpp b/src/report.cpp index 791c64951..a0a28d030 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -93,6 +93,10 @@ int shortUsage (std::string &outs) table.addCell (row, 1, "task annotate ID desc..."); table.addCell (row, 2, "Adds an annotation to an existing task."); + row = table.addRow (); + table.addCell (row, 1, "task denotate ID desc..."); + table.addCell (row, 2, "Deletes an annotation of an existing task."); + row = table.addRow (); table.addCell (row, 1, "task ID [tags] [attrs] [desc...]"); table.addCell (row, 2, "Modifies the existing task with provided arguments."); diff --git a/src/tests/denotate.t b/src/tests/denotate.t new file mode 100755 index 000000000..7cfecff7c --- /dev/null +++ b/src/tests/denotate.t @@ -0,0 +1,119 @@ +#! /usr/bin/perl +################################################################################ +## task - a command line task list manager. +## +## Copyright 2006 - 2010, 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 => 29; + +# Create the rc file. +if (open my $fh, '>', 'denotate.rc') +{ + # Note: Use 'rrr' to guarantee a unique report name. Using 'r' conflicts + # with 'recurring'. + print $fh "data.location=.\n", + "confirmation=off\n", + "report.rrr.description=rrr\n", + "report.rrr.columns=id,description\n", + "report.rrr.sort=id+\n"; + close $fh; + ok (-r 'denotate.rc', 'Created denotate.rc'); +} + +# Add four tasks, annotate one three times, one twice, one just once and one none. +qx{../task rc:denotate.rc add one}; +qx{../task rc:denotate.rc annotate 1 Ernie}; +sleep 1; +qx{../task rc:denotate.rc annotate 1 Bert}; +sleep 1; +qx{../task rc:denotate.rc annotate 1 Bibo}; +sleep 1; +qx{../task rc:denotate.rc annotate 1 Kermit the frog}; +sleep 1; +qx{../task rc:denotate.rc annotate 1 Kermit the frog}; +sleep 1; +qx{../task rc:denotate.rc annotate 1 Kermit}; +sleep 1; +qx{../task rc:denotate.rc annotate 1 Kermit and Miss Piggy}; + +my $output = qx{../task rc:denotate.rc rrr}; + +like ($output, qr/1 one/, 'task 1'); +like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} Ernie/ms, 'first annotation'); +like ($output, qr/Ernie.+\d{1,2}\/\d{1,2}\/\d{4} Bert/ms, 'second annotation'); +like ($output, qr/Bert.+\d{1,2}\/\d{1,2}\/\d{4} Bibo/ms, 'third annotation'); +like ($output, qr/Bibo.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'fourth annotation'); +like ($output, qr/frog.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'fifth annotation'); +like ($output, qr/frog.+\d{1,2}\/\d{1,2}\/\d{4} Kermit/ms, 'sixth annotation'); +like ($output, qr/Kermit.+\d{1,2}\/\d{1,2}\/\d{4} Kermit and Miss Piggy/ms, 'seventh annotation'); +like ($output, qr/1 task/, 'count'); + +qx{../task rc:denotate.rc denotate 1 Ernie}; +$output = qx{../task rc:denotate.rc rrr}; +unlike ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} Ernie/ms, 'Delete annotation'); +like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} Bert/ms, 'Bert now first annotationt'); + +qx{../task rc:denotate.rc denotate 1 Bi}; +$output = qx{../task rc:denotate.rc rrr}; +unlike ($output, qr/Bert.+\d{1,2}\/\d{1,2}\/\d{4} Bibo/ms, 'Delete partial match'); +like ($output, qr/Bert.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'Kermit the frog now second annotation'); + +qx{../task rc:denotate.rc denotate 1 BErt}; +$output = qx{../task rc:denotate.rc rrr}; +like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} Bert/ms, 'Denotate is case sensitive'); +like ($output, qr/Bert.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'Kermit the frog still second annoation'); + +qx{../task rc:denotate.rc denotate 1 Kermit}; +$output = qx{../task rc:denotate.rc rrr}; +like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} Bert/ms, 'Exact match deletion - Bert'); +like ($output, qr/Bert.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'Exact match deletion - Kermit the frog'); +like ($output, qr/frog.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'Exact match deletion - Kermit the frog'); +like ($output, qr/frog.+\d{1,2}\/\d{1,2}\/\d{4} Kermit and Miss Piggy/ms, 'Exact match deletion - Kermit and Miss Piggy'); + +qx{../task rc:denotate.rc denotate 1 Kermit the}; +$output = qx{../task rc:denotate.rc rrr}; +like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} Bert/ms, 'Delete just one annotation - Bert'); +like ($output, qr/Bert.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'Delete just one annotation - Kermit the frog'); +like ($output, qr/frog.+\d{1,2}\/\d{1,2}\/\d{4} Kermit and Miss Piggy/ms, 'Delete just one annotation - Kermit and Miss Piggy'); + +qx{../task rc:denotate.rc denotate 1 Kermit a}; +$output = qx{../task rc:denotate.rc rrr}; +like ($output, qr/one.+\d{1,2}\/\d{1,2}\/\d{4} Bert/ms, 'Delete partial match - Bert'); +like ($output, qr/Bert.+\d{1,2}\/\d{1,2}\/\d{4} Kermit the frog/ms, 'Delete partial match - Kermit the frog'); +unlike ($output, qr/frog.+\d{1,2}\/\d{1,2}\/\d{4} Kermit and Miss Piggy/ms, 'Delete partial match - Kermit and Miss Piggy'); + +# Cleanup. +unlink 'pending.data'; +ok (!-r 'pending.data', 'Removed pending.data'); + +unlink 'undo.data'; +ok (!-r 'undo.data', 'Removed undo.data'); + +unlink 'denotate.rc'; +ok (!-r 'denotate.rc', 'Removed denotate.rc'); + +exit 0;