Files
ImpTune/imptune/api/printers.py
T
kawa 4b212b6da9 feat(11-02): PATCH /printers/{id} route handler and updated _render_printer_list
- Add PATCH /{printer_id} route with validation and in-place update
- Optional ip_address/port_name fall back to existing values when not sent
- updated_at set explicitly on save (datetime.now(UTC))
- Update _render_printer_list to include clients and driver_data in context
- Import Driver at module level for use in both handler and helper
2026-04-15 11:08:02 +02:00

175 lines
5.7 KiB
Python

"""Printer CRUD API — POST /printers, DELETE /printers/{id}, PATCH /printers/{id}."""
from __future__ import annotations
from collections import defaultdict
from pathlib import Path
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from peewee import JOIN
from imptune.db.models import Client, Driver, Printer
router = APIRouter(prefix="/printers")
templates = Jinja2Templates(
directory=str(Path(__file__).parent.parent / "templates")
)
_VALID_DUPLEX = {"OneSided", "LongEdge", "ShortEdge"}
_VALID_PAPER = {"A4", "Letter", "Legal"}
def _error_response(message: str, status_code: int = 400) -> HTMLResponse:
"""Return an HTMX-friendly error fragment swapped into #printer-list."""
return HTMLResponse(
content=f"<div id='printer-list' class='error'><p>{message}</p></div>",
status_code=status_code,
)
def _render_printer_list(request: Request) -> HTMLResponse:
"""Query printers with LEFT JOIN on client and render grouped partial."""
import json
query = (
Printer.select(Printer, Client)
.join(Client, JOIN.LEFT_OUTER)
.order_by(Client.name, Printer.name)
)
grouped: dict[str, list[Printer]] = 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 = [
{"driver": d, "names": json.loads(d.driver_desc) if d.driver_desc else []}
for d in all_drivers
]
return templates.TemplateResponse(
request=request,
name="partials/printer_list.html",
context={"grouped": grouped, "clients": clients, "driver_data": driver_data},
)
@router.post("", response_class=HTMLResponse)
def create_printer(
request: Request,
name: str = Form(...),
ip_address: str = Form(...),
port_name: str = Form(...),
duplex_mode: str = Form("OneSided"),
color_mode: str = Form(""),
paper_size: str = Form("A4"),
collate: str = Form(""),
client_id: str = Form(""),
driver_id: str = Form(""),
) -> HTMLResponse:
"""Create a new printer configuration.
Boolean fields (color_mode, collate) use HTML checkbox convention:
"on" = True, absent/empty = False.
"""
name = name.strip()
ip_address = ip_address.strip()
port_name = port_name.strip()
if not name:
return _error_response("Printer name is required.")
if not ip_address:
return _error_response("IP address is required.")
if not port_name:
return _error_response("Port name is required.")
if duplex_mode not in _VALID_DUPLEX:
return _error_response(f"Invalid duplex mode: {duplex_mode}.")
if paper_size not in _VALID_PAPER:
return _error_response(f"Invalid paper size: {paper_size}.")
# Convert checkbox values
color_mode_bool = color_mode == "on"
collate_bool = collate == "on"
# Resolve optional FK IDs
client_fk = int(client_id) if client_id.strip() else None
driver_fk = int(driver_id) if driver_id.strip() else None
Printer.create(
name=name,
ip_address=ip_address,
port_name=port_name,
duplex_mode=duplex_mode,
color_mode=color_mode_bool,
paper_size=paper_size,
collate=collate_bool,
client=client_fk,
driver=driver_fk,
)
return RedirectResponse(url="/printers", status_code=303)
@router.delete("/{printer_id}", response_class=HTMLResponse)
def delete_printer(request: Request, printer_id: int) -> HTMLResponse:
"""Delete a printer by ID. Returns updated printer list partial."""
deleted = Printer.delete().where(Printer.id == printer_id).execute()
if not deleted:
return _error_response(f"Printer {printer_id} not found.", status_code=404)
return _render_printer_list(request)
@router.patch("/{printer_id}", response_class=HTMLResponse)
def update_printer(
request: Request,
printer_id: int,
name: str = Form(...),
ip_address: str = Form(""),
port_name: str = Form(""),
duplex_mode: str = Form("OneSided"),
color_mode: str = Form(""),
paper_size: str = Form("A4"),
collate: str = Form(""),
client_id: str = Form(""),
driver_id: str = Form(""),
) -> HTMLResponse:
"""Update an existing printer configuration in-place."""
from datetime import UTC, datetime
printer = Printer.get_or_none(Printer.id == printer_id)
if printer is None:
return _error_response(f"Printer {printer_id} not found.", status_code=404)
name = name.strip()
ip_address = ip_address.strip() or printer.ip_address
port_name = port_name.strip() or printer.port_name
if not name:
return _error_response("Printer name is required.")
if not ip_address:
return _error_response("IP address is required.")
if not port_name:
return _error_response("Port name is required.")
if duplex_mode not in _VALID_DUPLEX:
return _error_response(f"Invalid duplex mode: {duplex_mode}.")
if paper_size not in _VALID_PAPER:
return _error_response(f"Invalid paper size: {paper_size}.")
printer.name = name
printer.ip_address = ip_address
printer.port_name = port_name
printer.duplex_mode = duplex_mode
printer.color_mode = color_mode == "on"
printer.paper_size = paper_size
printer.collate = collate == "on"
printer.client = int(client_id) if client_id.strip() else None
printer.driver = int(driver_id) if driver_id.strip() else None
printer.updated_at = datetime.now(UTC).replace(tzinfo=None)
printer.save()
return _render_printer_list(request)