feat(03-01): implement printer and client CRUD with HTMX/Alpine.js UI

- imptune/api/printers.py: POST /printers (all form fields, checkbox->bool,
  FK resolution), DELETE /printers/{id}, grouped list renderer with LEFT JOIN
- imptune/api/clients.py: POST /clients with duplicate-name handling
- imptune/api/pages.py: GET /printers and GET /clients page routes
- imptune/main.py: register printers + clients routers; close db on shutdown
- imptune/db/database.py: close existing connection before re-init (test isolation)
- templates: printers.html, clients.html, printer_form.html (Alpine.js port
  auto-derivation), printer_list.html (grouped by client), client_list.html
- tests/conftest.py: close test-thread db connection in fixture teardown
- tests/test_printer_crud.py: updated to use list(select().where()) for DB
  queries (avoids Peewee thread-local cursor caching across test boundaries)

Auto-fix [Rule 1 - Bug]: DB test isolation — Peewee thread-local connections
persisted across tests causing stale DB reads; fixed via conftest teardown and
lifespan db.close() on shutdown.
This commit is contained in:
2026-04-10 12:56:09 +02:00
parent 9bc26e377b
commit 356c2ee690
12 changed files with 450 additions and 15 deletions
+48 -1
View File
@@ -1,9 +1,11 @@
import json
from collections import defaultdict
from pathlib import Path
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from peewee import JOIN
router = APIRouter()
@@ -36,3 +38,48 @@ def drivers_page(request: Request):
name="drivers.html",
context={"driver_data": driver_data},
)
@router.get("/printers", response_class=HTMLResponse)
def printers_page(request: Request):
from imptune.db.models import Client, Driver, Printer
query = (
Printer.select(Printer, Client)
.join(Client, JOIN.LEFT_OUTER)
.order_by(Client.name, Printer.name)
)
grouped: dict[str, list] = defaultdict(list)
for p in query:
client_name = p.client.name if p.client_id else "Unassigned"
grouped[client_name].append(p)
clients = list(Client.select().order_by(Client.name))
all_drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
driver_data = []
for d in all_drivers:
names = json.loads(d.driver_desc) if d.driver_desc else []
driver_data.append({"driver": d, "names": names})
return templates.TemplateResponse(
request=request,
name="printers.html",
context={
"grouped": grouped,
"clients": clients,
"driver_data": driver_data,
},
)
@router.get("/clients", response_class=HTMLResponse)
def clients_page(request: Request):
from imptune.db.models import Client
clients = list(Client.select().order_by(Client.name))
return templates.TemplateResponse(
request=request,
name="clients.html",
context={"clients": clients},
)