from flask import Flask, render_template, redirect, url_for, request, flash, abort from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker from models import Base, Battery, Device def create_app(config_object="config"): app = Flask(__name__) app.config.from_object(config_object) engine = create_engine( app.config["SQLALCHEMY_DATABASE_URI"], pool_pre_ping=True, ) Base.metadata.create_all(engine) db = scoped_session(sessionmaker(bind=engine)) @app.teardown_appcontext def remove_session(exc=None): db.remove() # ------------------------------------------------------------------ # # Dashboard # ------------------------------------------------------------------ # @app.route("/") def dashboard(): batteries = db.query(Battery).order_by(Battery.label).all() return render_template("dashboard.html", batteries=batteries) # ------------------------------------------------------------------ # # Battery — add # ------------------------------------------------------------------ # @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 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") return render_template("battery_add.html", form_label=label, form_brand=brand, form_status=status, form_notes=notes or ""), 400 battery = Battery(label=label, brand=brand, status=status, notes=notes) db.add(battery) db.commit() flash(f"Battery {label} added.", "success") return redirect(url_for("dashboard")) return render_template("battery_add.html") # ------------------------------------------------------------------ # # Battery — detail # ------------------------------------------------------------------ # @app.route("/battery/") def battery_detail(battery_id): battery = db.get(Battery, battery_id) if battery is None: abort(404) return render_template("battery_detail.html", battery=battery) # ------------------------------------------------------------------ # # Battery — edit notes # ------------------------------------------------------------------ # @app.route("/battery//edit-notes", methods=["POST"]) def battery_edit_notes(battery_id): battery = db.get(Battery, battery_id) if battery is None: abort(404) battery.notes = request.form.get("notes", "").strip() or None db.commit() flash("Notes updated.", "success") return redirect(url_for("battery_detail", battery_id=battery_id)) # ------------------------------------------------------------------ # # Battery — assign # ------------------------------------------------------------------ # @app.route("/battery//assign", methods=["GET", "POST"]) def battery_assign(battery_id): battery = db.get(Battery, battery_id) if battery is None: abort(404) if battery.is_retired(): flash("Cannot assign a retired battery.", "error") return redirect(url_for("battery_detail", battery_id=battery_id)) devices = db.query(Device).order_by(Device.name).all() if request.method == "POST": device_id = request.form.get("device_id", type=int) device = db.get(Device, device_id) if device is None: flash("Device not found.", "error") return render_template("assign.html", battery=battery, devices=devices) if device.installed_count() >= device.battery_slots: flash( f"{device.name} is already full " f"({device.battery_slots}/{device.battery_slots} slots used).", "error", ) return render_template("assign.html", battery=battery, devices=devices) # Warn on brand mix (non-blocking) existing_brands = device.installed_brands() if existing_brands and battery.brand not in existing_brands: flash( f"Warning: {device.name} already has batteries from " f"{', '.join(existing_brands)}. Mixing brands is not recommended.", "warning", ) # Unassign from previous device if needed if battery.device_id is not None: battery.device_id = None battery.status = "installed" battery.device_id = device.id db.commit() flash(f"{battery.label} assigned to {device.name}.", "success") return redirect(url_for("dashboard")) return render_template("assign.html", battery=battery, devices=devices) # ------------------------------------------------------------------ # # Battery — unassign # ------------------------------------------------------------------ # @app.route("/battery//unassign", methods=["POST"]) def battery_unassign(battery_id): battery = db.get(Battery, battery_id) if battery is None: abort(404) battery.status = "available" battery.device_id = None db.commit() flash(f"{battery.label} unassigned and marked available.", "success") return redirect(url_for("dashboard")) # ------------------------------------------------------------------ # # Battery — retire # ------------------------------------------------------------------ # @app.route("/battery//retire", methods=["POST"]) def battery_retire(battery_id): battery = db.get(Battery, battery_id) if battery is None: abort(404) if battery.is_retired(): flash(f"{battery.label} is already retired.", "error") return redirect(url_for("battery_detail", battery_id=battery_id)) battery.status = "retired" battery.device_id = None db.commit() flash(f"{battery.label} has been retired.", "success") return redirect(url_for("dashboard")) # ------------------------------------------------------------------ # # Battery — delete # ------------------------------------------------------------------ # @app.route("/battery//delete", methods=["GET", "POST"]) def battery_delete(battery_id): battery = db.get(Battery, battery_id) if battery is None: abort(404) if request.method == "POST": label = battery.label db.delete(battery) db.commit() flash(f"Battery {label} permanently deleted.", "success") return redirect(url_for("dashboard")) return render_template("battery_delete.html", battery=battery) # ------------------------------------------------------------------ # # Devices — list # ------------------------------------------------------------------ # @app.route("/device/") def device_list(): devices = db.query(Device).order_by(Device.name).all() return render_template("device_list.html", devices=devices) # ------------------------------------------------------------------ # # Devices — add # ------------------------------------------------------------------ # @app.route("/device/add", methods=["GET", "POST"]) def device_add(): if request.method == "POST": name = request.form.get("name", "").strip() slots_raw = request.form.get("battery_slots", "1").strip() notes = request.form.get("notes", "").strip() or None if not name: flash("Device name is required.", "error") return render_template("device_add.html"), 400 try: slots = int(slots_raw) if slots < 1: raise ValueError except ValueError: flash("Battery slots must be a positive integer.", "error") return render_template("device_add.html", form_name=name, form_notes=notes or ""), 400 if db.query(Device).filter_by(name=name).first(): flash(f"A device named '{name}' already exists.", "error") return render_template("device_add.html", form_name=name, form_slots=slots, form_notes=notes or ""), 400 device = Device(name=name, battery_slots=slots, notes=notes) db.add(device) db.commit() flash(f"Device '{name}' added.", "success") return redirect(url_for("device_list")) return render_template("device_add.html") # ------------------------------------------------------------------ # # Devices — detail # ------------------------------------------------------------------ # @app.route("/device/") def device_detail(device_id): device = db.get(Device, device_id) if device is None: abort(404) return render_template("device_detail.html", device=device) # ------------------------------------------------------------------ # # Devices — delete # ------------------------------------------------------------------ # @app.route("/device//delete", methods=["POST"]) def device_delete(device_id): device = db.get(Device, device_id) if device is None: abort(404) for battery in device.batteries: battery.status = "available" battery.device_id = None name = device.name db.delete(device) db.commit() flash(f"Device '{name}' deleted. All batteries marked available.", "success") return redirect(url_for("device_list")) return app app = create_app() if __name__ == "__main__": app.run(debug=True)