home · contact · privacy
Slightly improve and re-organize Condition tests.
[plomtask] / plomtask / processes.py
index 375a0bee622081fd8690cc0e6c1a8ae5241685e1..bb1de3a4a3356415473bc652d650e202886eb01b 100644 (file)
@@ -4,7 +4,7 @@ from dataclasses import dataclass
 from typing import Set, Any
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
 from typing import Set, Any
 from sqlite3 import Row
 from plomtask.db import DatabaseConnection, BaseModel
-from plomtask.misc import VersionedAttribute
+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)
@@ -17,46 +17,62 @@ class ProcessStepsNode:
     parent_id: int | None
     is_explicit: bool
     steps: dict[int, ProcessStepsNode]
     parent_id: int | None
     is_explicit: bool
     steps: dict[int, ProcessStepsNode]
-    seen: bool
+    seen: bool = False
+    is_suppressed: bool = False
 
 
 class Process(BaseModel[int], ConditionsRelations):
     """Template for, and metadata for, Todos, and their arrangements."""
 
 
 class Process(BaseModel[int], ConditionsRelations):
     """Template for, and metadata for, Todos, and their arrangements."""
-    table_name = 'processes'
-
     # pylint: disable=too-many-instance-attributes
     # pylint: disable=too-many-instance-attributes
-
-    def __init__(self, id_: int | None) -> None:
-        super().__init__(id_)
+    table_name = 'processes'
+    to_save = ['calendarize']
+    to_save_versioned = ['title', 'description', 'effort']
+    to_save_relations = [('process_conditions', 'process', 'conditions', 0),
+                         ('process_blockers', 'process', 'blockers', 0),
+                         ('process_enables', 'process', 'enables', 0),
+                         ('process_disables', 'process', 'disables', 0),
+                         ('process_step_suppressions', 'process',
+                          'suppressed_steps', 0)]
+    add_to_dict = ['explicit_steps']
+    to_search = ['title.newest', 'description.newest']
+    can_create_by_id = True
+    sorters = {'steps': lambda p: len(p.explicit_steps),
+               'owners': lambda p: p.n_owners,
+               'effort': lambda p: p.effort.newest,
+               'title': lambda p: p.title.newest}
+
+    def __init__(self, id_: int | None, calendarize: bool = False) -> None:
+        BaseModel.__init__(self, id_)
+        ConditionsRelations.__init__(self)
         self.title = VersionedAttribute(self, 'process_titles', 'UNNAMED')
         self.description = VersionedAttribute(self, 'process_descriptions', '')
         self.effort = VersionedAttribute(self, 'process_efforts', 1.0)
         self.explicit_steps: list[ProcessStep] = []
         self.title = VersionedAttribute(self, 'process_titles', 'UNNAMED')
         self.description = VersionedAttribute(self, 'process_descriptions', '')
         self.effort = VersionedAttribute(self, 'process_efforts', 1.0)
         self.explicit_steps: list[ProcessStep] = []
