home · contact · privacy
Improve placement of Todos and Conditions in Day view.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 21 Apr 2024 23:50:41 +0000 (01:50 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 21 Apr 2024 23:50:41 +0000 (01:50 +0200)
plomtask/http.py
plomtask/todos.py
templates/base.html
templates/day.html
tests/todos.py

index 4bf58b41a2cb264a0a40bf018ee9c5536e358bda..e541057ed793ebe2a53c55b50a11be7133c5884f 100644 (file)
@@ -120,6 +120,11 @@ class TaskHandler(BaseHTTPRequestHandler):
         """Show single Day of ?date=."""
         date = self.params.get_str('date', todays_date())
         conditions_listing = []
         """Show single Day of ?date=."""
         date = self.params.get_str('date', todays_date())
         conditions_listing = []
+        top_todos = [t for t in Todo.by_date(self.conn, date) if not t.parents]
+        seen_todos: set[int] = set()
+        seen_conditions: set[int] = set()
+        todo_trees = [t.get_step_tree(seen_todos, seen_conditions)
+                      for t in top_todos]
         for condition in Condition.all(self.conn):
             enablers = Todo.enablers_for_at(self.conn, condition, date)
             disablers = Todo.disablers_for_at(self.conn, condition, date)
         for condition in Condition.all(self.conn):
             enablers = Todo.enablers_for_at(self.conn, condition, date)
             disablers = Todo.disablers_for_at(self.conn, condition, date)
@@ -128,7 +133,7 @@ class TaskHandler(BaseHTTPRequestHandler):
                     'enablers': enablers,
                     'disablers': disablers}]
         return {'day': Day.by_id(self.conn, date, create=True),
                     'enablers': enablers,
                     'disablers': disablers}]
         return {'day': Day.by_id(self.conn, date, create=True),
-                'todos': Todo.by_date(self.conn, date),
+                'todo_trees': todo_trees,
                 'processes': Process.all(self.conn),
                 'conditions_listing': conditions_listing}
 
                 'processes': Process.all(self.conn),
                 'conditions_listing': conditions_listing}
 
index d060e230381d69dc53cacde6ab28fc37e6afefe6..336ec0350830ce5dfbdf5e85a92b3945608835d9 100644 (file)
@@ -1,5 +1,6 @@
 """Actionables."""
 from __future__ import annotations
 """Actionables."""
 from __future__ import annotations
+from collections import namedtuple
 from typing import Any
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
 from typing import Any
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
@@ -9,6 +10,10 @@ from plomtask.exceptions import (NotFoundException, BadFormatException,
                                  HandledException)
 
 
                                  HandledException)
 
 
+TodoStepsNode = namedtuple('TodoStepsNode',
+                           ('item', 'is_todo', 'children', 'seen'))
+
+
 class Todo(BaseModel, ConditionsRelations):
     """Individual actionable."""
 
 class Todo(BaseModel, ConditionsRelations):
     """Individual actionable."""
 
@@ -134,6 +139,36 @@ class Todo(BaseModel, ConditionsRelations):
                 for condition in self.disables:
                     condition.is_active = False
 
                 for condition in self.disables:
                     condition.is_active = False
 
+    def get_step_tree(self, seen_todos: set[int],
+                      seen_conditions: set[int]) -> TodoStepsNode:
+        """Return tree of depended-on Todos and Conditions."""
+
+        def make_node(step: Todo | Condition) -> TodoStepsNode:
+            assert isinstance(step.id_, int)
+            is_todo = isinstance(step, Todo)
+            children = []
+            if is_todo:
+                assert isinstance(step, Todo)
+                seen = step.id_ in seen_todos
+                seen_todos.add(step.id_)
+                potentially_enabled = set()
+                for child in step.children:
+                    for condition in child.enables:
+                        potentially_enabled.add(condition)
+                    children += [make_node(child)]
+                for condition in [c for c in step.conditions
+                                  if (not c.is_active)
+                                  and (c not in potentially_enabled)]:
+                    children += [make_node(condition)]
+            else:
+                assert isinstance(step, Condition)
+                seen = step.id_ in seen_conditions
+                seen_conditions.add(step.id_)
+            return TodoStepsNode(step, is_todo, children, seen)
+
+        node = make_node(self)
+        return node
+
     def add_child(self, child: Todo) -> None:
         """Add child to self.children, guard against recursion"""
         def walk_steps(node: Todo) -> None:
     def add_child(self, child: Todo) -> None:
         """Add child to self.children, guard against recursion"""
         def walk_steps(node: Todo) -> None:
