Merge branch '2.6.0' into getFromContext

This commit is contained in:
Tomas Babej 2021-08-28 23:53:40 -04:00 committed by GitHub
commit d91e30ee13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 640 additions and 302 deletions

View file

@ -24,9 +24,6 @@ jobs:
- name: "Fedora 34" - name: "Fedora 34"
runner: ubuntu-latest runner: ubuntu-latest
dockerfile: fedora34 dockerfile: fedora34
- name: "Debian Stable"
runner: ubuntu-latest
dockerfile: debianstable
- name: "Debian Testing" - name: "Debian Testing"
runner: ubuntu-latest runner: ubuntu-latest
dockerfile: debiantesting dockerfile: debiantesting

View file

@ -157,6 +157,7 @@ The following submitted code, packages or analysis, and deserve special thanks:
Julien Rabinow Julien Rabinow
Daniel Mowitz Daniel Mowitz
Scott Mcdermott Scott Mcdermott
Bharatvaj
Thanks to the following, who submitted detailed bug reports and excellent Thanks to the following, who submitted detailed bug reports and excellent
suggestions: suggestions:
@ -344,3 +345,4 @@ suggestions:
Arvedui Arvedui
reportaman reportaman
Pablo Vizcay Pablo Vizcay
Jake C.

View file

@ -8,32 +8,42 @@
- TW #1804 Importing malformed annotation (without entry timestamp) causes - TW #1804 Importing malformed annotation (without entry timestamp) causes
segmentation fault. segmentation fault.
Thanks to David Badura. Thanks to David Badura.
- TW #1824 Fixed countdown formatting
Thanks to Sebastian Uharek
- TW #1896 Parser cannot handle empty parentheses - TW #1896 Parser cannot handle empty parentheses
Thanks to Tomas Babej. Thanks to Tomas Babej.
- TW #1908 Cannot create task with explicit description 'start ....' - TW #1908 Cannot create task with explicit description 'start ....'
Thanks to Matt Chun-Lum. Thanks to Matt Chun-Lum.
- TW #1911 Support holidays longer then 1 day - TW #1911 Support holidays longer then 1 day
Thanks to Daniel Mowitz. Thanks to Daniel Mowitz.
- TW #1913 Project names with dashes and attribute names fail to parse
Thanks to Yanick Champoux.
- TW #1938 Missing annotation on import if entry is duplicated - TW #1938 Missing annotation on import if entry is duplicated
Thanks to Florian. Thanks to Florian.
- TW #1955 Adding tasks in context. - TW #1955 Adding tasks in context.
Thanks to Jean-Francois Joly, Matt Smith. Thanks to Jean-Francois Joly, Matt Smith.
- TW #1960 Fixed bug with double escaped single quotes.
Thanks to Sebastian Uharek
- TW #2004 "shell" should not be expand to "exec tasksh" - TW #2004 "shell" should not be expand to "exec tasksh"
Thanks to Arvedui Thanks to Arvedui
- TW #2007 Compute number of current tasks correctly - TW #2007 Compute number of current tasks correctly
Thanks to Janik Rabe Thanks to Janik Rabe
- TW #2017 Support 64-bit datetime values - TW #2017 Support 64-bit datetime values
Thanks to Evgeniy Vasilev Thanks to Evgeniy Vasilev
- TW #2257 UDA string fields can't start with certain keywords
Thanks to Michael Russell.
- TW #2060 Review timestamp is displayed as unix time, not formatted - TW #2060 Review timestamp is displayed as unix time, not formatted
Thanks to JavaZauber Thanks to JavaZauber
- TW #2093 wrong order under projects command - TW #2093 wrong order under projects command
Thanks to Beka, Max Rossmannek. Thanks to Beka, Max Rossmannek.
- TW #2101 Numeric UDA values above 2,147,483,647 overflow without warning - TW #2101 Numeric UDA values above 2,147,483,647 overflow without warning
Thanks to Adam Monsen. Thanks to Adam Monsen.
- TW #2136 Configuration options can be overwritten for current context
Thanks to Sebastian Uharek
- TW #2208 Feature: added coloring of dates with scheduled tasks to calendar
Thanks to Sebastian Uharek
- TW #2247 Configuration override rc.verbose:off not respected - TW #2247 Configuration override rc.verbose:off not respected
Thanks to Vignesh Prabhu. Thanks to Vignesh Prabhu.
- TW #2257 UDA string fields can't start with certain keywords
Thanks to Michael Russell.
- TW #2290 Support moving the config file to XDG_CONFIG_HOME - TW #2290 Support moving the config file to XDG_CONFIG_HOME
Thanks to Julien Rabinow. Thanks to Julien Rabinow.
- TW #2292 CmdEdit: Interruption should remove lock file - TW #2292 CmdEdit: Interruption should remove lock file
@ -77,13 +87,20 @@
- TW #2536 Feature: inclusive range-end attribute modifier 'by' so 'end of' - TW #2536 Feature: inclusive range-end attribute modifier 'by' so 'end of'
named dates can be filtered inclusively named dates can be filtered inclusively
Thanks to Scott Mcdermott Thanks to Scott Mcdermott
- TW #1824 Fixed countdown formatting - TW #2550 Write context skipped if description contains an identifier
Thanks to Sebastian Uharek Thanks to Sebastian Fricke.
- #2208 Feature: added coloring of dates with scheduled tasks to calendar - TW #2554 Remove the waiting state, and consider any task with wait>now to be
Thanks to Sebastian Uharek waiting
- Feature: Configuration options can be overwritten for current Thanks to Dustin J. Mitchell
context - TW #2560 Add report.<name>.context configuration variable
Thanks to Sebastian Uharek Thanks to Jake C.
- TW #2569 The `json.depends.array` configuration option is now ignored.
Dependencies are always represented as an array in JSON output.
Thanks to Dustin J. Mitchell
- TW #2580 Importing malformed JSON task crashes TW
Thanks to bharatvaj.
- TW #2581 Config entry with a trailing comment cannot be modified
------ current release --------------------------- ------ current release ---------------------------

17
NEWS
View file

