diff --git a/ChangeLog b/ChangeLog index 27af663fc..9893ec545 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,7 @@ (thanks to Renato Alves). - Eliminated some code that is not UTF8-safe. - Removed pthreads linkage. +- Implemented the context feature. - Closed dangling pipes in execute (), resolving problems when a hook script forks (thanks to Jens Erat). diff --git a/NEWS b/NEWS index cbc5371fd..bebc9560c 100644 --- a/NEWS +++ b/NEWS @@ -1,15 +1,17 @@ New Features in taskwarrior 2.4.2 - - None + - Ability to set context, which serves as a permanent user-defined filter. New commands in taskwarrior 2.4.2 - - None + - The 'context' command has been added, along with it subcommands 'define', + 'delete', 'show', 'list' and 'none'. New configuration options in taskwarrior 2.4.2 - - None + - 'context' to store the current context applied. + - 'context.' to store the definition of context 'name' Newly deprecated features in taskwarrior 2.4.2 diff --git a/doc/man/task.1.in b/doc/man/task.1.in index a55df9d67..65352f5d4 100644 --- a/doc/man/task.1.in +++ b/doc/man/task.1.in @@ -396,6 +396,46 @@ Finally, this command removes any 'name=...' entry from the .taskrc file: task config name +.TP +.B task context +Sets the currectly active context. See the CONTEXT section. + +Example: + + task context work + +.TP +.B task context delete +Deletes the context with the name . If the context being deleted is currently +set as active, it will be unset. + +Example: + + task context delete work + +.TP +.B task context define +Defines a new context with name and definition . This command +does not affect the currently set context, just adds a new context definition. + +Examples: + + task context define work project:Work + task context define home project:Home or +home + task context define superurgent due:today and +urgent + +.TP +.B task context list +Outputs a list of available contexts along with their definitions. + +.TP +.B task context none +Unsets the currently active context, if any was set. + +.TP +.B task context none +Shows the currently active context, along with its definition. + .TP .B task diagnostics Shows diagnostic information, of the kind needed when reporting a problem. @@ -474,6 +514,10 @@ Generates a list of all commands, for autocompletion purposes. .B task _config Lists all supported configuration variables, for completion purposes. +.TP +.B task _context +Lists all available context variables, for completion purposes. + .TP .B task _ids Shows only the IDs of matching tasks, in the form of a list. @@ -962,6 +1006,66 @@ biannual, biyearly, 2yr Every two years. .RE +.SH CONTEXT +Context is a user-defined filter, which is automatically applied to all commands +that filter the task list. In particular, any report command will have its +result affected by the current active context. + + $ task list + ID Age Project Description Urg + 1 2d Sport Run 5 miles 1.42 + 2 1d Home Clean the dishes 1.14 + + $ task context home + Context 'home' applied. + + $ task list + ID Age Project Description Urg + 2 1d Home Clean the dishes 1.14 + Context 'home' applied. + +As seen in the example above, context is applied by specifying its name to the +"context" command. To change the currently applied context, just pass the +new context's name to the 'context' command. + +To unset any context, use the 'none' subcommand. + + $ task context none + Context unset. + + $ task list + ID Age Project Description Urg + 1 2d Sport Run 5 miles 1.42 + 2 1d Home Clean the dishes 1.14 + +Context can be defined using the 'define' subcommand, specifying both the name +of the new context, and it's assigned filter. + + $ task context define home + Are you sure you want to add 'context.home' with a value of 'project:Home'? (yes/no) yes + Context 'home' successfully defined. + +To remove the definition, use the 'delete' subcommand. + + $ task context delete home + Are you sure you want to remove 'context.home'? (yes/no) yes + Context 'home' successfully undefined. + +To check what is the currently active context, use the 'show' subcommand. + + $ task context show + Context 'home' with filter 'project:Home' is currently applied. + +Contexts can store arbitrarily complex filters. + + $ task context define family project:Family or +paul or +nancy + Are you sure you want to add 'context.home' with a value of 'project:Family or +paul or +nancy'? (yes/no) yes + Context 'family' successfully defined. + +Contexts are permanent, and the currently set context name is stored in the +"context" configuration variable. The context definition is stored in the +"context." configuration variable. + .SH COMMAND ABBREVIATION All taskwarrior commands may be abbreviated as long as a unique prefix is used, for example: diff --git a/doc/man/taskrc.5.in b/doc/man/taskrc.5.in index f9a76a931..b0fa4cf79 100644 --- a/doc/man/taskrc.5.in +++ b/doc/man/taskrc.5.in @@ -1351,6 +1351,26 @@ of a task. .B uda.estimate.values=trivial,small,medium,large,huge .RE +.SS CONTEXT +Context setting is a mechanism which allows the user to set a permanent filter, +thus avoiding the need to specify one filter repeatedly. More details on usage +can be found in the task(1) manpage. + +The current context is stored in the taskrc file, along with definitions for +all user provided contexts. + +.TP +.B context= +.RS +Stores the value of the currently active context. +.RE + +.TP +.B context.= +.RS +Stores the definition of the context with the name . +.RE + .SS SYNC These configuration settings are used to connect and sync tasks with the task diff --git a/src/CLI.cpp b/src/CLI.cpp index 89a44f881..cc7f74ac2 100644 --- a/src/CLI.cpp +++ b/src/CLI.cpp @@ -374,6 +374,64 @@ void CLI::add (const std::string& arg) analyze (); } +//////////////////////////////////////////////////////////////////////////////// +void CLI::addContextFilter () +{ + // Detect if any context is set, and bail out if not + std::string contextName = context.config.get ("context"); + + if (contextName == "") + { + context.debug("No context applied."); + return; + } + + // Detect if UUID or ID is set, and bail out + if (_args.size ()) + { + std::vector ::const_iterator a; + for (a = _args.begin (); a != _args.end (); ++a) + { + if (a->hasTag ("FILTER") && + a->hasTag ("ATTRIBUTE") && + ! a->hasTag ("TERMINATED") && + ! a->hasTag ("WORD") && + (a->attribute ("raw") == "id" || a->attribute ("raw") == "uuid")) + { + context.debug(format("UUID/ID lexeme found '{1}', not applying context.", a->attribute ("raw"))); + return; + } + } + } + + // Apply context + context.debug("Applying context: " + contextName); + std::string contextFilter = context.config.get ("context." + contextName); + + if (contextFilter == "") + context.debug("Context '" + contextName + "' not defined!"); + else + { + addRawFilter("( " + contextFilter + " )"); + if (context.verbose ("context")) + context.footnote (format("Context '{1}' applied.", contextName)); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Process raw string into parsed filter. +// +void CLI::addRawFilter (const std::string& arg) +{ + std::string lexeme; + Lexer::Type type; + Lexer lex (arg); + lex.ambiguity (false); + + while (lex.token (lexeme, type)) + add (lexeme); +} + //////////////////////////////////////////////////////////////////////////////// // Intended to be called after ::initialize() and ::add(), to perform the final // analysis. Analysis is also performed directly after the above, because there @@ -476,6 +534,9 @@ void CLI::applyOverrides () // Extract all the FILTER-tagged items. const std::string CLI::getFilter () { + // Handle context setting + addContextFilter (); + std::string filter = ""; if (_args.size ()) { @@ -500,6 +561,7 @@ const std::string CLI::getFilter () filter = "( " + filter + " )"; } + context.debug("Derived filter: '" + filter + "'"); return filter; } diff --git a/src/CLI.h b/src/CLI.h index e855db697..634e05a91 100644 --- a/src/CLI.h +++ b/src/CLI.h @@ -77,6 +77,8 @@ public: void entity (const std::string&, const std::string&); void initialize (int, const char**); void add (const std::string&); + void addContextFilter (); + void addRawFilter (const std::string& arg); void analyze (bool parse = true, bool strict = false); void applyOverrides (); const std::string getFilter (); diff --git a/src/commands/CMakeLists.txt b/src/commands/CMakeLists.txt index 4e0288972..035d0dc14 100644 --- a/src/commands/CMakeLists.txt +++ b/src/commands/CMakeLists.txt @@ -18,6 +18,7 @@ set (commands_SRCS Command.cpp Command.h CmdColor.cpp CmdColor.h CmdColumns.cpp CmdColumns.h CmdConfig.cpp CmdConfig.h + CmdContext.cpp CmdContext.h CmdCount.cpp CmdCount.h CmdCustom.cpp CmdCustom.h CmdDelete.cpp CmdDelete.h diff --git a/src/commands/CmdConfig.cpp b/src/commands/CmdConfig.cpp index 8c605f527..52e883d45 100644 --- a/src/commands/CmdConfig.cpp +++ b/src/commands/CmdConfig.cpp @@ -46,6 +46,100 @@ CmdConfig::CmdConfig () _displays_id = false; } +//////////////////////////////////////////////////////////////////////////////// +bool CmdConfig::setConfigVariable (std::string name, std::string value, bool confirmation /* = false */) +{ + // Read .taskrc (or equivalent) + std::vector contents; + File::read (context.config._original_file, contents); + + bool found = false; + bool change = false; + + std::vector ::iterator line; + for (line = contents.begin (); line != contents.end (); ++line) + { + // If there is a comment on the line, it must follow the pattern. + std::string::size_type comment = line->find ("#"); + std::string::size_type pos = line->find (name + "="); + + if (pos != std::string::npos && + (comment == std::string::npos || + comment > pos)) + { + found = true; + if (!confirmation || + confirm (format (STRING_CMD_CONFIG_CONFIRM, name, context.config.get (name), value))) + { + if (comment != std::string::npos) + *line = name + "=" + json::encode (value) + " " + line->substr (comment); + else + *line = name + "=" + json::encode (value); + + change = true; + } + } + } + + // Not found, so append instead. + if (!found && + (!confirmation || + confirm (format (STRING_CMD_CONFIG_CONFIRM2, name, value)))) + { + contents.push_back (name + "=" + json::encode (value)); + change = true; + } + + if (change) + File::write (context.config._original_file, contents); + + return change; +} + +//////////////////////////////////////////////////////////////////////////////// +int CmdConfig::unsetConfigVariable (std::string name, bool confirmation /* = false */) +{ + // Read .taskrc (or equivalent) + std::vector contents; + File::read (context.config._original_file, contents); + + bool found = false; + bool change = false; + + std::vector ::iterator line; + for (line = contents.begin (); line != contents.end (); ++line) + { + // If there is a comment on the line, it must follow the pattern. + std::string::size_type comment = line->find ("#"); + std::string::size_type pos = line->find (name + "="); + + if (pos != std::string::npos && + (comment == std::string::npos || + comment > pos)) + { + found = true; + + // Remove name + if (!confirmation || + confirm (format (STRING_CMD_CONFIG_CONFIRM3, name))) + { + *line = ""; + change = true; + } + } + } + + if (change) + File::write (context.config._original_file, contents); + + if ( change && found ) + return 0; + else if ( found ) + return 1; + else + return 2; +} + //////////////////////////////////////////////////////////////////////////////// int CmdConfig::execute (std::string& output) { @@ -62,10 +156,13 @@ int CmdConfig::execute (std::string& output) if (words.size ()) { bool confirmation = context.config.getBoolean ("confirmation"); + bool change = false; + bool found = false; std::string name = words[0]; std::string value = ""; + // Join the remaining words into config variable's value if (words.size () > 1) { for (unsigned int i = 1; i < words.size (); ++i) @@ -81,85 +178,30 @@ int CmdConfig::execute (std::string& output) { bool change = false; - // Read .taskrc (or equivalent) - std::vector contents; - File::read (context.config._original_file, contents); - // task config name value // task config name "" if (words.size () > 1) - { - bool found = false; - std::vector ::iterator line; - for (line = contents.begin (); line != contents.end (); ++line) - { - // If there is a comment on the line, it must follow the pattern. - std::string::size_type comment = line->find ("#"); - std::string::size_type pos = line->find (name + "="); - - if (pos != std::string::npos && - (comment == std::string::npos || - comment > pos)) - { - found = true; - if (!confirmation || - confirm (format (STRING_CMD_CONFIG_CONFIRM, name, context.config.get (name), value))) - { - if (comment != std::string::npos) - *line = name + "=" + json::encode (value) + " " + line->substr (comment); - else - *line = name + "=" + json::encode (value); - - change = true; - } - } - } - - // Not found, so append instead. - if (!found && - (!confirmation || - confirm (format (STRING_CMD_CONFIG_CONFIRM2, name, value)))) - { - contents.push_back (name + "=" + json::encode (value)); - change = true; - } - } + change = setConfigVariable(name, value, confirmation); // task config name else { - bool found = false; - std::vector ::iterator line; - for (line = contents.begin (); line != contents.end (); ++line) + rc = unsetConfigVariable(name, confirmation); + if (rc == 0) { - // If there is a comment on the line, it must follow the pattern. - std::string::size_type comment = line->find ("#"); - std::string::size_type pos = line->find (name + "="); - - if (pos != std::string::npos && - (comment == std::string::npos || - comment > pos)) - { - found = true; - - // Remove name - if (!confirmation || - confirm (format (STRING_CMD_CONFIG_CONFIRM3, name))) - { - *line = ""; - change = true; - } - } + change = true; + found = true; } + else if (rc == 1) + found = true; if (!found) throw format (STRING_CMD_CONFIG_NO_ENTRY, name); } - // Write .taskrc (or equivalent) + // Show feedback depending on whether .taskrc has been rewritten if (change) { - File::write (context.config._original_file, contents); out << format (STRING_CMD_CONFIG_FILE_MOD, context.config._original_file._data) << "\n"; diff --git a/src/commands/CmdConfig.h b/src/commands/CmdConfig.h index 782cfaca1..f3f7e8263 100644 --- a/src/commands/CmdConfig.h +++ b/src/commands/CmdConfig.h @@ -34,6 +34,8 @@ class CmdConfig : public Command { public: CmdConfig (); + static bool setConfigVariable (std::string name, std::string value, bool confirmation = false); + static int unsetConfigVariable (std::string name, bool confirmation = false); int execute (std::string&); }; diff --git a/src/commands/CmdContext.cpp b/src/commands/CmdContext.cpp new file mode 100644 index 000000000..659f667a0 --- /dev/null +++ b/src/commands/CmdContext.cpp @@ -0,0 +1,351 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2006 - 2015, Paul Beckingham, Federico Hernandez. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// http://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include + +extern Context context; + +//////////////////////////////////////////////////////////////////////////////// +CmdContext::CmdContext () +{ + _keyword = "context"; + _usage = "task context [ | subcommand]"; + _description = STRING_CMD_CONTEXT_USAGE; + _read_only = true; + _displays_id = false; +} + +//////////////////////////////////////////////////////////////////////////////// +int CmdContext::execute (std::string& output) +{ + int rc = 0; + std::stringstream out; + + // Get the non-attribute, non-fancy command line arguments. + std::vector words = context.cli.getWords (); + + if (words.size () > 0) + { + std::string subcommand = words[0]; + + if (subcommand == "define") + rc = defineContext (words, out); + else if (subcommand == "delete") + rc = deleteContext (words, out); + else if (subcommand == "list") + rc = listContexts (words, out); + else if (subcommand == "none") + rc = unsetContext (words, out); + else if (subcommand == "show") + rc = showContext (words, out); + else + rc = setContext (words, out); + } + + output = out.str (); + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +// Joins all the words in the specified interval & words, unsigned int from, unsigned int to /* = 0 */) +{ + std::string value = ""; + + if (to == 0) + to = words.size(); + + for (unsigned int i = from; i < to; ++i) + { + if (i > from) + value += " "; + + value += words[i]; + } + + return value; +} + +//////////////////////////////////////////////////////////////////////////////// +// Returns all user defined contexts. +// +std::vector CmdContext::getContexts () +{ + std::vector contexts; + + Config::const_iterator name; + for (name = context.config.begin (); name != context.config.end (); ++name) + if (name->first.substr (0, 8) == "context.") + contexts.push_back (name->first.substr (8)); + + return contexts; +} + +//////////////////////////////////////////////////////////////////////////////// +// Defines a new user-provided context. +// - The context definition is written into .taskrc as a context. variable. +// - Deletion of the context requires confirmation if rc.confirmation=yes. +// +// Returns: 0 if the addition of the config variable was successful, 1 otherwise +// +// Invoked with: task context define +// Example: task context define home project:Home +// +int CmdContext::defineContext (std::vector & words, std::stringstream& out) +{ + int rc = 0; + + if (words.size () > 2) + { + std::string name = "context." + words[1]; + std::string value = joinWords (words, 2); + // TODO: Check if the value is a proper filter + + // Set context definition config variable + bool confirmation = context.config.getBoolean ("confirmation"); + bool success = CmdConfig::setConfigVariable (name, value, confirmation); + + if (success) + out << format (STRING_CMD_CONTEXT_DEF_SUCC, words[1]) << "\n"; + else + { + out << format (STRING_CMD_CONTEXT_DEF_FAIL, words[1]) << "\n"; + rc = 1; + } + } + else + throw STRING_CMD_CONTEXT_DEF_USAG; + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +// Deletes the specified context. +// - If the deleted context is currently active, unset it. +// - Deletion of the context requires confirmation if rc.confirmation=yes. +// +// Returns: 0 if the removal of the config variable was successful, 1 otherwise +// +// Invoked with: task context delete +// Example: task context delete home +// +int CmdContext::deleteContext (std::vector & words, std::stringstream& out) +{ + int rc = 0; + + if (words.size () > 1) + { + // Delete the specified context + std::string name = "context." + words[1]; + + bool confirmation = context.config.getBoolean ("confirmation"); + rc = CmdConfig::unsetConfigVariable(name, confirmation); + + // If the currently set context was deleted, unset it + std::string currentContext = context.config.get ("context"); + + if (currentContext == words[1]) + CmdConfig::unsetConfigVariable("context", false); + + // Output feedback + if (rc == 0) + out << format (STRING_CMD_CONTEXT_DEL_SUCC, words[1]) << "\n"; + else + out << format (STRING_CMD_CONTEXT_DEL_FAIL, words[1]) << "\n"; + } + else + throw STRING_CMD_CONTEXT_DEL_USAG; + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +// Render a list of context names and their definitions. +// +// Returns: 0 the resulting list is non-empty, 1 otherwise +// +// Invoked with: task context list +// Example: task context list +// +int CmdContext::listContexts (std::vector & words, std::stringstream& out) +{ + int rc = 0; + std::vector contexts = getContexts(); + + if (contexts.size ()) + { + std::sort (contexts.begin (), contexts.end ()); + + ViewText view; + view.width (context.getWidth ()); + view.add (Column::factory ("string", "Name")); + view.add (Column::factory ("string", "Definition")); + + Color label (context.config.get ("color.label")); + view.colorHeader (label); + + std::vector ::iterator userContext; + for (userContext = contexts.begin (); userContext != contexts.end (); ++userContext) + { + std::string definition = context.config.get ("context." + *userContext); + + int row = view.addRow (); + view.set (row, 0, *userContext); + view.set (row, 1, definition); + } + + out << optionalBlankLine () + << view.render () + << optionalBlankLine (); + } + else + { + out << STRING_CMD_CONTEXT_LIST_EMPT << "\n"; + rc = 1; + } + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +// Sets the specified context as currently active. +// - If some other context was active, the value of currently active context +// is replaced, not added. +// - Setting of the context does not require confirmation. +// +// Returns: 0 if the setting of the context was successful, 1 otherwise +// +// Invoked with: task context +// Example: task context home +// +int CmdContext::setContext (std::vector & words, std::stringstream& out) +{ + int rc = 0; + std::string value = words[0]; + std::vector contexts = getContexts (); + + // Check that the specified context is defined + if (std::find (contexts.begin (), contexts.end (), value) == contexts.end ()) + throw format (STRING_CMD_CONTEXT_SET_NFOU, value); + + // Set the active context. + // Should always succeed, as we do not require confirmation. + bool success = CmdConfig::setConfigVariable ("context", value, false); + + if (success) + out << format (STRING_CMD_CONTEXT_SET_SUCC, value) << "\n"; + else + { + out << format (STRING_CMD_CONTEXT_SET_FAIL, value) << "\n"; + rc = 1; + } + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +// Shows the currently active context. +// +// Returns: Always returns 0. +// +// Invoked with: task context show +// Example: task context show +// +int CmdContext::showContext (std::vector & words, std::stringstream& out) +{ + std::string currentContext = context.config.get ("context"); + + if (currentContext == "") + out << STRING_CMD_CONTEXT_SHOW_EMPT << "\n"; + else + { + std::string currentFilter = context.config.get ("context." + currentContext); + out << format (STRING_CMD_CONTEXT_SHOW, currentContext, currentFilter) << "\n"; + } + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// Unsets the currently active context. +// - Unsetting of the context does not require confirmation. +// +// Returns: 0 if the unsetting of the context was successful, 1 otherwise (also +// returned if no context is currently active) +// +// Invoked with: task context none +// Example: task context none +// +int CmdContext::unsetContext (std::vector & words, std::stringstream& out) +{ + int rc = 0; + int status = CmdConfig::unsetConfigVariable ("context", false); + + if (status == 0) + out << STRING_CMD_CONTEXT_NON_SUCC << "\n"; + else + { + out << STRING_CMD_CONTEXT_NON_FAIL << "\n"; + rc = 1; + } + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +CmdCompletionContext::CmdCompletionContext () +{ + _keyword = "_context"; + _usage = "task _context"; + _description = STRING_CMD_HCONTEXT_USAGE; + _read_only = true; + _displays_id = false; +} + +//////////////////////////////////////////////////////////////////////////////// +int CmdCompletionContext::execute (std::string& output) +{ + std::vector userContexts = CmdContext::getContexts (); + + std::vector ::iterator userContext; + for (userContext = userContexts.begin (); userContext != userContexts.end (); ++userContext) + output += *userContext + "\n"; + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/commands/CmdContext.h b/src/commands/CmdContext.h new file mode 100644 index 000000000..4d0f1df75 --- /dev/null +++ b/src/commands/CmdContext.h @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2006 - 2015, Paul Beckingham, Federico Hernandez. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// http://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDED_CMDCONTEXT +#define INCLUDED_CMDCONTEXT + +#include +#include + +class CmdContext : public Command +{ +public: + CmdContext (); + int execute (std::string&); + std::string joinWords (std::vector & words, unsigned int from, unsigned int to = 0); + static std::vector getContexts (); + int defineContext (std::vector & words, std::stringstream& out); + int deleteContext (std::vector & words, std::stringstream& out); + int listContexts (std::vector & words, std::stringstream& out); + int setContext (std::vector & words, std::stringstream& out); + int showContext (std::vector & words, std::stringstream& out); + int unsetContext (std::vector & words, std::stringstream& out); +}; + +class CmdCompletionContext : public Command +{ +public: + CmdCompletionContext (); + int execute (std::string&); +}; + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index 1876dab23..4bcd46e4c 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -82,15 +82,7 @@ int CmdCustom::execute (std::string& output) validateSortColumns (sortOrder); // Prepend the argument list with those from the report filter. - std::string lexeme; - Lexer::Type type; - Lexer lex (reportFilter); - lex.ambiguity (false); - while (lex.token (lexeme, type)) - context.cli.add (lexeme); - - // Reparse after tree change. - context.cli.analyze (); + context.cli.addRawFilter(reportFilter); // Apply filter. handleRecurrence (); diff --git a/src/commands/CmdShow.cpp b/src/commands/CmdShow.cpp index 38f7b9c94..2d33a4ead 100644 --- a/src/commands/CmdShow.cpp +++ b/src/commands/CmdShow.cpp @@ -127,6 +127,7 @@ int CmdShow::execute (std::string& output) " column.padding" " complete.all.tags" " confirmation" + " context" " data.location" " dateformat" " dateformat.annotation" @@ -230,6 +231,7 @@ int CmdShow::execute (std::string& output) i->first.substr (0, 14) != "color.project." && i->first.substr (0, 10) != "color.tag." && i->first.substr (0, 10) != "color.uda." && + i->first.substr (0, 8) != "context." && i->first.substr (0, 8) != "holiday." && i->first.substr (0, 7) != "report." && i->first.substr (0, 6) != "alias." && diff --git a/src/commands/Command.cpp b/src/commands/Command.cpp index f1b8e4220..74619ad38 100644 --- a/src/commands/Command.cpp +++ b/src/commands/Command.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,7 @@ void Command::factory (std::map & all) c = new CmdCompletionColumns (); all[c->keyword ()] = c; c = new CmdCompletionCommands (); all[c->keyword ()] = c; c = new CmdCompletionConfig (); all[c->keyword ()] = c; + c = new CmdCompletionContext (); all[c->keyword ()] = c; c = new CmdCompletionIds (); all[c->keyword ()] = c; c = new CmdCompletionUDAs (); all[c->keyword ()] = c; c = new CmdCompletionUuids (); all[c->keyword ()] = c; @@ -116,6 +118,7 @@ void Command::factory (std::map & all) c = new CmdCompletionTags (); all[c->keyword ()] = c; c = new CmdCompletionVersion (); all[c->keyword ()] = c; c = new CmdConfig (); all[c->keyword ()] = c; + c = new CmdContext (); all[c->keyword ()] = c; c = new CmdCount (); all[c->keyword ()] = c; c = new CmdDelete (); all[c->keyword ()] = c; c = new CmdDenotate (); all[c->keyword ()] = c; diff --git a/src/l10n/deu-DEU.h b/src/l10n/deu-DEU.h index 8847ab9c9..20d76442b 100644 --- a/src/l10n/deu-DEU.h +++ b/src/l10n/deu-DEU.h @@ -561,6 +561,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "Keine Änderungen durchgeführt." #define STRING_CMD_CONFIG_NO_NAME "Geben Sie den Wert der zu ändernden Option an." #define STRING_CMD_HCONFIG_USAGE "Zeigt alle unterstützten Konfigurations-Optionen zur AUtovervollständigung" +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "Die Anzahl von Spalten und Beschriftungen für Report '{1}' unterscheidet sich." #define STRING_CMD_CUSTOM_SHOWN "{1} gezeigt" #define STRING_CMD_CUSTOM_COUNT "1 Aufgabe" diff --git a/src/l10n/eng-USA.h b/src/l10n/eng-USA.h index a7aee545a..cc7838142 100644 --- a/src/l10n/eng-USA.h +++ b/src/l10n/eng-USA.h @@ -561,6 +561,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "No changes made." #define STRING_CMD_CONFIG_NO_NAME "Specify the name of a config variable to modify." #define STRING_CMD_HCONFIG_USAGE "Lists all supported configuration variables, for completion purposes" +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "There are different numbers of columns and labels for report '{1}'." #define STRING_CMD_CUSTOM_SHOWN "{1} shown" #define STRING_CMD_CUSTOM_COUNT "1 task" diff --git a/src/l10n/epo-RUS.h b/src/l10n/epo-RUS.h index f78c42162..cef22854d 100644 --- a/src/l10n/epo-RUS.h +++ b/src/l10n/epo-RUS.h @@ -561,6 +561,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "Ne ŝanĝis nenion." #define STRING_CMD_CONFIG_NO_NAME "Specifu la nomon de agordvariablo, kiun vi volas modifi." #define STRING_CMD_HCONFIG_USAGE "Listigas çiun subtenatan agordan variablon, por motivo memkompletada" +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "La nombroj de kolumnoj kaj de siaj titoloj ne kongruas por raporto '{1}'." #define STRING_CMD_CUSTOM_SHOWN "{1} montritaj" #define STRING_CMD_CUSTOM_COUNT "1 tasko" diff --git a/src/l10n/esp-ESP.h b/src/l10n/esp-ESP.h index fc1404724..6eb6e64e3 100644 --- a/src/l10n/esp-ESP.h +++ b/src/l10n/esp-ESP.h @@ -570,6 +570,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "No se hicieron cambios." #define STRING_CMD_CONFIG_NO_NAME "Especifique el nombre de una variable de configuración a modificar." #define STRING_CMD_HCONFIG_USAGE "Lista todas las variables de configuración soportadas, a fines de completado" +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "Hay diferente número de columnas y etiquetas para el informe '{1}'." #define STRING_CMD_CUSTOM_SHOWN "{1} mostrada(s)" #define STRING_CMD_CUSTOM_COUNT "1 tarea" diff --git a/src/l10n/fra-FRA.h b/src/l10n/fra-FRA.h index 7137d09ec..cdc7bbcb9 100644 --- a/src/l10n/fra-FRA.h +++ b/src/l10n/fra-FRA.h @@ -561,6 +561,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "No changes made." #define STRING_CMD_CONFIG_NO_NAME "Specify the name of a config variable to modify." #define STRING_CMD_HCONFIG_USAGE "Lists all supported configuration variables, for completion purposes" +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "There are different numbers of columns and labels for report '{1}'." #define STRING_CMD_CUSTOM_SHOWN "{1} affichées" #define STRING_CMD_CUSTOM_COUNT "1 tâche" diff --git a/src/l10n/ita-ITA.h b/src/l10n/ita-ITA.h index a7fda70e6..b3ec61fb1 100644 --- a/src/l10n/ita-ITA.h +++ b/src/l10n/ita-ITA.h @@ -560,6 +560,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "Nessuna modifica apportata." #define STRING_CMD_CONFIG_NO_NAME "Specificare il nome di una variabile di configurazione da modificare." #define STRING_CMD_HCONFIG_USAGE "Elenca le variabili di configurazione supportate, per autocompletamento" +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "Differente numero di colonne ed etichette per il report '{1}'." #define STRING_CMD_CUSTOM_SHOWN "{1} mostrato" #define STRING_CMD_CUSTOM_COUNT "1 task" diff --git a/src/l10n/pol-POL.h b/src/l10n/pol-POL.h index 8394d2f93..c296c2700 100644 --- a/src/l10n/pol-POL.h +++ b/src/l10n/pol-POL.h @@ -561,6 +561,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "Brak zmian." #define STRING_CMD_CONFIG_NO_NAME "Podaj nazwę zmiennej w konfiguracji do zmiany." #define STRING_CMD_HCONFIG_USAGE "Wylistuj wszystkie zmienne konfiguracji." +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "Liczba kolumn i nagłówków nie zgadza się dla raportu '{1}'." #define STRING_CMD_CUSTOM_SHOWN "{1} pokazanych" #define STRING_CMD_CUSTOM_COUNT "1 zadanie" diff --git a/src/l10n/por-PRT.h b/src/l10n/por-PRT.h index 281fe63f7..1587291ae 100644 --- a/src/l10n/por-PRT.h +++ b/src/l10n/por-PRT.h @@ -561,6 +561,22 @@ #define STRING_CMD_CONFIG_NO_CHANGE "Nenhuma alteração efectuada." #define STRING_CMD_CONFIG_NO_NAME "Especifique o nome da configuração a modificar." #define STRING_CMD_HCONFIG_USAGE "Lista todas as configurações suportadas, para fins de terminação automática" +#define STRING_CMD_CONTEXT_USAGE "Set and define contexts (default filters)" +#define STRING_CMD_CONTEXT_DEF_SUCC "Context '{1}' defined." +#define STRING_CMD_CONTEXT_DEF_FAIL "Context '{1}' not defined." +#define STRING_CMD_CONTEXT_DEF_USAG "Both context name and its definition must be provided." +#define STRING_CMD_CONTEXT_DEL_SUCC "Context '{1}' deleted." +#define STRING_CMD_CONTEXT_DEL_FAIL "Context '{1}' not deleted." +#define STRING_CMD_CONTEXT_DEL_USAG "Context name needs to be specified." +#define STRING_CMD_CONTEXT_LIST_EMPT "No contexts defined." +#define STRING_CMD_CONTEXT_SET_NFOU "Context '{1}' not found." +#define STRING_CMD_CONTEXT_SET_SUCC "Context '{1}' applied." +#define STRING_CMD_CONTEXT_SET_FAIL "Context '{1}' not applied." +#define STRING_CMD_CONTEXT_SHOW_EMPT "No context is currently applied." +#define STRING_CMD_CONTEXT_SHOW "Context '{1}' with filter '{2}' is currently applied." +#define STRING_CMD_CONTEXT_NON_SUCC "Context unset." +#define STRING_CMD_CONTEXT_NON_FAIL "Context not unset." +#define STRING_CMD_HCONTEXT_USAGE "Lists all supported contexts, for completion purposes" #define STRING_CMD_CUSTOM_MISMATCH "O número de colunas e de rótulos não é o mesmo no relatório '{1}'." #define STRING_CMD_CUSTOM_SHOWN "{1} visiveis" #define STRING_CMD_CUSTOM_COUNT "1 tarefa" diff --git a/test/basetest/task.py b/test/basetest/task.py index a6921f7e3..73c0d2af0 100644 --- a/test/basetest/task.py +++ b/test/basetest/task.py @@ -137,6 +137,15 @@ class Task(object): cmd = (self.taskw, "config", "--", var, value) return run_cmd_wait(cmd, env=self.env) + @property + def taskrc_content(self): + """ + Returns the contents of the taskrc file. + """ + + with open(self.taskrc, "r") as f: + return f.readlines() + def runSuccess(self, args=(), input=None, merge_streams=False, timeout=1): """Invoke task with given arguments and fail if exit code != 0 diff --git a/test/context.t b/test/context.t new file mode 100755 index 000000000..a185c77bc --- /dev/null +++ b/test/context.t @@ -0,0 +1,474 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- +################################################################################ +## +## Copyright 2006 - 2015, Paul Beckingham, Federico Hernandez. +## +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: +## +## The above copyright notice and this permission notice shall be included +## in all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +## OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +## SOFTWARE. +## +## http://www.opensource.org/licenses/mit-license.php +## +################################################################################ + +import sys +import os +import unittest +import re + +# Ensure python finds the local simpletap module +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from basetest import Task, TestCase + +class ContextManagementTest(TestCase): + def setUp(self): + self.t = Task() + + def test_context_define(self): + """ + Test simple context definition. + """ + + output = self.t(('context', 'define', 'work', 'project:Work'))[1] + + # Assert successful output + self.assertIn("Context 'work' defined.", output) + + # Assert the config contains context definition + self.assertIn('context.work=project:Work\n', self.t.taskrc_content) + + # Assert that it contains the definition only once + is_context_line = lambda x: x == 'context.work=project:Work\n' + self.assertEqual(len(filter(is_context_line, self.t.taskrc_content)), 1) + + def test_context_redefine_same_definition(self): + """ + Test re-defining the context with the same definition. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + output = self.t(('context', 'define', 'work', 'project:Work'))[1] + + # Assert successful output + self.assertIn("Context 'work' defined.", output) + + # Assert the config contains context definition + self.assertIn('context.work=project:Work\n', self.t.taskrc_content) + + # Assert that it contains the definition only once + is_context_line = lambda x: x == 'context.work=project:Work\n' + self.assertEqual(len(filter(is_context_line, self.t.taskrc_content)), 1) + + def test_context_redefine_different_definition(self): + """ + Test re-defining the context with different definition. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + output = self.t(('context', 'define', 'work', '+work'))[1] + + # Assert successful output + self.assertIn("Context 'work' defined.", output) + + # Assert the config does not contain the old context definition + self.assertNotIn('context.work=project:Work\n', self.t.taskrc_content) + + # Assert the config contains context definition + self.assertIn('context.work=+work\n', self.t.taskrc_content) + + # Assert that it contains the definition only once + is_context_line = lambda x: x == 'context.work=+work\n' + self.assertEqual(len(filter(is_context_line, self.t.taskrc_content)), 1) + + def test_context_delete(self): + """ + Test simple context deletion. + """ + + self.t(('context', 'define', 'work', 'project:Work')) + output = self.t(('context', 'delete', 'work'))[1] + + # Assert correct output + self.assertIn("Context 'work' undefined.", output) + + # Assert that taskrc does not countain context work definition + self.assertFalse(any('context.work=' in line for line in self.t.taskrc_content)) + + def test_context_delete_undefined(self): + """ + Test deletion of undefined context. + """ + + output = self.t.runError(('context', 'delete', 'work'))[1] + + # Assert correct output + self.assertIn("Context 'work' was not undefined.", output) + + # Assert that taskrc does not countain context work definition + self.assertFalse(any('context.work=' in line for line in self.t.taskrc_content)) + + def test_context_delete_unset_after_removal(self): + """ + Test that context is unset if its definition has been removed. + """ + + self.t(('context', 'define', 'work', 'project:Work')) + self.t(('context', 'work')) + output = self.t(('context', 'delete', 'work'))[1] + + # Assert correct output + self.assertIn("Context 'work' undefined.", output) + + # Assert that taskrc does not countain context work definition + self.assertFalse(any('context.work=' in line for line in self.t.taskrc_content)) + + # Aseert that the context is not set + output = self.t(('context', 'show'))[1] + self.assertIn('No context is currently applied.', output) + self.assertFalse(any(re.search("^context=", line) for line in self.t.taskrc_content)) + + def test_context_list(self): + """ + Test the 'context list' command. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + self.t(('context', 'define', 'home', '+home'))[1] + + output = self.t(('context', 'list'))[1] + + contains_work = lambda line: 'work' in line and 'project:Work' in line + contains_home = lambda line: 'home' in line and '+home' in line + + # Assert that output contains work and home context definitions exactly + # once + self.assertEqual(len(filter(contains_work, output.splitlines())), 1) + self.assertEqual(len(filter(contains_home, output.splitlines())), 1) + + def test_context_initially_empty(self): + """ + Test that no context is set initially. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + self.t(('context', 'define', 'home', '+home'))[1] + + output = self.t(('context', 'show'))[1] + self.assertIn('No context is currently applied.', output) + self.assertFalse(any(re.search("^context=", line) for line in self.t.taskrc_content)) + + def test_context_setting(self): + """ + Test simple context setting. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + self.t(('context', 'define', 'home', '+home'))[1] + + output = self.t(('context', 'home'))[1] + self.assertIn("Context 'home' applied.", output) + self.assertIn("context=home\n", self.t.taskrc_content) + + def test_context_resetting(self): + """ + Test resetting the same context. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + self.t(('context', 'define', 'home', '+home'))[1] + + self.t(('context', 'home'))[1] + output = self.t(('context', 'home'))[1] + self.assertIn("Context 'home' applied.", output) + + contains_home = lambda line: line == "context=home\n" + self.assertEqual(len(filter(contains_home, self.t.taskrc_content)), 1) + + def test_context_switching(self): + """ + Test changing the context. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + self.t(('context', 'define', 'home', '+home'))[1] + + contains_home = lambda line: line == "context=home\n" + contains_work = lambda line: line == "context=work\n" + + # Switch to home context + output = self.t(('context', 'home'))[1] + self.assertIn("Context 'home' applied.", output) + + self.assertEqual(len(filter(contains_home, self.t.taskrc_content)), 1) + + # Switch to work context + output = self.t(('context', 'work'))[1] + self.assertIn("Context 'work' applied.", output) + + self.assertNotIn("context=home\n", self.t.taskrc_content) + self.assertEqual(len(filter(contains_work, self.t.taskrc_content)), 1) + + # Switch back to home context + output = self.t(('context', 'home'))[1] + self.assertIn("Context 'home' applied.", output) + + self.assertNotIn("context=work\n", self.t.taskrc_content) + self.assertEqual(len(filter(contains_home, self.t.taskrc_content)), 1) + + def test_context_unsetting(self): + """ + Test removing the context. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + self.t(('context', 'define', 'home', '+home'))[1] + + self.t(('context', 'home')) + output = self.t(('context', 'none'))[1] + + # Assert expected output. + self.assertIn("Context unset.", output) + + # Assert no context definition in the taskrc + contains_any_context = lambda line: re.match('^context=', line) + self.assertFalse(any(contains_any_context(line) for line in self.t.taskrc_content)) + + # Assert no context showing up using show subcommand + output = self.t(('context', 'show'))[1] + self.assertIn("No context is currently applied.", output) + + def test_context_unsetting_after_switching(self): + """ + Test unsetting the context after changing the context around. + """ + + self.t(('context', 'define', 'work', 'project:Work'))[1] + self.t(('context', 'define', 'home', '+home'))[1] + + # Switch to contexts around + self.t(('context', 'home')) + self.t(('context', 'work')) + self.t(('context', 'home')) + + # Unset the context + output = self.t(('context', 'none'))[1] + + # Assert expected output. + self.assertIn("Context unset.", output) + + # Assert no context definition in the taskrc + contains_any_context = lambda line: re.match('^context=', line) + self.assertFalse(any(contains_any_context(line) for line in self.t.taskrc_content)) + + # Assert no context showing up using show subcommand + output = self.t(('context', 'show'))[1] + self.assertIn("No context is currently applied.", output) + + def test_context_unsetting_with_no_context_set(self): + """ + Test removing the context when no context is set. + """ + + self.t(('context', 'define', 'work', 'project:Work')) + self.t(('context', 'define', 'home', '+home')) + + output = self.t.runError(('context', 'none'))[1] + + # Assert expected output. + self.assertIn("Context not unset.", output) + + # Assert no context definition in the taskrc + contains_any_context = lambda line: re.match('^context=', line) + self.assertFalse(any(contains_any_context(line) for line in self.t.taskrc_content)) + + # Assert no context showing up using show subcommand + output = self.t(('context', 'show'))[1] + self.assertIn("No context is currently applied.", output) + + def test_context_completion(self): + """ + Test the _context command. + """ + + self.t(('context', 'define', 'work', 'project:Work')) + self.t(('context', 'define', 'home', '+home')) + + output = self.t(('_context',))[1] + + # Assert expected output. + self.assertIn("work", output.splitlines()) + self.assertIn("home", output.splitlines()) + self.assertEqual(len(output.splitlines()), 2) + + def test_context_completion(self): + """ + Test the _context command with some context set. + """ + + self.t(('context', 'define', 'work', 'project:Work')) + self.t(('context', 'define', 'home', '+home')) + + # Activete some context + self.t(('context', 'work')) + + output = self.t(('_context',))[1] + + # Assert expected output. + self.assertIn("work", output.splitlines()) + self.assertIn("home", output.splitlines()) + self.assertEqual(len(output.splitlines()), 2) + + +class ContextEvaluationTest(TestCase): + def setUp(self): + self.t = Task() + + # Setup contexts + self.t(('context', 'define', 'work', 'project:Work')) + self.t(('context', 'define', 'home', '+home')) + self.t(('context', 'define', 'today', 'due:today')) + + # Setup tasks + self.t(('add', 'project:Work', "work task")) + self.t(('add', '+home', "home task")) + self.t(('add', 'project:Work', 'due:today', 'work today task')) + self.t(('add', '+home', 'due:today', 'home today task')) + + def test_context_evaluation(self): + """ + Test the context applied with report list command. + """ + + output = self.t(('list',))[1] + + # Assert all the tasks are present in the output + self.assertIn("work task", output) + self.assertIn("home task", output) + self.assertIn("work today task", output) + self.assertIn("home today task", output) + + # Set the home context and rerun the report + self.t(('context', 'home')) + output = self.t(('list',))[1] + + # Assert all the tasks with the home tag are present in the output + self.assertNotIn("work task", output) + self.assertIn("home task", output) + self.assertNotIn("work today task", output) + self.assertIn("home today task", output) + + def test_context_evaluation_switching(self): + """ + Test swtiching context using the list report. + """ + + output = self.t(('list',))[1] + + # Assert all the tasks are present in the output + self.assertIn("work task", output) + self.assertIn("home task", output) + self.assertIn("work today task", output) + self.assertIn("home today task", output) + + # Set the home context and rerun the report + self.t(('context', 'home')) + output = self.t(('list',))[1] + + # Assert all the tasks with the home tag are present in the output + self.assertNotIn("work task", output) + self.assertIn("home task", output) + self.assertNotIn("work today task", output) + self.assertIn("home today task", output) + + # Set the work context and rerun the report + self.t(('context', 'work')) + output = self.t(('list',))[1] + + # Assert all the tasks with the home tag are present in the output + self.assertIn("work task", output) + self.assertNotIn("home task", output) + self.assertIn("work today task", output) + self.assertNotIn("home today task", output) + + # Set the today context and rerun the report + self.t(('context', 'today')) + output = self.t(('list',))[1] + + # Assert all the tasks with the home tag are present in the output + self.assertNotIn("work task", output) + self.assertNotIn("home task", output) + self.assertIn("work today task", output) + self.assertIn("home today task", output) + + def test_context_evaluation_unset(self): + """ + Test unsetting context with report list command. + """ + + self.t(('context', 'home')) + output = self.t(('list',))[1] + + # Assert all the tasks home tagged tasks are present + self.assertNotIn("work task", output) + self.assertIn("home task", output) + self.assertNotIn("work today task", output) + self.assertIn("home today task", output) + + # Set the context to none + self.t(('context', 'none')) + output = self.t(('list',))[1] + + # Assert all the tasks are present in the output + self.assertIn("work task", output) + self.assertIn("home task", output) + self.assertIn("work today task", output) + self.assertIn("home today task", output) + + def test_context_evaluation_with_user_filters(self): + """ + Test the context applied with report list command + combined with user filters. + """ + + # Set the home context + self.t(('context', 'home')) + output = self.t(('list', 'due:today'))[1] + + # Assert all the tasks are present in the output + self.assertNotIn("work task", output) + self.assertNotIn("home task", output) + self.assertNotIn("work today task", output) + self.assertIn("home today task", output) + + # Set the work context and rerun the report + self.t(('context', 'work')) + output = self.t(('list', 'due:today'))[1] + + # Assert all the tasks are present in the output + self.assertNotIn("work task", output) + self.assertNotIn("home task", output) + self.assertIn("work today task", output) + self.assertNotIn("home today task", output) + + +if __name__ == "__main__": + from simpletap import TAPTestRunner + unittest.main(testRunner=TAPTestRunner()) + +# vim: ai sts=4 et sw=4 syntax=python