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.
# 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
# it under the terms of the GNU General Public License as published by
......@@ -37,7 +37,9 @@ class PostponeCommandTest(CommandTest):
"Baz due:{} t:{}".format(self.today.isoformat(), self.start.isoformat()),
"Past due:{}".format(self.past.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)
......@@ -76,7 +78,7 @@ class PostponeCommandTest(CommandTest):
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output,
"| 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):
command = PostponeCommand(["3", "1w"], self.todolist, self.out,
......@@ -219,7 +221,7 @@ class PostponeCommandTest(CommandTest):
self.assertTrue(self.todolist.dirty)
# 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.errors, "")
self.assertEqual(self.errors, "Warning: todo item has no (valid) start date, therefore it was not adjusted.\n")
def test_postpone17(self):
command = PostponeCommand(["1", "2", "3"], self.todolist, self.out,
......@@ -259,6 +261,39 @@ class PostponeCommandTest(CommandTest):
self.assertEqual(self.errors,
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):
command = PostponeCommand(["-e", "due:tod", "2w"], self.todolist,
self.out, self.error, None)
......
......@@ -114,5 +114,13 @@ class TodoTest(TopydoTest):
todo = Todo("(C) 2015-11-18 Foo due:2015-11-16)")
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__':
unittest.main()
......@@ -240,6 +240,16 @@ class TodoBaseTester(TopydoTest):
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):
todo = TodoBase("(A) Foo")
todo.set_completed()
......@@ -350,5 +360,21 @@ class TodoBaseTester(TopydoTest):
self.assertEqual(todo.creation_date(), creation_date)
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__':
unittest.main()
......@@ -45,6 +45,7 @@ class PostponeCommand(MultiCommand):
def _get_offset(p_todo):
offset = p_todo.tag_value(
config().tag_due(), date.today().isoformat())
offset_date = date_string_to_date(offset)
if offset_date < date.today():
......@@ -56,15 +57,22 @@ class PostponeCommand(MultiCommand):
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
for todo in self.todos:
offset = _get_offset(todo)
try:
offset = _get_offset(todo)
except ValueError:
self.error("Postponing todo item failed: invalid due date.")
break
new_due = relative_date_to_date(pattern, offset)
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()
new_start = new_due - timedelta(length)
# pylint: disable=E1103
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
todo.set_tag(config().tag_due(), new_due.isoformat())
......
......@@ -69,17 +69,28 @@ def parse_line(p_string):
result['completed'] = True
completion_date = completed_head.group('completionDate')
result['completionDate'] = date_string_to_date(completion_date)
try:
result['completionDate'] = date_string_to_date(completion_date)
except ValueError:
pass
creation_date = completed_head.group('creationDate')
result['creationDate'] = date_string_to_date(creation_date)
try:
result['creationDate'] = date_string_to_date(creation_date)
except ValueError:
pass
rest = completed_head.group('rest')
elif normal_head:
result['priority'] = normal_head.group('priority')
creation_date = normal_head.group('creationDate')
result['creationDate'] = date_string_to_date(creation_date)
try:
result['creationDate'] = date_string_to_date(creation_date)
except ValueError:
pass
rest = normal_head.group('rest')
......
......@@ -27,8 +27,8 @@ from datetime import date
def date_string_to_date(p_date):
"""
Given a date in YYYY-MM-DD, returns a Python date object. Returns None
if the date is invalid.
Given a date in YYYY-MM-DD, returns a Python date object. Throws a
ValueError if the date is invalid.
"""
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