2 from __future__ import annotations
3 from collections import namedtuple
5 from sqlite3 import Row
6 from plomtask.db import DatabaseConnection, BaseModel
7 from plomtask.processes import Process
8 from plomtask.conditions import Condition, ConditionsRelations
9 from plomtask.exceptions import (NotFoundException, BadFormatException,
13 TodoStepsNode = namedtuple('TodoStepsNode',
14 ('item', 'is_todo', 'children', 'seen'))
17 class Todo(BaseModel, ConditionsRelations):
18 """Individual actionable."""
20 # pylint: disable=too-many-instance-attributes
23 to_save = ['process_id', 'is_done', 'date']
25 def __init__(self, id_: int | None, process: Process,
26 is_done: bool, date: str) -> None:
28 self.process = process
29 self._is_done = is_done
31 self.children: list[Todo] = []
32 self.parents: list[Todo] = []
33 self.conditions: list[Condition] = []
34 self.enables: list[Condition] = []
35 self.disables: list[Condition] = []
37 self.conditions = process.conditions[:]
38 self.enables = process.enables[:]
39 self.disables = process.disables[:]
42 def from_table_row(cls, db_conn: DatabaseConnection,
43 row: Row | list[Any]) -> Todo:
44 """Make from DB row, write to DB cache."""
46 raise NotFoundException('calling Todo of '
48 row_as_list = list(row)
49 row_as_list[1] = Process.by_id(db_conn, row[1])
50 todo = super().from_table_row(db_conn, row_as_list)
51 assert isinstance(todo, Todo)
55 def by_id(cls, db_conn: DatabaseConnection, id_: int) -> Todo:
56 """Get Todo of .id_=id_ and children (from DB cache if possible)."""
57 todo, from_cache = super()._by_id(db_conn, id_)
59 raise NotFoundException(f'Todo of ID not found: {id_}')
61 for t_id in db_conn.column_where('todo_children', 'child',
63 todo.children += [cls.by_id(db_conn, t_id)]
64 for t_id in db_conn.column_where('todo_children', 'parent',
66 todo.parents += [cls.by_id(db_conn, t_id)]
67 for name in ('conditions', 'enables', 'disables'):
68 table = f'todo_{name}'
69 for cond_id in db_conn.column_where(table, 'condition',
71 target = getattr(todo, name)
72 target += [Condition.by_id(db_conn, cond_id)]
73 assert isinstance(todo, Todo)
77 def by_date(cls, db_conn: DatabaseConnection, date: str) -> list[Todo]:
78 """Collect all Todos for Day of date."""
80 for id_ in db_conn.column_where('todos', 'id', 'day', date):
81 todos += [cls.by_id(db_conn, id_)]
85 def _x_ablers_for_at(db_conn: DatabaseConnection, name: str,
86 cond: Condition, date: str) -> list[Todo]:
87 """Collect all Todos of day that [name] condition."""
88 assert isinstance(cond.id_, int)
90 table = f'todo_{name}'
91 for id_ in db_conn.column_where(table, 'todo', 'condition', cond.id_):
92 todo = Todo.by_id(db_conn, id_)
98 def enablers_for_at(cls, db_conn: DatabaseConnection,
99 condition: Condition, date: str) -> list[Todo]:
100 """Collect all Todos of day that enable condition."""
101 return cls._x_ablers_for_at(db_conn, 'enables', condition, date)
104 def disablers_for_at(cls, db_conn: DatabaseConnection,
105 condition: Condition, date: str) -> list[Todo]:
106 """Collect all Todos of day that disable condition."""
107 return cls._x_ablers_for_at(db_conn, 'disables', condition, date)
110 def is_doable(self) -> bool:
111 """Decide whether .is_done settable based on children, Conditions."""
112 for child in self.children:
113 if not child.is_done:
115 for condition in self.conditions:
116 if not condition.is_active:
121 def process_id(self) -> int | str | None:
122 """Return ID of tasked Process."""
123 return self.process.id_
126 def is_done(self) -> bool:
127 """Wrapper around self._is_done so we can control its setter."""
131 def is_done(self, value: bool) -> None:
132 if value != self.is_done and not self.is_doable:
133 raise BadFormatException('cannot change doneness of undoable Todo')
134 if self._is_done != value:
135 self._is_done = value
137 for condition in self.enables:
138 condition.is_active = True
139 for condition in self.disables:
140 condition.is_active = False
142 def get_step_tree(self, seen_todos: set[int],
143 seen_conditions: set[int]) -> TodoStepsNode:
144 """Return tree of depended-on Todos and Conditions."""
146 def make_node(step: Todo | Condition) -> TodoStepsNode:
147 assert isinstance(step.id_, int)
148 is_todo = isinstance(step, Todo)
151 assert isinstance(step, Todo)
152 seen = step.id_ in seen_todos
153 seen_todos.add(step.id_)
154 potentially_enabled = set()
155 for child in step.children:
156 for condition in child.enables:
157 potentially_enabled.add(condition)
158 children += [make_node(child)]
159 for condition in [c for c in step.conditions
161 and (c not in potentially_enabled)]:
162 children += [make_node(condition)]
164 assert isinstance(step, Condition)
165 seen = step.id_ in seen_conditions
166 seen_conditions.add(step.id_)
167 return TodoStepsNode(step, is_todo, children, seen)
169 node = make_node(self)
172 def add_child(self, child: Todo) -> None:
173 """Add child to self.children, avoid recursion, update parenthoods."""
174 def walk_steps(node: Todo) -> None:
175 if node.id_ == self.id_:
176 raise BadFormatException('bad child choice causes recursion')
177 for child in node.children:
180 raise HandledException('Can only add children to saved Todos.')
181 if child.id_ is None:
182 raise HandledException('Can only add saved children to Todos.')
183 if child in self.children:
184 raise BadFormatException('cannot adopt same child twice')
186 self.children += [child]
187 child.parents += [self]
189 def remove_child(self, child: Todo) -> None:
190 """Remove child from self.children, update counter relations."""
191 if child not in self.children:
192 raise HandledException('Cannot remove un-parented child.')
193 self.children.remove(child)
194 child.parents.remove(self)
196 def save(self, db_conn: DatabaseConnection) -> None:
197 """Write self and children to DB and its cache."""
198 if self.process.id_ is None:
199 raise NotFoundException('Process of Todo without ID (not saved?)')
200 self.save_core(db_conn)
201 assert isinstance(self.id_, int)
202 db_conn.cached_todos[self.id_] = self
203 db_conn.rewrite_relations('todo_children', 'parent', self.id_,
204 [[c.id_] for c in self.children])
205 db_conn.rewrite_relations('todo_conditions', 'todo', self.id_,
206 [[c.id_] for c in self.conditions])
207 db_conn.rewrite_relations('todo_enables', 'todo', self.id_,
208 [[c.id_] for c in self.enables])
209 db_conn.rewrite_relations('todo_disables', 'todo', self.id_,
210 [[c.id_] for c in self.disables])