Add logbook entries, data export page, and JSON import
This commit is contained in:
+204
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user