Commit 4ba571ca authored by Bram Schoenmakers's avatar Bram Schoenmakers

Allow 'do' and 'del' to complete/delete multiple todos at once.

Fixes issue #1.
parent 7b4ccc57
...@@ -116,6 +116,27 @@ class DeleteCommandTest(CommandTest.CommandTest): ...@@ -116,6 +116,27 @@ class DeleteCommandTest(CommandTest.CommandTest):
self.assertEquals(str(self.todolist), "Foo") self.assertEquals(str(self.todolist), "Foo")
self.assertRaises(InvalidTodoException, self.todolist.todo, 'b0n') self.assertRaises(InvalidTodoException, self.todolist.todo, 'b0n')
def test_multi_del1(self):
""" Test deletion of multiple items. """
command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertEquals(self.todolist.count(), 0)
def test_multi_del2(self):
""" Test deletion of multiple items. """
command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
self.assertEquals(self.todolist.count(), 0)
def test_multi_del3(self):
""" Test deletion of multiple items. """
command = DeleteCommand(["99", "2"], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
self.assertEquals(self.todolist.count(), 1)
def test_empty(self): def test_empty(self):
command = DeleteCommand([], self.todolist, self.out, self.error) command = DeleteCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -279,6 +279,52 @@ class DoCommandTest(CommandTest.CommandTest): ...@@ -279,6 +279,52 @@ class DoCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, "| 9| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {yesterday} Strict due:2014-01-01 rec:1d\n".format(today=self.today, yesterday=self.yesterday)) self.assertEquals(self.output, "| 9| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {yesterday} Strict due:2014-01-01 rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEquals(self.errors, "") self.assertEquals(self.errors, "")
def test_multi_do1(self):
command = DoCommand(["1", "3"], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
self.assertTrue(self.todolist.todo(1).is_completed())
self.assertTrue(self.todolist.todo(2).is_completed())
self.assertTrue(self.todolist.todo(3).is_completed())
self.assertEquals(self.output, "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x {today} Bar p:1\nCompleted: x {today} Baz p:1\nCompleted: x {today} Foo id:1\n".format(today=self.today))
def test_multi_do2(self):
command = DoCommand(["1", "3"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertTrue(self.todolist.todo(1).is_completed())
self.assertFalse(self.todolist.todo(2).is_completed())
self.assertTrue(self.todolist.todo(3).is_completed())
self.assertEquals(self.output, "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x {today} Foo id:1\nCompleted: x {today} Baz p:1\n".format(today=self.today))
def test_multi_do3(self):
command = DoCommand(["3", "3"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertTrue(self.todolist.todo(3).is_completed())
self.assertEquals(self.output, "Completed: x {} Baz p:1\n".format(self.today))
def test_multi_do4(self):
command = DoCommand(["99", "3"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertTrue(self.todolist.todo(3).is_completed())
self.assertEquals(self.output, "Completed: x {} Baz p:1\n".format(self.today))
self.assertEquals(self.errors, "Invalid todo number given.\n")
def test_multi_do5(self):
"""
When a todo item was generated by a recurring todo item, make sure
it cannot be completed in the same invocation.
"""
command = DoCommand(["4", "9"], self.todolist, self.out, self.error, _no_prompt)
command.execute()
self.assertTrue(self.todolist.todo(4).is_completed())
self.assertFalse(self.todolist.todo(9).is_completed())
def test_empty(self): def test_empty(self):
command = DoCommand([], self.todolist, self.out, self.error) command = DoCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -38,10 +38,12 @@ class DCommand(Command): ...@@ -38,10 +38,12 @@ class DCommand(Command):
self.process_flags() self.process_flags()
self.length = len(self.todolist.todos()) # to determine newly activated todos self.length = len(self.todolist.todos()) # to determine newly activated todos
self.todos = []
for number in self.args:
try: try:
self.todo = self.todolist.todo(self.argument(0)) self.todos.append(self.todolist.todo(number))
except (InvalidCommandArgument, InvalidTodoException): except InvalidTodoException:
self.todo = None self.todos.append(None)
def get_flags(self): def get_flags(self):
""" Default implementation of getting specific flags. """ """ Default implementation of getting specific flags. """
...@@ -80,8 +82,8 @@ class DCommand(Command): ...@@ -80,8 +82,8 @@ class DCommand(Command):
""" Prefix to use when printing a todo. """ """ Prefix to use when printing a todo. """
return "" return ""
def _process_subtasks(self): def _process_subtasks(self, p_todo):
children = self._uncompleted_children(self.todo) children = self._uncompleted_children(p_todo)
if children: if children:
self._print_list(children) self._print_list(children)
...@@ -112,7 +114,7 @@ class DCommand(Command): ...@@ -112,7 +114,7 @@ class DCommand(Command):
return [todo for todo in self.todolist.todos()[:self.length] return [todo for todo in self.todolist.todos()[:self.length]
if not self._uncompleted_children(todo) and todo.is_active()] if not self._uncompleted_children(todo) and todo.is_active()]
def condition(self): def condition(self, p_todo):
""" """
An additional condition whether execute_specific should be executed. An additional condition whether execute_specific should be executed.
""" """
...@@ -121,7 +123,7 @@ class DCommand(Command): ...@@ -121,7 +123,7 @@ class DCommand(Command):
def condition_failed_text(self): def condition_failed_text(self):
return "" return ""
def execute_specific(self): def execute_specific(self, _):
pass pass
def execute_specific_core(self, p_todo): def execute_specific_core(self, p_todo):
...@@ -137,14 +139,18 @@ class DCommand(Command): ...@@ -137,14 +139,18 @@ class DCommand(Command):
if len(self.args) == 0: if len(self.args) == 0:
self.error(self.usage()) self.error(self.usage())
elif not self.todo: else:
self.error("Invalid todo number given.")
elif self.todo and self.condition():
old_active = self._active_todos() old_active = self._active_todos()
self._process_subtasks()
self.execute_specific() for todo in self.todos:
current_active = self._active_todos() if not todo:
self._print_unlocked_todos(old_active, current_active) self.error("Invalid todo number given.")
elif todo and self.condition(todo):
self._process_subtasks(todo)
self.execute_specific(todo)
else: else:
self.error(self.condition_failed_text()) self.error(self.condition_failed_text())
current_active = self._active_todos()
self._print_unlocked_todos(old_active, current_active)
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -34,12 +34,13 @@ class DeleteCommand(DCommand): ...@@ -34,12 +34,13 @@ class DeleteCommand(DCommand):
def execute_specific_core(self, p_todo): def execute_specific_core(self, p_todo):
self.todolist.delete(p_todo) self.todolist.delete(p_todo)
def execute_specific(self): def execute_specific(self, p_todo):
self.out(self.prefix() + pretty_print(self.todo)) self.out(self.prefix() + pretty_print(p_todo))
self.execute_specific_core(self.todo) self.execute_specific_core(p_todo)
def usage(self): def usage(self):
return """Synopsis: del [-f] <NUMBER>""" return """Synopsis: del [-f] <NUMBER1> [<NUMBER2> ...]"""
def help(self): def help(self):
return """Deletes the todo item with the given number from the list.""" return """\
Deletes the todo item(s) with the given number(s) from the list."""
...@@ -46,13 +46,13 @@ class DoCommand(DCommand): ...@@ -46,13 +46,13 @@ class DoCommand(DCommand):
except ValueError: except ValueError:
self.completion_date = date.today() self.completion_date = date.today()
def _handle_recurrence(self): def _handle_recurrence(self, p_todo):
if self.todo.has_tag('rec'): if p_todo.has_tag('rec'):
if self.strict_recurrence: if self.strict_recurrence:
new_todo = strict_advance_recurring_todo(self.todo, new_todo = strict_advance_recurring_todo(p_todo,
self.completion_date) self.completion_date)
else: else:
new_todo = advance_recurring_todo(self.todo, new_todo = advance_recurring_todo(p_todo,
self.completion_date) self.completion_date)
self.todolist.add_todo(new_todo) self.todolist.add_todo(new_todo)
...@@ -64,20 +64,20 @@ class DoCommand(DCommand): ...@@ -64,20 +64,20 @@ class DoCommand(DCommand):
def prefix(self): def prefix(self):
return "Completed: " return "Completed: "
def condition(self): def condition(self, p_todo):
""" """
An additional condition whether execute_specific should be executed. An additional condition whether execute_specific should be executed.
""" """
return not self.todo.is_completed() return not p_todo.is_completed()
def condition_failed_text(self): def condition_failed_text(self):
return "Todo has already been completed." return "Todo has already been completed."
def execute_specific(self): def execute_specific(self, p_todo):
""" Actions specific to this command. """ """ Actions specific to this command. """
self._handle_recurrence() self._handle_recurrence(p_todo)
self.execute_specific_core(self.todo) self.execute_specific_core(p_todo)
self.out(self.prefix() + pretty_print(self.todo)) self.out(self.prefix() + pretty_print(p_todo))
def execute_specific_core(self, p_todo): def execute_specific_core(self, p_todo):
""" """
...@@ -87,16 +87,16 @@ class DoCommand(DCommand): ...@@ -87,16 +87,16 @@ class DoCommand(DCommand):
self.todolist.set_todo_completed(p_todo, self.completion_date) self.todolist.set_todo_completed(p_todo, self.completion_date)
def usage(self): def usage(self):
return """Synopsis: do [--date] [--force] [--strict] <NUMBER>""" return """Synopsis: do [--date] [--force] [--strict] <NUMBER1> [<NUMBER2> ...]"""
def help(self): def help(self):
return """Marks the todo with given number as complete. return """Marks the todo(s) with given number(s) as complete.
In case the todo has subitems, a question is asked whether the subitems should In case a todo has subitems, a question is asked whether the subitems should be
be marked as completed as well. When --force is given, no interaction is marked as completed as well. When --force is given, no interaction is required
required and the subitems are not marked completed. and the subitems are not marked completed.
In case the completed todo is recurring, a new todo will be added to the list, In case a completed todo is recurring, a new todo will be added to the list,
while the given todo item is marked as complete. The new date is calculated while the given todo item is marked as complete. The new date is calculated
based on the todo item's due date. If the due date is in the past, today's date based on the todo item's due date. If the due date is in the past, today's date
is used to calculate the new recurrence date. Using --strict prevents this, is used to calculate the new recurrence date. Using --strict prevents this,
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -90,6 +90,7 @@ class TodoList(TodoListBase): ...@@ -90,6 +90,7 @@ class TodoList(TodoListBase):
def delete(self, p_todo): def delete(self, p_todo):
""" Deletes a todo item from the list. """ """ Deletes a todo item from the list. """
try:
number = self._todos.index(p_todo) number = self._todos.index(p_todo)
for child in self.children(p_todo): for child in self.children(p_todo):
...@@ -102,6 +103,9 @@ class TodoList(TodoListBase): ...@@ -102,6 +103,9 @@ class TodoList(TodoListBase):
self._update_todo_ids() self._update_todo_ids()
self.dirty = True self.dirty = True
except ValueError:
# todo item couldn't be found, ignore
pass
def add_dependency(self, p_from_todo, p_to_todo): def add_dependency(self, p_from_todo, p_to_todo):
""" Adds a dependency from task 1 to task 2. """ """ Adds a dependency from task 1 to task 2. """
......
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -153,10 +153,14 @@ class TodoListBase(object): ...@@ -153,10 +153,14 @@ class TodoListBase(object):
def delete(self, p_todo): def delete(self, p_todo):
""" Deletes a todo item from the list. """ """ Deletes a todo item from the list. """
try:
number = self._todos.index(p_todo) number = self._todos.index(p_todo)
del self._todos[number] del self._todos[number]
self._update_todo_ids() self._update_todo_ids()
self.dirty = True self.dirty = True
except ValueError:
# todo item couldn't be found, ignore
pass
def erase(self): def erase(self):
""" Erases all todos from the list. """ """ Erases all todos from the list. """
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment