1 """Test Todos module."""
2 from tests.utils import TestCaseSansDB, TestCaseWithDB, TestCaseWithServer
3 from plomtask.todos import Todo, TodoNode
4 from plomtask.processes import Process, ProcessStep
5 from plomtask.conditions import Condition
6 from plomtask.exceptions import (NotFoundException, BadFormatException,
10 class TestsWithDB(TestCaseWithDB, TestCaseSansDB):
11 """Tests requiring DB, but not server setup.
13 NB: We subclass TestCaseSansDB too, to run any tests there that due to any
14 Todo requiring a _saved_ Process wouldn't run without a DB.
17 default_init_kwargs = {'process': None, 'is_done': False,
20 def setUp(self) -> None:
22 self.date1 = '2024-01-01'
23 self.date2 = '2024-01-02'
24 self.proc = Process(None)
25 self.proc.save(self.db_conn)
26 self.cond1 = Condition(None)
27 self.cond1.save(self.db_conn)
28 self.cond2 = Condition(None)
29 self.cond2.save(self.db_conn)
30 self.default_init_kwargs['process'] = self.proc
32 def test_Todo_init(self) -> None:
33 """Test creation of Todo and what they default to."""
34 process = Process(None)
35 with self.assertRaises(NotFoundException):
36 Todo(None, process, False, self.date1)
37 process.save(self.db_conn)
38 assert isinstance(self.cond1.id_, int)
39 assert isinstance(self.cond2.id_, int)
40 process.set_conditions(self.db_conn, [self.cond1.id_, self.cond2.id_])
41 process.set_enables(self.db_conn, [self.cond1.id_])
42 process.set_disables(self.db_conn, [self.cond2.id_])
43 todo_no_id = Todo(None, process, False, self.date1)
44 self.assertEqual(todo_no_id.conditions, [self.cond1, self.cond2])
45 self.assertEqual(todo_no_id.enables, [self.cond1])
46 self.assertEqual(todo_no_id.disables, [self.cond2])
47 todo_yes_id = Todo(5, process, False, self.date1)
48 self.assertEqual(todo_yes_id.conditions, [])
49 self.assertEqual(todo_yes_id.enables, [])
50 self.assertEqual(todo_yes_id.disables, [])
52 def test_Todo_by_date(self) -> None:
53 """Test findability of Todos by date."""
54 t1 = Todo(None, self.proc, False, self.date1)
56 t2 = Todo(None, self.proc, False, self.date1)
58 self.assertEqual(Todo.by_date(self.db_conn, self.date1), [t1, t2])
59 self.assertEqual(Todo.by_date(self.db_conn, self.date2), [])
60 with self.assertRaises(BadFormatException):
61 self.assertEqual(Todo.by_date(self.db_conn, 'foo'), [])
63 def test_Todo_on_conditions(self) -> None:
64 """Test effect of Todos on Conditions."""
65 assert isinstance(self.cond1.id_, int)
66 assert isinstance(self.cond2.id_, int)
67 todo = Todo(None, self.proc, False, self.date1)
68 todo.save(self.db_conn)
69 todo.set_enables(self.db_conn, [self.cond1.id_])
70 todo.set_disables(self.db_conn, [self.cond2.id_])
72 self.assertEqual(self.cond1.is_active, True)
73 self.assertEqual(self.cond2.is_active, False)
75 self.assertEqual(self.cond1.is_active, True)
76 self.assertEqual(self.cond2.is_active, False)
78 def test_Todo_children(self) -> None:
79 """Test Todo.children relations."""
80 todo_1 = Todo(None, self.proc, False, self.date1)
81 todo_2 = Todo(None, self.proc, False, self.date1)
82 todo_2.save(self.db_conn)
83 with self.assertRaises(HandledException):
84 todo_1.add_child(todo_2)
85 todo_1.save(self.db_conn)
86 todo_3 = Todo(None, self.proc, False, self.date1)
87 with self.assertRaises(HandledException):
88 todo_1.add_child(todo_3)
89 todo_3.save(self.db_conn)
90 todo_1.add_child(todo_3)
91 todo_1.save(self.db_conn)
92 assert isinstance(todo_1.id_, int)
93 todo_retrieved = Todo.by_id(self.db_conn, todo_1.id_)
94 self.assertEqual(todo_retrieved.children, [todo_3])
95 with self.assertRaises(BadFormatException):
96 todo_3.add_child(todo_1)
98 def test_Todo_conditioning(self) -> None:
99 """Test Todo.doability conditions."""
100 assert isinstance(self.cond1.id_, int)
101 todo_1 = Todo(None, self.proc, False, self.date1)
102 todo_1.save(self.db_conn)
103 todo_2 = Todo(None, self.proc, False, self.date1)
104 todo_2.save(self.db_conn)
105 todo_2.add_child(todo_1)
106 with self.assertRaises(BadFormatException):
107 todo_2.is_done = True
108 todo_1.is_done = True
109 todo_2.is_done = True
110 todo_2.is_done = False
111 todo_2.set_conditions(self.db_conn, [self.cond1.id_])
112 with self.assertRaises(BadFormatException):
113 todo_2.is_done = True
114 self.cond1.is_active = True
115 todo_2.is_done = True
117 def test_Todo_step_tree(self) -> None:
118 """Test self-configuration of TodoStepsNode tree for Day view."""
119 todo_1 = Todo(None, self.proc, False, self.date1)
120 todo_1.save(self.db_conn)
121 assert isinstance(todo_1.id_, int)
123 node_0 = TodoNode(todo_1, False, [])
124 self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
125 # test non_emtpy seen_todo does something
127 self.assertEqual(todo_1.get_step_tree({todo_1.id_}).as_dict,
129 # test child shows up
130 todo_2 = Todo(None, self.proc, False, self.date1)
131 todo_2.save(self.db_conn)
132 assert isinstance(todo_2.id_, int)
133 todo_1.add_child(todo_2)
134 node_2 = TodoNode(todo_2, False, [])
135 node_0.children = [node_2]
137 self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
138 # test child shows up with child
139 todo_3 = Todo(None, self.proc, False, self.date1)
140 todo_3.save(self.db_conn)
141 assert isinstance(todo_3.id_, int)
142 todo_2.add_child(todo_3)
143 node_3 = TodoNode(todo_3, False, [])
144 node_2.children = [node_3]
145 self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
146 # test same todo can be child-ed multiple times at different locations
147 todo_1.add_child(todo_3)
148 node_4 = TodoNode(todo_3, True, [])
149 node_0.children += [node_4]
150 self.assertEqual(todo_1.get_step_tree(set()).as_dict, node_0.as_dict)
152 def test_Todo_create_with_children(self) -> None:
153 """Test parenthood guaranteeds of Todo.create_with_children."""
154 assert isinstance(self.proc.id_, int)
155 proc2 = Process(None)
156 proc2.save(self.db_conn)
157 assert isinstance(proc2.id_, int)
158 proc3 = Process(None)
159 proc3.save(self.db_conn)
160 assert isinstance(proc3.id_, int)
161 proc4 = Process(None)
162 proc4.save(self.db_conn)
163 assert isinstance(proc4.id_, int)
164 # make proc4 step of proc3
165 step = ProcessStep(None, proc3.id_, proc4.id_, None)
166 proc3.set_steps(self.db_conn, [step])
167 # give proc2 three steps; 2× proc1, 1× proc3
168 step1 = ProcessStep(None, proc2.id_, self.proc.id_, None)
169 step2 = ProcessStep(None, proc2.id_, self.proc.id_, None)
170 step3 = ProcessStep(None, proc2.id_, proc3.id_, None)
171 proc2.set_steps(self.db_conn, [step1, step2, step3])
172 # test mere creation does nothing
173 todo_ignore = Todo(None, proc2, False, self.date1)
174 todo_ignore.save(self.db_conn)
175 self.assertEqual(todo_ignore.children, [])
176 # test create_with_children on step-less does nothing
177 todo_1 = Todo.create_with_children(self.db_conn, self.proc.id_,
179 self.assertEqual(todo_1.children, [])
180 self.assertEqual(len(Todo.all(self.db_conn)), 2)
181 # test create_with_children adopts and creates, and down tree too
182 todo_2 = Todo.create_with_children(self.db_conn, proc2.id_, self.date1)
183 self.assertEqual(3, len(todo_2.children))
184 self.assertEqual(todo_1, todo_2.children[0])
185 self.assertEqual(self.proc, todo_2.children[2].process)
186 self.assertEqual(proc3, todo_2.children[1].process)
187 todo_3 = todo_2.children[1]
188 self.assertEqual(len(todo_3.children), 1)
189 self.assertEqual(todo_3.children[0].process, proc4)
191 def test_Todo_remove(self) -> None:
193 todo_1 = Todo(None, self.proc, False, self.date1)
194 todo_1.save(self.db_conn)
195 assert todo_1.id_ is not None
196 todo_0 = Todo(None, self.proc, False, self.date1)
197 todo_0.save(self.db_conn)
198 todo_0.add_child(todo_1)
199 todo_2 = Todo(None, self.proc, False, self.date1)
200 todo_2.save(self.db_conn)
201 todo_1.add_child(todo_2)
202 todo_1_id = todo_1.id_
203 todo_1.remove(self.db_conn)
204 with self.assertRaises(NotFoundException):
205 Todo.by_id(self.db_conn, todo_1_id)
206 self.assertEqual(todo_0.children, [])
207 self.assertEqual(todo_2.parents, [])
208 todo_2.comment = 'foo'
209 with self.assertRaises(HandledException):
210 todo_2.remove(self.db_conn)
213 with self.assertRaises(HandledException):
214 todo_2.remove(self.db_conn)
216 def test_Todo_autoremoval(self) -> None:
217 """"Test automatic removal for Todo.effort < 0."""
218 todo_1 = Todo(None, self.proc, False, self.date1)
219 todo_1.save(self.db_conn)
220 todo_1.comment = 'foo'
222 todo_1.save(self.db_conn)
223 assert todo_1.id_ is not None
224 Todo.by_id(self.db_conn, todo_1.id_)
226 todo_1_id = todo_1.id_
227 todo_1.save(self.db_conn)
228 with self.assertRaises(NotFoundException):
229 Todo.by_id(self.db_conn, todo_1_id)
232 class TestsWithServer(TestCaseWithServer):
233 """Tests against our HTTP server/handler (and database)."""
235 def test_do_POST_day(self) -> None:
236 """Test Todo posting of POST /day."""
239 proc = Process.by_id(self.db_conn, 1)
240 proc2 = Process.by_id(self.db_conn, 2)
241 form_data = {'day_comment': '', 'make_type': 'full'}
242 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
243 self.assertEqual(Todo.by_date(self.db_conn, '2024-01-01'), [])
244 proc = Process.by_id(self.db_conn, 1)
245 form_data['new_todo'] = str(proc.id_)
246 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
247 todos = Todo.by_date(self.db_conn, '2024-01-01')
248 self.assertEqual(1, len(todos))
250 self.assertEqual(todo1.id_, 1)
251 proc = Process.by_id(self.db_conn, 1)
252 self.assertEqual(todo1.process.id_, proc.id_)
253 self.assertEqual(todo1.is_done, False)
254 proc2 = Process.by_id(self.db_conn, 2)
255 form_data['new_todo'] = str(proc2.id_)
256 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
257 todos = Todo.by_date(self.db_conn, '2024-01-01')
259 self.assertEqual(todo1.id_, 2)
260 proc2 = Process.by_id(self.db_conn, 1)
261 todo1 = Todo.by_date(self.db_conn, '2024-01-01')[0]
262 self.assertEqual(todo1.id_, 1)
263 self.assertEqual(todo1.process.id_, proc2.id_)
264 self.assertEqual(todo1.is_done, False)
266 def test_do_POST_todo(self) -> None:
267 """Test POST /todo."""
268 def post_and_reload(form_data: dict[str, object], status: int = 302,
269 redir_url: str = '/todo?id=1') -> Todo:
270 self.check_post(form_data, '/todo?id=1', status, redir_url)
271 return Todo.by_date(self.db_conn, '2024-01-01')[0]
274 self.check_post({'day_comment': '', 'new_todo': 1,
275 'make_type': 'full'},
276 '/day?date=2024-01-01&make_type=full', 302)
277 # test posting to bad URLs
278 self.check_post({}, '/todo=', 404)
279 self.check_post({}, '/todo?id=', 404)
280 self.check_post({}, '/todo?id=FOO', 400)
281 self.check_post({}, '/todo?id=0', 404)
282 # test posting naked entity
283 todo1 = post_and_reload({})
284 self.assertEqual(todo1.children, [])
285 self.assertEqual(todo1.parents, [])
286 self.assertEqual(todo1.is_done, False)
287 # test posting doneness
288 todo1 = post_and_reload({'done': ''})
289 self.assertEqual(todo1.is_done, True)
290 # test implicitly posting non-doneness
291 todo1 = post_and_reload({})
292 self.assertEqual(todo1.is_done, False)
293 # test malformed adoptions
294 self.check_post({'adopt': 'foo'}, '/todo?id=1', 400)
295 self.check_post({'adopt': 1}, '/todo?id=1', 400)
296 self.check_post({'adopt': 2}, '/todo?id=1', 404)
297 # test posting second todo of same process
298 self.check_post({'day_comment': '', 'new_todo': 1,
299 'make_type': 'full'},
300 '/day?date=2024-01-01&make_type=full', 302)
301 # test todo 1 adopting todo 2
302 todo1 = post_and_reload({'adopt': 2})
303 todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
304 self.assertEqual(todo1.children, [todo2])
305 self.assertEqual(todo1.parents, [])
306 self.assertEqual(todo2.children, [])
307 self.assertEqual(todo2.parents, [todo1])
308 # test todo1 cannot be set done with todo2 not done yet
309 todo1 = post_and_reload({'done': '', 'adopt': 2}, 400)
310 self.assertEqual(todo1.is_done, False)
311 # test todo1 un-adopting todo 2 by just not sending an adopt
312 todo1 = post_and_reload({}, 302)
313 todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
314 self.assertEqual(todo1.children, [])
315 self.assertEqual(todo1.parents, [])
316 self.assertEqual(todo2.children, [])
317 self.assertEqual(todo2.parents, [])
318 # test todo1 deletion
319 todo1 = post_and_reload({'delete': ''}, 302, '/')
321 def test_do_POST_day_todo_adoption(self) -> None:
322 """Test Todos posted to Day view may adopt existing Todos."""
323 form_data = self.post_process()
324 form_data = self.post_process(2, form_data | {'new_top_step': 1})
325 form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'}
326 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
327 form_data['new_todo'] = 2
328 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
329 todo1 = Todo.by_date(self.db_conn, '2024-01-01')[0]
330 todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
331 self.assertEqual(todo1.children, [])
332 self.assertEqual(todo1.parents, [todo2])
333 self.assertEqual(todo2.children, [todo1])
334 self.assertEqual(todo2.parents, [])
336 def test_do_POST_day_todo_multiple(self) -> None:
337 """Test multiple Todos can be posted to Day view."""
338 form_data = self.post_process()
339 form_data = self.post_process(2)
340 form_data = {'day_comment': '', 'new_todo': [1, 2],
342 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
343 todo1 = Todo.by_date(self.db_conn, '2024-01-01')[0]
344 todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
345 self.assertEqual(todo1.process.id_, 1)
346 self.assertEqual(todo2.process.id_, 2)
348 def test_do_POST_day_todo_multiple_inner_adoption(self) -> None:
349 """Test multiple Todos can be posted to Day view w. inner adoption."""
351 def key_order_func(t: Todo) -> int:
352 assert isinstance(t.process.id_, int)
355 def check_adoption(date: str, new_todos: list[int]) -> None:
356 form_data = {'day_comment': '', 'new_todo': new_todos,
358 self.check_post(form_data, f'/day?date={date}&make_type=full', 302)
359 day_todos = Todo.by_date(self.db_conn, date)
360 day_todos.sort(key=key_order_func)
363 self.assertEqual(todo1.children, [])
364 self.assertEqual(todo1.parents, [todo2])
365 self.assertEqual(todo2.children, [todo1])
366 self.assertEqual(todo2.parents, [])
368 def check_nesting_adoption(process_id: int, date: str,
369 new_top_steps: list[int]) -> None:
370 form_data = {'title': '', 'description': '', 'effort': 1,
372 form_data = self.post_process(1, form_data)
373 form_data['new_top_step'] = new_top_steps
374 form_data['step_of'] = []
375 form_data = self.post_process(process_id, form_data)
376 form_data = {'day_comment': '', 'new_todo': [process_id],
378 self.check_post(form_data, f'/day?date={date}&make_type=full', 302)
379 day_todos = Todo.by_date(self.db_conn, date)
380 day_todos.sort(key=key_order_func, reverse=True)
381 self.assertEqual(len(day_todos), 3)
382 todo1 = day_todos[0] # process of process_id
383 todo2 = day_todos[1] # process 2
384 todo3 = day_todos[2] # process 1
385 self.assertEqual(sorted(todo1.children), sorted([todo2, todo3]))
386 self.assertEqual(todo1.parents, [])
387 self.assertEqual(todo2.children, [todo3])
388 self.assertEqual(todo2.parents, [todo1])
389 self.assertEqual(todo3.children, [])
390 self.assertEqual(sorted(todo3.parents), sorted([todo2, todo1]))
392 form_data = self.post_process()
393 form_data = self.post_process(2, form_data | {'new_top_step': 1})
394 check_adoption('2024-01-01', [1, 2])
395 check_adoption('2024-01-02', [2, 1])
396 check_nesting_adoption(3, '2024-01-03', [1, 2])
397 check_nesting_adoption(4, '2024-01-04', [2, 1])
399 def test_do_POST_day_todo_doneness(self) -> None:
400 """Test Todo doneness can be posted to Day view."""
402 form_data = {'day_comment': '', 'new_todo': [1], 'make_type': 'full'}
403 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
404 todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
405 form_data = {'day_comment': '', 'todo_id': [1], 'make_type': 'full',
406 'comment': [''], 'done': [], 'effort': ['']}
407 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
408 todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
409 self.assertEqual(todo.is_done, False)
410 form_data = {'day_comment': '', 'todo_id': [1], 'done': [1],
411 'make_type': 'full', 'comment': [''], 'effort': ['']}
412 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
413 todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
414 self.assertEqual(todo.is_done, True)
416 def test_do_GET_todo(self) -> None:
417 """Test GET /todo response codes."""
419 form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'}
420 self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
421 self.check_get('/todo', 404)
422 self.check_get('/todo?id=', 404)
423 self.check_get('/todo?id=foo', 400)
424 self.check_get('/todo?id=0', 404)
425 self.check_get('/todo?id=1', 200)