index 399e1cc0dcdd1a86cc7478cf5bf6b31b68ac8fec..3408d676929c74b5dd4829fee3b0dd2710de7406 100644 (file)
@@ -1,6 +1,14 @@
 <!DOCTYPE html>
 <html>
 <meta charset="UTF-8">
 <!DOCTYPE html>
 <html>
 <meta charset="UTF-8">
+<style>
+body {
+  font-family: monospace;
+}
+ul {
+  list-style-type: none;
+}
+</style>
 <body>
 <a href="processes">processes</a>
 <a href="conditions">conditions</a>
 <body>
 <a href="processes">processes</a>
 <a href="conditions">conditions</a>
index 82eaa56fb7b9acfd6aa55b7db39f080f15459ce4..a089037a829092c7e1a29cda45775dd170e1f35e 100644 (file)
@@ -6,7 +6,20 @@
 {{ todo_with_children(child, indent+1) }}
 {% endfor %}
 {% for condition in todo.conditions %}
 {{ todo_with_children(child, indent+1) }}
 {% endfor %}
 {% for condition in todo.conditions %}
-<li>{% for i in range(indent+1) %}+{% endfor %} [{% if condition.is_active %}x{% else %} {% endif %}] <a href="condition?id={{condition.id_}}">{{condition.title.newest|e}}</a>
+<li>{% for i in range(indent) %}&nbsp;{% endfor %}&nbsp; &lt;[{% if condition.is_active %}x{% else %} {% endif %}] <a href="condition?id={{condition.id_}}">{{condition.title.newest|e}}</a>
+{% endfor %}
+{% endmacro %}
+
+{% macro node_with_children(node, indent) %}
+<li>{% for i in range(indent) %}+{% endfor %}
+{% if node.is_todo %}
+{% if not node.item.is_doable %}<del>{% endif %}[{% if node.item.is_done %}x{% else %} {% endif %}]{% if not node.item.is_doable %}</del>{% endif %}
+{% if node.seen %}({% else %}{% endif %}<a href="todo?id={{node.item.id_}}">{{node.item.process.title.newest|e}}</a>{% if node.seen %}){% else %}{% endif %}
+{% else %}
+&lt; {% if node.seen %}({% else %}{% endif %}<a href="condition?id={{node.item.id_}}">{{node.item.title.newest|e}}</a>{% if node.seen %}){% else %}{% endif %}
+{% endif %}
+{% for child in node.children %}
+{{ node_with_children(child, indent+1) }}
 {% endfor %}
 {% endmacro %}
 
 {% endfor %}
 {% endmacro %}
 
@@ -30,18 +43,18 @@ add todo: <input name="new_todo" list="processes" autocomplete="off" />
 <li>[{% if node['condition'].is_active %}x{% else %} {% endif %}] <a href="condition?id={{node['condition'].id_}}">{{node['condition'].title.newest|e}}</a>
 <ul>
 {% for enabler in node['enablers'] %}
 <li>[{% if node['condition'].is_active %}x{% else %} {% endif %}] <a href="condition?id={{node['condition'].id_}}">{{node['condition'].title.newest|e}}</a>
 <ul>
 {% for enabler in node['enablers'] %}
-<li>[+] {{enabler.process.title.newest|e}}</li>
+<li>&lt; {{enabler.process.title.newest|e}}</li>
 {% endfor %}
 {% for disabler in node['disablers'] %}
 {% endfor %}
 {% for disabler in node['disablers'] %}
-<li>[-] {{disabler.process.title.newest|e}}</li>
+<li>! {{disabler.process.title.newest|e}}</li>
 {% endfor %}
 </ul>
 </li>
 {% endfor %}
 <h4>todos</h4>
 <ul>
 {% endfor %}
 </ul>
 </li>
 {% endfor %}
 <h4>todos</h4>
 <ul>
