diff --git a/app.py b/app.py index 8f35ae2..c946abc 100644 --- a/app.py +++ b/app.py @@ -98,7 +98,7 @@ def create_app(config_object="config"): .distinct().order_by(Battery.storage_location).all() ] devices = db.query(Device).order_by(Device.name).all() - devices_with_slots = [d for d in devices if d.installed_count() < d.battery_slots] + devices_with_slots = [d for d in devices if d.installed_count() < d.battery_slots and not d.has_children()] today = date.today() one_year_ago = (today - timedelta(days=365)).isoformat() total_charges = db.query(func.count(ChargeLog.id)).scalar() or 0 @@ -397,7 +397,7 @@ def create_app(config_object="config"): return redirect(url_for("battery_detail", battery_id=battery_id)) devices = db.query(Device).order_by(Device.name).all() - devices_with_slots = [d for d in devices if d.installed_count() < d.battery_slots] + devices_with_slots = [d for d in devices if d.installed_count() < d.battery_slots and not d.has_children()] if request.method == "POST": device_id = request.form.get("device_id", type=int) @@ -406,6 +406,10 @@ def create_app(config_object="config"): flash("Device not found.", "error") return render_template("assign.html", battery=battery, devices=devices_with_slots) + if device.has_children(): + flash(f"{device.name} has sub-components; assign batteries to its sub-components instead.", "error") + return render_template("assign.html", battery=battery, devices=devices_with_slots) + if device.installed_count() >= device.battery_slots: flash( f"{device.name} is already full " @@ -556,6 +560,10 @@ def create_app(config_object="config"): flash("Device not found.", "error") return redirect(url_for("dashboard")) + if device.has_children(): + flash(f"{device.name} has sub-components; install batteries into those instead.", "error") + return redirect(url_for("dashboard")) + already_here = [b for b in batteries if b.device_id == device.id] retired_sel = [b for b in batteries if b.is_retired()] to_process = [b for b in batteries @@ -692,6 +700,16 @@ def create_app(config_object="config"): device_types=device_types, device_locations=device_locations, device_battery_sizes=device_battery_sizes), 400 + if parent_device.battery_slots != 0 or parent_device.battery_size: + flash( + f"'{parent_device.name}' must have battery slots set to 0 " + f"and battery size cleared before sub-components can be added.", + "error", + ) + return render_template("device_add.html", + device_types=device_types, + device_locations=device_locations, + device_battery_sizes=device_battery_sizes), 400 if not name: flash("Device name is required.", "error") @@ -701,22 +719,12 @@ def create_app(config_object="config"): device_battery_sizes=device_battery_sizes, prefill_parent=parent_device), 400 - if not battery_size: - flash("Battery size is required.", "error") - return render_template("device_add.html", - device_types=device_types, - device_locations=device_locations, - device_battery_sizes=device_battery_sizes, - form_name=name, form_notes=notes or "", - form_device_type=request.form.get("device_type", ""), - prefill_parent=parent_device), 400 - try: slots = int(slots_raw) - if slots < 1: + if slots < 0: raise ValueError except ValueError: - flash("Battery slots must be a positive integer.", "error") + flash("Battery slots must be a non-negative integer.", "error") return render_template("device_add.html", device_types=device_types, device_locations=device_locations, @@ -750,7 +758,9 @@ def create_app(config_object="config"): return render_template("device_add.html", device_types=device_types, device_locations=device_locations, device_battery_sizes=device_battery_sizes, - prefill_parent=prefill_parent) + prefill_parent=prefill_parent, + form_device_type=prefill_parent.device_type if prefill_parent else None, + form_location=prefill_parent.location if prefill_parent else None) # ------------------------------------------------------------------ # # Devices — detail @@ -823,7 +833,9 @@ def create_app(config_object="config"): return redirect(url_for("device_detail", device_id=device_id)) try: slots = int(slots_raw) - if slots < 1: + if slots < 0: + raise ValueError + if slots < 1 and not device.has_children(): raise ValueError except ValueError: flash("Battery slots must be a positive integer.", "error") @@ -846,17 +858,28 @@ def create_app(config_object="config"): if new_parent.is_subcomponent(): flash("Cannot nest sub-components more than one level deep.", "error") return redirect(url_for("device_detail", device_id=device_id)) + if new_parent.battery_slots != 0 or new_parent.battery_size: + flash( + f"'{new_parent.name}' must have battery slots set to 0 " + f"and battery size cleared before sub-components can be added.", + "error", + ) + return redirect(url_for("device_detail", device_id=device_id)) device.parent_id = new_parent.id else: device.parent_id = None device.name = name - device.battery_slots = slots device.notes = notes device.device_type = device_type - new_battery_size = request.form.get("battery_size", "").strip() or None - if new_battery_size is not None: - device.battery_size = new_battery_size + if device.has_children(): + device.battery_slots = 0 + device.battery_size = None + else: + device.battery_slots = slots + new_battery_size = request.form.get("battery_size", "").strip() or None + if new_battery_size is not None: + device.battery_size = new_battery_size device.location = request.form.get("location", "").strip() or None device.ha_entity_id = request.form.get("ha_entity_id", "").strip() or None db.commit() @@ -873,6 +896,10 @@ def create_app(config_object="config"): if device is None: abort(404) + if device.has_children(): + flash(f"{device.name} has sub-components; install batteries into those instead.", "error") + return redirect(url_for("device_detail", device_id=device_id)) + brands = request.form.getlist("brand[]") qtys_raw = request.form.getlist("qty[]") @@ -945,6 +972,9 @@ def create_app(config_object="config"): device = db.get(Device, device_id) if device is None: abort(404) + if device.has_children(): + flash(f"{device.name} has sub-components; install batteries into those instead.", "error") + return redirect(url_for("device_detail", device_id=device_id)) battery_id = request.form.get("battery_id", type=int) battery = db.get(Battery, battery_id) if battery is None or not battery.is_available(): @@ -1034,6 +1064,9 @@ def create_app(config_object="config"): device = db.get(Device, device_id) if device is None: abort(404) + if device.has_children(): + flash(f"{device.name} has sub-components; install batteries into those instead.", "error") + return redirect(url_for("device_detail", device_id=device_id)) battery_ids = request.form.getlist("battery_ids") free = device.battery_slots - device.installed_count() if not battery_ids: diff --git a/models.py b/models.py index b9ea531..dad379a 100644 --- a/models.py +++ b/models.py @@ -28,7 +28,7 @@ class Device(Base): name = Column(String(100), nullable=False, unique=True) battery_slots = Column(Integer, nullable=False, default=1) device_type = Column(String(50), nullable=True) - battery_size = Column(String(20), nullable=False) # AA, AAA, 9V, CR2032 … + battery_size = Column(String(20), nullable=True) # AA, AAA, 9V, CR2032 …; null for parent-only devices location = Column(String(100), nullable=True) notes = Column(Text, nullable=True) ha_entity_id = Column(String(100), nullable=True) # e.g. "sensor.tv_remote_battery" diff --git a/templates/device_add.html b/templates/device_add.html index ab2bbfa..b561574 100644 --- a/templates/device_add.html +++ b/templates/device_add.html @@ -18,6 +18,16 @@ placeholder="e.g. Game Controller" required> + {% if not prefill_parent %} +
+ +
+ {% endif %} + +
+
@@ -68,14 +79,18 @@
+ {% set _cur_loc = form_location|default('') %} -
@@ -93,6 +108,26 @@
+{% endif %} +{% if not device.has_children() %}

