From 010ef4bfea17be7436a937f28e7b54d3da17a1e1 Mon Sep 17 00:00:00 2001
From: Christian Heller <c.heller@plomlompom.de>
Date: Mon, 22 Apr 2024 02:13:39 +0200
Subject: [PATCH] Allow Todo adoptions to be un-done in Todo view.

---
 plomtask/http.py    | 11 +++++++++--
 plomtask/todos.py   |  9 ++++++++-
 templates/todo.html |  3 ++-
 tests/todos.py      | 18 ++++++++++++------
 4 files changed, 31 insertions(+), 10 deletions(-)

diff --git a/plomtask/http.py b/plomtask/http.py
index e541057..b5a6c16 100644
--- a/plomtask/http.py
+++ b/plomtask/http.py
@@ -205,8 +205,15 @@ class TaskHandler(BaseHTTPRequestHandler):
         """Update Todo and its children."""
         id_ = self.params.get_int('id')
         todo = Todo.by_id(self.conn, id_)
-        child_id = self.form_data.get_int_or_none('adopt')
-        if child_id is not None:
+        adopted_child_ids = self.form_data.get_all_int('adopt')
+        for child in todo.children:
+            if child.id_ not in adopted_child_ids:
+                assert isinstance(child.id_, int)
+                child = Todo.by_id(self.conn, child.id_)
+                todo.remove_child(child)
+        for child_id in adopted_child_ids:
+            if child_id in [c.id_ for c in todo.children]:
+                continue
             child = Todo.by_id(self.conn, child_id)
             todo.add_child(child)
         todo.set_conditions(self.conn, self.form_data.get_all_int('condition'))
diff --git a/plomtask/todos.py b/plomtask/todos.py
index 336ec03..cf4c330 100644
--- a/plomtask/todos.py
+++ b/plomtask/todos.py
@@ -170,7 +170,7 @@ class Todo(BaseModel, ConditionsRelations):
         return node
 
     def add_child(self, child: Todo) -> None:
-        """Add child to self.children, guard against recursion"""
+        """Add child to self.children, avoid recursion, update parenthoods."""
         def walk_steps(node: Todo) -> None:
             if node.id_ == self.id_:
                 raise BadFormatException('bad child choice causes recursion')
@@ -186,6 +186,13 @@ class Todo(BaseModel, ConditionsRelations):
         self.children += [child]
         child.parents += [self]
 
+    def remove_child(self, child: Todo) -> None:
+        """Remove child from self.children, update counter relations."""
+        if child not in self.children:
+            raise HandledException('Cannot remove un-parented child.')
+        self.children.remove(child)
+        child.parents.remove(self)
+
     def save(self, db_conn: DatabaseConnection) -> None:
         """Write self and children to DB and its cache."""
         if self.process.id_ is None:
diff --git a/templates/todo.html b/templates/todo.html
index 92b0657..9debffc 100644
--- a/templates/todo.html
+++ b/templates/todo.html
@@ -65,7 +65,8 @@ add disables: <input name="disables" list="condition_candidates" autocomplete="o
 <h4>children</h4>
 <ul>
 {% for child in todo.children %}
-<li><a href="todo?id={{child.id_}}">{{child.process.title.newest|e}}</a>
+<li><input type="checkbox" name="adopt" value="{{child.id_}}" checked />
+<a href="todo?id={{child.id_}}">{{child.process.title.newest|e}}</a>
 {% endfor %}
 </ul>
 adopt: <input name="adopt" list="todo_candidates" autocomplete="off" />
diff --git a/tests/todos.py b/tests/todos.py
index 52363c0..4ab34ab 100644
--- a/tests/todos.py
+++ b/tests/todos.py
@@ -270,9 +270,9 @@ class TestsWithServer(TestCaseWithServer):
             return Todo.by_date(self.db_conn, '2024-01-01')[0]
         # test minimum
         form_data = {'title': '', 'description': '', 'effort': 1}
-        self.check_post(form_data, '/process', 302, '/')
+        self.check_post(form_data, '/process', 302)
         form_data = {'comment': '', 'new_todo': 1}
-        self.check_post(form_data, '/day?date=2024-01-01', 302, '/')
+        self.check_post(form_data, '/day?date=2024-01-01', 302)
         # test posting to bad URLs
         form_data = {}
         self.check_post(form_data, '/todo=', 404)
@@ -301,7 +301,7 @@ class TestsWithServer(TestCaseWithServer):
         self.check_post(form_data, '/todo?id=1', 404)
         # test posting second todo of same process
         form_data = {'comment': '', 'new_todo': 1}
-        self.check_post(form_data, '/day?date=2024-01-01', 302, '/')
+        self.check_post(form_data, '/day?date=2024-01-01', 302)
         # test todo 1 adopting todo 2
         form_data = {'adopt': 2}
         todo1 = post_and_reload(form_data)
@@ -310,12 +310,18 @@ class TestsWithServer(TestCaseWithServer):
         self.assertEqual(todo1.parents, [])
         self.assertEqual(todo2.children, [])
         self.assertEqual(todo2.parents, [todo1])
-        # test failure of re-adopting same child
-        self.check_post(form_data, '/todo?id=1', 400, '/')
         # test todo1 cannot be set done with todo2 not done yet
-        form_data = {'done': ''}
+        form_data = {'done': '', 'adopt': 2}
         todo1 = post_and_reload(form_data, 400)
         self.assertEqual(todo1.is_done, False)
+        # test todo1 un-adopting todo 2 by just not sending an adopt
+        form_data = {}
+        todo1 = post_and_reload(form_data, 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, [])
 
     def test_do_GET_todo(self) -> None:
         """Test GET /todo response codes."""
-- 
2.30.2