Add inline assign from dashboard, specific battery picker on device, dynamic install rows

- Dashboard: replace Assign link with device dropdown + arrow button for
  quick inline assignment without leaving the page
- Device detail: replace hardcoded 4-row install form with 1 row + '+ Add
  brand' button that clones rows dynamically
- Device detail: add 'Install Specific Battery' card with dropdown of all
  available batteries (label, brand, size, notes) via new /device/<id>/install-one route
- Tests: 4 new acceptance tests covering dashboard quick-assign and
  install-one, including capacity enforcement on both paths (39 total)
This commit is contained in:
2026-04-12 20:15:29 -05:00
parent 4ad29558b3
commit 81e87d2fe2
4 changed files with 170 additions and 26 deletions
+50
View File
@@ -294,6 +294,56 @@ def test_device_install_over_capacity(client):
assert b"slot" in resp.data.lower()
# ------------------------------------------------------------------ #
# Dashboard — quick-assign
# ------------------------------------------------------------------ #
def test_dashboard_quick_assign(client):
"""Quick-assign from dashboard (battery_assign POST) succeeds."""
client.post("/device/add", data={"name": "Box", "battery_slots": "2"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
resp = client.post("/battery/1/assign", data={"device_id": "1"},
follow_redirects=True)
assert resp.status_code == 200
assert b"Box" in resp.data
def test_dashboard_quick_assign_full_device_blocked(client):
"""Quick-assign from dashboard to a full device is blocked."""
client.post("/device/add", data={"name": "Box", "battery_slots": "1"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
client.post("/battery/1/assign", data={"device_id": "1"}) # fills the slot
resp = client.post("/battery/2/assign", data={"device_id": "1"},
follow_redirects=True)
assert resp.status_code == 200
assert b"full" in resp.data.lower()
# ------------------------------------------------------------------ #
# Device — install-one (specific battery)
# ------------------------------------------------------------------ #
def test_install_one_specific_battery(client):
"""device_install_one installs a chosen battery."""
client.post("/device/add", data={"name": "Box", "battery_slots": "2"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
resp = client.post("/device/1/install-one", data={"battery_id": "1"},
follow_redirects=True)
assert resp.status_code == 200
assert b"Eneloop 001" in resp.data
def test_install_one_over_capacity_blocked(client):
"""device_install_one to a full device is blocked."""
client.post("/device/add", data={"name": "Box", "battery_slots": "1"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
client.post("/device/1/install-one", data={"battery_id": "1"}) # fills slot
resp = client.post("/device/1/install-one", data={"battery_id": "2"},
follow_redirects=True)
assert resp.status_code == 200
assert b"full" in resp.data.lower()
# ------------------------------------------------------------------ #
# Device — list
# ------------------------------------------------------------------ #