Add logbook entries, data export page, and JSON import

This commit is contained in:
2026-04-26 20:03:58 -05:00
parent 52d1105997
commit 3b2029d3b8
8 changed files with 877 additions and 6 deletions
+204 -1
View File
@@ -603,4 +603,207 @@ def test_bulk_install_filters_by_size(client):
data={"brand[]": "Energizer", "qty[]": "1"},
follow_redirects=True)
assert resp.status_code == 200
assert b"only 0 available" in resp.data
# ------------------------------------------------------------------ #
# Import
# ------------------------------------------------------------------ #
import io
import json as _json
def _make_import_payload(devices=None, batteries=None,
charge_logs=None, capacity_tests=None, pct_logs=None):
data = {
"exported_at": "2026-01-01T00:00:00",
"devices": devices or [],
"batteries": batteries or [],
"charge_logs": charge_logs or [],
"capacity_tests": capacity_tests or [],
"pct_logs": pct_logs or [],
}
return (io.BytesIO(_json.dumps(data).encode()), "export.json")
def _post_import(client, payload_tuple):
buf, fname = payload_tuple
return client.post(
"/import",
data={"file": (buf, fname)},
content_type="multipart/form-data",
)
def _bat(id, label, brand="Brand", status="available", device_id=None, **kw):
return {"id": id, "label": label, "brand": brand, "status": status,
"device_id": device_id, "size": None, "chemistry": None, "capacity_mah": None,
"tested_capacity_mah": None, "tested_date": None, "charge_cycles": None,
"purchase_date": None, "storage_location": None, "battery_percentage": None,
"notes": None, **kw}
def _dev(id, name, battery_slots=2, battery_size="AA", **kw):
return {"id": id, "name": name, "battery_slots": battery_slots,
"battery_size": battery_size, "device_type": None,
"location": None, "ha_entity_id": None, "notes": None, **kw}
def test_import_page_loads(client):
resp = client.get("/import")
assert resp.status_code == 200
assert b"Import" in resp.data
assert b'enctype="multipart/form-data"' in resp.data
def test_import_no_file_returns_400(client):
resp = client.post("/import", data={}, content_type="multipart/form-data")
assert resp.status_code == 400
def test_import_invalid_json_returns_400(client):
buf = io.BytesIO(b"this is not json")
resp = client.post("/import",
data={"file": (buf, "bad.json")},
content_type="multipart/form-data")
assert resp.status_code == 400
assert b"Invalid JSON" in resp.data
def test_import_missing_required_keys_returns_400(client):
buf = io.BytesIO(_json.dumps({"exported_at": "2026-01-01"}).encode())
resp = client.post("/import",
data={"file": (buf, "bad.json")},
content_type="multipart/form-data")
assert resp.status_code == 400
def test_import_creates_devices_and_batteries(client):
payload = _make_import_payload(
devices=[_dev(1, "RC Car")],
batteries=[_bat(1, "Eneloop 001", brand="Eneloop")],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
assert b"Import Results" in resp.data
dash = client.get("/")
assert b"Eneloop 001" in dash.data
assert b"RC Car" in dash.data
def test_import_results_show_correct_counts(client):
payload = _make_import_payload(
devices=[_dev(10, "DevX")],
batteries=[_bat(10, "BrandX 001"), _bat(11, "BrandX 002")],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
# devices_created=1, batteries_created=2 appear in the results table
assert b"Import Results" in resp.data
def test_import_charge_logs_remapped_correctly(client):
payload = _make_import_payload(
batteries=[_bat(99, "Eneloop 001", brand="Eneloop")],
charge_logs=[{"id": 1, "battery_id": 99, "battery_label": "Eneloop 001",
"charged_date": "2026-01-15", "increment_cycles": 1, "notes": None}],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
detail = client.get("/battery/1")
assert b"2026-01-15" in detail.data
def test_import_capacity_tests_remapped(client):
payload = _make_import_payload(
batteries=[_bat(5, "Test 001", capacity_mah=2000)],
capacity_tests=[{"id": 1, "battery_id": 5, "battery_label": "Test 001",
"tested_capacity_mah": 1850, "tested_date": "2026-02-01", "notes": None}],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
detail = client.get("/battery/1")
assert b"1850" in detail.data
def test_import_skips_existing_device(client):
client.post("/device/add", data={"name": "RC Car", "battery_slots": "2", "battery_size": "AA"})
payload = _make_import_payload(
devices=[_dev(1, "RC Car", battery_slots=4)],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
# Original 2-slot device not overwritten
detail = client.get("/device/1")
assert b"RC Car" in detail.data
def test_import_skips_existing_battery(client):
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
payload = _make_import_payload(
batteries=[_bat(999, "Eneloop 001", brand="Eneloop", status="retired")],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
detail = client.get("/battery/1")
assert b"available" in detail.data.lower()
def test_import_log_skipped_when_battery_not_in_map(client):
payload = _make_import_payload(
batteries=[],
charge_logs=[{"id": 1, "battery_id": 42, "battery_label": "Ghost",
"charged_date": "2026-01-01", "increment_cycles": 1, "notes": None}],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
assert b"Import Results" in resp.data
def test_import_battery_gets_installed_when_device_maps(client):
payload = _make_import_payload(
devices=[_dev(7, "My Device")],
batteries=[_bat(7, "Linked 001", status="installed", device_id=7)],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
detail = client.get("/battery/1")
assert b"installed" in detail.data.lower()
assert b"My Device" in detail.data
def test_import_battery_retired_preserved_when_no_device(client):
payload = _make_import_payload(
batteries=[_bat(3, "Old 001", status="retired")],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
detail = client.get("/battery/1")
assert b"retired" in detail.data.lower()
def test_import_battery_device_id_unknown_becomes_available(client):
payload = _make_import_payload(
devices=[],
batteries=[_bat(5, "Orphan 001", status="installed", device_id=99)],
)
resp = _post_import(client, payload)
assert resp.status_code == 200
detail = client.get("/battery/1")
assert b"available" in detail.data.lower()
def test_full_roundtrip_export_import(client):
client.post("/device/add", data={"name": "Router", "battery_slots": "1", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
client.post("/battery/1/assign", data={"device_id": "1"})
export_resp = client.get("/export/all.json")
assert export_resp.status_code == 200
buf = io.BytesIO(export_resp.data)
resp = client.post("/import",
data={"file": (buf, "export.json")},
content_type="multipart/form-data")
assert resp.status_code == 200
assert b"Import Results" in resp.data