-{% for todo in todos %}
-{{ todo_with_children(todo, 0) }}
+{% for node in todo_trees %}
+{{ node_with_children(node, 0) }}
 {% endfor %}
 </ul>
 {% endblock %}
 {% endfor %}
 </ul>
 {% endblock %}
index c704c276b7f556a7de9113b294071111763d8c46..52363c07b7ddbd49ba8a2ee8e4f09adda68fb16e 100644 (file)
@@ -1,6 +1,6 @@
 """Test Todos module."""
 from tests.utils import TestCaseWithDB, TestCaseWithServer
 """Test Todos module."""
 from tests.utils import TestCaseWithDB, TestCaseWithServer
-from plomtask.todos import Todo
+from plomtask.todos import Todo, TodoStepsNode
 from plomtask.processes import Process
 from plomtask.conditions import Condition
 from plomtask.exceptions import (NotFoundException, BadFormatException,
 from plomtask.processes import Process
 from plomtask.conditions import Condition
 from plomtask.exceptions import (NotFoundException, BadFormatException,
@@ -156,6 +156,70 @@ class TestsWithDB(TestCaseWithDB):
         self.cond1.is_active = True
         todo_2.is_done = True
 
         self.cond1.is_active = True
         todo_2.is_done = True
 
+    def test_Todo_step_tree(self) -> None:
+        """Test self-configuration of TodoStepsNode tree for Day view."""
+        assert isinstance(self.cond1.id_, int)
+        assert isinstance(self.cond2.id_, int)
+        todo_1 = Todo(None, self.proc, False, self.date1)
+        todo_1.save(self.db_conn)
+        assert isinstance(todo_1.id_, int)
+        # test minimum
+        tree_expected = TodoStepsNode(todo_1, True, [], False)
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+        # test non_emtpy seen_todo does something
+        tree_expected = TodoStepsNode(todo_1, True, [], True)
+        self.assertEqual(todo_1.get_step_tree({todo_1.id_}, set()),
+                         tree_expected)
+        # test child shows up
+        todo_2 = Todo(None, self.proc, False, self.date1)
+        todo_2.save(self.db_conn)
+        assert isinstance(todo_2.id_, int)
+        todo_1.add_child(todo_2)
+        node_todo_2 = TodoStepsNode(todo_2, True, [], False)
+        tree_expected = TodoStepsNode(todo_1, True, [node_todo_2], False)
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+        # test child shows up with child
+        todo_3 = Todo(None, self.proc, False, self.date1)
+        todo_3.save(self.db_conn)
+        assert isinstance(todo_3.id_, int)
+        todo_2.add_child(todo_3)
+        node_todo_3 = TodoStepsNode(todo_3, True, [], False)
+        node_todo_2 = TodoStepsNode(todo_2, True, [node_todo_3], False)
+        tree_expected = TodoStepsNode(todo_1, True, [node_todo_2], False)
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+        # test same todo can be child-ed multiple times at different locations
+        todo_1.add_child(todo_3)
+        node_todo_4 = TodoStepsNode(todo_3, True, [], True)
+        tree_expected = TodoStepsNode(todo_1, True,
+                                      [node_todo_2, node_todo_4], False)
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+        # test condition shows up
+        todo_1.set_conditions(self.db_conn, [self.cond1.id_])
+        node_cond_1 = TodoStepsNode(self.cond1, False, [], False)
+        tree_expected = TodoStepsNode(todo_1, True,
+                                      [node_todo_2, node_todo_4, node_cond_1],
+                                      False)
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+        # test second condition shows up
+        todo_2.set_conditions(self.db_conn, [self.cond2.id_])
+        node_cond_2 = TodoStepsNode(self.cond2, False, [], False)
+        node_todo_2 = TodoStepsNode(todo_2, True,
+                                    [node_todo_3, node_cond_2], False)
+        tree_expected = TodoStepsNode(todo_1, True,
+                                      [node_todo_2, node_todo_4, node_cond_1],
+                                      False)
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+        # test second condition is not hidden if fulfilled by non-sibling
+        todo_1.set_enables(self.db_conn, [self.cond2.id_])
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+        # test second condition is hidden if fulfilled by sibling
+        todo_3.set_enables(self.db_conn, [self.cond2.id_])
+        node_todo_2 = TodoStepsNode(todo_2, True, [node_todo_3], False)
+        tree_expected = TodoStepsNode(todo_1, True,
+                                      [node_todo_2, node_todo_4, node_cond_1],
+                                      False)
+        self.assertEqual(todo_1.get_step_tree(set(), set()), tree_expected)
+
     def test_Todo_singularity(self) -> None:
         """Test pointers made for single object keep pointing to it."""
         todo = Todo(None, self.proc, False, self.date1)
     def test_Todo_singularity(self) -> None:
         """Test pointers made for single object keep pointing to it."""
         todo = Todo(None, self.proc, False, self.date1)
@@ -204,33 +268,41 @@ class TestsWithServer(TestCaseWithServer):
             self.check_post(form_data, '/todo?id=1', status, '/')
             self.db_conn.cached_todos = {}
             return Todo.by_date(self.db_conn, '2024-01-01')[0]
             self.check_post(form_data, '/todo?id=1', status, '/')
             self.db_conn.cached_todos = {}
             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, '/')
         form_data = {'comment': '', 'new_todo': 1}
         self.check_post(form_data, '/day?date=2024-01-01', 302, '/')
         form_data = {'title': '', 'description': '', 'effort': 1}
         self.check_post(form_data, '/process', 302, '/')
         form_data = {'comment': '', 'new_todo': 1}
         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)
         self.check_post(form_data, '/todo?id=', 400)
         self.check_post(form_data, '/todo?id=FOO', 400)
         self.check_post(form_data, '/todo?id=0', 404)
         form_data = {}
         self.check_post(form_data, '/todo=', 404)
         self.check_post(form_data, '/todo?id=', 400)
         self.check_post(form_data, '/todo?id=FOO', 400)
         self.check_post(form_data, '/todo?id=0', 404)