-        self.conditions: list[Condition] = []
-        self.enables: list[Condition] = []
-        self.disables: list[Condition] = []
+        self.suppressed_steps: list[ProcessStep] = []
+        self.calendarize = calendarize
+        self.n_owners: int | None = None  # only set by from_table_row
 
     @classmethod
     def from_table_row(cls, db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> Process:
         """Make from DB row, with dependencies."""
         process = super().from_table_row(db_conn, row)
 
     @classmethod
     def from_table_row(cls, db_conn: DatabaseConnection,
                        row: Row | list[Any]) -> Process:
         """Make from DB row, with dependencies."""
         process = super().from_table_row(db_conn, row)
-        assert isinstance(process.id_, int)
-        for name in ('title', 'description', 'effort'):
-            table = f'process_{name}s'
-            for row_ in db_conn.row_where(table, 'parent', process.id_):
-                getattr(process, name).history_from_row(row_)
-        for row_ in db_conn.row_where('process_steps', 'owner',
-                                      process.id_):
-            step = ProcessStep.from_table_row(db_conn, row_)
-            process.explicit_steps += [step]  # pylint: disable=no-member
-        for name in ('conditions', 'enables', 'disables'):
+        assert process.id_ is not None
+        for name in ('conditions', 'blockers', 'enables', 'disables'):
             table = f'process_{name}'
             assert isinstance(process.id_, int)
             for c_id in db_conn.column_where(table, 'condition',
                                              'process', process.id_):
                 target = getattr(process, name)
                 target += [Condition.by_id(db_conn, c_id)]
             table = f'process_{name}'
             assert isinstance(process.id_, int)
             for c_id in db_conn.column_where(table, 'condition',
                                              'process', process.id_):
                 target = getattr(process, name)
                 target += [Condition.by_id(db_conn, c_id)]
+        for row_ in db_conn.row_where('process_steps', 'owner', process.id_):
+            step = ProcessStep.from_table_row(db_conn, row_)
+            process.explicit_steps += [step]
+        for row_ in db_conn.row_where('process_step_suppressions', 'process',
+                                      process.id_):
+            step = ProcessStep.by_id(db_conn, row_[1])
+            process.suppressed_steps += [step]
+        process.n_owners = len(process.used_as_step_by(db_conn))
         return process
 
     def used_as_step_by(self, db_conn: DatabaseConnection) -> list[Process]:
         return process
 
     def used_as_step_by(self, db_conn: DatabaseConnection) -> list[Process]:
@@ -73,23 +89,27 @@ class Process(BaseModel[int], ConditionsRelations):
                   Process | None = None) -> dict[int, ProcessStepsNode]:
         """Return tree of depended-on explicit and implicit ProcessSteps."""
 
                   Process | None = None) -> dict[int, ProcessStepsNode]:
         """Return tree of depended-on explicit and implicit ProcessSteps."""
 
-        def make_node(step: ProcessStep) -> ProcessStepsNode:
+        def make_node(step: ProcessStep, suppressed: bool) -> ProcessStepsNode:
             is_explicit = False
             if external_owner is not None:
                 is_explicit = step.owner_id == external_owner.id_
             process = self.__class__.by_id(db_conn, step.step_process_id)
             is_explicit = False
             if external_owner is not None:
                 is_explicit = step.owner_id == external_owner.id_
             process = self.__class__.by_id(db_conn, step.step_process_id)
-            step_steps = process.get_steps(db_conn, external_owner)
+            step_steps = {}
+            if not suppressed:
+                step_steps = process.get_steps(db_conn, external_owner)
             return ProcessStepsNode(process, step.parent_step_id,
             return ProcessStepsNode(process, step.parent_step_id,
-                                    is_explicit, step_steps, False)
+                                    is_explicit, step_steps, False, suppressed)
 
         def walk_steps(node_id: int, node: ProcessStepsNode) -> None:
 
         def walk_steps(node_id: int, node: ProcessStepsNode) -> None:
+            node.seen = node_id in seen_step_ids
+            seen_step_ids.add(node_id)
+            if node.is_suppressed:
+                return
             explicit_children = [s for s in self.explicit_steps
                                  if s.parent_step_id == node_id]
             for child in explicit_children:
                 assert isinstance(child.id_, int)
             explicit_children = [s for s in self.explicit_steps
                                  if s.parent_step_id == node_id]
             for child in explicit_children:
                 assert isinstance(child.id_, int)
-                node.steps[child.id_] = make_node(child)
-            node.seen = node_id in seen_step_ids
-            seen_step_ids.add(node_id)
+                node.steps[child.id_] = make_node(child, False)
             for id_, step in node.steps.items():
                 walk_steps(id_, step)
 
             for id_, step in node.steps.items():
                 walk_steps(id_, step)
 
@@ -100,26 +120,27 @@ class Process(BaseModel[int], ConditionsRelations):
         for step in [s for s in self.explicit_steps
                      if s.parent_step_id is None]:
             assert isinstance(step.id_, int)
         for step in [s for s in self.explicit_steps
                      if s.parent_step_id is None]:
             assert isinstance(step.id_, int)
