Commit 8d5f43ba authored by Bram Schoenmakers's avatar Bram Schoenmakers

Better handling of invalid dates

The postpone subcommand would crash when postponing a todo item with an
invalid due date (e.g. due:2017-06-31).

When postponing, the due date serves as an offset. With an invalid due
date, no proper offset can be found. Instead of (silently) falling back
to today's due date, show an error instead.

Moreover, invalid creation/completion dates are also ignored.
parent 6627fb99
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <bram@topydo.org> # Copyright (C) 2015 - 2015 Bram Schoenmakers <bram@topydo.org>
# #
# 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
...@@ -37,7 +37,9 @@ class PostponeCommandTest(CommandTest): ...@@ -37,7 +37,9 @@ class PostponeCommandTest(CommandTest):
"Baz due:{} t:{}".format(self.today.isoformat(), self.start.isoformat()), "Baz due:{} t:{}".format(self.today.isoformat(), self.start.isoformat()),
"Past due:{}".format(self.past.isoformat()), "Past due:{}".format(self.past.isoformat()),
"Future due:{} t:{}".format(self.future.isoformat(), self.future_start.isoformat()), "Future due:{} t:{}".format(self.future.isoformat(), self.future_start.isoformat()),
"FutureStart t:{}".format(self.future.isoformat()) "FutureStart t:{}".format(self.future.isoformat()),
"InvalidDueDate due:2017-06-31",
"InvalidStartDate t:2017-06-31",
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
...@@ -76,7 +78,7 @@ class PostponeCommandTest(CommandTest): ...@@ -76,7 +78,7 @@ class PostponeCommandTest(CommandTest):
self.assertTrue(self.todolist.dirty) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 2| Bar due:{}\n".format(due.isoformat())) "| 2| Bar due:{}\n".format(due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "Warning: todo item has no (valid) start date, therefore it was not adjusted.\n")
def test_postpone04(self): def test_postpone04(self):
command = PostponeCommand(["3", "1w"], self.todolist, self.out, command = PostponeCommand(["3", "1w"], self.todolist, self.out,
...@@ -219,7 +221,7 @@ class PostponeCommandTest(CommandTest): ...@@ -219,7 +221,7 @@ class PostponeCommandTest(CommandTest):
self.assertTrue(self.todolist.dirty) self.assertTrue(self.todolist.dirty)
# pylint: disable=E1103 # pylint: disable=E1103
self.assertEqual(self.output, "| 2| Bar due:{}\n| 3| Baz due:{} t:{}\n".format(due.isoformat(), due.isoformat(), start.isoformat())) self.assertEqual(self.output, "| 2| Bar due:{}\n| 3| Baz due:{} t:{}\n".format(due.isoformat(), due.isoformat(), start.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "Warning: todo item has no (valid) start date, therefore it was not adjusted.\n")
def test_postpone17(self): def test_postpone17(self):
command = PostponeCommand(["1", "2", "3"], self.todolist, self.out, command = PostponeCommand(["1", "2", "3"], self.todolist, self.out,
...@@ -259,6 +261,39 @@ class PostponeCommandTest(CommandTest): ...@@ -259,6 +261,39 @@ class PostponeCommandTest(CommandTest):
self.assertEqual(self.errors, self.assertEqual(self.errors,
u"Invalid todo number given: Fo\u00d3B\u0105r.\n") u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
def test_postpone21(self):
"""
Show an error when a todo item has an invalid due date.
"""
command = PostponeCommand(["7", "1d"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Postponing todo item failed: invalid due date.\n")
def test_postpone22(self):
"""
Todo item has an invalid start date.
"""
command = PostponeCommand(["8", "1d"], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 8| InvalidStartDate t:2017-06-31 due:{}\n".format(self.future.isoformat()))
self.assertEqual(self.errors, "")
def test_postpone23(self):
"""
Todo item has an invalid start date.
"""
command = PostponeCommand(["-s", "8", "1d"], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 8| InvalidStartDate t:2017-06-31 due:{}\n".format(self.future.isoformat()))
self.assertEqual(self.errors, "Warning: todo item has no (valid) start date, therefore it was not adjusted.\n")
def test_expr_postpone1(self): def test_expr_postpone1(self):
command = PostponeCommand(["-e", "due:tod", "2w"], self.todolist, command = PostponeCommand(["-e", "due:tod", "2w"], self.todolist,
self.out, self.error, None) self.out, self.error, None)
......
...@@ -114,5 +114,13 @@ class TodoTest(TopydoTest): ...@@ -114,5 +114,13 @@ class TodoTest(TopydoTest):
todo = Todo("(C) 2015-11-18 Foo due:2015-11-16)") todo = Todo("(C) 2015-11-18 Foo due:2015-11-16)")
self.assertEqual(todo.length(), 0) self.assertEqual(todo.length(), 0)
def test_length10(self):
todo = Todo("(C) Foo t:2017-06-30")
self.assertEqual(todo.length(), 0)
def test_length11(self):
todo = Todo("(C) Foo t:2017-06-31 due:2017-07-01")
self.assertEqual(todo.length(), 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -240,6 +240,16 @@ class TodoBaseTester(TopydoTest): ...@@ -240,6 +240,16 @@ class TodoBaseTester(TopydoTest):
self.assertFalse(todo.is_completed()) self.assertFalse(todo.is_completed())
def test_completion5(self):
"""
A todo item with an invalid completion date is still considered as
completed, but without a creation date.
"""
todo = TodoBase("x 2017-06-31 Invalid date")
self.assertTrue(todo.is_completed())
self.assertIsNone(todo.completion_date())
def test_set_complete1(self): def test_set_complete1(self):
todo = TodoBase("(A) Foo") todo = TodoBase("(A) Foo")
todo.set_completed() todo.set_completed()
...@@ -350,5 +360,21 @@ class TodoBaseTester(TopydoTest): ...@@ -350,5 +360,21 @@ class TodoBaseTester(TopydoTest):
self.assertEqual(todo.creation_date(), creation_date) self.assertEqual(todo.creation_date(), creation_date)
self.assertEqual(todo.src, "x 2014-07-25 2014-07-24 Foo") self.assertEqual(todo.src, "x 2014-07-25 2014-07-24 Foo")
def test_set_creation_date6(self):
"""
A todo item with an invalid creation date is not considered to have
one.
"""
todo = TodoBase("2017-06-31 Invalid")
self.assertIsNone(todo.creation_date())
def test_set_creation_date7(self):
"""
A todo item with an invalid creation date is not considered to have
one.
"""
todo = TodoBase("x 2017-07-01 2017-06-31 Invalid")
self.assertIsNone(todo.creation_date())
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -45,6 +45,7 @@ class PostponeCommand(MultiCommand): ...@@ -45,6 +45,7 @@ class PostponeCommand(MultiCommand):
def _get_offset(p_todo): def _get_offset(p_todo):
offset = p_todo.tag_value( offset = p_todo.tag_value(
config().tag_due(), date.today().isoformat()) config().tag_due(), date.today().isoformat())
offset_date = date_string_to_date(offset) offset_date = date_string_to_date(offset)
if offset_date < date.today(): if offset_date < date.today():
...@@ -56,15 +57,22 @@ class PostponeCommand(MultiCommand): ...@@ -56,15 +57,22 @@ class PostponeCommand(MultiCommand):
self.printer.add_filter(PrettyPrinterNumbers(self.todolist)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
for todo in self.todos: for todo in self.todos:
try:
offset = _get_offset(todo) offset = _get_offset(todo)
except ValueError:
self.error("Postponing todo item failed: invalid due date.")
break
new_due = relative_date_to_date(pattern, offset) new_due = relative_date_to_date(pattern, offset)
if new_due: if new_due:
if self.move_start_date and todo.has_tag(config().tag_start()): if self.move_start_date and todo.start_date():
length = todo.length() length = todo.length()
new_start = new_due - timedelta(length) new_start = new_due - timedelta(length)
# pylint: disable=E1103 # pylint: disable=E1103
todo.set_tag(config().tag_start(), new_start.isoformat()) todo.set_tag(config().tag_start(), new_start.isoformat())
elif self.move_start_date and not todo.start_date():
self.error("Warning: todo item has no (valid) start date, therefore it was not adjusted.")
# pylint: disable=E1103 # pylint: disable=E1103
todo.set_tag(config().tag_due(), new_due.isoformat()) todo.set_tag(config().tag_due(), new_due.isoformat())
......
...@@ -69,17 +69,28 @@ def parse_line(p_string): ...@@ -69,17 +69,28 @@ def parse_line(p_string):
result['completed'] = True result['completed'] = True
completion_date = completed_head.group('completionDate') completion_date = completed_head.group('completionDate')
try:
result['completionDate'] = date_string_to_date(completion_date) result['completionDate'] = date_string_to_date(completion_date)
except ValueError:
pass
creation_date = completed_head.group('creationDate') creation_date = completed_head.group('creationDate')
try:
result['creationDate'] = date_string_to_date(creation_date) result['creationDate'] = date_string_to_date(creation_date)
except ValueError:
pass
rest = completed_head.group('rest') rest = completed_head.group('rest')
elif normal_head: elif normal_head:
result['priority'] = normal_head.group('priority') result['priority'] = normal_head.group('priority')
creation_date = normal_head.group('creationDate') creation_date = normal_head.group('creationDate')
try:
result['creationDate'] = date_string_to_date(creation_date) result['creationDate'] = date_string_to_date(creation_date)
except ValueError:
pass
rest = normal_head.group('rest') rest = normal_head.group('rest')
......
...@@ -27,8 +27,8 @@ from datetime import date ...@@ -27,8 +27,8 @@ from datetime import date
def date_string_to_date(p_date): def date_string_to_date(p_date):
""" """
Given a date in YYYY-MM-DD, returns a Python date object. Returns None Given a date in YYYY-MM-DD, returns a Python date object. Throws a
if the date is invalid. ValueError if the date is invalid.
""" """
result = None result = None
......
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