home · contact · privacy
Add Todo.effort.
[plomtask] / plomtask / todos.py
index cf4c33048ba779fb2145e21a4f325f55b6bfe024..7cbe989b538018e480d24752d2d0cc44450f7396 100644 (file)
@@ -1,76 +1,83 @@
 """Actionables."""
 from __future__ import annotations
 """Actionables."""
 from __future__ import annotations
-from collections import namedtuple
+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.versioned_attributes import VersionedAttribute
 from plomtask.conditions import Condition, ConditionsRelations
 from plomtask.exceptions import (NotFoundException, BadFormatException,
                                  HandledException)
 
 
 from plomtask.conditions import Condition, ConditionsRelations
 from plomtask.exceptions import (NotFoundException, BadFormatException,
                                  HandledException)
 
 
-TodoStepsNode = namedtuple('TodoStepsNode',
-                           ('item', 'is_todo', 'children', 'seen'))
+@dataclass
+class TodoNode:
+    """Collects what's useful to know for Todo/Condition tree display."""
+    todo: Todo
+    seen: bool
+    children: list[TodoNode]
 
 
 
 
-class Todo(BaseModel, ConditionsRelations):
+class Todo(BaseModel[int], ConditionsRelations):
     """Individual actionable."""
     """Individual actionable."""
-
     # pylint: disable=too-many-instance-attributes
     # pylint: disable=too-many-instance-attributes
-
     table_name = 'todos'
     table_name = 'todos'
-    to_save = ['process_id', 'is_done', 'date']
-
+    to_save = ['process_id', 'is_done', 'date', 'comment', 'effort']
+    to_save_relations = [('todo_conditions', 'todo', 'conditions'),
+                         ('todo_enables', 'todo', 'enables'),
+                         ('todo_disables', 'todo', 'disables'),
+                         ('todo_children', 'parent', 'children'),
+                         ('todo_children', 'child', 'parents')]
+
+    # pylint: disable=too-many-arguments
     def __init__(self, id_: int | None, process: Process,
     def __init__(self, id_: int | None, process: Process,
-                 is_done: bool, date: str) -> None:
-        self.set_int_id(id_)
+                 is_done: bool, date: str, comment: str = '',
+                 effort: None | float = None) -> None:
+        super().__init__(id_)
+        if process.id_ is None:
+            raise NotFoundException('Process of Todo without ID (not saved?)')
         self.process = process
         self._is_done = is_done
         self.date = date
         self.process = process
         self._is_done = is_done
         self.date = date
+        self.comment = comment
+        self.effort = effort
         self.children: list[Todo] = []
         self.parents: list[Todo] = []
         self.conditions: list[Condition] = []
         self.enables: list[Condition] = []
         self.disables: list[Condition] = []
         if not self.id_:
         self.children: list[Todo] = []
         self.parents: list[Todo] = []
         self.conditions: list[Condition] = []
         self.enables: list[Condition] = []
         self.disables: list[Condition] = []
         if not self.id_:
-            self.conditions = process.conditions[:]
-            self.enables = process.enables[:]
-            self.disables = process.disables[:]
+            self.conditions = self.process.conditions[:]
+            self.enables = self.process.enables[:]
+            self.disables = self.process.disables[:]
 
     @classmethod
     def from_table_row(cls, db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> Todo:
 
     @classmethod
     def from_table_row(cls, db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> Todo:
-        """Make from DB row, write to DB cache."""
+        """Make from DB row, with dependencies."""
         if row[1] == 0:
             raise NotFoundException('calling Todo of '
                                     'unsaved Process')
         row_as_list = list(row)
         row_as_list[1] = Process.by_id(db_conn, row[1])
         todo = super().from_table_row(db_conn, row_as_list)
         if row[1] == 0:
             raise NotFoundException('calling Todo of '
                                     'unsaved Process')
         row_as_list = list(row)
         row_as_list[1] = Process.by_id(db_conn, row[1])
         todo = super().from_table_row(db_conn, row_as_list)
-        assert isinstance(todo, Todo)
-        return todo
-
-    @classmethod
-    def by_id(cls, db_conn: DatabaseConnection, id_: int) -> Todo:
-        """Get Todo of .id_=id_ and children (from DB cache if possible)."""
-        todo, from_cache = super()._by_id(db_conn, id_)
-        if todo is None:
-            raise NotFoundException(f'Todo of ID not found: {id_}')
-        if not from_cache:
-            for t_id in db_conn.column_where('todo_children', 'child',
-                                             'parent', id_):
-                todo.children += [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', 'enables', 'disables'):
-                table = f'todo_{name}'
-                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)
+        assert isinstance(todo.id_, int)
+        for t_id in db_conn.column_where('todo_children', 'child',
+                                         'parent', todo.id_):
+            # pylint: disable=no-member
+            todo.children += [cls.by_id(db_conn, t_id)]
+        for t_id in db_conn.column_where('todo_children', 'parent',
+                                         'child', todo.id_):
+            # pylint: disable=no-member
+            todo.parents += [cls.by_id(db_conn, t_id)]
+        for name in ('conditions', 'enables', 'disables'):
+            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)]
         return todo
 
     @classmethod
         return todo
 
     @classmethod
@@ -81,31 +88,6 @@ class Todo(BaseModel, ConditionsRelations):
             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
-    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 is_doable(self) -> bool:
         """Decide whether .is_done settable based on children, Conditions."""
@@ -119,9 +101,19 @@ class Todo(BaseModel, ConditionsRelations):
 
     @property
     def process_id(self) -> int | str | None:
 
     @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_
 
         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."""
@@ -139,43 +131,53 @@ class Todo(BaseModel, ConditionsRelations):
                 for condition in self.disables:
                     condition.is_active = False
 
                 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)
+    @property
+    def title(self) -> VersionedAttribute:
+        """Shortcut to .process.title."""
+        return self.process.title
+
+    def adopt_from(self, todos: list[Todo]) -> bool:
+        """As far as possible, fill unsatisfied dependencies from todos."""
+        adopted = False
+        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)
+                adopted = True
+                break
+        return adopted
+
+    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]) -> TodoNode:
+        """Return tree of depended-on Todos."""
+
+        def make_node(todo: Todo) -> TodoNode:
             children = []
             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
+            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)
+
+        return make_node(self)
 
     def add_child(self, child: Todo) -> None:
         """Add child to self.children, avoid recursion, update parenthoods."""
 
     def add_child(self, child: Todo) -> None:
         """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:
@@ -193,18 +195,12 @@ class Todo(BaseModel, ConditionsRelations):
         self.children.remove(child)
         child.parents.remove(self)
 
         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)
-        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_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])
+    def remove(self, db_conn: DatabaseConnection) -> None:
+        """Remove from DB, including relations."""
+        children_to_remove = self.children[:]
+        parents_to_remove = self.parents[:]
+        for child in children_to_remove:
+            self.remove_child(child)
+        for parent in parents_to_remove:
+            parent.remove_child(self)
+        super().remove(db_conn)