-            steps[step.id_] = make_node(step)
+            new_node = make_node(step, step in external_owner.suppressed_steps)
+            steps[step.id_] = new_node
         for step_id, step_node in steps.items():
             walk_steps(step_id, step_node)
         return steps
 
         for step_id, step_node in steps.items():
             walk_steps(step_id, step_node)
         return steps
 
-    def _add_step(self,
-                  db_conn: DatabaseConnection,
-                  id_: int | None,
-                  step_process_id: int,
-                  parent_step_id: int | None) -> ProcessStep:
-        """Create new ProcessStep, save and add it to self.explicit_steps.
+    def set_step_suppressions(self, db_conn: DatabaseConnection,
+                              step_ids: list[int]) -> None:
+        """Set self.suppressed_steps from step_ids."""
+        assert isinstance(self.id_, int)
+        db_conn.delete_where('process_step_suppressions', 'process', self.id_)
+        self.suppressed_steps = [ProcessStep.by_id(db_conn, s)
+                                 for s in step_ids]
 
 
-        Also checks against step recursion.
+    def set_steps(self, db_conn: DatabaseConnection,
+                  steps: list[ProcessStep]) -> None:
+        """Set self.explicit_steps in bulk.
 
 
-        The new step's parent_step_id will fall back to None either if no
-        matching ProcessStep is found (which can be assumed in case it was
-        just deleted under its feet), or if the parent step would not be
-        owned by the current Process.
+        Checks against recursion, and turns into top-level steps any of
+        unknown or non-owned parent.
         """
         """
-
         def walk_steps(node: ProcessStep) -> None:
             if node.step_process_id == self.id_:
                 raise BadFormatException('bad step selection causes recursion')
         def walk_steps(node: ProcessStep) -> None:
             if node.step_process_id == self.id_:
                 raise BadFormatException('bad step selection causes recursion')
@@ -127,64 +148,62 @@ class Process(BaseModel[int], ConditionsRelations):
             for step in step_process.explicit_steps:
                 walk_steps(step)
 
             for step in step_process.explicit_steps:
                 walk_steps(step)
 
-        if parent_step_id is not None:
-            try:
-                parent_step = ProcessStep.by_id(db_conn, parent_step_id)
-                if parent_step.owner_id != self.id_:
-                    parent_step_id = None
-            except NotFoundException:
-                parent_step_id = None
         assert isinstance(self.id_, int)
         assert isinstance(self.id_, int)
-        step = ProcessStep(id_, self.id_, step_process_id, parent_step_id)
-        walk_steps(step)
-        self.explicit_steps += [step]
-        step.save(db_conn)  # NB: This ensures a non-None step.id_.
-        return step
+        for step in [s for s in self.explicit_steps if s not in steps]:
+            step.remove(db_conn)
+        for step in [s for s in steps if s not in self.explicit_steps]:
+            if step.parent_step_id is not None:
+                try:
+                    parent_step = ProcessStep.by_id(db_conn,
+                                                    step.parent_step_id)
+                    if parent_step.owner_id != self.id_:
+                        step.parent_step_id = None
+                except NotFoundException:
+                    step.parent_step_id = None
+            walk_steps(step)
+            step.save(db_conn)
 
 
-    def set_steps(self, db_conn: DatabaseConnection,
-                  steps: list[tuple[int | None, int, int | None]]) -> None:
-        """Set self.explicit_steps in bulk."""
-        assert isinstance(self.id_, int)
-        for step in self.explicit_steps:
-            step.uncache()
-        self.explicit_steps = []
-        db_conn.delete_where('process_steps', 'owner', self.id_)
-        for step_tuple in steps:
-            self._add_step(db_conn, step_tuple[0],
-                           step_tuple[1], step_tuple[2])
+    def set_owners(self, db_conn: DatabaseConnection,
+                   owner_ids: list[int]) -> None:
+        """Re-set owners to those identified in owner_ids."""
+        owners_old = self.used_as_step_by(db_conn)
+        losers = [o for o in owners_old if o.id_ not in owner_ids]
+        owners_old_ids = [o.id_ for o in owners_old]
+        winners = [Process.by_id(db_conn, id_) for id_ in owner_ids
+                   if id_ not in owners_old_ids]
+        steps_to_remove = []
+        for loser in losers:
+            steps_to_remove += [s for s in loser.explicit_steps
+                                if s.step_process_id == self.id_]
+        for step in steps_to_remove:
+            step.remove(db_conn)
+        for winner in winners:
+            assert isinstance(winner.id_, int)
+            assert isinstance(self.id_, int)
+            new_step = ProcessStep(None, winner.id_, self.id_, None)
+            new_explicit_steps = winner.explicit_steps + [new_step]
+            winner.set_steps(db_conn, new_explicit_steps)
 
     def save(self, db_conn: DatabaseConnection) -> None:
         """Add (or re-write) self and connected items to DB."""
 
     def save(self, db_conn: DatabaseConnection) -> None:
         """Add (or re-write) self and connected items to DB."""