@ -27,7 +27,13 @@ New Features in Taskwarrior 2.6.0
'due.by:eod', which it would not otherwise. It also works with 'due.by:eod', which it would not otherwise. It also works with
whole units like days, e.g. 'add test due:2021-07-17' would not match whole units like days, e.g. 'add test due:2021-07-17' would not match
'due.before:tomorrow' (on the 16th), but would match 'due.by:tomorrow'. 'due.before:tomorrow' (on the 16th), but would match 'due.by:tomorrow'.
- Waiting is now an entirely "virtual" concept, based on a task's
'wait' property and the current time. Task is consiered "waiting" if its
wait attribute is in the future. TaskWarrior no longer explicitly
"unwaits" a task (the wait attribute is not removed once its value is in
the past), so the "unwait' verbosity token is no longer available.
This allows for filtering for tasks that were waiting in the past
intervals, but are not waiting anymore.
New Commands in Taskwarrior 2.6.0 New Commands in Taskwarrior 2.6.0
@ -38,6 +44,10 @@ New Configuration Options in Taskwarrior 2.6.0
- The context definitions for reporting commmands are now stored in - The context definitions for reporting commmands are now stored in
"context.<name>.read". Context definitions for write commands are now "context.<name>.read". Context definitions for write commands are now
supported using "context.<name>.write" configuration variable. supported using "context.<name>.write" configuration variable.
- Each report (and the timesheet command) can explicitly opt-out from the
currently active context by setting the report.<name>.context variable to 0
(defaults to 1). Useful for defining universal reports that ignore
currently set context, such as 'inbox' report for GTD methodology.
- Multi-day holidays are now supported. Use holiday.<name>.start=<date> and - Multi-day holidays are now supported. Use holiday.<name>.start=<date> and
holiday.<name>.end=<date> to specify a range-based holiday, such as a holiday.<name>.end=<date> to specify a range-based holiday, such as a
vacation. vacation.
@ -46,6 +56,8 @@ New Configuration Options in Taskwarrior 2.6.0
Newly Deprecated Features in Taskwarrior 2.6.0 Newly Deprecated Features in Taskwarrior 2.6.0
- The 'PARENT' and 'CHILD' virtual tags are replaced by 'TEMPLATE' and 'INSTANCE'. - The 'PARENT' and 'CHILD' virtual tags are replaced by 'TEMPLATE' and 'INSTANCE'.
- The 'waiting' status is now deprecated. We recommend using +WAITING virtual tag
or wait-attribute based filters, such as 'wait.before:eow' instead.
Fixed regressions in 2.6.0 Fixed regressions in 2.6.0
@ -56,6 +68,9 @@ Fixed regressions in 2.6.0
the first configuration override. Otherwise task would by default inform the first configuration override. Otherwise task would by default inform
about the other overrides (see #2247). This was a regression introduced in about the other overrides (see #2247). This was a regression introduced in
2.5.2. 2.5.2.
- The attribute values of the form "<attribute name>-<arbitrary string>", for
example "due-nextweek" or "scheduled-work" would fail to parse (see
#1913). This was a regression introduced in 2.5.1.
Removed Features in 2.6.0 Removed Features in 2.6.0

View file

@ -304,7 +304,7 @@ value.
.B task <filter> ready .B task <filter> ready
Shows a page of the most urgent ready tasks, sorted by urgency with started Shows a page of the most urgent ready tasks, sorted by urgency with started
tasks first. A ready task is one that is either unscheduled, or has a scheduled tasks first. A ready task is one that is either unscheduled, or has a scheduled
date that is past and has no wait date. date that is past and is not waiting.
.TP .TP
.B task <filter> oldest .B task <filter> oldest
@ -796,9 +796,10 @@ to 25 lines.
.TP .TP
.B wait:<wait-date> .B wait:<wait-date>
When a task is given a wait date, it is hidden from most reports by changing When a task is given a wait date, it is hidden from most built-in reports, which
its status to 'waiting'. When that date is passed, the status is changed back exclude +WAITING. When the date is in the past, the task is not considered +WAITING,
to 'pending', and the task becomes visible. and again becomes visible. Note that, for compatibilty, such tasks are shown as
having status "waiting", but this will change in a future release.
.TP .TP
.B depends:<id1,id2 ...> .B depends:<id1,id2 ...>

View file

@ -299,11 +299,10 @@ control specific occasions when output is generated. This list may contain:
sync Feedback about sync sync Feedback about sync
filter Shows the filter used in the command filter Shows the filter used in the command
context Show the current context. Displayed in footnote. context Show the current context. Displayed in footnote.
unwait Notification when a task leaves the 'waiting' state
override Notification when configuration options are overridden override Notification when configuration options are overridden
recur Notification when a new recurring task instance is created recur Notification when a new recurring task instance is created
"affected", "new-id", "new-uuid", "project", "unwait", "override" and "recur" "affected", "new-id", "new-uuid", "project", "override" and "recur"
imply "footnote". imply "footnote".
Note that the "1" setting is equivalent to all the tokens being specified, Note that the "1" setting is equivalent to all the tokens being specified,
@ -312,7 +311,7 @@ and the "nothing" setting is equivalent to none of the tokens being specified.
Here are the shortcut equivalents: Here are the shortcut equivalents:
verbose=on verbose=on
verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,unwait,override,recur verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,override,recur
verbose=0 verbose=0
verbose=blank,label,new-id,edit verbose=blank,label,new-id,edit
@ -425,12 +424,6 @@ array.
With json.array=0, export writes raw JSON objects to STDOUT, one per line. With json.array=0, export writes raw JSON objects to STDOUT, one per line.
Defaults to "1". Defaults to "1".
.TP
.B json.depends.array=1
Determines whether the export command encodes dependencies as an array of string
UUIDs, or one comma-separated string.
Defaults to "1".
.TP .TP
.B _forcecolor=1 .B _forcecolor=1
Taskwarrior shuts off color automatically when the output is not sent directly Taskwarrior shuts off color automatically when the output is not sent directly
@ -1286,6 +1279,12 @@ The description for report X when running the "task help" command.
This is a comma-separated list of columns and formatting specifiers. See the This is a comma-separated list of columns and formatting specifiers. See the
command 'task columns' for a full list of options and examples. command 'task columns' for a full list of options and examples.
.TP
.B report.X.context
A boolean value representing whether the given report should respect (apply)
the currently active context. See CONTEXT section for details about context.
Defaults to 1.
.TP .TP
.B report.X.labels .B report.X.labels
The labels for each column that will be used when generating report X. The The labels for each column that will be used when generating report X. The

View file

@ -34,6 +34,10 @@
#include <Color.h> #include <Color.h>
#include <shared.h> #include <shared.h>
#include <format.h> #include <format.h>
#include <CmdCustom.h>
#include <CmdTimesheet.h>
#include <utf8.h>
// Overridden by rc.abbreviation.minimum. // Overridden by rc.abbreviation.minimum.
int CLI2::minimumMatchLength = 3; int CLI2::minimumMatchLength = 3;
@ -411,14 +415,33 @@ void CLI2::lexArguments ()
_args.push_back (a); _args.push_back (a);
} }
// Process muktiple-token arguments. // Process multiple-token arguments.
else else
{ {
std::string quote = "'"; const std::string quote = "'";
std::string escaped = _original_args[i].attribute ("raw");
escaped = str_replace (escaped, quote, "\\'"); // Escape unescaped single quotes
std::string escaped = "";
// For performance reasons. The escaped string is as long as the original.
escaped.reserve (_original_args[i].attribute ("raw").size ());
std::string::size_type cursor = 0; std::string::size_type cursor = 0;
bool nextEscaped = false;
while (int num = utf8_next_char (_original_args[i].attribute ("raw"), cursor))
{
std::string character = utf8_character (num);
if (!nextEscaped && (character == "\\"))
nextEscaped = true;
else {
if (character == quote && !nextEscaped)
escaped += "\\";
nextEscaped = false;
}
escaped += character;
}
cursor = 0;
std::string word; std::string word;
if (Lexer::readWord (quote + escaped + quote, quote, cursor, word)) if (Lexer::readWord (quote + escaped + quote, quote, cursor, word))
{ {
@ -607,17 +630,18 @@ void CLI2::addContext (bool readable, bool writeable)
if (contextString.empty ()) if (contextString.empty ())
return; return;
// Detect if UUID or ID is set, and bail out // For readable contexts: Detect if UUID or ID is set, and bail out
for (auto& a : _args) if (readable)
{ for (auto& a : _args)
if (a._lextype == Lexer::Type::uuid ||
a._lextype == Lexer::Type::number ||
a._lextype == Lexer::Type::set)
{ {
Context::getContext ().debug (format ("UUID/ID argument found '{1}', not applying context.", a.attribute ("raw"))); if (a._lextype == Lexer::Type::uuid ||
return; a._lextype == Lexer::Type::number ||
a._lextype == Lexer::Type::set)
{
Context::getContext ().debug (format ("UUID/ID argument found '{1}', not applying context.", a.attribute ("raw")));
return;
}
} }
}
// Apply the context. Readable (filtering) takes precedence. Also set the // Apply the context. Readable (filtering) takes precedence. Also set the
// block now, since addFilter calls analyze(), which calls addContext(). // block now, since addFilter calls analyze(), which calls addContext().
@ -944,7 +968,23 @@ void CLI2::categorizeArgs ()
// Context is only applied for commands that request it. // Context is only applied for commands that request it.
std::string command = getCommand (); std::string command = getCommand ();
Command* cmd = Context::getContext ().commands[command]; Command* cmd = Context::getContext ().commands[command];
if (cmd && cmd->uses_context ())
// Determine if the command uses Context. CmdCustom and CmdTimesheet need to
// be handled separately, as they override the parent Command::use_context
// method, and this is a pointer to Command class.
//
// All Command classes overriding uses_context () getter need to be specified
// here.
bool uses_context;
if (dynamic_cast<CmdCustom*> (cmd))
uses_context = (dynamic_cast<CmdCustom*> (cmd))->uses_context ();
else if (dynamic_cast<CmdTimesheet*> (cmd))
uses_context = (dynamic_cast<CmdTimesheet*> (cmd))->uses_context ();
else if (cmd)
uses_context = cmd->uses_context ();
// Apply the context, if applicable
if (cmd && uses_context)
addContext (cmd->accepts_filter (), cmd->accepts_modifications ()); addContext (cmd->accepts_filter (), cmd->accepts_modifications ());
bool changes = false; bool changes = false;

View file

@ -87,7 +87,7 @@ std::string configurationDefaults =
"\n" "\n"
"# Miscellaneous\n" "# Miscellaneous\n"
"# # Comma-separated list. May contain any subset of:\n" "# # Comma-separated list. May contain any subset of:\n"
"verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,unwait,override,recur\n" "verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,override,recur\n"
"confirmation=1 # Confirmation on delete, big changes\n" "confirmation=1 # Confirmation on delete, big changes\n"
"recurrence=1 # Enable recurrence\n" "recurrence=1 # Enable recurrence\n"
"recurrence.confirmation=prompt # Confirmation for propagating changes among recurring tasks (yes/no/prompt)\n" "recurrence.confirmation=prompt # Confirmation for propagating changes among recurring tasks (yes/no/prompt)\n"
@ -109,7 +109,6 @@ std::string configurationDefaults =
"xterm.title=0 # Sets xterm title for some commands\n" "xterm.title=0 # Sets xterm title for some commands\n"
"expressions=infix # Prefer infix over postfix expressions\n" "expressions=infix # Prefer infix over postfix expressions\n"
"json.array=1 # Enclose JSON output in [ ]\n" "json.array=1 # Enclose JSON output in [ ]\n"
"json.depends.array=0 # Encode dependencies as a JSON array\n"
"abbreviation.minimum=2 # Shortest allowed abbreviation\n" "abbreviation.minimum=2 # Shortest allowed abbreviation\n"
"\n" "\n"
"# Dates\n" "# Dates\n"
@ -292,105 +291,123 @@ std::string configurationDefaults =
"report.long.description=All details of tasks\n" "report.long.description=All details of tasks\n"
"report.long.labels=ID,A,Created,Mod,Deps,P,Project,Tags,Recur,Wait,Sched,Due,Until,Description\n" "report.long.labels=ID,A,Created,Mod,Deps,P,Project,Tags,Recur,Wait,Sched,Due,Until,Description\n"
"report.long.columns=id,start.active,entry,modified.age,depends,priority,project,tags,recur,wait.remaining,scheduled,due,until,description\n" "report.long.columns=id,start.active,entry,modified.age,depends,priority,project,tags,recur,wait.remaining,scheduled,due,until,description\n"
"report.long.filter=status:pending\n" "report.long.filter=status:pending -WAITING\n"
"report.long.sort=modified-\n" "report.long.sort=modified-\n"
"report.long.context=1\n"
"\n" "\n"
"report.list.description=Most details of tasks\n" "report.list.description=Most details of tasks\n"
"report.list.labels=ID,Active,Age,D,P,Project,Tags,R,Sch,Due,Until,Description,Urg\n" "report.list.labels=ID,Active,Age,D,P,Project,Tags,R,Sch,Due,Until,Description,Urg\n"
"report.list.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due,until.remaining,description.count,urgency\n" "report.list.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due,until.remaining,description.count,urgency\n"
"report.list.filter=status:pending\n" "report.list.filter=status:pending -WAITING\n"
"report.list.sort=start-,due+,project+,urgency-\n" "report.list.sort=start-,due+,project+,urgency-\n"
"report.list.context=1\n"
"\n" "\n"
"report.ls.description=Few details of tasks\n" "report.ls.description=Few details of tasks\n"
"report.ls.labels=ID,A,D,Project,Tags,R,Wait,S,Due,Until,Description\n" "report.ls.labels=ID,A,D,Project,Tags,R,Wait,S,Due,Until,Description\n"
"report.ls.columns=id,start.active,depends.indicator,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due.countdown,until.countdown,description.count\n" "report.ls.columns=id,start.active,depends.indicator,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due.countdown,until.countdown,description.count\n"
"report.ls.filter=status:pending\n" "report.ls.filter=status:pending -WAITING\n"
"report.ls.sort=start-,description+\n" "report.ls.sort=start-,description+\n"
"report.ls.context=1\n"
"\n" "\n"
"report.minimal.description=Minimal details of tasks\n" "report.minimal.description=Minimal details of tasks\n"
"report.minimal.labels=ID,Project,Tags,Description\n" "report.minimal.labels=ID,Project,Tags,Description\n"
"report.minimal.columns=id,project,tags.count,description.count\n" "report.minimal.columns=id,project,tags.count,description.count\n"
"report.minimal.filter=status:pending or status:waiting\n" "report.minimal.filter=status:pending\n"
"report.minimal.sort=project+/,description+\n" "report.minimal.sort=project+/,description+\n"
"report.minimal.context=1\n"
"\n" "\n"
"report.newest.description=Newest tasks\n" "report.newest.description=Newest tasks\n"
"report.newest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n" "report.newest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.newest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n" "report.newest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
"report.newest.filter=status:pending or status:waiting\n" "report.newest.filter=status:pending\n"
"report.newest.sort=entry-\n" "report.newest.sort=entry-\n"
"report.newest.context=1\n"
"\n" "\n"
"report.oldest.description=Oldest tasks\n" "report.oldest.description=Oldest tasks\n"
"report.oldest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n" "report.oldest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.oldest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n" "report.oldest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
"report.oldest.filter=status:pending or status:waiting\n" "report.oldest.filter=status:pending\n"
"report.oldest.sort=entry+\n" "report.oldest.sort=entry+\n"
"report.oldest.context=1\n"
"\n" "\n"
"report.overdue.description=Overdue tasks\n" "report.overdue.description=Overdue tasks\n"
"report.overdue.labels=ID,Active,Age,Deps,P,Project,Tag,R,S,Due,Until,Description,Urg\n" "report.overdue.labels=ID,Active,Age,Deps,P,Project,Tag,R,S,Due,Until,Description,Urg\n"
"report.overdue.columns=id,start.age,entry.age,depends,priority,project,tags,recur.indicator,scheduled.countdown,due,until,description,urgency\n" "report.overdue.columns=id,start.age,entry.age,depends,priority,project,tags,recur.indicator,scheduled.countdown,due,until,description,urgency\n"
"report.overdue.filter=(status:pending or status:waiting) and +OVERDUE\n" "report.overdue.filter=status:pending and +OVERDUE\n"
"report.overdue.sort=urgency-,due+\n" "report.overdue.sort=urgency-,due+\n"
"report.overdue.context=1\n"
"\n" "\n"
"report.active.description=Active tasks\n" "report.active.description=Active tasks\n"
"report.active.labels=ID,Started,Active,Age,D,P,Project,Tags,Recur,W,Sch,Due,Until,Description\n" "report.active.labels=ID,Started,Active,Age,D,P,Project,Tags,Recur,W,Sch,Due,Until,Description\n"
"report.active.columns=id,start,start.age,entry.age,depends.indicator,priority,project,tags,recur,wait,scheduled.remaining,due,until,description\n" "report.active.columns=id,start,start.age,entry.age,depends.indicator,priority,project,tags,recur,wait,scheduled.remaining,due,until,description\n"
"report.active.filter=status:pending and +ACTIVE\n" "report.active.filter=status:pending and +ACTIVE\n"
"report.active.sort=project+,start+\n" "report.active.sort=project+,start+\n"
"report.active.context=1\n"
"\n" "\n"
"report.completed.description=Completed tasks\n" "report.completed.description=Completed tasks\n"
"report.completed.labels=ID,UUID,Created,Completed,Age,Deps,P,Project,Tags,R,Due,Description\n" "report.completed.labels=ID,UUID,Created,Completed,Age,Deps,P,Project,Tags,R,Due,Description\n"
"report.completed.columns=id,uuid.short,entry,end,entry.age,depends,priority,project,tags,recur.indicator,due,description\n" "report.completed.columns=id,uuid.short,entry,end,entry.age,depends,priority,project,tags,recur.indicator,due,description\n"
"report.completed.filter=status:completed\n" "report.completed.filter=status:completed\n"
"report.completed.sort=end+\n" "report.completed.sort=end+\n"
"report.completed.context=1\n"
"\n" "\n"
"report.recurring.description=Recurring Tasks\n" "report.recurring.description=Recurring Tasks\n"
"report.recurring.labels=ID,Active,Age,D,P,Project,Tags,Recur,Sch,Due,Until,Description,Urg\n" "report.recurring.labels=ID,Active,Age,D,P,Project,Tags,Recur,Sch,Due,Until,Description,Urg\n"
"report.recurring.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur,scheduled.countdown,due,until.remaining,description,urgency\n" "report.recurring.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur,scheduled.countdown,due,until.remaining,description,urgency\n"
"report.recurring.filter=(status:pending or status:waiting) and (+PARENT or +CHILD)\n" "report.recurring.filter=status:pending and (+PARENT or +CHILD)\n"
"report.recurring.sort=due+,urgency-,entry+\n" "report.recurring.sort=due+,urgency-,entry+\n"
"report.recurring.context=1\n"
"\n" "\n"
"report.waiting.description=Waiting (hidden) tasks\n" "report.waiting.description=Waiting (hidden) tasks\n"
"report.waiting.labels=ID,A,Age,D,P,Project,Tags,R,Wait,Remaining,Sched,Due,Until,Description\n" "report.waiting.labels=ID,A,Age,D,P,Project,Tags,R,Wait,Remaining,Sched,Due,Until,Description\n"
"report.waiting.columns=id,start.active,entry.age,depends.indicator,priority,project,tags,recur.indicator,wait,wait.remaining,scheduled,due,until,description\n" "report.waiting.columns=id,start.active,entry.age,depends.indicator,priority,project,tags,recur.indicator,wait,wait.remaining,scheduled,due,until,description\n"
"report.waiting.filter=+WAITING\n" "report.waiting.filter=+WAITING\n"
"report.waiting.sort=due+,wait+,entry+\n" "report.waiting.sort=due+,wait+,entry+\n"
"report.waiting.context=1\n"
"\n" "\n"
"report.all.description=All tasks\n" "report.all.description=All tasks\n"
"report.all.labels=ID,St,UUID,A,Age,Done,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n" "report.all.labels=ID,St,UUID,A,Age,Done,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.all.columns=id,status.short,uuid.short,start.active,entry.age,end.age,depends.indicator,priority,project.parent,tags.count,recur.indicator,wait.remaining,scheduled.remaining,due,until.remaining,description\n" "report.all.columns=id,status.short,uuid.short,start.active,entry.age,end.age,depends.indicator,priority,project.parent,tags.count,recur.indicator,wait.remaining,scheduled.remaining,due,until.remaining,description\n"
"report.all.sort=entry-\n" "report.all.sort=entry-\n"
"report.all.context=1\n"
"\n" "\n"
"report.next.description=Most urgent tasks\n" "report.next.description=Most urgent tasks\n"
"report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Due,Until,Description,Urg\n" "report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Due,Until,Description,Urg\n"
"report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency\n" "report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency\n"
"report.next.filter=status:pending limit:page\n" "report.next.filter=status:pending -WAITING limit:page\n"
"report.next.sort=urgency-\n" "report.next.sort=urgency-\n"
"report.next.context=1\n"
"\n" "\n"
"report.ready.description=Most urgent actionable tasks\n" "report.ready.description=Most urgent actionable tasks\n"
"report.ready.labels=ID,Active,Age,D,P,Project,Tags,R,S,Due,Until,Description,Urg\n" "report.ready.labels=ID,Active,Age,D,P,Project,Tags,R,S,Due,Until,Description,Urg\n"
"report.ready.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due.countdown,until.remaining,description,urgency\n" "report.ready.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due.countdown,until.remaining,description,urgency\n"
"report.ready.filter=+READY\n" "report.ready.filter=+READY\n"
"report.ready.sort=start-,urgency-\n" "report.ready.sort=start-,urgency-\n"
"report.ready.context=1\n"
"\n" "\n"
"report.blocked.description=Blocked tasks\n" "report.blocked.description=Blocked tasks\n"
"report.blocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n" "report.blocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
"report.blocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n" "report.blocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
"report.blocked.sort=due+,priority-,start-,project+\n" "report.blocked.sort=due+,priority-,start-,project+\n"
"report.blocked.filter=status:pending +BLOCKED\n" "report.blocked.filter=status:pending -WAITING +BLOCKED\n"
"report.blocked.context=1\n"
"\n" "\n"
"report.unblocked.description=Unblocked tasks\n" "report.unblocked.description=Unblocked tasks\n"
"report.unblocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n" "report.unblocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
"report.unblocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n" "report.unblocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
"report.unblocked.sort=due+,priority-,start-,project+\n" "report.unblocked.sort=due+,priority-,start-,project+\n"
"report.unblocked.filter=status:pending -BLOCKED\n" "report.unblocked.filter=status:pending -WAITING -BLOCKED\n"
"report.unblocked.context=1\n"
"\n" "\n"
"report.blocking.description=Blocking tasks\n" "report.blocking.description=Blocking tasks\n"
"report.blocking.labels=ID,UUID,A,Deps,Project,Tags,R,W,Sch,Due,Until,Description,Urg\n" "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.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.sort=urgency-,due+,entry+\n"
"report.blocking.filter=status:pending +BLOCKING\n" "report.blocking.filter=status:pending -WAITING +BLOCKING\n"
"report.blocking.context=1\n"
"\n" "\n"
"report.timesheet.filter=(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)\n" "report.timesheet.filter=(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)\n"
"report.timesheet.context=0\n"
"\n"; "\n";
// Supported modifiers, synonyms on the same line. // Supported modifiers, synonyms on the same line.
@ -1029,7 +1046,6 @@ bool Context::verbose (const std::string& token)
v != "project" && // v != "project" && //
v != "sync" && // v != "sync" && //
v != "filter" && // v != "filter" && //
v != "unwait" && //
v != "override" && // v != "override" && //
v != "context" && // v != "context" && //
v != "recur") // v != "recur") //
@ -1043,7 +1059,7 @@ bool Context::verbose (const std::string& token)
if (! verbosity.count ("footnote")) if (! verbosity.count ("footnote"))
{ {
// TODO: Some of these may not use footnotes yet. They should. // TODO: Some of these may not use footnotes yet. They should.
for (auto flag : {"affected", "new-id", "new-uuid", "project", "unwait", "override", "recur"}) for (auto flag : {"affected", "new-id", "new-uuid", "project", "override", "recur"})
{ {
if (verbosity.count (flag)) if (verbosity.count (flag))
{ {

View file

@ -330,6 +330,14 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
return true; return true;
} }
// Special handling of status required for virtual waiting status
// implementation. Remove in 3.0.0.
if (ref.data.size () && size == 1 && canonical == "status")
{
value = Variant (ref.statusToText (ref.getStatus ()));
return true;
}
Column* column = Context::getContext ().columns[canonical]; Column* column = Context::getContext ().columns[canonical];
if (ref.data.size () && size == 1 && column) if (ref.data.size () && size == 1 && column)

View file

@ -355,22 +355,13 @@ void TF2::load_gc (Task& task)
{ {
Context::getContext ().tdb2.pending._tasks.push_back (task); Context::getContext ().tdb2.pending._tasks.push_back (task);
} }
// 2.6.0: Waiting status is deprecated. Convert to pending to upgrade status
// field value in the data files.
else if (status == "waiting") else if (status == "waiting")
{ {
Datetime wait (task.get_date ("wait")); task.set ("status", "pending");
if (wait < now)
{
task.set ("status", "pending");
task.remove ("wait");
// Unwaiting pending tasks is the only case not caught by the size()
// checks in TDB2::gc(), so we need to signal it here.
Context::getContext ().tdb2.pending._dirty = true;
if (Context::getContext ().verbose ("unwait"))
Context::getContext ().footnote (format ("Un-waiting task {1} '{2}'", task.id, task.get ("description")));
}
Context::getContext ().tdb2.pending._tasks.push_back (task); Context::getContext ().tdb2.pending._tasks.push_back (task);
Context::getContext ().tdb2.pending._dirty = true;
} }
else else
{ {
@ -525,29 +516,26 @@ void TF2::dependency_scan ()
// Iterate and modify TDB2 in-place. Don't do this at home. // Iterate and modify TDB2 in-place. Don't do this at home.
for (auto& left : _tasks) for (auto& left : _tasks)
{ {
if (left.has ("depends")) for (auto& dep : left.getDependencyUUIDs ())
{ {
for (auto& dep : left.getDependencyUUIDs ()) for (auto& right : _tasks)
{ {
for (auto& right : _tasks) if (right.get ("uuid") == dep)
{ {
if (right.get ("uuid") == dep) // GC hasn't run yet, check both tasks for their current status
Task::status lstatus = left.getStatus ();
Task::status rstatus = right.getStatus ();
if (lstatus != Task::completed &&
lstatus != Task::deleted &&
rstatus != Task::completed &&
rstatus != Task::deleted)
{ {
// GC hasn't run yet, check both tasks for their current status left.is_blocked = true;
Task::status lstatus = left.getStatus (); right.is_blocking = true;
Task::status rstatus = right.getStatus ();
if (lstatus != Task::completed &&
lstatus != Task::deleted &&
rstatus != Task::completed &&
rstatus != Task::deleted)
{
left.is_blocked = true;
right.is_blocking = true;
}
// Only want to break out of the "right" loop.
break;
} }
// Only want to break out of the "right" loop.
break;
} }
} }
} }
@ -1249,7 +1237,6 @@ void TDB2::show_diff (
// Possible scenarios: // Possible scenarios:
// - task in pending that needs to be in completed // - task in pending that needs to be in completed
// - task in completed that needs to be in pending // - task in completed that needs to be in pending
// - waiting task in pending that needs to be un-waited
void TDB2::gc () void TDB2::gc ()
{ {
Timer timer; Timer timer;

View file

@ -147,7 +147,9 @@ Task::status Task::textToStatus (const std::string& input)
else if (input[0] == 'c') return Task::completed; else if (input[0] == 'c') return Task::completed;
else if (input[0] == 'd') return Task::deleted; else if (input[0] == 'd') return Task::deleted;
else if (input[0] == 'r') return Task::recurring; else if (input[0] == 'r') return Task::recurring;
else if (input[0] == 'w') return Task::waiting; // for compatibility, parse `w` as pending; Task::getStatus will
// apply the virtual waiting status if appropriate
else if (input[0] == 'w') return Task::pending;
throw format ("The status '{1}' is not valid.", input); throw format ("The status '{1}' is not valid.", input);
} }
@ -197,7 +199,7 @@ bool Task::has (const std::string& name) const
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::vector <std::string> Task::all () std::vector <std::string> Task::all () const
{ {
std::vector <std::string> all; std::vector <std::string> all;
for (const auto& i : data) for (const auto& i : data)
@ -301,12 +303,26 @@ Task::status Task::getStatus () const
if (! has ("status")) if (! has ("status"))
return Task::pending; return Task::pending;
return textToStatus (get ("status")); auto status = textToStatus (get ("status"));
// Implement the "virtual" Task::waiting status, which is not stored on-disk
// but is defined as a pending task with a `wait` attribute in the future.
// This is workaround for 2.6.0, remove in 3.0.0.
if (status == Task::pending && is_waiting ()) {
return Task::waiting;
}
return status;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void Task::setStatus (Task::status status) void Task::setStatus (Task::status status)
{ {
// the 'waiting' status is a virtual version of 'pending', so translate
// that back to 'pending' here
if (status == Task::waiting)
status = Task::pending;
set ("status", statusToText (status)); set ("status", statusToText (status));
recalc_urgency = true; recalc_urgency = true;
@ -559,6 +575,23 @@ bool Task::is_overdue () const
} }
#endif #endif
////////////////////////////////////////////////////////////////////////////////
bool Task::is_waiting () const
{
// note that is_waiting can return true for tasks in an actual status other
// than pending; in this case +WAITING will be set but the status will not be
// "waiting"
if (has ("wait"))
{
Datetime now;
Datetime wait (get_date ("wait"));
if (wait > now)
return true;
}
return false;
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Attempt an FF4 parse first, using Task::parse, and in the event of an error // Attempt an FF4 parse first, using Task::parse, and in the event of an error
// try a JSON parse, otherwise a legacy parse (currently no legacy formats are // try a JSON parse, otherwise a legacy parse (currently no legacy formats are
@ -639,6 +672,14 @@ void Task::parse (const std::string& input)
// ..and similarly, update `tags` to match the `tag_..` attributes // ..and similarly, update `tags` to match the `tag_..` attributes
fixTagsAttribute(); fixTagsAttribute();
// same for `depends` / `dep_..`
if (data.find ("depends") != data.end ()) {
for (auto& dep : split(data["depends"], ',')) {
data[dep2Attr(dep)] = "x";
}
}
fixDependsAttribute();
recalc_urgency = true; recalc_urgency = true;
} }
@ -752,6 +793,11 @@ void Task::parseJSON (const json::object* root_obj)
{ {
std::map <std::string, std::string> annos; std::map <std::string, std::string> annos;
// Fail if 'annotations' is not an array
if (i.second->type() != json::j_array) {
throw format ("Annotations is malformed: {1}", i.second->dump ());
}
auto atts = (json::array*)i.second; auto atts = (json::array*)i.second;
for (auto& annotations : atts->_data) for (auto& annotations : atts->_data)
{ {
@ -905,8 +951,10 @@ std::string Task::composeJSON (bool decorate /*= false*/) const
if (! i.first.compare (0, 11, "annotation_", 11)) if (! i.first.compare (0, 11, "annotation_", 11))
continue; continue;
// Tags and dependencies are handled below
if (i.first == "tags" || isTagAttr (i.first)) if (i.first == "tags" || isTagAttr (i.first))
// Tags are handled below continue;
if (i.first == "depends" || isDepAttr (i.first))
continue; continue;
// If value is an empty string, do not ever output it // If value is an empty string, do not ever output it
@ -950,45 +998,6 @@ std::string Task::composeJSON (bool decorate /*= false*/) const
++attributes_written; ++attributes_written;
} }
// Dependencies are an array by default.
else if (i.first == "depends"
#ifdef PRODUCT_TASKWARRIOR
// 2016-02-20: Taskwarrior 2.5.0 introduced the 'json.depends.array' setting
// which defaulted to 'on', and emitted this JSON for
// dependencies:
//
// With json.depends.array=on "depends":["<uuid>","<uuid>"]
// With json.depends.array=off "depends":"<uuid>,<uuid>"
//
// Taskwarrior 2.5.1 defaults this to 'off', because Taskserver
// 1.0.0 and 1.1.0 both expect that. Taskserver 1.2.0 will
// accept both forms, but emit the 'off' variant.
//
// When Taskwarrior 2.5.0 is no longer the dominant version,
// and Taskserver 1.2.0 is released, the default for
// 'json.depends.array' can revert to 'on'.
&& Context::getContext ().config.getBoolean ("json.depends.array")
#endif
)
{
auto deps = split (i.second, ',');
out << "\"depends\":[";
int count = 0;
for (const auto& i : deps)
{
if (count++)
out << ',';
out << '"' << i << '"';
}
out << ']';
++attributes_written;
}
// Everything else is a quoted value. // Everything else is a quoted value.
else else
{ {
@ -1049,6 +1058,25 @@ std::string Task::composeJSON (bool decorate /*= false*/) const
++attributes_written; ++attributes_written;
} }
auto depends = getDependencyUUIDs ();
if (depends.size() > 0)
{
out << ','
<< "\"depends\":[";
int count = 0;
for (const auto& dep : depends)
{
if (count++)
out << ',';
out << '"' << dep << '"';
}
out << ']';
++attributes_written;
}
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
// Include urgency. // Include urgency.
if (decorate) if (decorate)
@ -1153,8 +1181,10 @@ void Task::addDependency (int depid)
if (uuid == "") if (uuid == "")
throw format ("Could not create a dependency on task {1} - not found.", depid); throw format ("Could not create a dependency on task {1} - not found.", depid);
std::string depends = get ("depends"); // the addDependency(&std::string) overload will check this, too, but here we
if (depends.find (uuid) != std::string::npos) // can give an more natural error message containing the id the user
// provided.
if (hasDependency (uuid))
{ {
Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", id, depid)); Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", id, depid));
return; return;
@ -1170,23 +1200,16 @@ void Task::addDependency (const std::string& uuid)
if (uuid == get ("uuid")) if (uuid == get ("uuid"))
throw std::string ("A task cannot be dependent on itself."); throw std::string ("A task cannot be dependent on itself.");
// Store the dependency. if (hasDependency (uuid))
std::string depends = get ("depends");
if (depends != "")
{ {
// Check for extant dependency.
if (depends.find (uuid) == std::string::npos)
set ("depends", depends + ',' + uuid);
else
{
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", get ("uuid"), uuid)); Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", get ("uuid"), uuid));
#endif #endif
return; return;
}
} }
else
set ("depends", uuid); // Store the dependency.
set (dep2Attr (uuid), "x");
// Prevent circular dependencies. // Prevent circular dependencies.
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
@ -1195,64 +1218,102 @@ void Task::addDependency (const std::string& uuid)
#endif #endif
recalc_urgency = true; recalc_urgency = true;
fixDependsAttribute();
} }
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void Task::removeDependency (const std::string& uuid) void Task::removeDependency (int id)
{ {
auto deps = split (get ("depends"), ','); std::string uuid = Context::getContext ().tdb2.pending.uuid (id);
auto i = std::find (deps.begin (), deps.end (), uuid); // The removeDependency(std::string&) method will check this too, but here we
if (i != deps.end ()) // can give a more natural error message containing the id provided by the user
{ if (uuid == "" || !has (dep2Attr (uuid)))
deps.erase (i); throw format ("Could not delete a dependency on task {1} - not found.", id);
set ("depends", join (",", deps)); removeDependency (uuid);
recalc_urgency = true;
}
else
throw format ("Could not delete a dependency on task {1} - not found.", uuid);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void Task::removeDependency (int id) void Task::removeDependency (const std::string& uuid)
{ {
std::string depends = get ("depends"); auto depattr = dep2Attr (uuid);
std::string uuid = Context::getContext ().tdb2.pending.uuid (id); if (has (depattr))
if (uuid != "" && depends.find (uuid) != std::string::npos) remove (depattr);
removeDependency (uuid);
else else
throw format ("Could not delete a dependency on task {1} - not found.", id); throw format ("Could not delete a dependency on task {1} - not found.", uuid);
recalc_urgency = true;
fixDependsAttribute();
}
////////////////////////////////////////////////////////////////////////////////
bool Task::hasDependency (const std::string& uuid) const
{
auto depattr = dep2Attr (uuid);
return has (depattr);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::vector <int> Task::getDependencyIDs () const std::vector <int> Task::getDependencyIDs () const
{ {
std::vector <int> all; std::vector <int> ids;
for (auto& dep : split (get ("depends"), ',')) for (auto& attr : all ()) {
all.push_back (Context::getContext ().tdb2.pending.id (dep)); if (!isDepAttr (attr))
continue;
auto dep = attr2Dep (attr);
ids.push_back (Context::getContext ().tdb2.pending.id (dep));
}
return all; return ids;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::vector <std::string> Task::getDependencyUUIDs () const std::vector <std::string> Task::getDependencyUUIDs () const
{ {
return split (get ("depends"), ','); std::vector <std::string> uuids;
for (auto& attr : all ()) {
if (!isDepAttr (attr))
continue;
auto dep = attr2Dep (attr);
uuids.push_back (dep);
}
return uuids;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::vector <Task> Task::getDependencyTasks () const std::vector <Task> Task::getDependencyTasks () const
{ {
std::vector <Task> all; auto uuids = getDependencyUUIDs ();
for (auto& dep : split (get ("depends"), ','))
{
Task task;
Context::getContext ().tdb2.get (dep, task);
all.push_back (task);
}
return all; // NOTE: this may seem inefficient, but note that `TDB2::get` performs a
// linear search on each invocation, so scanning *once* is quite a bit more
// efficient.
std::vector <Task> blocking;
if (uuids.size() > 0)
for (auto& it : Context::getContext ().tdb2.pending.get_tasks ())
if (it.getStatus () != Task::completed &&
it.getStatus () != Task::deleted &&
std::find (uuids.begin (), uuids.end (), it.get ("uuid")) != uuids.end ())
blocking.push_back (it);
return blocking;
}
////////////////////////////////////////////////////////////////////////////////
std::vector <Task> Task::getBlockedTasks () const
{
auto uuid = get ("uuid");
std::vector <Task> blocked;
for (auto& it : Context::getContext ().tdb2.pending.get_tasks ())
if (it.getStatus () != Task::completed &&
it.getStatus () != Task::deleted &&
it.hasDependency (uuid))
blocked.push_back (it);
return blocked;
} }
#endif #endif
@ -1311,10 +1372,10 @@ bool Task::hasTag (const std::string& tag) const
if (tag == "TAGGED") return getTagCount() > 0; if (tag == "TAGGED") return getTagCount() > 0;
if (tag == "PARENT") return has ("mask") || has ("last"); // 2017-01-07: Deprecated in 2.6.0 if (tag == "PARENT") return has ("mask") || has ("last"); // 2017-01-07: Deprecated in 2.6.0
if (tag == "TEMPLATE") return has ("last") || has ("mask"); if (tag == "TEMPLATE") return has ("last") || has ("mask");
if (tag == "WAITING") return get ("status") == "waiting"; if (tag == "WAITING") return is_waiting ();
if (tag == "PENDING") return get ("status") == "pending"; if (tag == "PENDING") return getStatus () == Task::pending;
if (tag == "COMPLETED") return get ("status") == "completed"; if (tag == "COMPLETED") return getStatus () == Task::completed;
if (tag == "DELETED") return get ("status") == "deleted"; if (tag == "DELETED") return getStatus () == Task::deleted;
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
if (tag == "UDA") return is_udaPresent (); if (tag == "UDA") return is_udaPresent ();
if (tag == "ORPHAN") return is_orphanPresent (); if (tag == "ORPHAN") return is_orphanPresent ();
@ -1434,6 +1495,40 @@ const std::string Task::attr2Tag (const std::string& attr) const
return attr.substr(5); return attr.substr(5);
} }
////////////////////////////////////////////////////////////////////////////////
void Task::fixDependsAttribute ()
{
// Fix up the old `depends` attribute to match the `dep_..` attributes (or
// remove it if there are no deps)
auto deps = getDependencyUUIDs ();
if (deps.size () > 0) {
set ("depends", join (",", deps));
} else {
remove ("depends");
}
}
////////////////////////////////////////////////////////////////////////////////
bool Task::isDepAttr(const std::string& attr) const
{
return attr.compare(0, 4, "dep_") == 0;
}
////////////////////////////////////////////////////////////////////////////////
const std::string Task::dep2Attr (const std::string& tag) const
{
std::stringstream tag_attr;
tag_attr << "dep_" << tag;
return tag_attr.str();
}
////////////////////////////////////////////////////////////////////////////////
const std::string Task::attr2Dep (const std::string& attr) const
{
assert (isDepAttr (attr));
return attr.substr(4);
}
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// A UDA Orphan is an attribute that is not represented in context.columns. // A UDA Orphan is an attribute that is not represented in context.columns.
@ -2005,9 +2100,9 @@ float Task::urgency_inherit () const
{ {
float v = FLT_MIN; float v = FLT_MIN;
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
// Calling dependencyGetBlocked is rather expensive. // Calling getBlockedTasks is rather expensive.
// It is called recursively for each dependency in the chain here. // It is called recursively for each dependency in the chain here.
for (auto& task : dependencyGetBlocked (*this)) for (auto& task : getBlockedTasks ())
{ {
// Find highest urgency in all blocked tasks. // Find highest urgency in all blocked tasks.
v = std::max (v, task.urgency ()); v = std::max (v, task.urgency ());
@ -2048,7 +2143,7 @@ float Task::urgency_scheduled () const
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
float Task::urgency_waiting () const float Task::urgency_waiting () const
{ {
if (get_ref ("status") == "waiting") if (is_waiting ())
return 1.0; return 1.0;
return 0.0; return 0.0;

View file

@ -88,7 +88,7 @@ public:
void setAsNow (const std::string&); void setAsNow (const std::string&);
bool has (const std::string&) const; bool has (const std::string&) const;
std::vector <std::string> all (); std::vector <std::string> all () const;
const std::string identifier (bool shortened = false) const; const std::string identifier (bool shortened = false) const;
const std::string get (const std::string&) const; const std::string get (const std::string&) const;
const std::string& get_ref (const std::string&) const; const std::string& get_ref (const std::string&) const;
@ -114,6 +114,7 @@ public:
bool is_udaPresent () const; bool is_udaPresent () const;
bool is_orphanPresent () const; bool is_orphanPresent () const;
#endif #endif
bool is_waiting () const;
status getStatus () const; status getStatus () const;
void setStatus (status); void setStatus (status);
@ -143,8 +144,10 @@ public:
#ifdef PRODUCT_TASKWARRIOR #ifdef PRODUCT_TASKWARRIOR
void removeDependency (int); void removeDependency (int);
void removeDependency (const std::string&); void removeDependency (const std::string&);
bool hasDependency (const std::string&) const;
std::vector <int> getDependencyIDs () const; std::vector <int> getDependencyIDs () const;
std::vector <std::string> getDependencyUUIDs () const; std::vector <std::string> getDependencyUUIDs () const;
std::vector <Task> getBlockedTasks () const;
std::vector <Task> getDependencyTasks () const; std::vector <Task> getDependencyTasks () const;
std::vector <std::string> getUDAOrphanUUIDs () const; std::vector <std::string> getUDAOrphanUUIDs () const;
@ -173,6 +176,10 @@ private:
bool isTagAttr (const std::string&) const; bool isTagAttr (const std::string&) const;
const std::string tag2Attr (const std::string&) const; const std::string tag2Attr (const std::string&) const;
const std::string attr2Tag (const std::string&) const; const std::string attr2Tag (const std::string&) const;
bool isDepAttr (const std::string&) const;
const std::string dep2Attr (const std::string&) const;
const std::string attr2Dep (const std::string&) const;
void fixDependsAttribute ();
void fixTagsAttribute (); void fixTagsAttribute ();
public: public:

View file

@ -68,7 +68,9 @@ void ColumnDepends::setStyle (const std::string& value)
void ColumnDepends::measure (Task& task, unsigned int& minimum, unsigned int& maximum) void ColumnDepends::measure (Task& task, unsigned int& minimum, unsigned int& maximum)
{ {
minimum = maximum = 0; minimum = maximum = 0;
if (task.has (_name)) auto deptasks = task.getDependencyTasks ();
if (deptasks.size () > 0)
{ {
if (_style == "indicator") if (_style == "indicator")
{ {
@ -77,25 +79,24 @@ void ColumnDepends::measure (Task& task, unsigned int& minimum, unsigned int& ma
else if (_style == "count") else if (_style == "count")
{ {
minimum = maximum = 2 + format ((int) dependencyGetBlocking (task).size ()).length (); minimum = maximum = 2 + format ((int) deptasks.size ()).length ();
} }
else if (_style == "default" || else if (_style == "default" ||
_style == "list") _style == "list")
{ {
minimum = maximum = 0; minimum = maximum = 0;
auto blocking = dependencyGetBlocking (task);
std::vector <int> blocking_ids; std::vector <int> blocking_ids;
blocking_ids.reserve(blocking.size()); blocking_ids.reserve(deptasks.size());
for (auto& i : blocking) for (auto& i : deptasks)
blocking_ids.push_back (i.id); blocking_ids.push_back (i.id);
auto all = join (" ", blocking_ids); auto all = join (" ", blocking_ids);
maximum = all.length (); maximum = all.length ();
unsigned int length; unsigned int length;
for (auto& i : blocking) for (auto& i : deptasks)
{ {
length = format (i.id).length (); length = format (i.id).length ();
if (length > minimum) if (length > minimum)
@ -112,7 +113,9 @@ void ColumnDepends::render (
int width, int width,
Color& color) Color& color)
{ {
if (task.has (_name)) auto deptasks = task.getDependencyTasks ();
if (deptasks.size () > 0)
{ {
if (_style == "indicator") if (_style == "indicator")
{ {
@ -121,17 +124,15 @@ void ColumnDepends::render (
else if (_style == "count") else if (_style == "count")
{ {
renderStringRight (lines, width, color, '[' + format (static_cast <int>(dependencyGetBlocking (task).size ())) + ']'); renderStringRight (lines, width, color, '[' + format (static_cast <int>(deptasks.size ())) + ']');
} }
else if (_style == "default" || else if (_style == "default" ||
_style == "list") _style == "list")
{ {
auto blocking = dependencyGetBlocking (task);
std::vector <int> blocking_ids; std::vector <int> blocking_ids;
blocking_ids.reserve(blocking.size()); blocking_ids.reserve(deptasks.size());
for (const auto& t : blocking) for (const auto& t : deptasks)
blocking_ids.push_back (t.id); blocking_ids.push_back (t.id);
auto combined = join (" ", blocking_ids); auto combined = join (" ", blocking_ids);

View file

@ -64,23 +64,28 @@ bool CmdConfig::setConfigVariable (
for (auto& line : contents) for (auto& line : contents)
{ {
// Get l-trimmed version of the line
auto trimmed_line = trim (line, " ");
// If there is a comment on the line, it must follow the pattern. // If there is a comment on the line, it must follow the pattern.
auto comment = line.find ('#'); auto comment = line.find ('#');
auto pos = line.find (name + '='); auto pos = trimmed_line.find (name + '=');
if (pos != std::string::npos && // TODO: Use std::regex here
(comment == std::string::npos || if (pos == 0)
comment > pos))
{ {
found = true; found = true;
if (!confirmation || if (!confirmation ||
confirm (format ("Are you sure you want to change the value of '{1}' from '{2}' to '{3}'?", name, Context::getContext ().config.get (name), value))) confirm (format ("Are you sure you want to change the value of '{1}' from '{2}' to '{3}'?", name, Context::getContext ().config.get (name), value)))
{ {
line = name + '=' + json::encode (value); auto new_line = line.substr (0, pos + name.length () + 1) + json::encode (value);
// Preserve the comment
if (comment != std::string::npos) if (comment != std::string::npos)
line += ' ' + line.substr (comment); new_line += " " + line.substr (comment);
// Rewrite the line
line = new_line;
change = true; change = true;
} }
} }
@ -115,13 +120,13 @@ int CmdConfig::unsetConfigVariable (const std::string& name, bool confirmation /
{ {
auto lineDeleted = false; auto lineDeleted = false;
// If there is a comment on the line, it must follow the pattern. // Get l-trimmed version of the line
auto comment = line->find ('#');
auto pos = line->find (name + '=');
if (pos != std::string::npos && // If there is a comment on the line, it must follow the pattern.
(comment == std::string::npos || auto pos = trim (*line, " ").find (name + '=');
comment > pos))
// TODO: Use std::regex here
if (pos == 0)
{ {
found = true; found = true;

View file

@ -59,6 +59,21 @@ CmdCustom::CmdCustom (
_category = Category::report; _category = Category::report;
} }
////////////////////////////////////////////////////////////////////////////////
// Whether a report uses context is defined by the report.<name>.context
// configuration variable.
//
bool CmdCustom::uses_context () const
{
auto config = Context::getContext ().config;
auto key = "report." + _keyword + ".context";
if (config.has (key))
return config.getBoolean (key);
else
return _uses_context;
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int CmdCustom::execute (std::string& output) int CmdCustom::execute (std::string& output)
{ {

View file

@ -34,6 +34,7 @@ class CmdCustom : public Command
{ {
public: public:
CmdCustom (const std::string&, const std::string&, const std::string&); CmdCustom (const std::string&, const std::string&, const std::string&);
bool uses_context () const override;
int execute (std::string&); int execute (std::string&);
private: private:

View file

@ -92,10 +92,6 @@ int CmdDelete::execute (std::string&)
if (! task.has ("end")) if (! task.has ("end"))
task.setAsNow ("end"); task.setAsNow ("end");
// Un-wait the task, if waiting.
if (task.has ("wait"))
task.remove ("wait");
if (permission (question, filtered.size ())) if (permission (question, filtered.size ()))
{ {
updateRecurrenceMask (task); updateRecurrenceMask (task);

View file

@ -98,10 +98,6 @@ int CmdDone::execute (std::string&)
task.addAnnotation (Context::getContext ().config.get ("journal.time.stop.annotation")); task.addAnnotation (Context::getContext ().config.get ("journal.time.stop.annotation"));
} }
// Un-wait the task, if waiting.
if (task.has ("wait"))
task.remove ("wait");
if (permission (taskDifferences (before, task) + question, filtered.size ())) if (permission (taskDifferences (before, task) + question, filtered.size ()))
{ {
updateRecurrenceMask (task); updateRecurrenceMask (task);

View file

@ -667,7 +667,8 @@ void CmdEdit::parseTask (Task& task, const std::string& after, const std::string
value = findValue (after, "\n Dependencies:"); value = findValue (after, "\n Dependencies:");
auto dependencies = split (value, ','); auto dependencies = split (value, ',');
task.remove ("depends"); for (auto& dep : task.getDependencyUUIDs ())
task.removeDependency (dep);
for (auto& dep : dependencies) for (auto& dep : dependencies)
{ {
if (dep.length () >= 7) if (dep.length () >= 7)

View file

@ -147,7 +147,7 @@ int CmdInfo::execute (std::string& output)
// dependencies: blocked // dependencies: blocked
{ {
auto blocked = dependencyGetBlocking (task); auto blocked = task.getDependencyTasks ();
if (blocked.size ()) if (blocked.size ())
{ {
std::stringstream message; std::stringstream message;
@ -162,7 +162,7 @@ int CmdInfo::execute (std::string& output)
// dependencies: blocking // dependencies: blocking
{ {
auto blocking = dependencyGetBlocked (task); auto blocking = task.getBlockedTasks ();
if (blocking.size ()) if (blocking.size ())
{ {
std::stringstream message; std::stringstream message;
@ -414,6 +414,7 @@ int CmdInfo::execute (std::string& output)
{ {
if (att.substr (0, 11) != "annotation_" && if (att.substr (0, 11) != "annotation_" &&
att.substr (0, 5) != "tags_" && att.substr (0, 5) != "tags_" &&
att.substr (0, 4) != "dep_" &&
Context::getContext ().columns.find (att) == Context::getContext ().columns.end ()) Context::getContext ().columns.find (att) == Context::getContext ().columns.end ())
{ {
row = view.addRow (); row = view.addRow ();

View file

@ -71,8 +71,7 @@ void CmdPurge::handleDeps (Task& task)
for (auto& blockedConst: Context::getContext ().tdb2.all_tasks ()) for (auto& blockedConst: Context::getContext ().tdb2.all_tasks ())
{ {
Task& blocked = const_cast<Task&>(blockedConst); Task& blocked = const_cast<Task&>(blockedConst);
if (blocked.has ("depends") && if (blocked.hasDependency (uuid))
blocked.get ("depends").find (uuid) != std::string::npos)
{ {
blocked.removeDependency (uuid); blocked.removeDependency (uuid);
Context::getContext ().tdb2.modify (blocked); Context::getContext ().tdb2.modify (blocked);

View file

@ -172,7 +172,6 @@ int CmdShow::execute (std::string& output)
" journal.time.start.annotation" " journal.time.start.annotation"
" journal.time.stop.annotation" " journal.time.stop.annotation"
" json.array" " json.array"
" json.depends.array"
" list.all.projects" " list.all.projects"
" list.all.tags" " list.all.tags"
" locking" " locking"

View file

@ -53,6 +53,22 @@ CmdTimesheet::CmdTimesheet ()
_category = Command::Category::report; _category = Command::Category::report;
} }
////////////////////////////////////////////////////////////////////////////////
// Whether a the timesheet uses context is defined by the
// report.timesheet.context configuration variable.
//
bool CmdTimesheet::uses_context () const
{
auto config = Context::getContext ().config;
auto key = "report.timesheet.context";
if (config.has (key))
return config.getBoolean (key);
else
return _uses_context;
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int CmdTimesheet::execute (std::string& output) int CmdTimesheet::execute (std::string& output)
{ {

View file

@ -35,6 +35,7 @@ class CmdTimesheet : public Command
public: public:
CmdTimesheet (); CmdTimesheet ();
int execute (std::string&); int execute (std::string&);
bool uses_context () const override;
}; };
#endif #endif

View file

@ -63,7 +63,7 @@ public:
bool read_only () const; bool read_only () const;
bool displays_id () const; bool displays_id () const;
bool needs_gc () const; bool needs_gc () const;
bool uses_context () const; virtual bool uses_context () const;
bool accepts_filter () const; bool accepts_filter () const;
bool accepts_modifications () const; bool accepts_modifications () const;
bool accepts_miscellaneous () const; bool accepts_miscellaneous () const;

View file

@ -36,38 +36,6 @@
#define STRING_DEPEND_BLOCKED "Task {1} is blocked by:" #define STRING_DEPEND_BLOCKED "Task {1} is blocked by:"
////////////////////////////////////////////////////////////////////////////////
std::vector <Task> dependencyGetBlocked (const Task& task)
{
auto uuid = task.get ("uuid");
std::vector <Task> blocked;
for (auto& it : Context::getContext ().tdb2.pending.get_tasks ())
if (it.getStatus () != Task::completed &&
it.getStatus () != Task::deleted &&
it.has ("depends") &&
it.get ("depends").find (uuid) != std::string::npos)
blocked.push_back (it);
return blocked;
}
////////////////////////////////////////////////////////////////////////////////
std::vector <Task> dependencyGetBlocking (const Task& task)
{
auto depends = task.get ("depends");
std::vector <Task> blocking;
if (depends != "")
for (auto& it : Context::getContext ().tdb2.pending.get_tasks ())
if (it.getStatus () != Task::completed &&
it.getStatus () != Task::deleted &&
depends.find (it.get ("uuid")) != std::string::npos)
blocking.push_back (it);
return blocking;
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Returns true if the supplied task adds a cycle to the dependency chain. // Returns true if the supplied task adds a cycle to the dependency chain.
bool dependencyIsCircular (const Task& task) bool dependencyIsCircular (const Task& task)
@ -150,12 +118,12 @@ bool dependencyIsCircular (const Task& task)
// //
void dependencyChainOnComplete (Task& task) void dependencyChainOnComplete (Task& task)
{ {
auto blocking = dependencyGetBlocking (task); auto blocking = task.getDependencyTasks ();
// If the task is anything but the tail end of a dependency chain. // If the task is anything but the tail end of a dependency chain.
if (blocking.size ()) if (blocking.size ())
{ {
auto blocked = dependencyGetBlocked (task); auto blocked = task.getBlockedTasks ();
// Nag about broken chain. // Nag about broken chain.
if (Context::getContext ().config.getBoolean ("dependency.reminder")) if (Context::getContext ().config.getBoolean ("dependency.reminder"))
@ -207,7 +175,7 @@ void dependencyChainOnStart (Task& task)
{ {
if (Context::getContext ().config.getBoolean ("dependency.reminder")) if (Context::getContext ().config.getBoolean ("dependency.reminder"))
{ {
auto blocking = dependencyGetBlocking (task); auto blocking = task.getDependencyTasks ();
// If the task is anything but the tail end of a dependency chain, nag about // If the task is anything but the tail end of a dependency chain, nag about
// broken chain. // broken chain.

View file

@ -78,6 +78,7 @@ std::string taskDifferences (const Task& before, const Task& after)
<< format ("{1} will be deleted.", Lexer::ucFirst (name)) << format ("{1} will be deleted.", Lexer::ucFirst (name))
<< "\n"; << "\n";
// TODO: #2572 - rewrite to look at dep_ and tag_
for (auto& name : afterOnly) for (auto& name : afterOnly)
{ {
if (name == "depends") if (name == "depends")
@ -384,12 +385,12 @@ void feedback_unblocked (const Task& task)
if (Context::getContext ().verbose ("affected")) if (Context::getContext ().verbose ("affected"))
{ {
// Get a list of tasks that depended on this task. // Get a list of tasks that depended on this task.
auto blocked = dependencyGetBlocked (task); auto blocked = task.getBlockedTasks ();
// Scan all the tasks that were blocked by this task // Scan all the tasks that were blocked by this task
for (auto& i : blocked) for (auto& i : blocked)
{ {
auto blocking = dependencyGetBlocking (i); auto blocking = i.getDependencyTasks ();
if (blocking.size () == 0) if (blocking.size () == 0)
{ {
if (i.id) if (i.id)

View file

@ -59,8 +59,6 @@ std::string colorizeError (const std::string&);
std::string colorizeDebug (const std::string&); std::string colorizeDebug (const std::string&);
// dependency.cpp // dependency.cpp
std::vector <Task> dependencyGetBlocked (const Task&);
std::vector <Task> dependencyGetBlocking (const Task&);
bool dependencyIsCircular (const Task&); bool dependencyIsCircular (const Task&);
void dependencyChainOnComplete (Task&); void dependencyChainOnComplete (Task&);
void dependencyChainOnStart (Task&); void dependencyChainOnStart (Task&);

View file

@ -200,22 +200,25 @@ static bool sort_compare (int left, int right)
// Depends string. // Depends string.
else if (field == "depends") else if (field == "depends")
{ {
// Raw data is a comma-separated list of uuids // Raw data is an un-sorted list of UUIDs. We just need a stable
auto left_string = (*global_data)[left].get_ref (field); // sort, so we sort them lexically.
auto right_string = (*global_data)[right].get_ref (field); auto left_deps = (*global_data)[left].getDependencyUUIDs ();
std::sort(left_deps.begin(), left_deps.end());
auto right_deps = (*global_data)[right].getDependencyUUIDs ();
std::sort(right_deps.begin(), right_deps.end());
if (left_string == right_string) if (left_deps == right_deps)
continue; continue;
if (left_string == "" && right_string != "") if (left_deps.size () == 0 && right_deps.size () > 0)
return ascending; return ascending;
if (left_string != "" && right_string == "") if (left_deps.size () > 0 && right_deps.size () == 0)
return !ascending; return !ascending;
// Sort on the first dependency. // Sort on the first dependency.
left_number = Context::getContext ().tdb2.id (left_string.substr (0, 36)); left_number = Context::getContext ().tdb2.id (left_deps[0]);
right_number = Context::getContext ().tdb2.id (right_string.substr (0, 36)); right_number = Context::getContext ().tdb2.id (right_deps[0]);
if (left_number == right_number) if (left_number == right_number)
continue; continue;

View file

@ -490,6 +490,33 @@ class ContextEvaluationTest(TestCase):
self.assertNotIn("work today task", output) self.assertNotIn("work today task", output)
self.assertNotIn("home today task", output) self.assertNotIn("home today task", output)
def test_context_ignored(self):
"""Test the context is not applied with report list command if
report.list.context is set to 0."""
# Turn off context for this report
self.t.config("report.list.context", "0")
# Get the tasks
code, out, err = self.t('list')
# Assert all the tasks are present in the output
self.assertIn("work task", out)
self.assertIn("home task", out)
self.assertIn("work today task", out)
self.assertIn("home today task", out)
# Set the home context and rerun the report
self.t('context home')
code, out, err = self.t('list')
# Assert nothing changed - all the tasks are present in the output
self.assertIn("work task", out)
self.assertIn("home task", out)
self.assertIn("work today task", out)
self.assertIn("home today task", out)
class ContextErrorHandling(TestCase): class ContextErrorHandling(TestCase):
def setUp(self): def setUp(self):

View file

@ -146,7 +146,6 @@ class TestExportCommand(TestCase):
self.t(('add', 'everything depends on me task')) self.t(('add', 'everything depends on me task'))
self.t(('add', 'wrong, everything depends on me task')) self.t(('add', 'wrong, everything depends on me task'))
self.t('1 modify depends:2,3') self.t('1 modify depends:2,3')
self.t.config('json.depends.array', 'on')
deps = self.export(1)['depends'] deps = self.export(1)['depends']
self.assertType(deps, list) self.assertType(deps, list)
@ -155,19 +154,6 @@ class TestExportCommand(TestCase):
for uuid in deps: for uuid in deps:
self.assertString(uuid, UUID_REGEXP, regexp=True) self.assertString(uuid, UUID_REGEXP, regexp=True)
def test_export_depends_oldformat(self):
self.t(('add', 'everything depends on me task'))
self.t(('add', 'wrong, everything depends on me task'))
self.t('1 modify depends:2,3')
code, out, err = self.t("rc.json.array=off rc.json.depends.array=off 1 export")
deps = json.loads(out)["depends"]
self.assertString(deps)
self.assertEqual(len(deps.split(",")), 2)
for uuid in deps.split(','):
self.assertString(uuid, UUID_REGEXP, regexp=True)
def test_export_urgency(self): def test_export_urgency(self):
self.t('add urgent task +urgent') self.t('add urgent task +urgent')

View file

@ -1130,6 +1130,24 @@ class TestBug1915(TestCase):
self.assertIn("thingB", out) self.assertIn("thingB", out)
self.assertNotIn("thingC", out) self.assertNotIn("thingC", out)
class Test2577(TestCase):
def setUp(self):
self.t = Task()
def test_filtering_for_datetime_like(self):
"""2577: Check that filtering for datetime-like project names works"""
self.t('add one pro:sat') # looks like "saturday"
self.t('add two pro:whatever')
# This should not fail (fails on 2.5.3)
code, out, err = self.t('pro:sat')
# Assert expected output, but the crucial part of this test is success
# of the call above
self.assertIn("one", out)
if __name__ == "__main__": if __name__ == "__main__":
from simpletap import TAPTestRunner from simpletap import TAPTestRunner
unittest.main(testRunner=TAPTestRunner()) unittest.main(testRunner=TAPTestRunner())

View file

@ -187,15 +187,6 @@ class TestImport(TestCase):
self.assertIn("Imported 3 tasks", err) self.assertIn("Imported 3 tasks", err)
self.assertData1() self.assertData1()
def test_import_old_depend(self):
"""One dependency used to be a plain string"""
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":"a1111111-a111-a111-a111-a11111111111","description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
self.t("import", input=self.data1)
self.t("import", input=_data)
self.t.config("json.depends.array", "0")
_t = self.t.export("a0000000-a000-a000-a000-a00000000000")[0]
self.assertEqual(_t["depends"], "a1111111-a111-a111-a111-a11111111111")
def test_import_old_depends(self): def test_import_old_depends(self):
"""Several dependencies used to be a comma seperated string""" """Several dependencies used to be a comma seperated string"""
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":"a1111111-a111-a111-a111-a11111111111,a2222222-a222-a222-a222-a22222222222","description":"zero","project":"A","status":"pending","entry":"1234567889"}""" _data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":"a1111111-a111-a111-a111-a11111111111,a2222222-a222-a222-a222-a22222222222","description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
@ -207,7 +198,6 @@ class TestImport(TestCase):
def test_import_new_depend(self): def test_import_new_depend(self):
"""One dependency is a single array element""" """One dependency is a single array element"""
self.t.config('json.depends.array', 'on')
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":["a1111111-a111-a111-a111-a11111111111"],"description":"zero","project":"A","status":"pending","entry":"1234567889"}""" _data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":["a1111111-a111-a111-a111-a11111111111"],"description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
self.t("import", input=self.data1) self.t("import", input=self.data1)
self.t("import", input=_data) self.t("import", input=_data)
@ -216,7 +206,6 @@ class TestImport(TestCase):
def test_import_new_depends(self): def test_import_new_depends(self):
"""Several dependencies are an array""" """Several dependencies are an array"""
self.t.config('json.depends.array', 'on')
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":["a1111111-a111-a111-a111-a11111111111","a2222222-a222-a222-a222-a22222222222"],"description":"zero","project":"A","status":"pending","entry":"1234567889"}""" _data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":["a1111111-a111-a111-a111-a11111111111","a2222222-a222-a222-a222-a22222222222"],"description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
self.t("import", input=self.data1) self.t("import", input=self.data1)
self.t("import", input=_data) self.t("import", input=_data)
@ -303,6 +292,12 @@ class TestImportValidate(TestCase):
code, out, err = self.t.runError("import", input=j) code, out, err = self.t.runError("import", input=j)
self.assertIn("The status 'foo' is not valid.", err) self.assertIn("The status 'foo' is not valid.", err)
def test_import_malformed_annotation(self):
"""Verify invalid 'annnotations' is caught"""
j = '{"description": "bad", "annotations": "bad"}'
code, out, err = self.t.runError("import", input=j)
self.assertIn('Annotations is malformed: "bad"', err)
class TestImportWithoutISO(TestCase): class TestImportWithoutISO(TestCase):
def setUp(self): def setUp(self):

View file

@ -480,6 +480,26 @@ class TestBug1627(TestCase):
self.assertEqual("mon\n", out) self.assertEqual("mon\n", out)
class TestBug1900(TestCase):
def setUp(self):
"""Executed before each test in the class"""
self.t = Task()
def test_project_eval(self):
"""1900: Project name can contain dashes"""
self.t("add foo project:due-b")
code, out, err = self.t("_get 1.project")
self.assertEqual("due-b\n", out)
self.t("add foo project:scheduled-home")
code, out, err = self.t("_get 2.project")
self.assertEqual("scheduled-home\n", out)
self.t("add foo project:entry-work")
code, out, err = self.t("_get 3.project")
self.assertEqual("entry-work\n", out)
class TestBug1904(TestCase): class TestBug1904(TestCase):
def setUp(self): def setUp(self):
"""Executed before each test in the class""" """Executed before each test in the class"""

17
test/tw-2189.t Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
. bash_tap_tw.sh
task add "foo \' bar"
task list
# Assert the task was correctly added
[[ ! -z `task list | grep "foo ' bar"` ]]
[[ `task _get 1.description` == "foo ' bar" ]]
# Bonus: Assert escaped double quotes are also handled correctly
task add 'foo \" bar'
task list
# Assert the task was correctly added
[[ ! -z `task list | grep 'foo " bar'` ]]
[[ `task _get 2.description` == 'foo " bar' ]]

33
test/tw-2550.t Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
. bash_tap_tw.sh
# Setup the context
task config context.work.read '+work'
task config context.work.write '+work'
# Create a task outside of the context
task add outside
# Activate the context
task context work
# Add multiple tasks within the context, some of which contain numbers or uuids
task add inside
task add inside 2
task add inside 3
task add inside aabbccdd
task add inside 4-5
# Assertion: Task defined outside of the context should not show up
[[ -z `task all | grep outside` ]]
# Five tasks were defined within the context
task count
[[ `task count` == "5" ]]
# Unset the context
task context none
# Exactly five tasks have the tag work
task +work count
[[ `task +work count` == "5" ]]

45
test/tw-2563.t Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# This tests the migration path from 2.5.3 or earlier to 2.6.0 with respect to
# the upgrade of the status field from waiting to pending
. bash_tap_tw.sh
# Setup
task add Actionable task wait:yesterday
task add Non-actionable task wait:tomorrow+1h
# Simulate this was created in 2.5.3 or earlier (status is equal to waiting,
# not pending). Using more cumbersome sed syntax for Mac OS-X compatibility.
sed -i".bak" 's/pending/waiting/g' $TASKDATA/pending.data
rm -f $TASKDATA/pending.data.bak
# Trigger upgrade
task all
# Report file content
echo pending.data
cat $TASKDATA/pending.data
echo completed.data
cat $TASKDATA/completed.data
# Assertion: Exactly one task is considered waiting
[[ `task +WAITING count` == "1" ]]
[[ `task status:waiting count` == "1" ]]
# Assertion: Exactly one task is considered pending
[[ `task +PENDING count` == "1" ]]
[[ `task status:pending count` == "1" ]]
# Assertion: Task 1 is pending
[[ `task _get 1.status` == "pending" ]]
# Assertion: Task 2 is waiting
[[ `task _get 2.status` == "waiting" ]]
# Assertion: No lines in data files with "waiting" status
[[ -z `cat $TASKDATA/pending.data | grep waiting` ]]
[[ -z `cat $TASKDATA/completed.data | grep waiting` ]]
# Assertion: No tasks were moved into completed.data
cat $TASKDATA/pending.data | wc -l | tr -d ' '
[[ `cat $TASKDATA/pending.data | wc -l | tr -d ' '` == "2" ]]
[[ `cat $TASKDATA/completed.data | wc -l | tr -d ' '` == "0" ]]

14
test/tw-2581.t Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Test setting configuration variable with a trailing comment works
. bash_tap_tw.sh
# Add configuration variable with a trailing comment into taskrc
echo 'weekstart=Monday # Europe standard' >> taskrc
cat taskrc
# Use config the change the value to "Sunday"
task config weekstart Sunday
# Ensure the comment was preserved and value changed
cat taskrc | grep weekstart=Sunday
[[ `cat taskrc | grep weekstart=Sunday` == 'weekstart=Sunday # Europe standard' ]]

View file

@ -62,8 +62,6 @@ class TestWait(TestCase):
self.assertIn("visible", out) self.assertIn("visible", out)
self.assertIn("hidden", out) self.assertIn("hidden", out)
self.assertIn("Un-waiting task 2 'hidden'", err)
class TestBug434(TestCase): class TestBug434(TestCase):
# Bug #434: Task should not prevent users from marking as done tasks with # Bug #434: Task should not prevent users from marking as done tasks with
@ -100,30 +98,30 @@ class Test1486(TestCase):
self.assertNotIn('regular', out) self.assertNotIn('regular', out)
class TestFeature2322(TestCase): class TestFeature2563(TestCase):
def setUp(self): def setUp(self):
"""Executed before each test in the class""" """Executed before each test in the class"""
self.t = Task() self.t = Task()
def test_done_unwait(self): def test_done_unwait(self):
"""2322: Done should un-wait a waiting task""" """2563: Done should NOT remove the wait attribute"""
self.t("add foo wait:tomorrow") self.t("add foo wait:tomorrow")
code, out, err = self.t("export") code, out, err = self.t("export")
self.assertIn('"wait":', out) self.assertIn('"wait":', out)
self.t("1 done") self.t("1 done")
code, out, err = self.t("export") code, out, err = self.t("export")
self.assertNotIn('"wait":', out) self.assertIn('"wait":', out)
def test_delete_unwait(self): def test_delete_unwait(self):
"""2322: Deleteion should un-wait a waiting task""" """2563: Delete should NOT remove the wait attribute"""
self.t("add bar wait:tomorrow") self.t("add bar wait:tomorrow")
code, out, err = self.t("export") code, out, err = self.t("export")
self.assertIn('"wait":', out) self.assertIn('"wait":', out)
self.t("1 delete", input="y\n") self.t("1 delete", input="y\n")
code, out, err = self.t("export") code, out, err = self.t("export")
self.assertNotIn('"wait":', out) self.assertIn('"wait":', out)
if __name__ == "__main__": if __name__ == "__main__":