fix: persist top/left/right connection-point counts (issue #243)
The backend only stored bottom_handles, so top/left/right_handles were dropped on canvas save and reset to defaults on reload — a left/right snappoint would vanish after reload. - models.py: add top_handles (default 1), left_handles (0), right_handles (0) - database.py: ALTER TABLE migrations for the three columns - schemas: add fields to NodeBase, NodeUpdate, and the canvas-save node schema - tests: save+reload round-trip and default-fallback coverage ha-relevant: no
This commit is contained in:
@@ -99,6 +99,12 @@ async def init_db() -> None:
|
|||||||
await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN height REAL")
|
await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN height REAL")
|
||||||
with suppress(OperationalError):
|
with suppress(OperationalError):
|
||||||
await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN bottom_handles INTEGER NOT NULL DEFAULT 1")
|
await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN bottom_handles INTEGER NOT NULL DEFAULT 1")
|
||||||
|
with suppress(OperationalError):
|
||||||
|
await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN top_handles INTEGER NOT NULL DEFAULT 1")
|
||||||
|
with suppress(OperationalError):
|
||||||
|
await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN left_handles INTEGER NOT NULL DEFAULT 0")
|
||||||
|
with suppress(OperationalError):
|
||||||
|
await conn.exec_driver_sql("ALTER TABLE nodes ADD COLUMN right_handles INTEGER NOT NULL DEFAULT 0")
|
||||||
with suppress(OperationalError):
|
with suppress(OperationalError):
|
||||||
await conn.exec_driver_sql("ALTER TABLE pending_devices ADD COLUMN discovery_source TEXT")
|
await conn.exec_driver_sql("ALTER TABLE pending_devices ADD COLUMN discovery_source TEXT")
|
||||||
with suppress(OperationalError):
|
with suppress(OperationalError):
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ class Node(Base):
|
|||||||
width: Mapped[float | None] = mapped_column(Float, nullable=True)
|
width: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
height: Mapped[float | None] = mapped_column(Float, nullable=True)
|
height: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
bottom_handles: Mapped[int] = mapped_column(Integer, default=1)
|
bottom_handles: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
top_handles: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
|
left_handles: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
right_handles: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
ieee_address: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
ieee_address: Mapped[str | None] = mapped_column(String, index=True, nullable=True)
|
||||||
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
last_scan: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
last_scan: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ class NodeSave(BaseModel):
|
|||||||
width: float | None = None
|
width: float | None = None
|
||||||
height: float | None = None
|
height: float | None = None
|
||||||
bottom_handles: int = 1
|
bottom_handles: int = 1
|
||||||
|
top_handles: int = 1
|
||||||
|
left_handles: int = 0
|
||||||
|
right_handles: int = 0
|
||||||
pos_x: float = 0
|
pos_x: float = 0
|
||||||
pos_y: float = 0
|
pos_y: float = 0
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class NodeBase(BaseModel):
|
|||||||
width: float | None = None
|
width: float | None = None
|
||||||
height: float | None = None
|
height: float | None = None
|
||||||
bottom_handles: int = 1
|
bottom_handles: int = 1
|
||||||
|
top_handles: int = 1
|
||||||
|
left_handles: int = 0
|
||||||
|
right_handles: int = 0
|
||||||
|
|
||||||
|
|
||||||
class NodeCreate(NodeBase):
|
class NodeCreate(NodeBase):
|
||||||
@@ -66,6 +69,9 @@ class NodeUpdate(BaseModel):
|
|||||||
width: float | None = None
|
width: float | None = None
|
||||||
height: float | None = None
|
height: float | None = None
|
||||||
bottom_handles: int | None = None
|
bottom_handles: int | None = None
|
||||||
|
top_handles: int | None = None
|
||||||
|
left_handles: int | None = None
|
||||||
|
right_handles: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeResponse(NodeBase):
|
class NodeResponse(NodeBase):
|
||||||
|
|||||||
@@ -51,6 +51,32 @@ async def test_save_canvas_creates_nodes_and_edges(client: AsyncClient, headers:
|
|||||||
assert canvas["viewport"] == {"x": 1, "y": 2, "zoom": 1.5}
|
assert canvas["viewport"] == {"x": 1, "y": 2, "zoom": 1.5}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_save_canvas_round_trips_per_side_handles(client: AsyncClient, headers: dict):
|
||||||
|
# Regression (#243): top/left/right_handles must persist across save+reload,
|
||||||
|
# not just bottom_handles.
|
||||||
|
n1 = node_payload(label="Cam", type="camera", top_handles=2, bottom_handles=3, left_handles=1, right_handles=4)
|
||||||
|
res = await client.post("/api/v1/canvas/save", json={"nodes": [n1], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1}}, headers=headers)
|
||||||
|
assert res.status_code == 200
|
||||||
|
|
||||||
|
node = (await client.get("/api/v1/canvas", headers=headers)).json()["nodes"][0]
|
||||||
|
assert node["top_handles"] == 2
|
||||||
|
assert node["bottom_handles"] == 3
|
||||||
|
assert node["left_handles"] == 1
|
||||||
|
assert node["right_handles"] == 4
|
||||||
|
|
||||||
|
|
||||||
|
async def test_save_canvas_defaults_per_side_handles(client: AsyncClient, headers: dict):
|
||||||
|
# Nodes saved without the new fields fall back to top/bottom=1, left/right=0.
|
||||||
|
n1 = node_payload(label="Srv", type="server")
|
||||||
|
await client.post("/api/v1/canvas/save", json={"nodes": [n1], "edges": [], "viewport": {"x": 0, "y": 0, "zoom": 1}}, headers=headers)
|
||||||
|
|
||||||
|
node = (await client.get("/api/v1/canvas", headers=headers)).json()["nodes"][0]
|
||||||
|
assert node["top_handles"] == 1
|
||||||
|
assert node["bottom_handles"] == 1
|
||||||
|
assert node["left_handles"] == 0
|
||||||
|
assert node["right_handles"] == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_load_canvas_exposes_inventory_timestamps(client: AsyncClient, headers: dict):
|
async def test_load_canvas_exposes_inventory_timestamps(client: AsyncClient, headers: dict):
|
||||||
n1 = node_payload(label="Router", type="router")
|
n1 = node_payload(label="Router", type="router")
|
||||||
await client.post("/api/v1/canvas/save", json={"nodes": [n1], "edges": [], "viewport": {}}, headers=headers)
|
await client.post("/api/v1/canvas/save", json={"nodes": [n1], "edges": [], "viewport": {}}, headers=headers)
|
||||||
|
|||||||
Reference in New Issue
Block a user