+        # test posting naked entity
         todo1 = post_and_reload(form_data)
         self.assertEqual(todo1.children, [])
         self.assertEqual(todo1.parents, [])
         self.assertEqual(todo1.is_done, False)
         todo1 = post_and_reload(form_data)
         self.assertEqual(todo1.children, [])
         self.assertEqual(todo1.parents, [])
         self.assertEqual(todo1.is_done, False)
+        # test posting doneness
         form_data = {'done': ''}
         todo1 = post_and_reload(form_data)
         self.assertEqual(todo1.is_done, True)
         form_data = {'done': ''}
         todo1 = post_and_reload(form_data)
         self.assertEqual(todo1.is_done, True)
+        # test implicitly posting non-doneness
         form_data = {}
         todo1 = post_and_reload(form_data)
         self.assertEqual(todo1.is_done, False)
         form_data = {}
         todo1 = post_and_reload(form_data)
         self.assertEqual(todo1.is_done, False)
+        # test malformed adoptions
         form_data = {'adopt': 'foo'}
         self.check_post(form_data, '/todo?id=1', 400)
         form_data = {'adopt': 1}
         self.check_post(form_data, '/todo?id=1', 400)
         form_data = {'adopt': 2}
         self.check_post(form_data, '/todo?id=1', 404)
         form_data = {'adopt': 'foo'}
         self.check_post(form_data, '/todo?id=1', 400)
         form_data = {'adopt': 1}
         self.check_post(form_data, '/todo?id=1', 400)
         form_data = {'adopt': 2}
         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, '/')
         form_data = {'comment': '', 'new_todo': 1}
         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)
         todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
         form_data = {'adopt': 2}
         todo1 = post_and_reload(form_data)
         todo2 = Todo.by_date(self.db_conn, '2024-01-01')[1]
@@ -238,7 +310,9 @@ class TestsWithServer(TestCaseWithServer):
         self.assertEqual(todo1.parents, [])
         self.assertEqual(todo2.children, [])
         self.assertEqual(todo2.parents, [todo1])
         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, '/')
         self.check_post(form_data, '/todo?id=1', 400, '/')
+        # test todo1 cannot be set done with todo2 not done yet
         form_data = {'done': ''}
         todo1 = post_and_reload(form_data, 400)
         self.assertEqual(todo1.is_done, False)
         form_data = {'done': ''}
         todo1 = post_and_reload(form_data, 400)
         self.assertEqual(todo1.is_done, False)