+ return cls.by_date_range(db_conn, (date, date))
+
+ @property
+ def is_doable(self) -> bool:
+ """Decide whether .is_done settable based on children, Conditions."""
+ for child in self.children:
+ if not child.is_done:
+ return False
+ for condition in self.conditions:
+ if not condition.is_active:
+ return False
+ for condition in self.blockers:
+ if condition.is_active:
+ return False
+ return True
+
+ @property
+ def is_deletable(self) -> bool:
+ """Decide whether self be deletable (not if preserve-worthy values)."""
+ if self.comment:
+ return False
+ if self.effort and self.effort >= 0:
+ return False
+ return True
+
+ @property
+ def process_id(self) -> int | str | None:
+ """Needed for super().save to save Processes as attributes."""
+ 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."""
+ return self._is_done
+
+ @is_done.setter
+ def is_done(self, value: bool) -> None:
+ if value != self.is_done and not self.is_doable:
+ raise BadFormatException('cannot change doneness of undoable Todo')
+ if self._is_done != value:
+ self._is_done = value
+ if value is True:
+ for condition in self.enables:
+ condition.is_active = True
+ for condition in self.disables:
+ condition.is_active = False
+
+ @property
+ def title(self) -> VersionedAttribute:
+ """Shortcut to .process.title."""
+ return self.process.title
+
+ @property
+ def title_then(self) -> str:
+ """Shortcut to .process.title.at(self.date)"""
+ title_then = self.process.title.at(self.date)
+ assert isinstance(title_then, str)
+ return title_then
+
+ @property
+ def effort_then(self) -> float:
+ """Shortcut to .process.effort.at(self.date)"""
+ effort_then = self.process.effort.at(self.date)
+ assert isinstance(effort_then, float)
+ return effort_then
+
+ 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."""
+ new_todos = self.__class__.create_with_children(
+ db_conn, self.date, self.unsatisfied_dependencies)
+ for todo in new_todos:
+ 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 = []
+ 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)