+ def used_as_step_by(self, db_conn: DatabaseConnection) -> list[Process]:
+ """Return Processes using self for a ProcessStep."""
+ owner_ids = set()
+ for owner_id in db_conn.exec('SELECT owner_id FROM process_steps WHERE'
+ ' step_process_id = ?', (self.id_,)):
+ owner_ids.add(owner_id[0])
+ return [self.__class__.by_id(db_conn, id_) for id_ in owner_ids]
+
+ def get_steps(self, db_conn: DatabaseConnection, external_owner:
+ Process | None = None) -> dict[int, dict[str, object]]:
+ """Return tree of depended-on explicit and implicit ProcessSteps."""
+
+ def make_node(step: ProcessStep) -> dict[str, object]:
+ 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)
+ return {'process': process, 'parent_id': step.parent_step_id,
+ 'is_explicit': is_explicit, 'steps': step_steps}
+
+ def walk_steps(node_id: int, node: dict[str, Any]) -> None:
+ explicit_children = [s for s in self.explicit_steps
+ if s.parent_step_id == node_id]
+ for child in explicit_children:
+ node['steps'][child.id_] = make_node(child)
+ node['seen'] = node_id in seen_step_ids
+ seen_step_ids.add(node_id)
+ for id_, step in node['steps'].items():
+ walk_steps(id_, step)
+
+ steps: dict[int, dict[str, object]] = {}
+ seen_step_ids: Set[int] = set()
+ if external_owner is None:
+ external_owner = self
+ 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)
+ for step_id, step_node in steps.items():
+ walk_steps(step_id, step_node)
+ return steps
+
+ def set_conditions(self, db_conn: DatabaseConnection, ids: list[int],
+ trgt: str = 'conditions') -> None:
+ """Set self.[target] to Conditions identified by ids."""
+ trgt_list = getattr(self, trgt)
+ while len(trgt_list) > 0:
+ trgt_list.pop()
+ for id_ in ids:
+ trgt_list += [Condition.by_id(db_conn, id_)]
+
+ 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_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 _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.
+
+ Also checks against step recursion.
+
+ 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.
+ """
+ def walk_steps(node: ProcessStep) -> None:
+ if node.step_process_id == self.id_:
+ raise BadFormatException('bad step selection causes recursion')
+ step_process = self.by_id(db_conn, node.step_process_id)
+ 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)
+ 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
+
+ def set_steps(self, db_conn: DatabaseConnection,
+ steps: list[tuple[int | None, int, int | None]]) -> None:
+ """Set self.explicit_steps in bulk."""
+ for step in self.explicit_steps:
+ assert isinstance(step.id_, int)
+ del db_conn.cached_process_steps[step.id_]
+ self.explicit_steps = []
+ db_conn.exec('DELETE FROM process_steps WHERE owner_id = ?',
+ (self.id_,))
+ for step_tuple in steps:
+ self._add_step(db_conn, step_tuple[0],
+ step_tuple[1], step_tuple[2])
+