"""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
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=."""
@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):
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."""
@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
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."""
#!/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}/ …"
{% 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>
<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 %}
"""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,
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)
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."""