+ # 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')