Simplify battery management: bulk add, device-level auto-install, mass operations

- Replace single-battery add form with bulk add (brand + count, auto-generated labels)
- Add device-level install form: specify brand+qty pairs, system autoselects available batteries
- Add bulk actions on dashboard: retire, delete, unassign, change brand (checkbox multi-select)
- Keep per-battery assign route for special cases (e.g. known low-capacity battery)
- Remove unique constraint on Battery.label (labels are now auto-generated)
- Add *.snapshot to .gitignore for DB snapshot files
- Rewrite tests: 35 passing (11 new tests for bulk-add, device-install, bulk-actions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 12:57:10 -05:00
parent 2e36d5f53c
commit 1f5234a3e9
8 changed files with 403 additions and 131 deletions
+135 -18
View File
@@ -1,5 +1,5 @@
from flask import Flask, render_template, redirect, url_for, request, flash, abort
from sqlalchemy import create_engine
from sqlalchemy import create_engine, func
from sqlalchemy.orm import scoped_session, sessionmaker
from models import Base, Battery, Device
@@ -37,31 +37,27 @@ def create_app(config_object="config"):
@app.route("/battery/add", methods=["GET", "POST"])
def battery_add():
if request.method == "POST":
label = request.form.get("label", "").strip()
brand = request.form.get("brand", "").strip()
status = request.form.get("status", "available").strip()
notes = request.form.get("notes", "").strip() or None
try:
count = max(1, min(50, int(request.form.get("count", 1) or 1)))
except (ValueError, TypeError):
count = 1
if not label or not brand:
flash("Label and brand are required.", "error")
return render_template("battery_add.html"), 400
if status not in ("available", "installed", "retired"):
status = "available"
if db.query(Battery).filter_by(label=label).first():
flash(f"A battery with label '{label}' already exists.", "error")
if not brand:
flash("Brand is required.", "error")
return render_template("battery_add.html",
form_label=label, form_brand=brand,
form_status=status, form_notes=notes or ""), 400
form_brand="", form_count=1, form_notes=notes or ""), 400
battery = Battery(label=label, brand=brand, status=status, notes=notes)
db.add(battery)
existing = db.query(func.count(Battery.id)).filter_by(brand=brand).scalar()
for i in range(count):
label = f"{brand} {existing + i + 1:03d}"
db.add(Battery(label=label, brand=brand, status="available", notes=notes))
db.commit()
flash(f"Battery {label} added.", "success")
flash(f"Added {count} {brand} batter{'y' if count == 1 else 'ies'}.", "success")
return redirect(url_for("dashboard"))
return render_template("battery_add.html")
return render_template("battery_add.html", form_count=1)
# ------------------------------------------------------------------ #
# Battery — detail
@@ -190,6 +186,54 @@ def create_app(config_object="config"):
return redirect(url_for("dashboard"))
return render_template("battery_delete.html", battery=battery)
# ------------------------------------------------------------------ #
# Battery — bulk action
# ------------------------------------------------------------------ #
@app.route("/battery/bulk-action", methods=["POST"])
def battery_bulk_action():
ids = request.form.getlist("battery_ids", type=int)
if not ids:
flash("No batteries selected.", "error")
return redirect(url_for("dashboard"))
batteries = db.query(Battery).filter(Battery.id.in_(ids)).all()
action = request.form.get("action")
n = len(batteries)
if action == "retire":
for b in batteries:
b.status = "retired"
b.device_id = None
db.commit()
flash(f"Retired {n} batter{'y' if n == 1 else 'ies'}.", "success")
elif action == "delete":
for b in batteries:
db.delete(b)
db.commit()
flash(f"Deleted {n} batter{'y' if n == 1 else 'ies'}.", "success")
elif action == "unassign":
count = sum(1 for b in batteries if b.is_installed())
for b in batteries:
if b.is_installed():
b.status = "available"
b.device_id = None
db.commit()
flash(f"Unassigned {count} batter{'y' if count == 1 else 'ies'}.", "success")
elif action == "set_brand":
new_brand = request.form.get("new_brand", "").strip()
if not new_brand:
flash("Brand name is required.", "error")
return redirect(url_for("dashboard"))
for b in batteries:
b.brand = new_brand
db.commit()
flash(f"Updated brand to '{new_brand}' for {n} batter{'y' if n == 1 else 'ies'}.", "success")
else:
flash("Unknown action.", "error")
return redirect(url_for("dashboard"))
# ------------------------------------------------------------------ #
# Devices — list
# ------------------------------------------------------------------ #
@@ -248,6 +292,79 @@ def create_app(config_object="config"):
abort(404)
return render_template("device_detail.html", device=device)
# ------------------------------------------------------------------ #
# Devices — install batteries
# ------------------------------------------------------------------ #
@app.route("/device/<int:device_id>/install", methods=["POST"])
def device_install(device_id):
device = db.get(Device, device_id)
if device is None:
abort(404)
brands = request.form.getlist("brand[]")
qtys_raw = request.form.getlist("qty[]")
pairs = []
for brand, qty_raw in zip(brands, qtys_raw):
brand = brand.strip()
try:
qty = int(qty_raw)
except (ValueError, TypeError):
qty = 0
if brand and qty > 0:
pairs.append((brand, qty))
if not pairs:
flash("No batteries specified.", "error")
return redirect(url_for("device_detail", device_id=device_id))
free_slots = device.battery_slots - device.installed_count()
total_requested = sum(qty for _, qty in pairs)
if total_requested > free_slots:
flash(
f"Only {free_slots} slot(s) free, but {total_requested} requested.",
"error",
)
return redirect(url_for("device_detail", device_id=device_id))
# Validate availability before writing anything
for brand, qty in pairs:
available_count = (
db.query(func.count(Battery.id))
.filter_by(brand=brand, status="available")
.scalar()
)
if available_count < qty:
flash(
f"Need {qty} {brand}, but only {available_count} available.",
"error",
)
return redirect(url_for("device_detail", device_id=device_id))
# All checks passed — perform installs
total_installed = 0
for brand, qty in pairs:
batch = (
db.query(Battery)
.filter_by(brand=brand, status="available")
.order_by(Battery.id)
.limit(qty)
.all()
)
for b in batch:
b.status = "installed"
b.device_id = device.id
total_installed += 1
db.commit()
flash(
f"Installed {total_installed} batter{'y' if total_installed == 1 else 'ies'}"
f" into {device.name}.",
"success",
)
return redirect(url_for("device_detail", device_id=device_id))
# ------------------------------------------------------------------ #
# Devices — delete
# ------------------------------------------------------------------ #