home · contact · privacy
Fix bug of same dict being used for different Classes' caches.
[plomtask] / plomtask / todos.py
index fd72af6bf8c0842f2a2727185c2b4c3f707a20d4..5901571d6e2e52c1539e48587d6e8ea7590fb1d1 100644 (file)
@@ -1,15 +1,25 @@
 """Actionables."""
 from __future__ import annotations
 """Actionables."""
 from __future__ import annotations
+from dataclasses import dataclass
 from typing import Any
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
 from plomtask.processes import Process
 from typing import Any
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
 from plomtask.processes import Process
-from plomtask.conditions import Condition
+from plomtask.conditions import Condition, ConditionsRelations
 from plomtask.exceptions import (NotFoundException, BadFormatException,
                                  HandledException)
 
 
 from plomtask.exceptions import (NotFoundException, BadFormatException,
                                  HandledException)
 
 
-class Todo(BaseModel):
+@dataclass
+class TodoStepsNode:
+    """Collects what's useful to know for Todo/Condition tree display."""
+    item: Todo | Condition
+    is_todo: bool
+    children: list[TodoStepsNode]
+    seen: bool
+
+
+class Todo(BaseModel[int], ConditionsRelations):
     """Individual actionable."""
 
     # pylint: disable=too-many-instance-attributes
     """Individual actionable."""
 
     # pylint: disable=too-many-instance-attributes
@@ -26,12 +36,12 @@ class Todo(BaseModel):
         self.children: list[Todo] = []
         self.parents: list[Todo] = []
         self.conditions: list[Condition] = []
         self.children: list[Todo] = []
         self.parents: list[Todo] = []
         self.conditions: list[Condition] = []
-        self.fulfills: list[Condition] = []
-        self.undoes: list[Condition] = []
+        self.enables: list[Condition] = []
+        self.disables: list[Condition] = []
         if not self.id_:
         if not self.id_:
