feat(measure): proxy JSON per il progresso di un task (#37.8, parziale)
GET /measure/api/task-progress/<task_id>: il browser non puo' chiamare /api/measurements/task-progress del backend (niente X-API-Key). Login + stesso controllo di stazione di task_execute() (_recipe_allowed_at_station), poi inoltra a _task_progress() con lo stesso version_id/production_run_id gia' usato da task_list.html. Non tocca task_execute.html: /api/measurements/task-progress risponde con conteggi aggregati per task (quotes/measured/state), non con le quote-per-quota misurate (subtask_id, value, pass_fail, deviation) che `this.measurements` richiede - ripristinarlo avrebbe richiesto inventare valori o uno stato non offerto dal backend. MANCA: mostrato in dettaglio nel rapporto finale. Test Flask della route: riga per il task richiesto, default a "empty" se assente, 403 fuori stazione, propagazione 404, login richiesto. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -565,6 +565,38 @@ def save_measurement():
|
||||
return jsonify(resp), 201
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Route: task progress (#37.8) - browser cannot call the backend directly
|
||||
# (X-API-Key), same as every other /api/* proxy in this file.
|
||||
# ---------------------------------------------------------------------------
|
||||
@measure_bp.route("/api/task-progress/<int:task_id>", methods=["GET"])
|
||||
@login_required
|
||||
@role_required("MeasurementTec")
|
||||
def api_task_progress(task_id: int):
|
||||
"""Proxy: how many quotes of this task are already measured.
|
||||
|
||||
Reuses _task_progress() - same version_id/production_run_id scoping as
|
||||
task_list.html's badges - so this answers with exactly the counts
|
||||
(quotes/measured/state) the backend can give without conflating one
|
||||
cycle with another; it does not, and cannot, say which quotes or with
|
||||
what values (task-progress is an aggregate, not a per-subtask list).
|
||||
"""
|
||||
task_resp = api_client.get(f"/api/tasks/{task_id}")
|
||||
if isinstance(task_resp, dict) and task_resp.get("error"):
|
||||
return jsonify(task_resp), task_resp.get("status_code", 500)
|
||||
|
||||
recipe_id = task_resp.get("recipe_id")
|
||||
if recipe_id and not _recipe_allowed_at_station(recipe_id):
|
||||
abort(403, description=_("Ricetta non assegnata a questa stazione"))
|
||||
|
||||
version_id = task_resp.get("version_id")
|
||||
progress = _task_progress(version_id, _current_run_id())
|
||||
row = progress.get(task_id, {
|
||||
"task_id": task_id, "quotes": 0, "measured": 0, "state": "empty",
|
||||
})
|
||||
return jsonify(row), 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes: Out-of-tolerance authorisation (point 5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -167,6 +167,87 @@ class TestTaskList:
|
||||
assert "Misura" in resp.get_data(as_text=True)
|
||||
|
||||
|
||||
class TestApiTaskProgress:
|
||||
"""GET /measure/api/task-progress/<task_id> tests (gitea#37.8).
|
||||
|
||||
The browser cannot call /api/measurements/task-progress directly (no
|
||||
X-API-Key); this proxy forwards it with the same version_id/
|
||||
production_run_id scoping _task_progress() already uses for task_list.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _station_configured(self, monkeypatch):
|
||||
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||
|
||||
def test_returns_this_task_progress_row(self, logged_in_client, mock_api_client):
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/tasks/7": {"id": 7, "recipe_id": 1, "version_id": 42},
|
||||
"/api/production-runs/current": None,
|
||||
"/api/measurements/task-progress": {
|
||||
"tasks": [{"task_id": 7, "quotes": 3, "measured": 1, "state": "partial"}],
|
||||
},
|
||||
})
|
||||
|
||||
resp = logged_in_client.get("/measure/api/task-progress/7")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json() == {
|
||||
"task_id": 7, "quotes": 3, "measured": 1, "state": "partial",
|
||||
}
|
||||
# No production run open: the backend call must not carry a
|
||||
# production_run_id it would then use to scope by the wrong cycle.
|
||||
progress_call = next(
|
||||
call for call in mock_api_client.get.call_args_list
|
||||
if call.args[0] == "/api/measurements/task-progress"
|
||||
)
|
||||
assert progress_call.kwargs["params"] == {"version_id": 42}
|
||||
|
||||
def test_task_absent_from_progress_defaults_to_empty(
|
||||
self, logged_in_client, mock_api_client,
|
||||
):
|
||||
"""A task with no quotes yet has no row in the backend's answer."""
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/tasks/7": {"id": 7, "recipe_id": 1, "version_id": 42},
|
||||
"/api/production-runs/current": None,
|
||||
"/api/measurements/task-progress": {"tasks": []},
|
||||
})
|
||||
|
||||
resp = logged_in_client.get("/measure/api/task-progress/7")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json() == {
|
||||
"task_id": 7, "quotes": 0, "measured": 0, "state": "empty",
|
||||
}
|
||||
|
||||
def test_refuses_a_recipe_not_assigned_to_this_station(
|
||||
self, logged_in_client, mock_api_client,
|
||||
):
|
||||
"""Same #24 guard task_execute() uses: no bypass through this proxy."""
|
||||
mock_api_client.get_station_recipes.return_value = [{"id": 99}]
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/tasks/7": {"id": 7, "recipe_id": 1, "version_id": 42},
|
||||
})
|
||||
|
||||
resp = logged_in_client.get("/measure/api/task-progress/7")
|
||||
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_propagates_a_task_not_found(self, logged_in_client, mock_api_client):
|
||||
mock_api_client.get.side_effect = api_get_router({
|
||||
"/api/tasks/999": {"error": True, "status_code": 404, "detail": "not found"},
|
||||
})
|
||||
|
||||
resp = logged_in_client.get("/measure/api/task-progress/999")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_requires_login(self, client):
|
||||
resp = client.get("/measure/api/task-progress/7")
|
||||
assert resp.status_code in (302, 401)
|
||||
|
||||
|
||||
class TestStartSequence:
|
||||
"""GET /measure/start/<recipe_id> — the operator's way in."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user