feat(client): filter select_recipe by STATION_CODE with error fallback
Replace generic /api/recipes call with api_client.get_station_recipes(STATION_CODE). Return 503 station_not_configured.html when STATION_CODE env var is unset. Add station indicator to recipe selection page header. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,18 @@ measure_bp = Blueprint("measure", __name__)
|
|||||||
@role_required("MeasurementTec")
|
@role_required("MeasurementTec")
|
||||||
def select_recipe():
|
def select_recipe():
|
||||||
"""Recipe selection page with search and barcode support."""
|
"""Recipe selection page with search and barcode support."""
|
||||||
# Load recipes from API
|
# Fail-fast if STATION_CODE is not configured
|
||||||
resp = api_client.get("/api/recipes", params={"per_page": 100})
|
if not Config.STATION_CODE:
|
||||||
|
return render_template("errors/station_not_configured.html"), 503
|
||||||
|
|
||||||
|
# Load recipes filtered by station
|
||||||
|
try:
|
||||||
|
resp = api_client.get_station_recipes(Config.STATION_CODE)
|
||||||
|
except Exception as e:
|
||||||
|
return render_template(
|
||||||
|
"errors/station_not_configured.html", error=str(e),
|
||||||
|
), 502
|
||||||
|
|
||||||
if isinstance(resp, dict) and resp.get("error"):
|
if isinstance(resp, dict) and resp.get("error"):
|
||||||
flash(
|
flash(
|
||||||
_("Errore nel caricamento delle ricette: %(detail)s",
|
_("Errore nel caricamento delle ricette: %(detail)s",
|
||||||
@@ -43,6 +53,7 @@ def select_recipe():
|
|||||||
return render_template(
|
return render_template(
|
||||||
"measure/select_recipe.html",
|
"measure/select_recipe.html",
|
||||||
recipes=recipes,
|
recipes=recipes,
|
||||||
|
station_code=Config.STATION_CODE,
|
||||||
auto_recipe_code=auto_recipe_code,
|
auto_recipe_code=auto_recipe_code,
|
||||||
auto_lot=auto_lot,
|
auto_lot=auto_lot,
|
||||||
auto_serial=auto_serial,
|
auto_serial=auto_serial,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ _('Stazione non configurata') }} — TieMeasureFlow{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
||||||
|
<div class="mt-20 p-8 bg-[var(--bg-card)] rounded-xl shadow-lg text-center border border-red-200 dark:border-red-800">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full
|
||||||
|
bg-red-50 dark:bg-red-900/30 mb-6">
|
||||||
|
<svg class="w-8 h-8 text-measure-fail" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold text-measure-fail mb-4">
|
||||||
|
{{ _('Stazione non configurata') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="mb-4 text-[var(--text-primary)]">
|
||||||
|
{{ _('Questo client non ha impostato la variabile di ambiente STATION_CODE.') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-6 text-sm text-[var(--text-secondary)]">
|
||||||
|
{{ _('Contattare il responsabile IT: il file .env del container deve contenere STATION_CODE con il codice della stazione assegnata.') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<pre class="text-xs text-left text-[var(--text-secondary)] bg-[var(--bg-secondary)]
|
||||||
|
p-3 rounded-lg border border-[var(--border-color)] overflow-auto">{{ error }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -75,6 +75,9 @@
|
|||||||
<p class="mt-1 text-sm text-[var(--text-secondary)]">
|
<p class="mt-1 text-sm text-[var(--text-secondary)]">
|
||||||
{{ _('Scegli la ricetta di misura da eseguire') }}
|
{{ _('Scegli la ricetta di misura da eseguire') }}
|
||||||
</p>
|
</p>
|
||||||
|
<p class="mt-1 text-sm text-steel-500 dark:text-steel-400">
|
||||||
|
{{ _('Stazione') }}: <span class="font-mono font-bold">{{ station_code }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,18 @@ import pytest
|
|||||||
class TestSelectRecipe:
|
class TestSelectRecipe:
|
||||||
"""GET /measure/select tests."""
|
"""GET /measure/select tests."""
|
||||||
|
|
||||||
def test_select_recipe_renders(self, logged_in_client, mock_api_client):
|
def test_select_recipe_renders(self, logged_in_client, mock_api_client, monkeypatch):
|
||||||
"""Recipe selection page renders for MeasurementTec role."""
|
"""Recipe selection page renders for MeasurementTec role."""
|
||||||
mock_api_client.get.return_value = {
|
monkeypatch.setenv("STATION_CODE", "ST-TEST")
|
||||||
"items": [
|
import config
|
||||||
{"id": 1, "code": "REC-001", "name": "Test Recipe"},
|
import importlib
|
||||||
],
|
importlib.reload(config)
|
||||||
"total": 1,
|
import blueprints.measure
|
||||||
"pages": 1,
|
importlib.reload(blueprints.measure)
|
||||||
}
|
|
||||||
|
mock_api_client.get_station_recipes.return_value = [
|
||||||
|
{"id": 1, "code": "REC-001", "name": "Test Recipe"},
|
||||||
|
]
|
||||||
|
|
||||||
resp = logged_in_client.get("/measure/select")
|
resp = logged_in_client.get("/measure/select")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Verify that /measure/select reads STATION_CODE and filters recipes via the server."""
|
||||||
|
import importlib
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_measure(monkeypatch, station_code=None):
|
||||||
|
"""Reload config and measure module under the given STATION_CODE env."""
|
||||||
|
if station_code is None:
|
||||||
|
monkeypatch.delenv("STATION_CODE", raising=False)
|
||||||
|
else:
|
||||||
|
monkeypatch.setenv("STATION_CODE", station_code)
|
||||||
|
import config
|
||||||
|
importlib.reload(config)
|
||||||
|
import blueprints.measure
|
||||||
|
importlib.reload(blueprints.measure)
|
||||||
|
|
||||||
|
|
||||||
|
def test_select_recipe_calls_station_endpoint(logged_in_client, monkeypatch):
|
||||||
|
_reload_measure(monkeypatch, station_code="ST-TEST")
|
||||||
|
from blueprints import measure as measure_bp_mod
|
||||||
|
with patch.object(measure_bp_mod, "api_client") as mock_api:
|
||||||
|
mock_api.get_station_recipes.return_value = [
|
||||||
|
{"id": 1, "code": "R1", "name": "Recipe 1", "active": True},
|
||||||
|
]
|
||||||
|
resp = logged_in_client.get("/measure/select")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
mock_api.get_station_recipes.assert_called_once()
|
||||||
|
args, kwargs = mock_api.get_station_recipes.call_args
|
||||||
|
assert args[0] == "ST-TEST" or kwargs.get("station_code") == "ST-TEST"
|
||||||
|
|
||||||
|
|
||||||
|
def test_select_recipe_without_station_code_shows_error(logged_in_client, monkeypatch):
|
||||||
|
_reload_measure(monkeypatch, station_code=None)
|
||||||
|
resp = logged_in_client.get("/measure/select")
|
||||||
|
assert resp.status_code == 503
|
||||||
|
body = resp.data.lower()
|
||||||
|
assert b"station_code" in body or b"stazione" in body
|
||||||
Reference in New Issue
Block a user