Ripristino delle quote già misurate nel ciclo in corso dopo un reload (#40)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ from src.backend.models.orm.recipe import RecipeVersion
|
|||||||
from src.backend.models.orm.setting import SystemSetting
|
from src.backend.models.orm.setting import SystemSetting
|
||||||
from src.backend.models.orm.user import User
|
from src.backend.models.orm.user import User
|
||||||
from src.backend.models.api.measurement import (
|
from src.backend.models.api.measurement import (
|
||||||
|
CurrentCycleResponse,
|
||||||
MeasurementAuthorisation,
|
MeasurementAuthorisation,
|
||||||
MeasurementBatchCreate,
|
MeasurementBatchCreate,
|
||||||
MeasurementCreate,
|
MeasurementCreate,
|
||||||
@@ -97,6 +98,24 @@ async def get_task_progress(
|
|||||||
return TaskProgressListResponse(tasks=tasks)
|
return TaskProgressListResponse(tasks=tasks)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/current-cycle", response_model=CurrentCycleResponse)
|
||||||
|
async def get_current_cycle(
|
||||||
|
production_run_id: int = Query(..., gt=0),
|
||||||
|
task_id: int = Query(..., gt=0),
|
||||||
|
user: User = Depends(require_measurement_tec),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Quotes of this task already measured in the cycle under way.
|
||||||
|
|
||||||
|
Asked on load, so that a reload mid-cycle does not present quotes again that
|
||||||
|
were taken a moment ago.
|
||||||
|
"""
|
||||||
|
rows = await measurement_service.current_cycle_measurements(
|
||||||
|
db, production_run_id, task_id,
|
||||||
|
)
|
||||||
|
return CurrentCycleResponse(measurements=rows)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{measurement_id}/authorise", response_model=MeasurementResponse)
|
@router.post("/{measurement_id}/authorise", response_model=MeasurementResponse)
|
||||||
async def authorise_measurement(
|
async def authorise_measurement(
|
||||||
measurement_id: int,
|
measurement_id: int,
|
||||||
|
|||||||
@@ -91,6 +91,21 @@ class TaskProgressListResponse(BaseModel):
|
|||||||
tasks: list[TaskProgressResponse]
|
tasks: list[TaskProgressResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class CycleMeasurementResponse(BaseModel):
|
||||||
|
"""One quote already measured in the cycle under way."""
|
||||||
|
|
||||||
|
subtask_id: int
|
||||||
|
value: float
|
||||||
|
pass_fail: str
|
||||||
|
measured_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentCycleResponse(BaseModel):
|
||||||
|
"""The quotes to restore on the screen after a reload; empty if the task is closed."""
|
||||||
|
|
||||||
|
measurements: list[CycleMeasurementResponse]
|
||||||
|
|
||||||
|
|
||||||
class MeasurementQuery(BaseModel):
|
class MeasurementQuery(BaseModel):
|
||||||
"""Schema for measurement query filters."""
|
"""Schema for measurement query filters."""
|
||||||
recipe_id: Optional[int] = None
|
recipe_id: Optional[int] = None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from sqlalchemy import func, select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.backend.models.orm.measurement import Measurement
|
from src.backend.models.orm.measurement import Measurement
|
||||||
|
from src.backend.models.orm.production import ProductionEvent
|
||||||
from src.backend.models.orm.recipe import Recipe, RecipeVersion
|
from src.backend.models.orm.recipe import Recipe, RecipeVersion
|
||||||
from src.backend.models.orm.task import (
|
from src.backend.models.orm.task import (
|
||||||
MEASURING_TASK_TYPES, RecipeSubtask, RecipeTask,
|
MEASURING_TASK_TYPES, RecipeSubtask, RecipeTask,
|
||||||
@@ -241,6 +242,73 @@ async def task_progress(
|
|||||||
return progress
|
return progress
|
||||||
|
|
||||||
|
|
||||||
|
async def current_cycle_measurements(
|
||||||
|
db: AsyncSession, run_id: int, task_id: int,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""The quotes of a task already measured in the cycle that is under way.
|
||||||
|
|
||||||
|
A reload, a timer redirect or an auto-logout throws away what the page held in
|
||||||
|
memory; the operator would measure the same quotes again and the duplicates
|
||||||
|
would reach the SPC statistics. The cycle needs no column of its own: it starts
|
||||||
|
at the latest event of the run that opens one for this task - the run start, a
|
||||||
|
cycle_completed, or a remeasure of this task.
|
||||||
|
|
||||||
|
If a task_measured of this task follows that boundary the task is already
|
||||||
|
closed in this cycle and there is nothing to restore. Where a quote was measured
|
||||||
|
more than once, the last reading stands.
|
||||||
|
"""
|
||||||
|
boundary = (await db.execute(
|
||||||
|
select(ProductionEvent.id, ProductionEvent.created_at)
|
||||||
|
.where(
|
||||||
|
ProductionEvent.run_id == run_id,
|
||||||
|
(ProductionEvent.event_type.in_(("start", "cycle_completed")))
|
||||||
|
| (
|
||||||
|
(ProductionEvent.event_type == "remeasure")
|
||||||
|
& (ProductionEvent.task_id == task_id)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order_by(ProductionEvent.id.desc())
|
||||||
|
.limit(1)
|
||||||
|
)).first()
|
||||||
|
|
||||||
|
event_filter = [ProductionEvent.run_id == run_id]
|
||||||
|
measurement_filter = [Measurement.production_run_id == run_id]
|
||||||
|
if boundary is not None:
|
||||||
|
boundary_id, boundary_at = boundary
|
||||||
|
event_filter.append(ProductionEvent.id > boundary_id)
|
||||||
|
measurement_filter.append(Measurement.measured_at > boundary_at)
|
||||||
|
|
||||||
|
closed = (await db.execute(
|
||||||
|
select(ProductionEvent.id).where(
|
||||||
|
*event_filter,
|
||||||
|
ProductionEvent.event_type == "task_measured",
|
||||||
|
ProductionEvent.task_id == task_id,
|
||||||
|
).limit(1)
|
||||||
|
)).first()
|
||||||
|
if closed is not None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
rows = (await db.execute(
|
||||||
|
select(Measurement)
|
||||||
|
.join(RecipeSubtask, RecipeSubtask.id == Measurement.subtask_id)
|
||||||
|
.where(*measurement_filter, RecipeSubtask.task_id == task_id)
|
||||||
|
.order_by(Measurement.measured_at, Measurement.id)
|
||||||
|
)).scalars().all()
|
||||||
|
|
||||||
|
latest: dict[int, Measurement] = {}
|
||||||
|
for m in rows:
|
||||||
|
latest[m.subtask_id] = m
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"subtask_id": m.subtask_id,
|
||||||
|
"value": float(m.value),
|
||||||
|
"pass_fail": m.pass_fail,
|
||||||
|
"measured_at": m.measured_at,
|
||||||
|
}
|
||||||
|
for m in sorted(latest.values(), key=lambda m: (m.measured_at, m.id))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def validate_run_for_version(
|
async def validate_run_for_version(
|
||||||
db: AsyncSession, run_id: int, version_id: int,
|
db: AsyncSession, run_id: int, version_id: int,
|
||||||
) -> "ProductionRun":
|
) -> "ProductionRun":
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"""#40: after a reload, the quotes already measured in the cycle come back.
|
||||||
|
|
||||||
|
The cycle has no column of its own: it opens at the latest start, cycle_completed
|
||||||
|
or remeasure-of-this-task event of the run, and a task_measured of the task after
|
||||||
|
that boundary means the task is closed in this cycle.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from src.backend.models.orm.measurement import Measurement
|
||||||
|
from src.backend.models.orm.production import ProductionEvent, ProductionRun
|
||||||
|
from src.backend.models.orm.recipe import RecipeVersion
|
||||||
|
from src.backend.models.orm.station import Station
|
||||||
|
from src.backend.models.orm.task import RecipeSubtask, RecipeTask
|
||||||
|
from src.backend.tests.conftest import auth_headers, create_test_recipe
|
||||||
|
|
||||||
|
T0 = datetime(2026, 10, 1, 8, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
async def _setup(db, user):
|
||||||
|
recipe = await create_test_recipe(db, user.id)
|
||||||
|
version = (await db.execute(
|
||||||
|
select(RecipeVersion).where(
|
||||||
|
RecipeVersion.recipe_id == recipe.id, RecipeVersion.is_current == True, # noqa: E712
|
||||||
|
)
|
||||||
|
)).scalar_one()
|
||||||
|
task = (await db.execute(
|
||||||
|
select(RecipeTask).where(RecipeTask.version_id == version.id)
|
||||||
|
)).scalars().first()
|
||||||
|
first = (await db.execute(
|
||||||
|
select(RecipeSubtask).where(RecipeSubtask.task_id == task.id)
|
||||||
|
)).scalars().first()
|
||||||
|
second = RecipeSubtask(
|
||||||
|
task_id=task.id, marker_number=2, description="Quota 2",
|
||||||
|
nominal=10.0, utl=10.5, uwl=10.3, lwl=9.7, ltl=9.5, unit="mm",
|
||||||
|
)
|
||||||
|
db.add(second)
|
||||||
|
station = Station(code="ST-CYC", name="St", active=True, created_by=user.id)
|
||||||
|
db.add(station)
|
||||||
|
await db.commit()
|
||||||
|
run = ProductionRun(
|
||||||
|
station_id=station.id, recipe_id=recipe.id, version_id=version.id,
|
||||||
|
operator_id=user.id, status="running", active_station_id=station.id,
|
||||||
|
)
|
||||||
|
db.add(run)
|
||||||
|
await db.commit()
|
||||||
|
return run, version, task, first, second
|
||||||
|
|
||||||
|
|
||||||
|
async def _event(db, run, user, kind, minutes, task_id=None):
|
||||||
|
db.add(ProductionEvent(
|
||||||
|
run_id=run.id, event_type=kind, user_id=user.id, task_id=task_id,
|
||||||
|
created_at=T0 + timedelta(minutes=minutes),
|
||||||
|
))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _meas(db, run, version, subtask, user, minutes, value=10.0):
|
||||||
|
db.add(Measurement(
|
||||||
|
subtask_id=subtask.id, version_id=version.id, measured_by=user.id,
|
||||||
|
value=value, pass_fail="pass", production_run_id=run.id,
|
||||||
|
measured_at=T0 + timedelta(minutes=minutes),
|
||||||
|
))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _get(client, user, run, task):
|
||||||
|
resp = await client.get(
|
||||||
|
"/api/measurements/current-cycle", headers=auth_headers(user),
|
||||||
|
params={"production_run_id": run.id, "task_id": task.id},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
return resp.json()["measurements"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_only_second_cycle_after_cycle_completed(
|
||||||
|
client: AsyncClient, db_session, measurement_tec_user,
|
||||||
|
):
|
||||||
|
u = measurement_tec_user
|
||||||
|
run, version, task, q1, q2 = await _setup(db_session, u)
|
||||||
|
await _event(db_session, run, u, "start", 0)
|
||||||
|
await _meas(db_session, run, version, q1, u, 1, 10.1)
|
||||||
|
await _meas(db_session, run, version, q2, u, 2, 10.2)
|
||||||
|
await _event(db_session, run, u, "cycle_completed", 3)
|
||||||
|
await _meas(db_session, run, version, q1, u, 4, 10.3)
|
||||||
|
|
||||||
|
rows = await _get(client, u, run, task)
|
||||||
|
assert [(r["subtask_id"], r["value"]) for r in rows] == [(q1.id, 10.3)]
|
||||||
|
assert rows[0]["pass_fail"] == "pass"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_last_reading_of_a_quote_wins(
|
||||||
|
client: AsyncClient, db_session, measurement_tec_user,
|
||||||
|
):
|
||||||
|
u = measurement_tec_user
|
||||||
|
run, version, task, q1, q2 = await _setup(db_session, u)
|
||||||
|
await _event(db_session, run, u, "start", 0)
|
||||||
|
await _meas(db_session, run, version, q1, u, 1, 10.1)
|
||||||
|
await _meas(db_session, run, version, q1, u, 2, 10.4)
|
||||||
|
|
||||||
|
rows = await _get(client, u, run, task)
|
||||||
|
assert [(r["subtask_id"], r["value"]) for r in rows] == [(q1.id, 10.4)]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remeasure_of_the_task_empties_the_list(
|
||||||
|
client: AsyncClient, db_session, measurement_tec_user,
|
||||||
|
):
|
||||||
|
u = measurement_tec_user
|
||||||
|
run, version, task, q1, q2 = await _setup(db_session, u)
|
||||||
|
await _event(db_session, run, u, "start", 0)
|
||||||
|
await _meas(db_session, run, version, q1, u, 1)
|
||||||
|
await _event(db_session, run, u, "remeasure", 2, task_id=task.id)
|
||||||
|
|
||||||
|
assert await _get(client, u, run, task) == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remeasure_of_another_task_does_not_move_the_boundary(
|
||||||
|
client: AsyncClient, db_session, measurement_tec_user,
|
||||||
|
):
|
||||||
|
u = measurement_tec_user
|
||||||
|
run, version, task, q1, q2 = await _setup(db_session, u)
|
||||||
|
other = RecipeTask(
|
||||||
|
version_id=version.id, title="Altro", task_type=task.task_type, order_index=9,
|
||||||
|
)
|
||||||
|
db_session.add(other)
|
||||||
|
await db_session.commit()
|
||||||
|
await _event(db_session, run, u, "start", 0)
|
||||||
|
await _meas(db_session, run, version, q1, u, 1)
|
||||||
|
await _event(db_session, run, u, "remeasure", 2, task_id=other.id)
|
||||||
|
|
||||||
|
assert len(await _get(client, u, run, task)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_task_measured_after_boundary_means_closed(
|
||||||
|
client: AsyncClient, db_session, measurement_tec_user,
|
||||||
|
):
|
||||||
|
u = measurement_tec_user
|
||||||
|
run, version, task, q1, q2 = await _setup(db_session, u)
|
||||||
|
await _event(db_session, run, u, "start", 0)
|
||||||
|
await _meas(db_session, run, version, q1, u, 1)
|
||||||
|
await _meas(db_session, run, version, q2, u, 2)
|
||||||
|
await _event(db_session, run, u, "task_measured", 3, task_id=task.id)
|
||||||
|
|
||||||
|
assert await _get(client, u, run, task) == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_task_measured_before_boundary_does_not_close_next_cycle(
|
||||||
|
client: AsyncClient, db_session, measurement_tec_user,
|
||||||
|
):
|
||||||
|
u = measurement_tec_user
|
||||||
|
run, version, task, q1, q2 = await _setup(db_session, u)
|
||||||
|
await _event(db_session, run, u, "start", 0)
|
||||||
|
await _event(db_session, run, u, "task_measured", 1, task_id=task.id)
|
||||||
|
await _event(db_session, run, u, "cycle_completed", 2)
|
||||||
|
await _meas(db_session, run, version, q1, u, 3)
|
||||||
|
|
||||||
|
assert len(await _get(client, u, run, task)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_permissions(client: AsyncClient, db_session, measurement_tec_user, maker_user):
|
||||||
|
run, _v, task, _q1, _q2 = await _setup(db_session, measurement_tec_user)
|
||||||
|
params = {"production_run_id": run.id, "task_id": task.id}
|
||||||
|
|
||||||
|
no_key = await client.get("/api/measurements/current-cycle", params=params)
|
||||||
|
assert no_key.status_code in (401, 403)
|
||||||
|
|
||||||
|
forbidden = await client.get(
|
||||||
|
"/api/measurements/current-cycle", headers=auth_headers(maker_user), params=params,
|
||||||
|
)
|
||||||
|
assert forbidden.status_code == 403
|
||||||
@@ -595,6 +595,40 @@ def api_task_progress(task_id: int):
|
|||||||
return jsonify(row), 200
|
return jsonify(row), 200
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Route: quotes already measured in the current cycle (#40)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@measure_bp.route("/api/current-cycle/<int:task_id>", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
@role_required("MeasurementTec")
|
||||||
|
def api_current_cycle(task_id: int):
|
||||||
|
"""Proxy: quotes of this task already measured in the cycle under way.
|
||||||
|
|
||||||
|
The run comes from _current_run_id(), like the rest of the page. Outside a
|
||||||
|
production there is no cycle to restore and the answer is an empty 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"))
|
||||||
|
|
||||||
|
run_id = _current_run_id()
|
||||||
|
if not run_id:
|
||||||
|
return jsonify({"measurements": []}), 200
|
||||||
|
|
||||||
|
resp = api_client.get(
|
||||||
|
"/api/measurements/current-cycle",
|
||||||
|
params={"production_run_id": run_id, "task_id": task_id},
|
||||||
|
)
|
||||||
|
if not isinstance(resp, dict) or resp.get("error"):
|
||||||
|
# Never fatal: without the restore the screen behaves as it always did.
|
||||||
|
return jsonify({"measurements": []}), 200
|
||||||
|
return jsonify(resp), 200
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Routes: Out-of-tolerance authorisation (point 5)
|
# Routes: Out-of-tolerance authorisation (point 5)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1100,6 +1100,7 @@ function taskExecute() {
|
|||||||
this.inputStartedAt = Date.now();
|
this.inputStartedAt = Date.now();
|
||||||
// Rejoin whatever production is already open at this station.
|
// Rejoin whatever production is already open at this station.
|
||||||
await this.loadProductionRun();
|
await this.loadProductionRun();
|
||||||
|
await this.loadCurrentCycle();
|
||||||
await this.loadPendingAuthorisation();
|
await this.loadPendingAuthorisation();
|
||||||
// A tablet left to sleep drifts (#37.7): catch the countdown back up to
|
// A tablet left to sleep drifts (#37.7): catch the countdown back up to
|
||||||
// the server's own clock, the same source adoptProductionRun already
|
// the server's own clock, the same source adoptProductionRun already
|
||||||
@@ -1110,6 +1111,42 @@ function taskExecute() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* Quotes already measured in the cycle under way (#40).
|
||||||
|
|
||||||
|
A reload, a timer redirect or an auto-logout used to bring every quote back
|
||||||
|
and measuring them again put duplicates in the SPC statistics. Only inside
|
||||||
|
a production: outside one there is no cycle, and the screen starts empty as
|
||||||
|
it always did. */
|
||||||
|
async loadCurrentCycle() {
|
||||||
|
if (!this.isMeasureTask || !this.subtasks.length || !this.productionRun) return;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(
|
||||||
|
'{{ url_for("measure.api_current_cycle", task_id=task.id) }}',
|
||||||
|
);
|
||||||
|
if (!resp.ok) return;
|
||||||
|
const data = await resp.json();
|
||||||
|
const known = new Set(this.subtasks.map(s => s.id));
|
||||||
|
const restored = (data.measurements || []).filter(m => known.has(m.subtask_id));
|
||||||
|
if (!restored.length) return;
|
||||||
|
this.measurements = restored.map(m => {
|
||||||
|
const st = this.subtasks.find(s => s.id === m.subtask_id);
|
||||||
|
const nominal = st ? (st.nominal || 0) : 0;
|
||||||
|
return {
|
||||||
|
subtask_id: m.subtask_id,
|
||||||
|
value: m.value,
|
||||||
|
pass_fail: m.pass_fail,
|
||||||
|
deviation: m.value - nominal,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const next = this.subtasks.findIndex(
|
||||||
|
s => !this.measurements.some(m => m.subtask_id === s.id),
|
||||||
|
);
|
||||||
|
this.currentIndex = next !== -1 ? next : this.subtasks.length - 1;
|
||||||
|
} catch (e) {
|
||||||
|
// Offline: the screen starts empty, as before.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* Is a quote already waiting for the capoturno?
|
/* Is a quote already waiting for the capoturno?
|
||||||
|
|
||||||
Reloading the page was the simplest way past the old gate. It is not one
|
Reloading the page was the simplest way past the old gate. It is not one
|
||||||
|
|||||||
@@ -248,6 +248,67 @@ class TestApiTaskProgress:
|
|||||||
assert resp.status_code in (302, 401)
|
assert resp.status_code in (302, 401)
|
||||||
|
|
||||||
|
|
||||||
|
class TestApiCurrentCycle:
|
||||||
|
"""GET /measure/api/current-cycle/<task_id> (gitea#40)."""
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _station_configured(self, monkeypatch):
|
||||||
|
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
|
||||||
|
|
||||||
|
def test_forwards_run_and_task(self, logged_in_client, mock_api_client):
|
||||||
|
mock_api_client.get_station_recipes.return_value = [{"id": 1}]
|
||||||
|
payload = {"measurements": [
|
||||||
|
{"subtask_id": 3, "value": 10.1, "pass_fail": "pass",
|
||||||
|
"measured_at": "2026-10-01T08:01:00"},
|
||||||
|
]}
|
||||||
|
mock_api_client.get.side_effect = api_get_router({
|
||||||
|
"/api/tasks/7": {"id": 7, "recipe_id": 1, "version_id": 42},
|
||||||
|
"/api/production-runs/current": {"id": 5},
|
||||||
|
"/api/measurements/current-cycle": payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
resp = logged_in_client.get("/measure/api/current-cycle/7")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json() == payload
|
||||||
|
call = next(
|
||||||
|
c for c in mock_api_client.get.call_args_list
|
||||||
|
if c.args[0] == "/api/measurements/current-cycle"
|
||||||
|
)
|
||||||
|
assert call.kwargs["params"] == {"production_run_id": 5, "task_id": 7}
|
||||||
|
|
||||||
|
def test_no_run_means_empty_and_no_backend_call(
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
resp = logged_in_client.get("/measure/api/current-cycle/7")
|
||||||
|
|
||||||
|
assert resp.get_json() == {"measurements": []}
|
||||||
|
assert not any(
|
||||||
|
c.args[0] == "/api/measurements/current-cycle"
|
||||||
|
for c in mock_api_client.get.call_args_list
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_refuses_a_recipe_not_assigned_to_this_station(
|
||||||
|
self, logged_in_client, mock_api_client,
|
||||||
|
):
|
||||||
|
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},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert logged_in_client.get("/measure/api/current-cycle/7").status_code == 403
|
||||||
|
|
||||||
|
def test_requires_login(self, client):
|
||||||
|
resp = client.get("/measure/api/current-cycle/7")
|
||||||
|
assert resp.status_code in (302, 401)
|
||||||
|
|
||||||
|
|
||||||
class TestStartSequence:
|
class TestStartSequence:
|
||||||
"""GET /measure/start/<recipe_id> — the operator's way in."""
|
"""GET /measure/start/<recipe_id> — the operator's way in."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user