X-Git-Url: https://plomlompom.com/repos/berlin_corona.txt?a=blobdiff_plain;ds=inline;f=tests%2Ftodos.py;h=b73f5d7fcb7b0a2c53256593e8e9867e86d2b89e;hb=HEAD;hp=1a9eab61c0f77932c11b24ebae15cfc9a982287b;hpb=3b15110c22c17d938d182a3d1a37b81b875c397f;p=plomtask diff --git a/tests/todos.py b/tests/todos.py index 1a9eab6..25fa05b 100644 --- a/tests/todos.py +++ b/tests/todos.py @@ -1,5 +1,6 @@ """Test Todos module.""" -from tests.utils import TestCaseWithDB, TestCaseWithServer +from typing import Any +from tests.utils import TestCaseSansDB, TestCaseWithDB, TestCaseWithServer from plomtask.todos import Todo, TodoNode from plomtask.processes import Process, ProcessStep from plomtask.conditions import Condition @@ -7,8 +8,12 @@ from plomtask.exceptions import (NotFoundException, BadFormatException, HandledException) -class TestsWithDB(TestCaseWithDB): - """Tests requiring DB, but not server setup.""" +class TestsWithDB(TestCaseWithDB, TestCaseSansDB): + """Tests requiring DB, but not server setup. + + NB: We subclass TestCaseSansDB too, to run any tests there that due to any + Todo requiring a _saved_ Process wouldn't run without a DB. + """ checked_class = Todo default_init_kwargs = {'process': None, 'is_done': False, 'date': '2024-01-01'} @@ -33,9 +38,9 @@ class TestsWithDB(TestCaseWithDB): process.save(self.db_conn) assert isinstance(self.cond1.id_, int) assert isinstance(self.cond2.id_, int) - process.set_conditions(self.db_conn, [self.cond1.id_, self.cond2.id_]) - process.set_enables(self.db_conn, [self.cond1.id_]) - process.set_disables(self.db_conn, [self.cond2.id_]) + process.set_condition_relations(self.db_conn, + [self.cond1.id_, self.cond2.id_], [], + [self.cond1.id_], [self.cond2.id_]) todo_no_id = Todo(None, process, False, self.date1) self.assertEqual(todo_no_id.conditions, [self.cond1, self.cond2]) self.assertEqual(todo_no_id.enables, [self.cond1]) @@ -45,16 +50,6 @@ class TestsWithDB(TestCaseWithDB): self.assertEqual(todo_yes_id.enables, []) self.assertEqual(todo_yes_id.disables, []) - def test_Todo_by_id(self) -> None: - """Test findability of Todos.""" - todo = Todo(1, self.proc, False, self.date1) - todo.save(self.db_conn) - self.assertEqual(Todo.by_id(self.db_conn, 1), todo) - with self.assertRaises(NotFoundException): - Todo.by_id(self.db_conn, 0) - with self.assertRaises(NotFoundException): - Todo.by_id(self.db_conn, 2) - def test_Todo_by_date(self) -> None: """Test findability of Todos by date.""" t1 = Todo(None, self.proc, False, self.date1) @@ -66,14 +61,18 @@ class TestsWithDB(TestCaseWithDB): with self.assertRaises(BadFormatException): self.assertEqual(Todo.by_date(self.db_conn, 'foo'), []) + def test_Todo_by_date_range_with_limits(self) -> None: + """Test .by_date_range_with_limits.""" + self.check_by_date_range_with_limits('day') + def test_Todo_on_conditions(self) -> None: """Test effect of Todos on Conditions.""" assert isinstance(self.cond1.id_, int) assert isinstance(self.cond2.id_, int) todo = Todo(None, self.proc, False, self.date1) todo.save(self.db_conn) - todo.set_enables(self.db_conn, [self.cond1.id_]) - todo.set_disables(self.db_conn, [self.cond2.id_]) + todo.set_condition_relations(self.db_conn, [], [], + [self.cond1.id_], [self.cond2.id_]) todo.is_done = True self.assertEqual(self.cond1.is_active, True) self.assertEqual(self.cond2.is_active, False) @@ -114,7 +113,8 @@ class TestsWithDB(TestCaseWithDB): todo_1.is_done = True todo_2.is_done = True todo_2.is_done = False - todo_2.set_conditions(self.db_conn, [self.cond1.id_]) + todo_2.set_condition_relations( + self.db_conn, [self.cond1.id_], [], [], []) with self.assertRaises(BadFormatException): todo_2.is_done = True self.cond1.is_active = True @@ -122,15 +122,24 @@ class TestsWithDB(TestCaseWithDB): def test_Todo_step_tree(self) -> None: """Test self-configuration of TodoStepsNode tree for Day view.""" + + def todo_node_as_dict(node: TodoNode) -> dict[str, object]: + return {'todo': node.todo.id_, 'seen': node.seen, + 'children': [todo_node_as_dict(c) for c in node.children]} + todo_1 = Todo(None, self.proc, False, self.date1) todo_1.save(self.db_conn) assert isinstance(todo_1.id_, int) # test minimum node_0 = TodoNode(todo_1, False, []) - self.assertEqual(todo_1.get_step_tree(set()), node_0) + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) # test non_emtpy seen_todo does something node_0.seen = True - self.assertEqual(todo_1.get_step_tree({todo_1.id_}), node_0) + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree({todo_1.id_})) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) # test child shows up todo_2 = Todo(None, self.proc, False, self.date1) todo_2.save(self.db_conn) @@ -139,7 +148,9 @@ class TestsWithDB(TestCaseWithDB): node_2 = TodoNode(todo_2, False, []) node_0.children = [node_2] node_0.seen = False - self.assertEqual(todo_1.get_step_tree(set()), node_0) + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) # test child shows up with child todo_3 = Todo(None, self.proc, False, self.date1) todo_3.save(self.db_conn) @@ -147,15 +158,19 @@ class TestsWithDB(TestCaseWithDB): todo_2.add_child(todo_3) node_3 = TodoNode(todo_3, False, []) node_2.children = [node_3] - self.assertEqual(todo_1.get_step_tree(set()), node_0) + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) # test same todo can be child-ed multiple times at different locations todo_1.add_child(todo_3) node_4 = TodoNode(todo_3, True, []) node_0.children += [node_4] - self.assertEqual(todo_1.get_step_tree(set()), node_0) + cmp_0_dict = todo_node_as_dict(todo_1.get_step_tree(set())) + cmp_1_dict = todo_node_as_dict(node_0) + self.assertEqual(cmp_0_dict, cmp_1_dict) def test_Todo_create_with_children(self) -> None: - """Test parenthood guaranteeds of Todo.create_with_children.""" + """Test parenthood guarantees of Todo.create_with_children.""" assert isinstance(self.proc.id_, int) proc2 = Process(None) proc2.save(self.db_conn) @@ -187,140 +202,431 @@ class TestsWithDB(TestCaseWithDB): todo_2 = Todo.create_with_children(self.db_conn, proc2.id_, self.date1) self.assertEqual(3, len(todo_2.children)) self.assertEqual(todo_1, todo_2.children[0]) - self.assertEqual(self.proc, todo_2.children[1].process) - self.assertEqual(proc3, todo_2.children[2].process) - todo_3 = todo_2.children[2] + self.assertEqual(self.proc, todo_2.children[2].process) + self.assertEqual(proc3, todo_2.children[1].process) + todo_3 = todo_2.children[1] self.assertEqual(len(todo_3.children), 1) self.assertEqual(todo_3.children[0].process, proc4) - def test_Todo_singularity(self) -> None: - """Test pointers made for single object keep pointing to it.""" - self.check_singularity('is_done', True, self.proc, False, self.date1) - def test_Todo_remove(self) -> None: - """Test removal.""" - todo_1 = Todo(None, self.proc, False, self.date1) - todo_1.save(self.db_conn) - todo_0 = Todo(None, self.proc, False, self.date1) - todo_0.save(self.db_conn) - todo_0.add_child(todo_1) - todo_2 = Todo(None, self.proc, False, self.date1) - todo_2.save(self.db_conn) - todo_1.add_child(todo_2) - todo_1.remove(self.db_conn) - with self.assertRaises(NotFoundException): - Todo.by_id(self.db_conn, todo_1.id_) - self.assertEqual(todo_0.children, []) - self.assertEqual(todo_2.parents, []) - todo_2.comment = 'foo' - with self.assertRaises(HandledException): - todo_2.remove(self.db_conn) - todo_2.comment = '' - todo_2.effort = 5 - with self.assertRaises(HandledException): - todo_2.remove(self.db_conn) +class TestsWithServer(TestCaseWithServer): + """Tests against our HTTP server/handler (and database).""" - def test_Todo_autoremoval(self) -> None: - """"Test automatic removal for Todo.effort < 0.""" - todo_1 = Todo(None, self.proc, False, self.date1) - todo_1.save(self.db_conn) - todo_1.comment = 'foo' - todo_1.effort = -0.1 - todo_1.save(self.db_conn) - Todo.by_id(self.db_conn, todo_1.id_) - todo_1.comment = '' - todo_1.save(self.db_conn) - with self.assertRaises(NotFoundException): - Todo.by_id(self.db_conn, todo_1.id_) + def setUp(self) -> None: + super().setUp() + self._proc1_form_data: Any = self.post_process(1) + self._date = '2024-01-01' + @classmethod + def GET_todo_dict(cls, + target_id: int, + todos: list[dict[str, object]], + processes: list[dict[str, object]], + process_steps: list[dict[str, object]] | None = None, + conditions: list[dict[str, object]] | None = None + ) -> dict[str, object]: + """Return JSON of GET /todo to expect.""" + # pylint: disable=too-many-arguments + library = {'Todo': cls.as_refs(todos), + 'Process': cls.as_refs(processes)} + if process_steps: + library['ProcessStep'] = cls.as_refs(process_steps) + conditions = conditions if conditions else [] + if conditions: + library['Condition'] = cls.as_refs(conditions) + return {'todo': target_id, + 'steps_todo_to_process': [], + 'adoption_candidates_for': {}, + 'process_candidates': [p['id'] for p in processes], + 'todo_candidates': [t['id'] for t in todos + if t['id'] != target_id], + 'condition_candidates': [c['id'] for c in conditions], + '_library': library} -class TestsWithServer(TestCaseWithServer): - """Tests against our HTTP server/handler (and database).""" + @staticmethod + def _step_as_dict(node_id: int, + children: list[dict[str, object]], + process: int | None = None, + todo: int | None = None, + fillable: bool = False, + ) -> dict[str, object]: + return {'node_id': node_id, + 'children': children, + 'process': process, + 'fillable': fillable, + 'todo': todo} - def test_do_POST_day(self) -> None: - """Test Todo posting of POST /day.""" - self.post_process() - self.post_process(2) - proc = Process.by_id(self.db_conn, 1) - proc2 = Process.by_id(self.db_conn, 2) - form_data = {'day_comment': '', 'make_type': 'full'} - self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) - self.assertEqual(Todo.by_date(self.db_conn, '2024-01-01'), []) - form_data['new_todo'] = str(proc.id_) - self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) - todos = Todo.by_date(self.db_conn, '2024-01-01') - self.assertEqual(1, len(todos)) - todo1 = todos[0] - self.assertEqual(todo1.id_, 1) - self.assertEqual(todo1.process.id_, proc.id_) - self.assertEqual(todo1.is_done, False) - form_data['new_todo'] = str(proc2.id_) - self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) - todos = Todo.by_date(self.db_conn, '2024-01-01') - todo1 = todos[1] - self.assertEqual(todo1.id_, 2) - self.assertEqual(todo1.process.id_, proc2.id_) - self.assertEqual(todo1.is_done, False) - - def test_do_POST_todo(self) -> None: - """Test POST /todo.""" - def post_and_reload(form_data: dict[str, object], status: int = 302, - redir_url: str = '/todo?id=1') -> Todo: - self.check_post(form_data, '/todo?id=1', status, redir_url) - return Todo.by_date(self.db_conn, '2024-01-01')[0] - # test minimum - self.post_process() - self.check_post({'day_comment': '', 'new_todo': 1, - 'make_type': 'full'}, - '/day?date=2024-01-01&make_type=full', 302) - # test posting to bad URLs - self.check_post({}, '/todo=', 404) - self.check_post({}, '/todo?id=', 400) + def _make_todo_via_day_post(self, proc_id: int) -> None: + payload = {'day_comment': '', + 'new_todo': proc_id, + 'make_type': 'empty'} + self.check_post(payload, f'/day?date={self._date}&make_type=empty') + + def test_basic_fail_POST_todo(self) -> None: + """Test basic malformed/illegal POST /todo requests.""" + # test we cannot just POST into non-existing Todo + self.check_post({}, '/todo', 404) self.check_post({}, '/todo?id=FOO', 400) self.check_post({}, '/todo?id=0', 404) - # test posting naked entity - todo1 = post_and_reload({}) - self.assertEqual(todo1.children, []) - self.assertEqual(todo1.parents, []) - self.assertEqual(todo1.is_done, False) - # test posting doneness - todo1 = post_and_reload({'done': ''}) - self.assertEqual(todo1.is_done, True) - # test implicitly posting non-doneness - todo1 = post_and_reload({}) - self.assertEqual(todo1.is_done, False) - # test malformed adoptions - self.check_post({'adopt': 'foo'}, '/todo?id=1', 400) + self.check_post({}, '/todo?id=1', 404) + # test malformed values on existing Todo + self._make_todo_via_day_post(1) + for name in [ + 'adopt', 'effort', 'make_full', 'make_empty', 'step_filler', + 'conditions', 'disables', 'blockers', 'enables']: + self.check_post({name: 'x'}, '/todo?id=1', 400, '/todo') + for prefix in ['make_empty_', 'make_full_']: + for suffix in ['', 'x', '1.1']: + self.check_post({'step_filler': f'{prefix}{suffix}'}, + '/todo?id=1', 400, '/todo') + + def test_basic_POST_todo(self) -> None: + """Test basic POST /todo manipulations.""" + self._make_todo_via_day_post(1) + # test posting naked entity at first changes nothing + todo_dict = self.todo_as_dict(1, 1) + proc_dict = self.proc_as_dict(**self._proc1_form_data) + expected = self.GET_todo_dict(1, [todo_dict], [proc_dict]) + self.check_json_get('/todo?id=1', expected) + self.check_post({}, '/todo?id=1') + self.check_json_get('/todo?id=1', expected) + # test posting doneness, comment, calendarization, effort + todo_post = {'done': '', 'calendarize': '', 'comment': 'foo', + 'effort': 2.3} + todo_dict = self.todo_as_dict(1, 1, is_done=True, calendarize=True, + comment='foo', effort=2.3) + expected = self.GET_todo_dict(1, [todo_dict], [proc_dict]) + self.check_post(todo_post, '/todo?id=1') + self.check_json_get('/todo?id=1', expected) + # test implicitly un-setting all of those except effort by empty post + self.check_post({}, '/todo?id=1') + todo_dict = self.todo_as_dict(1, 1, effort=2.3) + expected = self.GET_todo_dict(1, [todo_dict], [proc_dict]) + self.check_json_get('/todo?id=1', expected) + # test empty effort post can be explicitly unset by "" post + self.check_post({'effort': ''}, '/todo?id=1') + todo_dict['effort'] = None + self.check_json_get('/todo?id=1', expected) + # test Condition posts + c1_post = {'title': 'foo', 'description': 'oof', 'is_active': False} + c2_post = {'title': 'bar', 'description': 'rab', 'is_active': True} + self.check_post(c1_post, '/condition', redir='/condition?id=1') + self.check_post(c2_post, '/condition', redir='/condition?id=2') + c1_dict = self.cond_as_dict(1, False, ['foo'], ['oof']) + c2_dict = self.cond_as_dict(2, True, ['bar'], ['rab']) + conditions = [c1_dict, c2_dict] + todo_post = {'conditions': [1], 'disables': [1], + 'blockers': [2], 'enables': [2]} + for k, v in todo_post.items(): + todo_dict[k] = v + self.check_post(todo_post, '/todo?id=1') + expected = self.GET_todo_dict(1, [todo_dict], [proc_dict], + conditions=conditions) + self.check_json_get('/todo?id=1', expected) + + def test_POST_todo_deletion(self) -> None: + """Test deletions via POST /todo.""" + self._make_todo_via_day_post(1) + todo_dict = self.todo_as_dict(1, process_id=1) + proc_dict = self.proc_as_dict(**self._proc1_form_data) + expected = self.GET_todo_dict(1, [todo_dict], [proc_dict]) + # test failure of deletion on non-existing Todo + self.check_post({'delete': ''}, '/todo?id=2', 404, '/') + # test deletion of existing Todo + self.check_post({'delete': ''}, '/todo?id=1', 302, '/') + self.check_get('/todo?id=1', 404) + # test deletion of adopted Todo + self._make_todo_via_day_post(1) + self._make_todo_via_day_post(1) + self.check_post({'adopt': 2}, '/todo?id=1') + self.check_post({'delete': ''}, '/todo?id=2', 302, '/') + self.check_json_get('/todo?id=1', expected) + # test deletion of adopting Todo + self._make_todo_via_day_post(1) + self.check_post({'adopt': 2}, '/todo?id=1') + self.check_post({'delete': ''}, '/todo?id=1', 302, '/') + todo_dict['id'] = 2 + expected = self.GET_todo_dict(2, [todo_dict], [proc_dict]) + self.check_json_get('/todo?id=2', expected) + # test cannot delete Todo with comment or effort + self.check_post({'comment': 'foo'}, '/todo?id=2') + self.check_post({'delete': ''}, '/todo?id=2', 500, '/') + self.check_post({'effort': 5}, '/todo?id=2') + self.check_post({'delete': ''}, '/todo?id=2', 500, '/') + # test deletion via effort < 0, but only once deletable + self.check_post({'effort': -1, 'comment': 'foo'}, '/todo?id=2') + todo_dict['comment'] = 'foo' + todo_dict['effort'] = -1 + self.check_json_get('/todo?id=2', expected) + self.check_post({}, '/todo?id=2') + self.check_get('/todo?id=2', 404) + + def test_POST_todo_adoption(self) -> None: + """Test adoption via POST /todo with "adopt".""" + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements + # post two Todos to Day, have first adopt second + self._make_todo_via_day_post(1) + self._make_todo_via_day_post(1) + proc1_dict = self.proc_as_dict(**self._proc1_form_data) + todo1_dict = self.todo_as_dict(1, process_id=1, children=[2]) + todo2_dict = self.todo_as_dict(2, process_id=1, parents=[1]) + todos = [todo1_dict, todo2_dict] + expected = self.GET_todo_dict(1, todos, [proc1_dict]) + expected['steps_todo_to_process'] = [self._step_as_dict(1, [], todo=2)] + self.check_post({'adopt': 2}, '/todo?id=1') + self.check_json_get('/todo?id=1', expected) + # test Todo un-adopting by just not sending an adopt + self.check_post({}, '/todo?id=1') + todo1_dict['children'] = [] + todo2_dict['parents'] = [] + expected['steps_todo_to_process'] = [] + self.check_json_get('/todo?id=1', expected) + # test fail on trying to adopt non-existing Todo + self.check_post({'adopt': 3}, '/todo?id=1', 404) + # test cannot self-adopt self.check_post({'adopt': 1}, '/todo?id=1', 400) - self.check_post({'adopt': 2}, '/todo?id=1', 404) - # test posting second todo of same process - self.check_post({'day_comment': '', 'new_todo': 1, - 'make_type': 'full'}, - '/day?date=2024-01-01&make_type=full', 302) - # test todo 1 adopting todo 2 - todo1 = post_and_reload({'adopt': 2}) - todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1] - self.assertEqual(todo1.children, [todo2]) - self.assertEqual(todo1.parents, []) - self.assertEqual(todo2.children, []) - self.assertEqual(todo2.parents, [todo1]) - # test todo1 cannot be set done with todo2 not done yet - todo1 = post_and_reload({'done': '', 'adopt': 2}, 400) - self.assertEqual(todo1.is_done, False) - # test todo1 un-adopting todo 2 by just not sending an adopt - todo1 = post_and_reload({}, 302) - todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1] - self.assertEqual(todo1.children, []) - self.assertEqual(todo1.parents, []) - self.assertEqual(todo2.children, []) - self.assertEqual(todo2.parents, []) - # test todo1 deletion - todo1 = post_and_reload({'delete': ''}, 302, '/') + # test cannot do 1-step circular adoption + self.check_post({'adopt': 1}, '/todo?id=2') + todo1_dict['parents'] = [2] + todo2_dict['children'] = [1] + self.check_post({'adopt': 2}, '/todo?id=1', 400) + # test cannot do 2-step circular adoption + self._make_todo_via_day_post(1) + self.check_post({'adopt': 2}, '/todo?id=3') + todo3_dict = self.todo_as_dict(3, process_id=1, children=[2]) + todo2_dict['parents'] = [3] + todos += [todo3_dict] + self.check_post({'adopt': 3}, '/todo?id=1', 400) + # test can adopt Todo into ProcessStep chain via its Process (with key + # 'step_filler' equivalent to single-element 'adopt' if intable) + proc_post = {'title': 'A', 'description': '', 'effort': 1.0} + self.post_process(3, proc_post) + self.post_process(2, proc_post) + self.post_process(1, self._proc1_form_data | {'new_top_step': [2, 3]}) + self._make_todo_via_day_post(2) + self._make_todo_via_day_post(3) + self.check_post({'step_filler': 5, 'adopt': [4]}, '/todo?id=1') + proc3_dict = self.proc_as_dict(3) + proc2_dict = self.proc_as_dict(2) + proc1_dict['explicit_steps'] = [1, 2] + procs = [proc1_dict, proc2_dict, proc3_dict] + procsteps = [self.procstep_as_dict(1, 1, 2), + self.procstep_as_dict(2, 1, 3)] + todo1_dict['children'] = [4, 5] + todo4_dict = self.todo_as_dict(4, process_id=2, parents=[1]) + todo5_dict = self.todo_as_dict(5, process_id=3, parents=[1]) + todos += [todo4_dict, todo5_dict] + expected = self.GET_todo_dict(1, todos, procs, procsteps) + step_proc2 = self._step_as_dict(1, [], 2, 4, True) + step_proc3 = self._step_as_dict(2, [], 3, 5, True) + expected['steps_todo_to_process'] = [step_proc2, step_proc3] + self.check_json_get('/todo?id=1', expected) + # test 'ignore' values for 'step_filler' are ignored, and intable + # 'step_filler' values are interchangeable with those of 'adopt' + todo_post = {'adopt': 5, 'step_filler': ['ignore', 4]} + self.check_post(todo_post, '/todo?id=1') + self.check_json_get('/todo?id=1', expected) + # test cannot adopt into non-top-level elements of chain + self.post_process(4, proc_post) + self.post_process(3, proc_post | {'new_top_step': 4, 'step_of': [1]}) + proc4_dict = self.proc_as_dict(4) + proc3_dict['explicit_steps'] = [3] + procs += [proc4_dict] + procsteps += [self.procstep_as_dict(3, 3, 4)] + step_proc4 = self._step_as_dict(3, [], 4, None, True) + step_proc3['children'] = [step_proc4] + self._make_todo_via_day_post(4) + self.check_post({'adopt': [4, 5, 6]}, '/todo?id=1') + todo6_dict = self.todo_as_dict(6, process_id=4, parents=[1]) + todo1_dict['children'] = [4, 5, 6] + todos += [todo6_dict] + expected = self.GET_todo_dict(1, todos, procs, procsteps) + step2_proc4 = self._step_as_dict(4, [], None, 6, False) + expected['steps_todo_to_process'] = [step_proc2, step_proc3, + step2_proc4] + expected['adoption_candidates_for'] = {'4': [6]} + self.check_json_get('/todo?id=1', expected) + + def test_POST_todo_make_full(self) -> None: + """Test creation and adoption via POST /todo with "make_full".""" + # pylint: disable=too-many-locals + # create chain of Processes + proc_post = {'title': 'A', 'description': '', 'effort': 1.0} + self.post_process(2, proc_post | {'new_top_step': 1}) + self.post_process(3, proc_post | {'new_top_step': 2}) + self.post_process(4, proc_post | {'new_top_step': 3}) + proc1_dict = self.proc_as_dict(**self._proc1_form_data) + proc2_dict = self.proc_as_dict(2, explicit_steps=[1]) + proc3_dict = self.proc_as_dict(3, explicit_steps=[2]) + proc4_dict = self.proc_as_dict(4, explicit_steps=[3]) + procs = [proc1_dict, proc2_dict, proc3_dict, proc4_dict] + procsteps = [self.procstep_as_dict(1, 2, 1), + self.procstep_as_dict(2, 3, 2), + self.procstep_as_dict(3, 4, 3)] + # post (childless) Todo of chain end, then make_full on next in line + self._make_todo_via_day_post(4) + todo1_dict = self.todo_as_dict(1, 4, children=[2]) + todo2_dict = self.todo_as_dict(2, 3, children=[3], parents=[1]) + todo3_dict = self.todo_as_dict(3, 2, parents=[2], children=[4]) + todo4_dict = self.todo_as_dict(4, 1, parents=[3]) + todos = [todo1_dict, todo2_dict, todo3_dict, todo4_dict] + expected = self.GET_todo_dict(1, todos, procs, procsteps) + step_proc1 = self._step_as_dict(3, [], 1, 4, True) + step_proc2 = self._step_as_dict(2, [step_proc1], 2, 3, True) + step_proc3 = self._step_as_dict(1, [step_proc2], 3, 2, True) + expected['steps_todo_to_process'] = [step_proc3] + self.check_post({'step_filler': 'make_full_3'}, '/todo?id=1') + self.check_json_get('/todo?id=1', expected) + # make new chain next to expected, find steps_todo_to_process extended, + # expect existing Todo demanded by new chain be adopted into new chain + self.check_post({'make_full': 2, 'adopt': [2]}, '/todo?id=1') + todo5_dict = self.todo_as_dict(5, 2, parents=[1], children=[4]) + todo1_dict['children'] = [2, 5] + todo4_dict['parents'] = [3, 5] + todos += [todo5_dict] + step2_proc1 = self._step_as_dict(5, [], None, 4) + step2_proc2 = self._step_as_dict(4, [step2_proc1], None, 5) + expected = self.GET_todo_dict(1, todos, procs, procsteps) + expected['steps_todo_to_process'] = [step_proc3, step2_proc2] + self.check_json_get('/todo?id=1', expected) + # fail on trying to call make_full on non-existing Process + self.check_post({'make_full': 5}, '/todo?id=1', 404) + + def test_POST_todo_make_empty(self) -> None: + """Test creation and adoption via POST /todo with "make_empty".""" + # pylint: disable=too-many-locals + # create chain of Processes + proc_post = {'title': 'A', 'description': '', 'effort': 1.0} + self.post_process(2, proc_post | {'new_top_step': 1}) + self.post_process(3, proc_post | {'new_top_step': 2}) + self.post_process(4, proc_post | {'new_top_step': 3}) + proc1_dict = self.proc_as_dict(**self._proc1_form_data) + proc2_dict = self.proc_as_dict(2, explicit_steps=[1]) + proc3_dict = self.proc_as_dict(3, explicit_steps=[2]) + proc4_dict = self.proc_as_dict(4, explicit_steps=[3]) + procs = [proc1_dict, proc2_dict, proc3_dict, proc4_dict] + procsteps = [self.procstep_as_dict(1, 2, 1), + self.procstep_as_dict(2, 3, 2), + self.procstep_as_dict(3, 4, 3)] + # post (childless) Todo of chain end, then make empty on next in line + self._make_todo_via_day_post(4) + todo1_dict = self.todo_as_dict(1, 4, children=[2]) + todo2_dict = self.todo_as_dict(2, 3, parents=[1]) + todos = [todo1_dict, todo2_dict] + expected = self.GET_todo_dict(1, todos, procs, procsteps) + step_proc1 = self._step_as_dict(3, [], 1, None) + step_proc2 = self._step_as_dict(2, [step_proc1], 2, None, True) + step_proc3 = self._step_as_dict(1, [step_proc2], 3, 2, True) + expected['steps_todo_to_process'] = [step_proc3] + expected['adoption_candidates_for'] = {'1': [], '2': []} + self.check_post({'step_filler': 'make_empty_3'}, '/todo?id=1') + self.check_json_get('/todo?id=1', expected) + # make new top-level Todo without chain implied by its Process + self.check_post({'make_empty': 2, 'adopt': [2]}, '/todo?id=1') + todo3_dict = self.todo_as_dict(3, 2, parents=[1], children=[]) + todo1_dict['children'] = [2, 3] + todos += [todo3_dict] + step2_proc2 = self._step_as_dict(4, [], None, 3) + expected = self.GET_todo_dict(1, todos, procs, procsteps) + expected['steps_todo_to_process'] = [step_proc3, step2_proc2] + expected['adoption_candidates_for'] = {'1': [], '2': [3]} + self.check_json_get('/todo?id=1', expected) + # fail on trying to call make_empty on non-existing Process + self.check_post({'make_full': 5}, '/todo?id=1', 404) + + def test_do_GET_todo(self) -> None: + """Test GET /todo response codes.""" + self._make_todo_via_day_post(1) + # test malformed or illegal parameter values + self.check_get('/todo', 404) + self.check_get('/todo?id=', 404) + self.check_get('/todo?id=foo', 400) + self.check_get('/todo?id=0', 404) + self.check_get('/todo?id=2', 404) + # test all existing Processes are shown as available + proc_post = {'title': 'A', 'description': '', 'effort': 1.0} + self.post_process(2, proc_post) + todo1_dict = self.todo_as_dict(1, process_id=1) + proc1_dict = self.proc_as_dict(1, **self._proc1_form_data) + proc2_dict = self.proc_as_dict(2) + procs = [proc1_dict, proc2_dict] + expected = self.GET_todo_dict(1, [todo1_dict], procs) + self.check_json_get('/todo?id=1', expected) + # test chain of Processes shown as potential step nodes + self.post_process(2, proc_post) + self.post_process(3, proc_post) + self.post_process(4, proc_post) + self.post_process(1, self._proc1_form_data | {'new_top_step': 2}) + self.post_process(2, proc_post | {'new_top_step': 3, 'step_of': [1]}) + self.post_process(3, proc_post | {'new_top_step': 4, 'step_of': [2]}) + proc1_dict['explicit_steps'] = [1] + proc2_dict['explicit_steps'] = [2] + proc3_dict = self.proc_as_dict(3, explicit_steps=[3]) + proc4_dict = self.proc_as_dict(4) + procs += [proc3_dict, proc4_dict] + procsteps = [self.procstep_as_dict(1, 1, 2, None), + self.procstep_as_dict(2, 2, 3, None), + self.procstep_as_dict(3, 3, 4, None)] + expected = self.GET_todo_dict(1, [todo1_dict], procs, procsteps) + step_proc4 = self._step_as_dict(3, [], 4) + step_proc3 = self._step_as_dict(2, [step_proc4], 3) + step_proc2 = self._step_as_dict(1, [step_proc3], 2, fillable=True) + expected['steps_todo_to_process'] = [step_proc2] + expected['adoption_candidates_for'] = {'2': [], '3': [], '4': []} + self.check_json_get('/todo?id=1', expected) + # test display of parallel chains + proc_steps_post = {'new_top_step': 4, 'keep_step': [1], + 'step_1_process_id': 2, 'steps': [1, 4]} + self.post_process(1, self._proc1_form_data | proc_steps_post) + proc1_dict['explicit_steps'] = [1, 4] + step2_proc4 = self._step_as_dict(4, [], 4, fillable=True) + procsteps += [self.procstep_as_dict(4, 1, 4, None)] + expected = self.GET_todo_dict(1, [todo1_dict], procs, procsteps) + expected['steps_todo_to_process'] = [step_proc2, step2_proc4] + expected['adoption_candidates_for'] = {'2': [], '3': [], '4': []} + self.check_json_get('/todo?id=1', expected) + + def test_do_POST_doneness_relations(self) -> None: + """Test Todo.is_done Condition, adoption relations for /todo POSTs.""" + # test Todo with adoptee can only be set done if adoptee is done too + self._make_todo_via_day_post(1) + self._make_todo_via_day_post(1) + self.check_post({'adopt': 2, 'done': ''}, '/todo?id=1', 400) + self.check_post({'done': ''}, '/todo?id=2') + self.check_post({'adopt': 2, 'done': ''}, '/todo?id=1', 302) + # test Todo cannot be set undone with adopted Todo not done yet + self.check_post({}, '/todo?id=2') + self.check_post({'adopt': 2}, '/todo?id=1', 400) + # test unadoption relieves block + self.check_post({}, '/todo?id=1', 302) + # test Condition being set or unset can block doneness setting + c1_post = {'title': '', 'description': '', 'is_active': False} + c2_post = {'title': '', 'description': '', 'is_active': True} + self.check_post(c1_post, '/condition', redir='/condition?id=1') + self.check_post(c2_post, '/condition', redir='/condition?id=2') + self.check_post({'conditions': [1], 'done': ''}, '/todo?id=1', 400) + self.check_post({'done': ''}, '/todo?id=1', 302) + self.check_post({'blockers': [2]}, '/todo?id=1', 400) + self.check_post({'done': ''}, '/todo?id=1', 302) + # test setting Todo doneness can set/un-set Conditions, but only on + # doneness change, not by mere passive state + self.check_post({'enables': [1], 'done': ''}, '/todo?id=1') + self.check_post({'conditions': [1], 'done': ''}, '/todo?id=2', 400) + self.check_post({'enables': [1]}, '/todo?id=1') + self.check_post({'enables': [1], 'done': ''}, '/todo?id=1') + self.check_post({'conditions': [1], 'done': ''}, '/todo?id=2') + self.check_post({'blockers': [1]}, '/todo?id=2', 400) + self.check_post({'disables': [1], 'done': ''}, '/todo?id=1') + self.check_post({'blockers': [1]}, '/todo?id=2', 400) + self.check_post({'disables': [1]}, '/todo?id=1') + self.check_post({'disables': [1], 'done': ''}, '/todo?id=1') + self.check_post({'blockers': [1]}, '/todo?id=2') def test_do_POST_day_todo_adoption(self) -> None: """Test Todos posted to Day view may adopt existing Todos.""" - form_data = self.post_process() - form_data = self.post_process(2, form_data | {'new_top_step': 1}) + form_data = self.post_process( + 2, self._proc1_form_data | {'new_top_step': 1}) form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'} self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) form_data['new_todo'] = 2 @@ -332,18 +638,6 @@ class TestsWithServer(TestCaseWithServer): self.assertEqual(todo2.children, [todo1]) self.assertEqual(todo2.parents, []) - def test_do_POST_day_todo_multiple(self) -> None: - """Test multiple Todos can be posted to Day view.""" - form_data = self.post_process() - form_data = self.post_process(2) - form_data = {'day_comment': '', 'new_todo': [1, 2], - 'make_type': 'full'} - self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) - todo1 = Todo.by_date(self.db_conn, '2024-01-01')[0] - todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1] - self.assertEqual(todo1.process.id_, 1) - self.assertEqual(todo2.process.id_, 2) - def test_do_POST_day_todo_multiple_inner_adoption(self) -> None: """Test multiple Todos can be posted to Day view w. inner adoption.""" @@ -388,8 +682,7 @@ class TestsWithServer(TestCaseWithServer): self.assertEqual(todo3.children, []) self.assertEqual(sorted(todo3.parents), sorted([todo2, todo1])) - form_data = self.post_process() - form_data = self.post_process(2, form_data | {'new_top_step': 1}) + self.post_process(2, self._proc1_form_data | {'new_top_step': 1}) check_adoption('2024-01-01', [1, 2]) check_adoption('2024-01-02', [2, 1]) check_nesting_adoption(3, '2024-01-03', [1, 2]) @@ -397,25 +690,16 @@ class TestsWithServer(TestCaseWithServer): def test_do_POST_day_todo_doneness(self) -> None: """Test Todo doneness can be posted to Day view.""" - form_data = self.post_process() form_data = {'day_comment': '', 'new_todo': [1], 'make_type': 'full'} self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) todo = Todo.by_date(self.db_conn, '2024-01-01')[0] - form_data = {'day_comment': '', 'todo_id': [1], 'make_type': 'full'} + form_data = {'day_comment': '', 'todo_id': [1], 'make_type': 'full', + 'comment': [''], 'done': [], 'effort': ['']} self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) + todo = Todo.by_date(self.db_conn, '2024-01-01')[0] self.assertEqual(todo.is_done, False) form_data = {'day_comment': '', 'todo_id': [1], 'done': [1], - 'make_type': 'full'} + 'make_type': 'full', 'comment': [''], 'effort': ['']} self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) + todo = Todo.by_date(self.db_conn, '2024-01-01')[0] self.assertEqual(todo.is_done, True) - - def test_do_GET_todo(self) -> None: - """Test GET /todo response codes.""" - self.post_process() - form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'} - self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302) - self.check_get('/todo', 400) - self.check_get('/todo?id=', 400) - self.check_get('/todo?id=foo', 400) - self.check_get('/todo?id=0', 404) - self.check_get('/todo?id=1', 200)