Installed Batteries

{% set installed = device.batteries | selectattr('status', 'eq', 'installed') | list %} @@ -326,6 +361,7 @@ function addInstallRow() {

No compatible batteries available.

{% endif %}
+{% endif %}

Logbook

@@ -365,10 +401,12 @@ function addInstallRow() {
+ {% if not device.has_children() %}
+ {% endif %}
{% set _preset_types = ['Remote Control','Game Controller','Flashlight','Lock','Sensor','Toy','Clock','Smoke Detector'] %} @@ -389,6 +427,7 @@ function addInstallRow() { placeholder="Enter device type" style="display:{% if device.device_type and device.device_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;">
+ {% if not device.has_children() %}
{% set _preset_sizes = ['AA','AAA','C','D','9V','CR2032','CR2025','CR2016','18650','14500','16340','26650','LR44/AG13'] %} @@ -412,6 +451,7 @@ function addInstallRow() { placeholder="e.g. CR123A" style="display:{% if device.battery_size and device.battery_size not in _preset_sizes %}''{% else %}none{% endif %};margin-top:0.4rem;">
+ {% endif %}
{% for d in device_list_all|default([]) %} - {% if d.id != device.id and not d.is_subcomponent() %} + {% if d.id != device.id and not d.is_subcomponent() and d.battery_slots == 0 and d.battery_size is none %} {% endif %} {% endfor %} diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 8bd23c0..bece348 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -584,12 +584,6 @@ def test_add_install_delete_battery(client): # Device — battery_size # ------------------------------------------------------------------ # -def test_add_device_requires_battery_size(client): - """POST without battery_size returns 400.""" - resp = client.post("/device/add", data={"name": "No Size Device", "battery_slots": "2"}) - assert resp.status_code == 400 - assert b"Battery size is required" in resp.data - def test_add_device_with_battery_size(client): """Device with battery_size shows it on detail page.""" @@ -893,7 +887,7 @@ def test_device_charge_all_no_installed(seeded_client): def _setup_rc_car(client): """Create RC Car Set with Remote and Car sub-components and 6 AA batteries.""" - client.post("/device/add", data={"name": "RC Car Set", "battery_slots": "6", "battery_size": "AA"}) + client.post("/device/add", data={"name": "RC Car Set", "battery_slots": "0", "battery_size": ""}) # id=1 above; add sub-components with parent_id=1 client.post("/device/add", data={"name": "Remote", "battery_slots": "2", "battery_size": "AA", "parent_id": "1"}) @@ -967,7 +961,7 @@ def test_device_without_parent_unchanged(client): def test_subcomponent_ha_entity_id(client): - client.post("/device/add", data={"name": "Hub", "battery_slots": "1", "battery_size": "AA"}) + client.post("/device/add", data={"name": "Hub", "battery_slots": "0", "battery_size": ""}) resp = client.post( "/device/add", data={"name": "Sensor A", "battery_slots": "1", "battery_size": "AA",