Repository: fniessink/next-action

Issue:
When picking the next action, give priority to tasks that belong to a project

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f75f42..c4ec0b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Added
+
+- Other properties being equal, task with more projects get precedence over tasks with fewer projects when selecting the next action. Closes #57.
+
 ## [0.4.0] - 2018-05-19
 
 ### Changed
diff --git a/README.md b/README.md
index 4a0f179..2337d3d 100644
--- a/README.md
+++ b/README.md
@@ -29,8 +29,9 @@ Don't know what *Todo.txt* is? See <https://github.com/todotxt/todo.txt> for the
 $ next-action --help
 usage: next-action [-h] [--version] [-f <filename>] [-n <number> | -a] [<context|project> ...]
 
-Show the next action in your todo.txt. The next action is selected from the tasks in the todo.txt file based on
-priority, due date, creation date, and supplied filters.
+Show the next action in your todo.txt. The next action is selected from the tasks in the todo.txt file based on task
+properties such as priority, due date, and creation date. Limit the tasks from which the next action is selected by
+specifying contexts the tasks must have and/or projects the tasks must belong to.
 
 optional arguments:
   -h, --help            show this help message and exit
@@ -56,7 +57,7 @@ $ next-action
 (A) Call mom @phone
 ```
 
-The next action is determined using priority. Due date is considered after priority, with tasks due earlier getting precedence over tasks due later. Creation date is considered after due date, with older tasks getting precedence over newer tasks.
+The next action is determined using priority. Due date is considered after priority, with tasks due earlier getting precedence over tasks due later. Creation date is considered after due date, with older tasks getting precedence over newer tasks. FInally, tasks that belong to more projects get precedence over tasks that belong to fewer projects.
 
 Completed tasks (~~`x This is a completed task`~~) and tasks with a creation date in the future (`9999-01-01 Start preparing for five-digit years`) are not considered when determining the next action.
 
diff --git a/next_action/arguments/parser.py b/next_action/arguments/parser.py
index 0d3ed9d..a53589e 100644
--- a/next_action/arguments/parser.py
+++ b/next_action/arguments/parser.py
@@ -10,7 +10,9 @@ def build_parser(default_filenames: List[str]) -> argparse.ArgumentParser:
     """ Create the arguments parsers. """
     parser = argparse.ArgumentParser(
         description="Show the next action in your todo.txt. The next action is selected from the tasks in the todo.txt \
-                     file based on priority, due date, creation date, and supplied filters.",
+                     file based on task properties such as priority, due date, and creation date. Limit the tasks from \
+                     which the next action is selected by specifying contexts the tasks must have and/or projects the \
+                     tasks must belong to.",
         formatter_class=argparse.ArgumentDefaultsHelpFormatter,
         usage="%(prog)s [-h] [--version] [-f <filename>] [-n <number> | -a] [<context|project> ...]")
     add_optional_arguments(parser, default_filenames)
diff --git a/next_action/pick_action.py b/next_action/pick_action.py
index 2542cfb..71646d4 100644
--- a/next_action/pick_action.py
+++ b/next_action/pick_action.py
@@ -6,9 +6,10 @@ from typing import Set, Sequence, Tuple
 from .todotxt import Task
 
 
-def sort_key(task: Task) -> Tuple[str, datetime.date, datetime.date]:
+def sort_key(task: Task) -> Tuple[str, datetime.date, datetime.date, int]:
     """ Return the sort key for a task. """
-    return (task.priority() or "ZZZ", task.due_date() or datetime.date.max, task.creation_date() or datetime.date.max)
+    return (task.priority() or "ZZZ", task.due_date() or datetime.date.max, task.creation_date() or datetime.date.max,
+            -len(task.projects()))
 
 
 def next_actions(tasks: Sequence[Task], contexts: Set[str] = None, projects: Set[str] = None,


Test patch:
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index 0cabed3..1352148 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -65,8 +65,9 @@ class CLITest(unittest.TestCase):
         self.assertEqual(call("""\
 usage: next-action [-h] [--version] [-f <filename>] [-n <number> | -a] [<context|project> ...]
 
-Show the next action in your todo.txt. The next action is selected from the tasks in the todo.txt file based on
-priority, due date, creation date, and supplied filters.
+Show the next action in your todo.txt. The next action is selected from the tasks in the todo.txt file based on task
+properties such as priority, due date, and creation date. Limit the tasks from which the next action is selected by
+specifying contexts the tasks must have and/or projects the tasks must belong to.
 
 optional arguments:
   -h, --help            show this help message and exit
diff --git a/tests/unittests/test_pick_action.py b/tests/unittests/test_pick_action.py
index 8fec9cc..8ccd64b 100644
--- a/tests/unittests/test_pick_action.py
+++ b/tests/unittests/test_pick_action.py
@@ -61,6 +61,18 @@ class PickActionTest(unittest.TestCase):
         task2 = todotxt.Task("Task 2 due:2018-1-1")
         self.assertEqual([task2, task1], pick_action.next_actions([task1, task2]))
 
+    def test_project(self):
+        """ Test that a task with a project takes precedence over a task without a project. """
+        task1 = todotxt.Task("Task 1")
+        task2 = todotxt.Task("Task 2 +Project")
+        self.assertEqual([task2, task1], pick_action.next_actions([task1, task2]))
+
+    def test_projects(self):
+        """ Test that a task with more projects takes precedence over a task with less projects. """
+        task1 = todotxt.Task("Task 1 +Project")
+        task2 = todotxt.Task("Task 2 +Project1 +Project2")
+        self.assertEqual([task2, task1], pick_action.next_actions([task1, task2]))
+
 
 class FilterTasksTest(unittest.TestCase):
     """ Test that the tasks from which the next action is picked, can be filtered. """