| 45 | |
| 46 | |
| 47 | class Node(Base): |
| 48 | __tablename__ = "node" |
| 49 | |
| 50 | id = Column(Integer, primary_key=True, autoincrement=False) |
| 51 | path = Column(String(500), nullable=False, index=True) |
| 52 | |
| 53 | # To find the descendants of this node, we look for nodes whose path |
| 54 | # starts with this node's path. |
| 55 | descendants = relationship( |
| 56 | "Node", |
| 57 | viewonly=True, |
| 58 | order_by=path, |
| 59 | primaryjoin=remote(foreign(path)).like(path.concat(".%")), |
| 60 | ) |
| 61 | |
| 62 | # Finding the ancestors is a little bit trickier. We need to create a fake |
| 63 | # secondary table since this behaves like a many-to-many join. |
| 64 | secondary = select( |
| 65 | id.label("id"), |
| 66 | func.unnest( |
| 67 | cast( |
| 68 | func.string_to_array( |
| 69 | func.regexp_replace(path, r"\.?\d+$", ""), "." |
| 70 | ), |
| 71 | ARRAY(Integer), |
| 72 | ) |
| 73 | ).label("ancestor_id"), |
| 74 | ).alias() |
| 75 | ancestors = relationship( |
| 76 | "Node", |
| 77 | viewonly=True, |
| 78 | secondary=secondary, |
| 79 | primaryjoin=id == secondary.c.id, |
| 80 | secondaryjoin=secondary.c.ancestor_id == id, |
| 81 | order_by=path, |
| 82 | ) |
| 83 | |
| 84 | @property |
| 85 | def depth(self): |
| 86 | return len(self.path.split(".")) - 1 |
| 87 | |
| 88 | def __repr__(self): |
| 89 | return f"Node(id={self.id})" |
| 90 | |
| 91 | def __str__(self): |
| 92 | root_depth = self.depth |
| 93 | s = [str(self.id)] |
| 94 | s.extend( |
| 95 | ((n.depth - root_depth) * " " + str(n.id)) |
| 96 | for n in self.descendants |
| 97 | ) |
| 98 | return "\n".join(s) |
| 99 | |
| 100 | def move_to(self, new_parent): |
| 101 | new_path = new_parent.path + "." + str(self.id) |
| 102 | for n in self.descendants: |
| 103 | n.path = new_path + n.path[len(self.path) :] |
| 104 | self.path = new_path |
no test coverage detected