Statistica: endpoint esiti, note, campi e allegati dei task di una sessione (#58)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NABedeMX43pyXZbDNUgA5B
This commit is contained in:
@@ -287,3 +287,116 @@ class TestRunFilter:
|
||||
headers=auth_headers(measurement_tec_user),
|
||||
params={"recipe_id": 1})
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
class TestRunTaskResults:
|
||||
"""#58: GET /api/statistics/runs/{id}/task-results."""
|
||||
|
||||
FIELDS = [
|
||||
{"key": "peso", "label": "Peso", "type": "number", "default": None, "attachment": False},
|
||||
{"key": "foto", "label": "Foto", "type": "text", "default": None, "attachment": True},
|
||||
]
|
||||
|
||||
async def _setup(self, client, db_session, maker_user, tec_user):
|
||||
from datetime import datetime
|
||||
from sqlalchemy import select
|
||||
from src.backend.models.orm.production import ProductionRun
|
||||
from src.backend.models.orm.recipe import RecipeVersion
|
||||
from src.backend.models.orm.station import Station
|
||||
from src.backend.models.orm.task_result import TaskResult
|
||||
recipe = await create_test_recipe(db_session, maker_user.id, code="TR58")
|
||||
version = (await db_session.execute(
|
||||
select(RecipeVersion).where(RecipeVersion.recipe_id == recipe.id)
|
||||
)).scalars().first()
|
||||
db_session.add(Station(code="ST-TR58", name="S", active=True, created_by=maker_user.id))
|
||||
await db_session.commit()
|
||||
station = (await db_session.execute(select(Station))).scalars().first()
|
||||
run = ProductionRun(
|
||||
station_id=station.id, recipe_id=recipe.id, version_id=version.id,
|
||||
operator_id=tec_user.id, status="open", started_at=datetime(2026, 1, 1),
|
||||
)
|
||||
db_session.add(run)
|
||||
await db_session.commit()
|
||||
h = auth_headers(maker_user)
|
||||
check = (await client.post(
|
||||
f"/api/recipes/{recipe.id}/tasks", headers=h,
|
||||
json={"title": "Controllo", "task_type": "check"},
|
||||
)).json()
|
||||
note = (await client.post(
|
||||
f"/api/recipes/{recipe.id}/tasks", headers=h,
|
||||
json={"title": "Nota", "task_type": "note", "fields": self.FIELDS},
|
||||
)).json()
|
||||
# Inseriti fuori ordine: l'API deve ordinare per created_at.
|
||||
rows = [
|
||||
TaskResult(task_id=note["id"], version_id=version.id, production_run_id=run.id,
|
||||
user_id=tec_user.id, outcome="done", note="ok finito",
|
||||
values={"peso": "12", "foto": "x.jpg"},
|
||||
attachments=[{"key": "foto", "path": "task_results/a.jpg", "name": "a.jpg"}],
|
||||
created_at=datetime(2026, 1, 1, 10, 3)),
|
||||
TaskResult(task_id=check["id"], version_id=version.id, production_run_id=run.id,
|
||||
user_id=tec_user.id, outcome="nok", note="graffio",
|
||||
created_at=datetime(2026, 1, 1, 10, 2)),
|
||||
TaskResult(task_id=check["id"], version_id=version.id, production_run_id=run.id,
|
||||
user_id=tec_user.id, outcome="ok",
|
||||
created_at=datetime(2026, 1, 1, 10, 1)),
|
||||
# Un esito di un'altra sessione (nessuna) non deve comparire.
|
||||
TaskResult(task_id=check["id"], version_id=version.id, production_run_id=None,
|
||||
user_id=tec_user.id, outcome="ok",
|
||||
created_at=datetime(2026, 1, 1, 9, 0)),
|
||||
]
|
||||
db_session.add_all(rows)
|
||||
await db_session.commit()
|
||||
return recipe, run, check, note
|
||||
|
||||
async def test_counts_order_labels_attachments(
|
||||
self, client, maker_user, measurement_tec_user, metrologist_user, db_session,
|
||||
):
|
||||
_, run, check, note = await self._setup(
|
||||
client, db_session, maker_user, measurement_tec_user)
|
||||
resp = await client.get(f"/api/statistics/runs/{run.id}/task-results",
|
||||
headers=auth_headers(metrologist_user))
|
||||
assert resp.status_code == 200, resp.text
|
||||
data = resp.json()
|
||||
assert data["run_id"] == run.id
|
||||
assert data["counts"] == {"ok": 1, "nok": 1, "done": 1}
|
||||
res = data["results"]
|
||||
assert [r["outcome"] for r in res] == ["ok", "nok", "done"] # per ora crescente
|
||||
assert res[1]["note"] == "graffio"
|
||||
assert res[1]["task_title"] == "Controllo"
|
||||
assert res[1]["task_type"] == "check"
|
||||
assert res[1]["user_name"] == measurement_tec_user.display_name
|
||||
assert res[1]["values"] == [] and res[1]["attachments"] == []
|
||||
n = res[2]
|
||||
assert n["task_title"] == "Nota" and n["note"] == "ok finito"
|
||||
assert n["values"] == [
|
||||
{"key": "peso", "label": "Peso", "value": "12"},
|
||||
{"key": "foto", "label": "Foto", "value": "x.jpg"},
|
||||
]
|
||||
assert n["attachments"] == [
|
||||
{"key": "foto", "label": "Foto", "name": "a.jpg", "path": "task_results/a.jpg"},
|
||||
]
|
||||
|
||||
async def test_label_falls_back_to_key_when_field_removed(
|
||||
self, client, maker_user, measurement_tec_user, metrologist_user, db_session,
|
||||
):
|
||||
from src.backend.models.orm.task import RecipeTask
|
||||
_, run, _, note = await self._setup(
|
||||
client, db_session, maker_user, measurement_tec_user)
|
||||
task = await db_session.get(RecipeTask, note["id"])
|
||||
task.fields = [self.FIELDS[0]] # il campo «foto» viene tolto dal task
|
||||
await db_session.commit()
|
||||
resp = await client.get(f"/api/statistics/runs/{run.id}/task-results",
|
||||
headers=auth_headers(metrologist_user))
|
||||
n = resp.json()["results"][2]
|
||||
assert {v["key"]: v["label"] for v in n["values"]} == {"peso": "Peso", "foto": "foto"}
|
||||
assert n["attachments"][0]["label"] == "foto"
|
||||
|
||||
async def test_unknown_run_is_404(self, client, metrologist_user):
|
||||
resp = await client.get("/api/statistics/runs/99999/task-results",
|
||||
headers=auth_headers(metrologist_user))
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_measurement_tec_is_403(self, client, measurement_tec_user):
|
||||
resp = await client.get("/api/statistics/runs/1/task-results",
|
||||
headers=auth_headers(measurement_tec_user))
|
||||
assert resp.status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user