Keep the printer's name on statistics after it is deleted (issue #2873)

Every per-printer breakdown resolved the name against the printers that
exist now, so deleting a printer and choosing to keep its prints turned
"Ultron" into "Printer 1" in Prints by Printer, the success-rate and
time-accuracy lists, and Failures by Printer. Archives lose their printer
on that delete as well, so nothing was left to read a name from.

The runs themselves recorded the name they printed on. /archives/stats now
reports the last name each id was known by - taken from the newest run that
has one, so a later name-less row cannot blank it - and failure analysis
falls back to the same thing for ids with no printer left. The client keeps
preferring a live printer's own record, so a rename still shows up straight
away rather than after the next print.
This commit is contained in:
maziggy
2026-08-18 14:04:18 +02:00
parent cfecfa360e
commit 28781ea558
10 changed files with 221 additions and 4 deletions
@@ -0,0 +1,112 @@
"""Statistics keeps naming a printer that was deleted with its history (#2873).
Deleting a printer while keeping its prints leaves the log rows pointing at an
id nothing resolves any more, so every per-printer breakdown fell back to
"Printer 1" for machines the reporter knew as "Ultron". Each run recorded the
name it printed on, so that is what the aggregates report now.
"""
from __future__ import annotations
from datetime import datetime, timedelta
import pytest
from httpx import AsyncClient
from backend.app.models.print_log import PrintLogEntry
@pytest.mark.asyncio
@pytest.mark.integration
async def test_stats_name_history_of_a_deleted_printer(async_client: AsyncClient, db_session):
base = datetime(2026, 7, 15, 10, 0, 0)
db_session.add_all(
[
PrintLogEntry(
printer_id=71,
printer_name="Ultron",
status="completed",
started_at=base,
completed_at=base + timedelta(hours=1),
duration_seconds=3600,
),
PrintLogEntry(
printer_id=71,
printer_name="Ultron",
status="failed",
started_at=base + timedelta(hours=2),
completed_at=base + timedelta(hours=3),
duration_seconds=3600,
),
]
)
await db_session.commit()
stats = (await async_client.get("/api/v1/archives/stats")).json()
assert stats["prints_by_printer"]["71"] == 2
assert stats["printer_names"]["71"] == "Ultron"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_stats_reports_the_last_name_the_printer_ran_under(async_client: AsyncClient, db_session):
"""A printer renamed before it was deleted is remembered by its last name."""
base = datetime(2026, 7, 15, 10, 0, 0)
db_session.add_all(
[
PrintLogEntry(printer_id=71, printer_name="Ultron", status="completed", started_at=base),
PrintLogEntry(
printer_id=71,
printer_name="Ultron Mk II",
status="completed",
started_at=base + timedelta(days=1),
),
# A run logged before names were recorded must not blank the label.
PrintLogEntry(printer_id=71, printer_name=None, status="completed", started_at=base + timedelta(days=2)),
]
)
await db_session.commit()
stats = (await async_client.get("/api/v1/archives/stats")).json()
assert stats["printer_names"]["71"] == "Ultron Mk II"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_stats_names_do_not_override_a_live_printer(async_client: AsyncClient, db_session, printer_factory):
"""Renaming a printer that still exists shows up straight away.
The client prefers the live record, so the historical name only has to be
present, not authoritative.
"""
printer = await printer_factory(name="Renamed Later")
db_session.add(PrintLogEntry(printer_id=printer.id, printer_name="Original Name", status="completed"))
await db_session.commit()
stats = (await async_client.get("/api/v1/archives/stats")).json()
printers = (await async_client.get("/api/v1/printers/")).json()
assert stats["printer_names"][str(printer.id)] == "Original Name"
assert [p["name"] for p in printers if p["id"] == printer.id] == ["Renamed Later"]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_failure_analysis_names_a_deleted_printer(async_client: AsyncClient, db_session, printer_factory):
"""Failures by printer reads the recorded name once the printer is gone."""
live = await printer_factory(name="Still Here")
db_session.add_all(
[
PrintLogEntry(printer_id=71, printer_name="Ultron", status="failed"),
PrintLogEntry(printer_id=live.id, printer_name="Older Name", status="failed"),
]
)
await db_session.commit()
analysis = (await async_client.get("/api/v1/archives/analysis/failures")).json()
assert analysis["failures_by_printer"]["Ultron"] == 1
assert analysis["failures_by_printer"]["Still Here"] == 1
assert "Printer 71" not in analysis["failures_by_printer"]