Commit 4570a210 authored by Bram Schoenmakers's avatar Bram Schoenmakers Committed by GitHub

Merge pull request #158 from mruwek/multi-4all

Allow actions on multiple ids without MultiCommand
parents 68fde1b1 89e96176
......@@ -405,6 +405,11 @@ class AddCommandTest(CommandTest):
"| 1| x 2015-01-01 {} Already completed\n".format(self.today))
self.assertEqual(self.errors, "")
def test_add_name(self):
name = AddCommand.AddCommand.name()
self.assertEqual(name, 'add')
def test_help(self):
command = AddCommand.AddCommand(["help"], self.todolist, self.out,
self.error)
......
......@@ -102,6 +102,11 @@ class AppendCommandTest(CommandTest):
"| 2| Qux due:%s t:%s p:1 p:2\n" % (self.today, self.today))
self.assertEqual(self.errors, "")
def test_append_name(self):
name = AppendCommand.name()
self.assertEqual(name, 'append')
def test_help(self):
command = AppendCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -243,6 +243,11 @@ class DeleteCommandTest(CommandTest):
self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n")
def test_delete_name(self):
name = DeleteCommand.name()
self.assertEqual(name, 'delete')
def test_help(self):
command = DeleteCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -360,6 +360,11 @@ node [ shape="none" margin="0" fontsize="9" fontname="Helvetica" ]
self.assertEqual(self.errors, command.usage() + "\n")
self.assertFalse(self.todolist.dirty)
def test_dep_name(self):
name = DepCommand.name()
self.assertEqual(name, 'dep')
def test_help(self):
command = DepCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -167,6 +167,11 @@ class DepriCommandTest(CommandTest):
self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n")
def test_depri_name(self):
name = DepriCommand.name()
self.assertEqual(name, 'depri')
def test_help(self):
command = DepriCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -454,6 +454,11 @@ class DoCommandTest(CommandTest):
self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n")
def test_do_name(self):
name = DoCommand.name()
self.assertEqual(name, 'do')
def test_help(self):
command = DoCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -196,6 +196,11 @@ class EditCommandTest(CommandTest):
self.assertEqual(self.todolist.print_todos(), result)
mock_call.assert_called_once_with([editor, todotxt])
def test_edit_name(self):
name = EditCommand.name()
self.assertEqual(name, 'edit')
def test_help(self):
command = EditCommand(["help"], self.todolist, self.out, self.error,
None)
......
......@@ -420,6 +420,11 @@ class ListCommandTest(CommandTest):
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEqual(self.errors, "")
def test_list_name(self):
name = ListCommand.name()
self.assertEqual(name, 'list')
def test_help(self):
command = ListCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -38,6 +38,11 @@ class ListContextCommandTest(CommandTest):
self.assertEqual(self.output, "Context1\nContext2\n")
self.assertFalse(self.errors)
def test_listcontext_name(self):
name = ListContextCommand.name()
self.assertEqual(name, 'listcontext')
def test_help(self):
command = ListContextCommand(["help"], None, self.out, self.error)
command.execute()
......
......@@ -38,6 +38,11 @@ class ListProjectCommandTest(CommandTest):
self.assertEqual(self.output, "Project1\nProject2\n")
self.assertFalse(self.errors)
def test_listproject_name(self):
name = ListProjectCommand.name()
self.assertEqual(name, 'listproject')
def test_help(self):
command = ListProjectCommand(["help"], None, self.out, self.error)
command.execute()
......
......@@ -314,6 +314,11 @@ class PostponeCommandTest(CommandTest):
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_postpone_name(self):
name = PostponeCommand.name()
self.assertEqual(name, 'postpone')
def test_help(self):
command = PostponeCommand(["help"], self.todolist, self.out,
self.error)
......
......@@ -249,6 +249,11 @@ class PriorityCommandTest(CommandTest):
self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n")
def test_priority_name(self):
name = PriorityCommand.name()
self.assertEqual(name, 'priority')
def test_help(self):
command = PriorityCommand(["help"], self.todolist, self.out,
self.error)
......
......@@ -57,7 +57,7 @@ class RevertCommandTest(CommandTest):
self.archive = TodoList([])
def test_revert01(self):
backup = ChangeSet(p_call=['do 1'])
backup = ChangeSet(p_label=['do 1'])
backup.add_todolist(self.todolist)
backup.add_archive(self.archive)
backup.timestamp = '1'
......@@ -318,6 +318,11 @@ class RevertCommandTest(CommandTest):
self.assertEqual(config().backup_count(), 5)
def test_revert_name(self):
name = RevertCommand.name()
self.assertEqual(name, 'revert')
def test_help(self):
command = RevertCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -53,6 +53,11 @@ class SortCommandTest(CommandTest):
self.assertEqual(todo1.source(), todo2.source())
def test_sort_name(self):
name = SortCommand.name()
self.assertEqual(name, 'sort')
def test_help(self):
command = SortCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -288,6 +288,11 @@ class TagCommandTest(CommandTest):
self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n")
def test_tag_name(self):
name = TagCommand.name()
self.assertEqual(name, 'tag')
def test_help(self):
command = TagCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -247,6 +247,20 @@ class TodoListTester(TopydoTest):
self.assertEqual(todo.src, results[i])
i += 1
def test_ids_linenumber(self):
""" Confirms the ids method lists all todo IDs as line-numbers. """
config(p_overrides={('topydo', 'identifiers'): 'linenumber'})
results = {'1', '2', '3', '4', '5'}
self.assertEqual(results, self.todolist.ids())
def test_ids_uids(self):
""" Confirms the ids method lists all todo IDs as text uids. """
config("test/data/todolist-uid.conf")
results = {'n8m', 'mfg', 'z63', 't5c', 'wa5'}
self.assertEqual(results, self.todolist.ids())
class TodoListDependencyTester(TopydoTest):
def setUp(self):
......
......@@ -43,7 +43,7 @@ class RevertCommand(Command):
archive_file.write(archive.print_todos())
last_change.delete()
self.out("Successfully reverted: " + last_change.call)
self.out("Successfully reverted: " + last_change.label)
except (ValueError, KeyError):
self.error('No backup was found for the current state of ' + config().todotxt())
......
......@@ -43,11 +43,11 @@ def get_backup_path():
class ChangeSet(object):
""" Class for operations related with backup management. """
def __init__(self, p_todolist=None, p_archive=None, p_call=[]):
def __init__(self, p_todolist=None, p_archive=None, p_label=[]):
self.todolist = deepcopy(p_todolist)
self.archive = deepcopy(p_archive)
self.timestamp = str(int(time.time()))
self.call = ' '.join(p_call)
self.label = ' '.join(p_label)
try:
self.json_file = open(get_backup_path(), 'r+b')
......@@ -104,7 +104,7 @@ class ChangeSet(object):
except AttributeError:
list_archive = []
self.backup_dict[self.timestamp] = (list_todo, list_archive, self.call)
self.backup_dict[self.timestamp] = (list_todo, list_archive, self.label)
index = self._get_index()
index.insert(0, (self.timestamp, current_hash))
......@@ -161,7 +161,7 @@ class ChangeSet(object):
def get_backup(self, p_todolist):
"""
Retrieves a backup for p_todolist from backup file and sets todolist,
archive and call attributes to appropriate data from it.
archive and label attributes to appropriate data from it.
"""
change_hash = hash_todolist(p_todolist)
......@@ -172,7 +172,7 @@ class ChangeSet(object):
self.todolist = TodoList(d[0])
self.archive = TodoList(d[1])
self.call = d[2]
self.label = d[2]
def apply(self, p_todolist, p_archive):
""" Applies backup on supplied p_todolist. """
......
......@@ -85,6 +85,11 @@ class Command(object):
return result
@classmethod
def name(cls):
"""" Returns short-name of the command. """
return cls.__name__[:-7].lower() # strip 'Command'
def usage(self):
""" Returns a one-line synopsis for this command. """
raise NotImplementedError
......
......@@ -289,4 +289,3 @@ class TodoList(TodoListBase):
self._depgraph.transitively_reduce()
clean_parent_relations()
clean_orphan_relations()
......@@ -280,3 +280,11 @@ class TodoListBase(object):
"""
printer = PrettyPrinter()
return "\n".join([str(s) for s in printer.print_list(self._todos)])
def ids(self):
""" Returns set with all todo IDs. """
if config().identifiers() == 'text':
ids = self._id_todo_map.keys()
else:
ids = [str(i + 1) for i in range(self.count())]
return set(ids)
......@@ -247,12 +247,13 @@ class CLIApplicationBase(object):
READ_ONLY_COMMANDS)
return p_command.__module__.endswith(read_only_commands)
def _backup(self, p_command, p_args):
def _backup(self, p_command, p_args=[], p_label=None):
if config().backup_count() > 0 and p_command and not self.is_read_only(p_command):
call = [p_command.__module__.lower()[16:-7]] + p_args # strip "topydo.commands" and "Command"
call = [p_command.name()]+ p_args
from topydo.lib.ChangeSet import ChangeSet
self.backup = ChangeSet(self.todolist, p_call=call)
label = p_label if p_label else call
self.backup = ChangeSet(self.todolist, p_label=label)
def _execute(self, p_command, p_args):
"""
......
......@@ -38,6 +38,7 @@ from topydo.ui.columns.ConsoleWidget import ConsoleWidget
from topydo.ui.columns.KeystateWidget import KeystateWidget
from topydo.ui.columns.TodoWidget import TodoWidget
from topydo.ui.columns.TodoListWidget import TodoListWidget
from topydo.ui.columns.Transaction import Transaction
from topydo.ui.columns.Utils import PaletteItem, to_urwid_color
from topydo.ui.columns.ViewWidget import ViewWidget
from topydo.ui.columns.ColumnLayout import columns
......@@ -118,7 +119,7 @@ class UIApplication(CLIApplicationBase):
self.todofile = TodoFileWatched(config().todotxt(), callback)
self.todolist = TodoList.TodoList(self.todofile.read())
self.marked_todos = []
self.marked_todos = set()
self.columns = urwid.Columns([], dividechars=0,
min_width=config().column_width())
......@@ -260,6 +261,24 @@ class UIApplication(CLIApplicationBase):
def _output(self, p_text):
self._print_to_console(p_text)
def _check_id_validity(self, p_ids):
"""
Checks if there are any invalid todo IDs in p_ids list.
Returns proper error message if any ID is invalid and None otherwise.
"""
errors = []
valid_ids = self.todolist.ids()
if len(p_ids) == 0:
errors.append('No todo item was selected')
else:
errors = ["Invalid todo ID: {}".format(todo_id)
for todo_id in p_ids - valid_ids]
errors = '\n'.join(errors) if errors else None
return errors
def _execute_handler(self, p_command, p_todo_id=None, p_output=None):
"""
Executes a command, given as a string.
......@@ -269,11 +288,6 @@ class UIApplication(CLIApplicationBase):
self._last_cmd = (p_command, p_output == self._output)
if '{}' in p_command:
if self._has_marked_todos():
p_todo_id = ' '.join(self.marked_todos)
p_command = p_command.format(p_todo_id)
try:
p_command = shlex.split(p_command)
except ValueError as verr:
......@@ -281,26 +295,37 @@ class UIApplication(CLIApplicationBase):
return
try:
(subcommand, args) = get_subcommand(p_command)
subcommand, args = get_subcommand(p_command)
except ConfigError as cerr:
self._print_to_console(
'Error: {}. Check your aliases configuration.'.format(cerr))
return
self._backup(subcommand, args)
env_args = (self.todolist, p_output, self._output, self._input)
ids = None
try:
command = subcommand(
args,
self.todolist,
p_output,
self._output,
self._input,
)
if '{}' in args:
if self._has_marked_todos():
ids = self.marked_todos
else:
ids = {p_todo_id} if p_todo_id else set()
if command.execute() != False:
self._post_execute()
invalid_ids = self._check_id_validity(ids)
if invalid_ids:
self._print_to_console('Error: ' + invalid_ids)
return
transaction = Transaction(subcommand, env_args, ids)
transaction.prepare(args)
label = transaction.label
self._backup(subcommand, p_label=label)
try:
if transaction.execute():
self._post_execute()
else:
self._rollback()
except TypeError:
# TODO: show error message
pass
......@@ -318,6 +343,12 @@ class UIApplication(CLIApplicationBase):
if dirty or self.marked_todos:
self._reset_state()
def _rollback(self):
try:
self.backup.apply(self.todolist, p_archive=None)
except AttributeError:
pass
def _repeat_last_cmd(self, p_todo_id=None):
try:
cmd, verbosity = self._last_cmd
......@@ -330,7 +361,7 @@ class UIApplication(CLIApplicationBase):
def _reset_state(self):
for widget in TodoWidget.cache.values():
widget.unmark()
self.marked_todos = []
self.marked_todos.clear()
self._update_all_columns()
def _blur_commandline(self):
......@@ -600,7 +631,7 @@ class UIApplication(CLIApplicationBase):
False otherwise.
"""
if p_todo_id not in self.marked_todos:
self.marked_todos.append(p_todo_id)
self.marked_todos.add(p_todo_id)
return True
else:
self.marked_todos.remove(p_todo_id)
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2017 Bram Schoenmakers <bram@topydo.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.MultiCommand import MultiCommand
class Transaction(object):
"""
This class implements basic handling of executing any subcommand on multiple
todo items.
"""
def __init__(self, p_subcommand=None, p_env_args=(), p_todo_ids=None):
self._multi = issubclass(p_subcommand, MultiCommand)
self._cmd = lambda op: p_subcommand(op, *p_env_args)
self._todo_ids = p_todo_ids
self._operations = []
self._cmd_name = p_subcommand.name()
self.label = []
def prepare(self, p_args):
"""
Prepares list of operations to execute based on p_args, list of
todo items contained in _todo_ids attribute and _subcommand
attribute.
"""
if self._todo_ids:
id_position = p_args.index('{}')
# Not using MultiCommand abilities would make EditCommand awkward
if self._multi:
p_args[id_position:id_position + 1] = self._todo_ids
self._operations.append(p_args)
else:
for todo_id in self._todo_ids:
operation_args = p_args[:]
operation_args[id_position] = todo_id
self._operations.append(operation_args)
else:
self._operations.append(p_args)
self._create_label()
def _create_label(self):
if len(self._operations) > 1:
for operation in self._operations:
self.label.append(self._cmd_name + ' ' +
' '.join(operation) + ';')
else:
self.label.append(self._cmd_name + ' ' +
' '.join(self._operations[0]))
def execute(self):
"""
Executes each operation from _operations attribute.
"""
last_operation = len(self._operations) - 1
for i, operation in enumerate(self._operations):
command = self._cmd(operation)
if command.execute() is False:
return False
elif i == last_operation:
return True
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