Bug Fix - #299 Can't use multiple project.hasnt:foo

- Fixed bug that prevented a filter from containing multiple references
  to the same attribute (thanks for John Florian).
- Added unit tests to verify.
This commit is contained in:
Paul Beckingham 2010-01-24 18:40:04 -05:00
parent cc82823c47
commit 48bf9d9f85
4 changed files with 150 additions and 60 deletions

View file

@ -66,7 +66,10 @@
+ Fixed bug that showed a calendar for the year 2037 when 'task calendar due'
was run, and there are no tasks with due dates.
+ Fixed bug #360 which prevented certain modifications to recurring tasks
(thanks to John Flrorian).
(thanks to John Florian).
+ Fixed bug #299 which prevented excluding multiple projects from a report,
by using "task list project.isnt:foo project.isnt:bar" (thanks to John
Florian).
------ old releases ------------------------------

View file

@ -591,6 +591,8 @@ void Context::parse (
parseTask[name + "." + mod] = attribute;
else
parseTask[name] = attribute;
autoFilter (attribute, parseFilter);
}
// *arg has the appearance of an attribute (foo:bar), but isn't
@ -656,6 +658,19 @@ void Context::parse (
{
debug ("parse description '" + descCandidate + "'");
parseTask.set ("description", descCandidate);
// Now convert the description to a filter on each word, if necessary.
if (parseCmd.isReadOnlyCommand ())
{
std::vector <std::string> words;
split (words, descCandidate, ' ');
std::vector <std::string>::iterator it;
for (it = words.begin (); it != words.end (); ++it)
{
Att a ("description", "contains", *it);
autoFilter (a, parseFilter);
}
}
}
// At this point, either a sequence or a command should have been found.
@ -663,9 +678,10 @@ void Context::parse (
parseCmd.parse (descCandidate);
// Read-only command (reports, status, info ...) use filters. Write commands
// (add, done ...) do not.
// (add, done ...) do not. The filter was constructed iteratively above, but
// tags were omitted, so they are added now.
if (parseCmd.isReadOnlyCommand ())
autoFilter (parseTask, parseFilter);
autoFilter (parseFilter);
// If no command was specified, and there were no command line arguments
// then invoke the default command.
@ -726,68 +742,70 @@ void Context::clear ()
////////////////////////////////////////////////////////////////////////////////
// Add all the attributes in the task to the filter. All except uuid.
void Context::autoFilter (Task& t, Filter& f)
{
foreach (att, t)
void Context::autoFilter (Att& a, Filter& f)
{
// Words are found in the description using the .has modifier.
if (att->second.name () == "description" && att->second.mod () == "")
if (a.name () == "description" && a.mod () == "")
{
std::vector <std::string> words;
split (words, att->second.value (), ' ');
split (words, a.value (), ' ');
foreach (word, words)
{
f.push_back (Att ("description", "has", *word));
debug ("auto filter: " + att->second.name () + ".has:" + *word);
debug ("auto filter: " + a.name () + ".has:" + *word);
}
}
// Projects are matched left-most.
else if (att->second.name () == "project" && att->second.mod () == "")
else if (a.name () == "project" && a.mod () == "")
{
if (att->second.value () != "")
if (a.value () != "")
{
f.push_back (Att ("project", "startswith", att->second.value ()));
debug ("auto filter: " + att->second.name () + ".startswith:" + att->second.value ());
f.push_back (Att ("project", "startswith", a.value ()));
debug ("auto filter: " + a.name () + ".startswith:" + a.value ());
}
else
{
f.push_back (Att ("project", "is", att->second.value ()));
debug ("auto filter: " + att->second.name () + ".is:" + att->second.value ());
f.push_back (Att ("project", "is", a.value ()));
debug ("auto filter: " + a.name () + ".is:" + a.value ());
}
}
// The limit attribute does not participate in filtering, and needs to be
// specifically handled in handleCustomReport.
else if (att->second.name () == "limit")
else if (a.name () == "limit")
{
}
// Every task has a unique uuid by default, and it shouldn't be included,
// because it is guaranteed to not match.
else if (att->second.name () == "uuid")
else if (a.name () == "uuid")
{
}
// The mechanism for filtering on tags is +/-<tag>.
// Do not handle here - see below.
else if (att->second.name () == "tags")
else if (a.name () == "tags")
{
}
// Generic attribute matching.
else
{
f.push_back (att->second);
f.push_back (a);
debug ("auto filter: " +
att->second.name () +
(att->second.mod () != "" ?
("." + att->second.mod () + ":") :
a.name () +
(a.mod () != "" ?
("." + a.mod () + ":") :
":") +
att->second.value ());
a.value ());
}
}
////////////////////////////////////////////////////////////////////////////////
// Add all the tags in the task to the filter.
void Context::autoFilter (Filter& f)
{
// This is now a correct implementation of a filter on the presence or absence
// of a tag. The prior code provided the illusion of leftmost partial tag
// matches, but was really using the 'contains' and 'nocontains' attribute

View file

@ -70,7 +70,8 @@ public:
private:
void loadCorrectConfigFile ();
void loadAliases ();
void autoFilter (Task&, Filter&);
void autoFilter (Att&, Filter&);
void autoFilter (Filter&);
public:
Config config;

68
src/tests/bug.299.t Executable file
View file

@ -0,0 +1,68 @@
#! /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 => 8;
# Create the rc file.
if (open my $fh, '>', 'bug.rc')
{
print $fh "data.location=.\n",
"confirmation=no\n";
close $fh;
ok (-r 'bug.rc', 'Created bug.rc');
}
# Setup: Add three unique tasks with different project names.
qx{../task rc:bug.rc add project:one foo};
qx{../task rc:bug.rc add project:two bar};
qx{../task rc:bug.rc add project:three baz};
# Result: Run list but exclude two of the three projects names using
# project.hasnt:<name>
my $output = qx{../task rc:bug.rc list project.isnt:one project.isnt:two};
unlike ($output, qr/one.*foo/ms, 'project.isnt:one project.isnt:two - no foo');
unlike ($output, qr/two.*bar/ms, 'project.isnt:one project.isnt:two - no bar');
like ($output, qr/three.*baz/ms, 'project.isnt:one project.isnt:two - yes baz');
# Cleanup.
unlink 'pending.data';
ok (!-r 'pending.data', 'Removed pending.data');
unlink 'completed.data';
ok (!-r 'completed.data', 'Removed completed.data');
unlink 'undo.data';
ok (!-r 'undo.data', 'Removed undo.data');
unlink 'bug.rc';
ok (!-r 'bug.rc', 'Removed bug.rc');
exit 0;