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 = []
+        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)
@@ -128,7 +133,7 @@ class TaskHandler(BaseHTTPRequestHandler):
                     '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}
 
index d060e230381d69dc53cacde6ab28fc37e6afefe6..336ec0350830ce5dfbdf5e85a92b3945608835d9 100644 (file)
@@ -1,5 +1,6 @@
 """Actionables."""
 from __future__ import annotations
+from collections import namedtuple
 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)
 
 
+TodoStepsNode = namedtuple('TodoStepsNode',
+                           ('item', 'is_todo', 'children', 'seen'))
+
+
 class Todo(BaseModel, ConditionsRelations):
     """Individual actionable."""
 
@@ -134,6 +139,36 @@ class Todo(BaseModel, ConditionsRelations):
                 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:
index 399e1cc0dcdd1a86cc7478cf5bf6b31b68ac8fec..3408d676929c74b5dd4829fee3b0dd2710de7406 100644 (file)
@@ -1,6 +1,14 @@
 <!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>
index 82eaa56fb7b9acfd6aa55b7db39f080f15459ce4..a089037a829092c7e1a29cda45775dd170e1f35e 100644 (file)
@@ -6,7 +6,20 @@
 {{ 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 %}
 
@@ -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>[+] {{enabler.process.title.newest|e}}</li>
+<li>&lt; {{enabler.process.title.newest|e}}</li>
 {% 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>
-{% for todo in todos %}
-{{ todo_with_children(todo, 0) }}
+{% for node in todo_trees %}
+{{ node_with_children(node, 0) }}
 {% endfor %}
 </ul>
 {% endblock %}
index c704c276b7f556a7de9113b294071111763d8c46..52363c07b7ddbd49ba8a2ee8e4f09adda68fb16e 100644 (file)
@@ -1,6 +1,6 @@
 """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,
@@ -156,6 +156,70 @@ class TestsWithDB(TestCaseWithDB):
         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)
@@ -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]
+        # 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, '/')
+        # 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)
+        # test posting naked entity
         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)
+        # test implicitly posting non-doneness
         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)
+        # test posting second todo of same process
         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]
@@ -238,7 +310,9 @@ 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': ''}
         todo1 = post_and_reload(form_data, 400)
         self.assertEqual(todo1.is_done, False)