From bdb37bdbfdc46a64631c0fb97d0d86540076165e Mon Sep 17 00:00:00 2001 From: Christian Heller <c.heller@plomlompom.de> Date: Wed, 15 May 2024 03:42:18 +0200 Subject: [PATCH] Simplify Todo steps tree calculation/display. --- plomtask/http.py | 40 +++++----- plomtask/todos.py | 105 ++++----------------------- scripts/pre-commit | 3 +- templates/day.html | 177 +++++++++++++++++++++++++++++++++++---------- tests/todos.py | 73 +++---------------- 5 files changed, 181 insertions(+), 217 deletions(-) diff --git a/plomtask/http.py b/plomtask/http.py index adac957..a1f85fd 100644 --- a/plomtask/http.py +++ b/plomtask/http.py @@ -1,5 +1,5 @@ """Web server stuff.""" -from typing import Any, NamedTuple +from typing import Any from http.server import BaseHTTPRequestHandler from http.server import HTTPServer from urllib.parse import urlparse, parse_qs @@ -118,29 +118,25 @@ class TaskHandler(BaseHTTPRequestHandler): def do_GET_day(self) -> dict[str, object]: """Show single Day of ?date=.""" - - class ConditionListing(NamedTuple): - """Listing of Condition augmented with its enablers, disablers.""" - condition: Condition - enablers: list[Todo] - disablers: list[Todo] - date = self.params.get_str('date', todays_date()) - top_todos = [t for t in Todo.by_date(self.conn, date) if not t.parents] - todo_trees = [t.get_undone_steps_tree() for t in top_todos] - done_trees = [] - for t in top_todos: - done_trees += t.get_done_steps_tree() - condition_listings: list[ConditionListing] = [] - for cond in Condition.all(self.conn): - enablers = Todo.enablers_for_at(self.conn, cond, date) - disablers = Todo.disablers_for_at(self.conn, cond, date) - condition_listings += [ConditionListing(cond, enablers, disablers)] + todays_todos = Todo.by_date(self.conn, date) + conditions_present = [] + enablers_for = {} + for todo in todays_todos: + for condition in todo.conditions: + if condition not in conditions_present: + conditions_present += [condition] + enablers_for[condition.id_] = [p for p in + Process.all(self.conn) + if condition in p.enables] + seen_todos: set[int] = set() + top_nodes = [t.get_step_tree(seen_todos) + for t in todays_todos if not t.parents] return {'day': Day.by_id(self.conn, date, create=True), - 'todo_trees': todo_trees, - 'done_trees': done_trees, - 'processes': Process.all(self.conn), - 'condition_listings': condition_listings} + 'top_nodes': top_nodes, + 'enablers_for': enablers_for, + 'conditions_present': conditions_present, + 'processes': Process.all(self.conn)} def do_GET_todo(self) -> dict[str, object]: """Show single Todo of ?id=.""" diff --git a/plomtask/todos.py b/plomtask/todos.py index fcb8617..e42c484 100644 --- a/plomtask/todos.py +++ b/plomtask/todos.py @@ -11,13 +11,11 @@ from plomtask.exceptions import (NotFoundException, BadFormatException, @dataclass -class TodoStepsNode: +class TodoNode: """Collects what's useful to know for Todo/Condition tree display.""" - item: Todo | Condition - is_todo: bool - children: list[TodoStepsNode] + todo: Todo seen: bool - hide: bool + children: list[TodoNode] class Todo(BaseModel[int], ConditionsRelations): @@ -85,31 +83,6 @@ class Todo(BaseModel[int], ConditionsRelations): todos += [cls.by_id(db_conn, id_)] return todos - @staticmethod - def _x_ablers_for_at(db_conn: DatabaseConnection, name: str, - cond: Condition, date: str) -> list[Todo]: - """Collect all Todos of day that [name] condition.""" - assert isinstance(cond.id_, int) - x_ablers = [] - table = f'todo_{name}' - for id_ in db_conn.column_where(table, 'todo', 'condition', cond.id_): - todo = Todo.by_id(db_conn, id_) - if todo.date == date: - x_ablers += [todo] - return x_ablers - - @classmethod - def enablers_for_at(cls, db_conn: DatabaseConnection, - condition: Condition, date: str) -> list[Todo]: - """Collect all Todos of day that enable condition.""" - return cls._x_ablers_for_at(db_conn, 'enables', condition, date) - - @classmethod - def disablers_for_at(cls, db_conn: DatabaseConnection, - condition: Condition, date: str) -> list[Todo]: - """Collect all Todos of day that disable condition.""" - return cls._x_ablers_for_at(db_conn, 'disables', condition, date) - @property def is_doable(self) -> bool: """Decide whether .is_done settable based on children, Conditions.""" @@ -123,7 +96,7 @@ class Todo(BaseModel[int], ConditionsRelations): @property def process_id(self) -> int | str | None: - """Return ID of tasked Process.""" + """Needed for super().save to save Processes as attributes.""" return self.process.id_ @property @@ -169,69 +142,19 @@ class Todo(BaseModel[int], ConditionsRelations): todo.save(db_conn) self.add_child(todo) - def get_step_tree(self, seen_todos: set[int], - seen_conditions: set[int]) -> TodoStepsNode: - """Return tree of depended-on Todos and Conditions.""" + def get_step_tree(self, seen_todos: set[int]) -> TodoNode: + """Return tree of depended-on Todos.""" - def make_node(step: Todo | Condition) -> TodoStepsNode: - assert isinstance(step.id_, int) - is_todo = isinstance(step, Todo) + def make_node(todo: Todo) -> TodoNode: 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.id_) - children += [make_node(child)] - for condition in [c for c in step.conditions - if (not c.is_active) - and (c.id_ not in potentially_enabled)]: - children += [make_node(condition)] - else: - seen = step.id_ in seen_conditions - seen_conditions.add(step.id_) - return TodoStepsNode(step, is_todo, children, seen, False) - - node = make_node(self) - return node - - def get_undone_steps_tree(self) -> TodoStepsNode: - """Return tree of depended-on undone Todos and Conditions.""" - - def walk_tree(node: TodoStepsNode) -> None: - if isinstance(node.item, Todo) and node.item.is_done: - node.hide = True - for child in node.children: - walk_tree(child) - - seen_todos: set[int] = set() - seen_conditions: set[int] = set() - step_tree = self.get_step_tree(seen_todos, seen_conditions) - walk_tree(step_tree) - return step_tree - - def get_done_steps_tree(self) -> list[TodoStepsNode]: - """Return tree of depended-on done Todos.""" - - def make_nodes(node: TodoStepsNode) -> list[TodoStepsNode]: - children: list[TodoStepsNode] = [] - if not isinstance(node.item, Todo): - return children - for child in node.children: - children += make_nodes(child) - if node.item.is_done: - node.children = children - return [node] - return children + seen = todo.id_ in seen_todos + assert isinstance(todo.id_, int) + seen_todos.add(todo.id_) + for child in todo.children: + children += [make_node(child)] + return TodoNode(todo, seen, children) - seen_todos: set[int] = set() - seen_conditions: set[int] = set() - step_tree = self.get_step_tree(seen_todos, seen_conditions) - nodes = make_nodes(step_tree) - return nodes + return make_node(self) def add_child(self, child: Todo) -> None: """Add child to self.children, avoid recursion, update parenthoods.""" diff --git a/scripts/pre-commit b/scripts/pre-commit index 2aaccb0..6f84c41 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -1,7 +1,6 @@ #!/bin/sh set -e -# for dir in $(echo '.' 'plomtask' 'tests'); do -for dir in $(echo 'tests'); do +for dir in $(echo '.' 'plomtask' 'tests'); do echo "Running mypy on ${dir}/ â¦." python3 -m mypy --strict ${dir}/*.py echo "Running flake8 on ${dir}/ â¦" diff --git a/templates/day.html b/templates/day.html index efa1c9b..f13eb5c 100644 --- a/templates/day.html +++ b/templates/day.html @@ -1,50 +1,101 @@ {% extends 'base.html' %} -{% macro show_node(node, indent) %} -{% if node.is_todo %} -{% for i in range(indent) %} {% endfor %} + -{% 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 %} -{% for i in range(indent) %} {% endfor %} + -{% if node.seen %}({% else %}{% endif %}<a href="condition?id={{node.item.id_}}">{{node.item.title.newest|e}}</a>{% if node.seen %}){% else %}{% endif %} -{% endif %} -{% endmacro %} +{% block css %} +td, th, tr, table { + padding: 0; + margin: 0; +} +th { + border: 1px solid black; +} +td.min_width { + min-width: 1em; +} +td.cond_line_0 { + background-color: #ffbbbb; +} +td.cond_line_1 { + background-color: #bbffbb; +} +td.cond_line_2 { + background-color: #bbbbff; +} +td.todo_line { + border-bottom: 1px solid #bbbbbb; +} +{% endblock %} -{% macro undone_with_children(node, indent) %} -{% if not node.hide %} + + +{% macro show_node_undone(node, indent) %} +{% if not node.todo.is_done %} <tr> -<td> -{% if node.is_todo %} -<input name="done" value="{{node.item.id_}}" type="checkbox" {% if node.seen or not node.item.is_doable %}disabled{% endif %} {% if node.item.is_done %} checked {% endif %} /> -{% endif %} -</td> -<td> -{{ show_node(node, indent) }} + +{% for condition in conditions_present %} +<td class="cond_line_{{loop.index0 % 3}} {% if not condition.is_active %}min_width{% endif %}">{% if condition in node.todo.conditions %}{% if not condition.is_active %}O{% endif %}{% endif %}</td> +{% endfor %} + +<td class="todo_line">-></td> +<td class="todo_line"><input type="checkbox" {% if node.todo.is_done %}checked disabled{% endif %} {% if not node.todo.is_doable %}disabled{% endif %}/></td> +<td class="todo_line"> +{% for i in range(indent) %} {% endfor %} + +{% if node.seen %}({% endif %}<a href="todo?id={{node.todo.id_}}">{{node.todo.process.title.newest|e}}</a>{% if node.seen %}){% endif %} </td> +<td class="todo_line">-></td> + +{% for condition in conditions_present|reverse %} +<td class="cond_line_{{(conditions_present|length - loop.index) % 3}} {% if condition in node.todo.enables or condition in node.todo.disables %}min_width{% endif %}">{% if condition in node.todo.enables %}+{% elif condition in node.todo.disables %}!{% endif %}</td> +{% endfor %} + +<td><input name="comment" /></td> + </tr> {% endif %} + +{% if not node.seen %} {% for child in node.children %} -{{ undone_with_children(child, indent+1) }} +{{ show_node_undone(child, indent+1) }} {% endfor %} +{% endif %} + {% endmacro %} -{% macro done_with_children(node, indent) %} -{% if not node.hide %} + +{% macro show_node_done(node, indent, path) %} +{% if node.todo.is_done %} + <tr> +{% if path|length > 0 and not path[-1].todo.is_done %} <td> -{{ show_node(node, indent) }} +({% for path_node in path %}<a href="todo?id={{path_node.todo.id_}}">{{path_node.todo.process.title.newest|e}}</a> <- {% endfor %}) </td> </tr> + +<tr> +<td> + + +{% else %} +<td> +{% for i in range(indent) %} {% endfor %} + +{% endif %} +{% if node.seen %}({% endif %}<a href="todo?id={{node.todo.id_}}">{{node.todo.process.title.newest|e}}</a>{% if node.seen %}){% endif %} +</td> +</tr> + {% endif %} +{% if not node.seen %} {% for child in node.children %} -{{ done_with_children(child, indent+1) }} +{{ show_node_done(child, indent+1, path + [node]) }} {% endfor %} +{% endif %} + {% endmacro %} + {% block content %} <h3>{{day.date}} / {{day.weekday}}</h3> <p> @@ -59,30 +110,78 @@ add todo: <input name="new_todo" list="processes" autocomplete="off" /> <option value="{{process.id_}}">{{process.title.newest|e}}</option> {% endfor %} </datalist> -<h4>conditions</h4> -<ul> -{% for node in condition_listings %} -<li>[{% if node.condition.is_active %}x{% else %} {% endif %}] <a href="condition?id={{node.condition.id_}}">{{node.condition.title.newest|e}}</a> -({% for enabler in node.enablers %} -< {{enabler.process.title.newest|e}}; + +<h4>todo</h4> + +<table> + +<tr> +<th colspan={{ conditions_present|length}}>c</th> +<th colspan=4>states</th> +<th colspan={{ conditions_present|length}}>t</th> +<th>add enabler</th> +</tr> + +{% for condition in conditions_present %} +{% set outer_loop = loop %} +<tr> + +{% for _ in conditions_present %} +{% if outer_loop.index > loop.index %} +<td class="cond_line_{{loop.index0 % 3}}"> +{% else %} +<td class="cond_line_{{outer_loop.index0 % 3}}"> +{% endif %} +{% if outer_loop.index == loop.index %} +{% endif %} +</td> {% endfor %} -{% for disabler in node.disablers %} -! {{disabler.process.title.newest|e}}; -{% endfor %}) + +<td class="cond_line_{{loop.index0 % 3}}">[{% if condition.is_active %}X{% else %} {% endif %}]</td> +<td colspan=3 class="cond_line_{{loop.index0 % 3}}"><a href="condition?id={{condition.id_}}">{{condition.title.newest|e}}</a></td> + +{% for _ in conditions_present %} +{% if outer_loop.index0 + loop.index0 < conditions_present|length %} +<td class="cond_line_{{outer_loop.index0 % 3}}"> +{% else %} +<td class="cond_line_{{(conditions_present|length - loop.index) % 3}}"> +{% endif %} {% endfor %} -</ul> -<h4>to do</h4> -<table> -{% for node in todo_trees %} -{{ undone_with_children(node, indent=0) }} + +<td><input list="todos_for_{{condition.id_}}" /></td> +<datalist name="new_todo" id="todos_for_{{condition.id_}}" /> +{% for process in enablers_for[condition.id_] %} +<option value="{{process.id_}}">{{process.title.newest|e}}</option> {% endfor %} +</datalist /> +</td> +</tr> +{% endfor %} + +<tr> +{% for condition in conditions_present %} +<td class="cond_line_{{loop.index0 % 3}}"></td> +{% endfor %} +<th colspan={{ 4 }}>doables</th> +{% for condition in conditions_present %} +<td class="cond_line_{{(conditions_present|length - loop.index) % 3}}"></td> +{% endfor %} +<th>comments</th> +</tr> +{% for node in top_nodes %} +{{ show_node_undone(node, 0) }} +{% endfor %} + </table> + <h4>done</h4> + <table> -{% for node in done_trees %} -{{ done_with_children(node, indent=0) }} +{% for node in top_nodes %} +{{ show_node_done(node, 0, []) }} {% endfor %} </table> + </form> {% endblock %} diff --git a/tests/todos.py b/tests/todos.py index 7471565..a8219fa 100644 --- a/tests/todos.py +++ b/tests/todos.py @@ -1,6 +1,6 @@ """Test Todos module.""" from tests.utils import TestCaseWithDB, TestCaseWithServer -from plomtask.todos import Todo, TodoStepsNode +from plomtask.todos import Todo, TodoNode from plomtask.processes import Process from plomtask.conditions import Condition from plomtask.exceptions import (NotFoundException, BadFormatException, @@ -88,40 +88,6 @@ class TestsWithDB(TestCaseWithDB): self.assertEqual(self.cond1.is_active, True) self.assertEqual(self.cond2.is_active, False) - def test_Todo_enablers_disablers(self) -> None: - """Test Todo.enablers_for_at/disablers_for_at.""" - assert isinstance(self.cond1.id_, int) - assert isinstance(self.cond2.id_, int) - todo1 = Todo(None, self.proc, False, self.date1) - todo1.save(self.db_conn) - todo1.set_enables(self.db_conn, [self.cond1.id_]) - todo1.set_disables(self.db_conn, [self.cond2.id_]) - todo1.save(self.db_conn) - todo2 = Todo(None, self.proc, False, self.date1) - todo2.save(self.db_conn) - todo2.set_enables(self.db_conn, [self.cond2.id_]) - todo2.save(self.db_conn) - todo3 = Todo(None, self.proc, False, self.date2) - todo3.save(self.db_conn) - todo3.set_enables(self.db_conn, [self.cond2.id_]) - todo3.save(self.db_conn) - enablers = Todo.enablers_for_at(self.db_conn, self.cond1, self.date1) - self.assertEqual(enablers, [todo1]) - enablers = Todo.enablers_for_at(self.db_conn, self.cond1, self.date2) - self.assertEqual(enablers, []) - disablers = Todo.disablers_for_at(self.db_conn, self.cond1, self.date1) - self.assertEqual(disablers, []) - disablers = Todo.disablers_for_at(self.db_conn, self.cond1, self.date2) - self.assertEqual(disablers, []) - enablers = Todo.enablers_for_at(self.db_conn, self.cond2, self.date1) - self.assertEqual(enablers, [todo2]) - enablers = Todo.enablers_for_at(self.db_conn, self.cond2, self.date2) - self.assertEqual(enablers, [todo3]) - disablers = Todo.disablers_for_at(self.db_conn, self.cond2, self.date1) - self.assertEqual(disablers, [todo1]) - disablers = Todo.disablers_for_at(self.db_conn, self.cond2, self.date2) - self.assertEqual(disablers, []) - def test_Todo_children(self) -> None: """Test Todo.children relations.""" todo_1 = Todo(None, self.proc, False, self.date1) @@ -163,56 +129,37 @@ class TestsWithDB(TestCaseWithDB): 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 - node_0 = TodoStepsNode(todo_1, True, [], False, False) - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) + node_0 = TodoNode(todo_1, False, []) + self.assertEqual(todo_1.get_step_tree(set()), node_0) # test non_emtpy seen_todo does something node_0.seen = True - self.assertEqual(todo_1.get_step_tree({todo_1.id_}, set()), node_0) + self.assertEqual(todo_1.get_step_tree({todo_1.id_}), node_0) # 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_2 = TodoStepsNode(todo_2, True, [], False, False) + node_2 = TodoNode(todo_2, False, []) node_0.children = [node_2] node_0.seen = False - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) + self.assertEqual(todo_1.get_step_tree(set()), node_0) # 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_3 = TodoStepsNode(todo_3, True, [], False, False) + node_3 = TodoNode(todo_3, False, []) node_2.children = [node_3] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) + self.assertEqual(todo_1.get_step_tree(set()), node_0) # test same todo can be child-ed multiple times at different locations todo_1.add_child(todo_3) - node_4 = TodoStepsNode(todo_3, True, [], True, False) + node_4 = TodoNode(todo_3, True, []) node_0.children += [node_4] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - # test condition shows up - todo_1.set_conditions(self.db_conn, [self.cond1.id_]) - node_5 = TodoStepsNode(self.cond1, False, [], False, False) - node_0.children += [node_5] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - # test second condition shows up - todo_2.set_conditions(self.db_conn, [self.cond2.id_]) - node_6 = TodoStepsNode(self.cond2, False, [], False, False) - node_2.children += [node_6] - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) - # 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()), node_0) - # test second condition is hidden if fulfilled by sibling - todo_3.set_enables(self.db_conn, [self.cond2.id_]) - node_2.children.remove(node_6) - self.assertEqual(todo_1.get_step_tree(set(), set()), node_0) + self.assertEqual(todo_1.get_step_tree(set()), node_0) def test_Todo_unsatisfied_steps(self) -> None: """Test options of satisfying unfulfilled Process.explicit_steps.""" -- 2.30.2