-            self.conditions = process.conditions[:]
-            self.fulfills = process.fulfills[:]
-            self.undoes = process.undoes[:]
+            self.conditions = self.process.conditions[:]
+            self.enables = self.process.enables[:]
+            self.disables = self.process.disables[:]
 
     @classmethod
     def from_table_row(cls, db_conn: DatabaseConnection,
 
     @classmethod
     def from_table_row(cls, db_conn: DatabaseConnection,
@@ -59,13 +69,13 @@ class Todo(BaseModel):
             for t_id in db_conn.column_where('todo_children', 'parent',
                                              'child', id_):
                 todo.parents += [cls.by_id(db_conn, t_id)]
             for t_id in db_conn.column_where('todo_children', 'parent',
                                              'child', id_):
                 todo.parents += [cls.by_id(db_conn, t_id)]
-            for name in ('conditions', 'fulfills', 'undoes'):
+            for name in ('conditions', 'enables', 'disables'):
                 table = f'todo_{name}'
                 table = f'todo_{name}'
+                assert isinstance(todo.id_, int)
                 for cond_id in db_conn.column_where(table, 'condition',
                                                     'todo', todo.id_):
                     target = getattr(todo, name)
                     target += [Condition.by_id(db_conn, cond_id)]
                 for cond_id in db_conn.column_where(table, 'condition',
                                                     'todo', todo.id_):
                     target = getattr(todo, name)
                     target += [Condition.by_id(db_conn, cond_id)]
-        assert isinstance(todo, Todo)
         return todo
 
     @classmethod
         return todo
 
     @classmethod
@@ -76,31 +86,30 @@ class Todo(BaseModel):
             todos += [cls.by_id(db_conn, id_)]
         return todos
 
             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
     @classmethod
-    def enablers_for_at(cls, db_conn: DatabaseConnection, condition: Condition,
-                        date: str) -> list[Todo]:
+    def enablers_for_at(cls, db_conn: DatabaseConnection,
+                        condition: Condition, date: str) -> list[Todo]:
         """Collect all Todos of day that enable condition."""
         """Collect all Todos of day that enable condition."""
-        assert isinstance(condition.id_, int)
-        enablers = []
-        for id_ in db_conn.column_where('todo_fulfills', 'todo', 'condition',
-                                        condition.id_):
-            todo = cls.by_id(db_conn, id_)
-            if todo.date == date:
-                enablers += [todo]
-        return enablers
+        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."""
 
     @classmethod
     def disablers_for_at(cls, db_conn: DatabaseConnection,
                          condition: Condition, date: str) -> list[Todo]:
         """Collect all Todos of day that disable condition."""
-        assert isinstance(condition.id_, int)
-        disablers = []
-        for id_ in db_conn.column_where('todo_undoes', 'todo', 'condition',
-                                        condition.id_):
-            todo = cls.by_id(db_conn, id_)
-            if todo.date == date:
-                disablers += [todo]
-        return disablers
+        return cls._x_ablers_for_at(db_conn, 'disables', condition, date)
 
     @property
     def is_doable(self) -> bool:
 
     @property
     def is_doable(self) -> bool:
@@ -118,6 +127,16 @@ class Todo(BaseModel):
         """Return ID of tasked Process."""
         return self.process.id_
 
         """Return ID of tasked Process."""
         return self.process.id_
 
+    @property
+    def unsatisfied_dependencies(self) -> list[int]:
+        """Return Process IDs of .process.explicit_steps not in .children."""
+        unsatisfied = [s.step_process_id for s in self.process.explicit_steps
+                       if s.parent_step_id is None]
+        for child_process_id in [c.process.id_ for c in self.children]:
+            if child_process_id in unsatisfied:
+                unsatisfied.remove(child_process_id)
+        return unsatisfied
+
     @property
     def is_done(self) -> bool:
         """Wrapper around self._is_done so we can control its setter."""
     @property
     def is_done(self) -> bool:
         """Wrapper around self._is_done so we can control its setter."""
@@ -130,36 +149,65 @@ class Todo(BaseModel):
         if self._is_done != value:
             self._is_done = value
             if value is True:
         if self._is_done != value:
             self._is_done = value
             if value is True:
-                for condition in self.fulfills:
+                for condition in self.enables:
                     condition.is_active = True
                     condition.is_active = True
-                for condition in self.undoes:
+                for condition in self.disables:
                     condition.is_active = False
 
                     condition.is_active = False
 
-    def set_undoes(self, db_conn: DatabaseConnection, ids: list[int]) -> None:
-        """Set self.undoes to Conditions identified by ids."""
-        self.set_conditions(db_conn, ids, 'undoes')
-
-    def set_fulfills(self, db_conn: DatabaseConnection,
-                     ids: list[int]) -> None:
-        """Set self.fulfills to Conditions identified by ids."""
-        self.set_conditions(db_conn, ids, 'fulfills')
-
-    def set_conditions(self, db_conn: DatabaseConnection, ids: list[int],
-                       target: str = 'conditions') -> None:
-        """Set self.[target] to Conditions identified by ids."""
-        target_list = getattr(self, target)
-        while len(target_list) > 0:
-            target_list.pop()
-        for id_ in ids:
-            target_list += [Condition.by_id(db_conn, id_)]
+    def adopt_from(self, todos: list[Todo]) -> None:
+        """As far as possible, fill unsatisfied dependencies from todos."""
+        for process_id in self.unsatisfied_dependencies:
+            for todo in [t for t in todos if t.process.id_ == process_id
+                         and t not in self.children]:
+                self.add_child(todo)
+                break
+
+    def make_missing_children(self, db_conn: DatabaseConnection) -> None:
+        """Fill unsatisfied dependencies with new Todos."""
+        for process_id in self.unsatisfied_dependencies:
+            process = Process.by_id(db_conn, process_id)
+            todo = self.__class__(None, process, False, self.date)
+            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 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:
+                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:
 
     def add_child(self, child: Todo) -> None:
-        """Add child to self.children, guard against recursion"""
+        """Add child to self.children, avoid recursion, update parenthoods."""
+
         def walk_steps(node: Todo) -> None:
             if node.id_ == self.id_:
                 raise BadFormatException('bad child choice causes recursion')
             for child in node.children:
                 walk_steps(child)
         def walk_steps(node: Todo) -> None:
             if node.id_ == self.id_:
                 raise BadFormatException('bad child choice causes recursion')
             for child in node.children:
                 walk_steps(child)
+
         if self.id_ is None:
             raise HandledException('Can only add children to saved Todos.')
         if child.id_ is None:
         if self.id_ is None:
             raise HandledException('Can only add children to saved Todos.')
         if child.id_ is None:
@@ -170,18 +218,24 @@ class Todo(BaseModel):
         self.children += [child]
         child.parents += [self]
 
         self.children += [child]
         child.parents += [self]
 
+    def remove_child(self, child: Todo) -> None:
+        """Remove child from self.children, update counter relations."""
+        if child not in self.children:
+            raise HandledException('Cannot remove un-parented child.')
+        self.children.remove(child)
+        child.parents.remove(self)
+
     def save(self, db_conn: DatabaseConnection) -> None:
         """Write self and children to DB and its cache."""
         if self.process.id_ is None:
             raise NotFoundException('Process of Todo without ID (not saved?)')
         self.save_core(db_conn)
         assert isinstance(self.id_, int)
     def save(self, db_conn: DatabaseConnection) -> None:
         """Write self and children to DB and its cache."""
         if self.process.id_ is None:
             raise NotFoundException('Process of Todo without ID (not saved?)')
         self.save_core(db_conn)
         assert isinstance(self.id_, int)
-        db_conn.cached_todos[self.id_] = self
         db_conn.rewrite_relations('todo_children', 'parent', self.id_,
                                   [[c.id_] for c in self.children])
         db_conn.rewrite_relations('todo_conditions', 'todo', self.id_,
                                   [[c.id_] for c in self.conditions])
         db_conn.rewrite_relations('todo_children', 'parent', self.id_,
                                   [[c.id_] for c in self.children])
         db_conn.rewrite_relations('todo_conditions', 'todo', self.id_,
                                   [[c.id_] for c in self.conditions])
-        db_conn.rewrite_relations('todo_fulfills', 'todo', self.id_,
-                                  [[c.id_] for c in self.fulfills])
-        db_conn.rewrite_relations('todo_undoes', 'todo', self.id_,
-                                  [[c.id_] for c in self.undoes])
+        db_conn.rewrite_relations('todo_enables', 'todo', self.id_,
+                                  [[c.id_] for c in self.enables])
+        db_conn.rewrite_relations('todo_disables', 'todo', self.id_,
+                                  [[c.id_] for c in self.disables])