-        self.save_core(db_conn)
+        super().save(db_conn)
         assert isinstance(self.id_, int)
         assert isinstance(self.id_, int)
-        self.title.save(db_conn)
-        self.description.save(db_conn)
-        self.effort.save(db_conn)
-        db_conn.rewrite_relations('process_conditions', 'process', self.id_,
-                                  [[c.id_] for c in self.conditions])
-        db_conn.rewrite_relations('process_enables', 'process', self.id_,
-                                  [[c.id_] for c in self.enables])
-        db_conn.rewrite_relations('process_disables', 'process', self.id_,
-                                  [[c.id_] for c in self.disables])
         db_conn.delete_where('process_steps', 'owner', self.id_)
         for step in self.explicit_steps:
             step.save(db_conn)
 
     def remove(self, db_conn: DatabaseConnection) -> None:
         db_conn.delete_where('process_steps', 'owner', self.id_)
         for step in self.explicit_steps:
             step.save(db_conn)
 
     def remove(self, db_conn: DatabaseConnection) -> None:
-        """Remove from DB, with dependencies."""
+        """Remove from DB, with dependencies.
+
+        Guard against removal of Processes in use.
+        """
         assert isinstance(self.id_, int)
         for _ in db_conn.row_where('process_steps', 'step_process', self.id_):
             raise HandledException('cannot remove Process in use')
         for _ in db_conn.row_where('todos', 'process', self.id_):
             raise HandledException('cannot remove Process in use')
         assert isinstance(self.id_, int)
         for _ in db_conn.row_where('process_steps', 'step_process', self.id_):
             raise HandledException('cannot remove Process in use')
         for _ in db_conn.row_where('todos', 'process', self.id_):
             raise HandledException('cannot remove Process in use')
-        db_conn.delete_where('process_conditions', 'process', self.id_)
-        db_conn.delete_where('process_enables', 'process', self.id_)
-        db_conn.delete_where('process_disables', 'process', self.id_)
         for step in self.explicit_steps:
             step.remove(db_conn)
         for step in self.explicit_steps:
             step.remove(db_conn)
-        db_conn.delete_where('process_titles', 'parent', self.id_)
-        db_conn.delete_where('process_descriptions', 'parent', self.id_)
-        db_conn.delete_where('process_efforts', 'parent', self.id_)
         super().remove(db_conn)
 
 
         super().remove(db_conn)
 
 
@@ -201,8 +220,14 @@ class ProcessStep(BaseModel[int]):
         self.parent_step_id = parent_step_id
 
     def save(self, db_conn: DatabaseConnection) -> None:
         self.parent_step_id = parent_step_id
 
     def save(self, db_conn: DatabaseConnection) -> None:
-        """Default to simply calling self.save_core for simple cases."""
-        self.save_core(db_conn)
+        """Update into DB/cache, and owner's .explicit_steps."""
+        super().save(db_conn)
+        owner = Process.by_id(db_conn, self.owner_id)
+        if self not in owner.explicit_steps:
+            for s in [s for s in owner.explicit_steps if s.id_ == self.id_]:
+                s.remove(db_conn)
+            owner.explicit_steps += [self]
+        owner.explicit_steps.sort(key=hash)
 
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB, and owner's .explicit_steps."""
 
     def remove(self, db_conn: DatabaseConnection) -> None:
         """Remove from DB, and owner's .explicit_steps."""