Merge branch 'feat/finish-card-status'

fix: card_status в отчёте ДДС берётся у карточки, активной при завершении
This commit is contained in:
GGlamer 2026-09-27 00:12:18 +03:00 • committed by GitHub
commit c0fa0a50a1
2 changed files with 113 additions and 16 deletions

View file

@ -150,6 +150,29 @@ def scoring_scenario(state):
return state.scenario or store.get(state.scenario_id) return state.scenario or store.get(state.scenario_id)
def score_live_dds_cards(state) -> list[DdsCardRecord]:
"""Оценить все неоценённые карточки очереди и вернуть пульт курсанту.
Оценка переключает рабочие поля сессии на каждую карточку по очереди.
Без возврата отчёт взял бы card_status у карточки, оценённой последней,
а пульт после завершения показывал бы чужую карточку.
"""
cards = list(state.dds_completed)
state.capture_active_dds()
active_id = state.dds_active_card_id
active_card = state.dispatched_card
for live in sorted(state.dds_live_cards, key=lambda item: item.original_index):
if any(item.card_id == live.card_id for item in cards):
continue
state.activate_dds_card(live.card_id, capture=False)
cards.append(score_current_dds(state))
state.dds_completed = cards
if active_id is None or not state.activate_dds_card(active_id, capture=False):
state.dds_active_card_id = active_id
state.dispatched_card = active_card
return cards
async def finish(session_id: UUID, state) -> None: async def finish(session_id: UUID, state) -> None:
if state.recorder is not None: if state.recorder is not None:
path = await asyncio.to_thread(state.recorder.finalize) path = await asyncio.to_thread(state.recorder.finalize)
@ -204,14 +227,7 @@ async def finish(session_id: UUID, state) -> None:
# Все карточки выданы одновременно: при досрочном завершении оцениваем # Все карточки выданы одновременно: при досрочном завершении оцениваем
# каждую, включая не открытую, потому что её норматив уже шёл. # каждую, включая не открытую, потому что её норматив уже шёл.
result = GostResult() result = GostResult()
cards = list(state.dds_completed) cards = score_live_dds_cards(state)
state.capture_active_dds()
for live in sorted(state.dds_live_cards, key=lambda item: item.original_index):
if any(item.card_id == live.card_id for item in cards):
continue
state.activate_dds_card(live.card_id, capture=False)
cards.append(score_current_dds(state))
state.dds_completed = cards
for card in cards: for card in cards:
result.metrics.extend(card.metrics) result.metrics.extend(card.metrics)
result.findings.extend(card.findings) result.findings.extend(card.findings)
@ -283,14 +299,7 @@ async def finish(session_id: UUID, state) -> None:
if state.handoff_to_dds and state.dds_scenarios: if state.handoff_to_dds and state.dds_scenarios:
# В связке КИО оценивается относительно эталона и весов упражнения # В связке КИО оценивается относительно эталона и весов упражнения
# 112, а каждая карточка очереди уже взвешена собственным сценарием. # 112, а каждая карточка очереди уже взвешена собственным сценарием.
cards = list(state.dds_completed) cards = score_live_dds_cards(state)
state.capture_active_dds()
for live in sorted(state.dds_live_cards, key=lambda item: item.original_index):
if any(item.card_id == live.card_id for item in cards):
continue
state.activate_dds_card(live.card_id, capture=False)
cards.append(score_current_dds(state))
state.dds_completed = cards
for card in cards: for card in cards:
result.metrics.extend(card.metrics) result.metrics.extend(card.metrics)
result.findings.extend(card.findings) result.findings.extend(card.findings)

View file

@ -0,0 +1,88 @@
"""card_status отчёта — статус карточки, активной у курсанта при завершении."""
import asyncio
from pathlib import Path
from uuid import uuid4
import pytest
from app.domain.events import Exercise, SessionMode
from app.domain.kio import KIO
from app.domain.statuses import CardStatus, ServiceStatus
from app.scenarios.loader import load_file
from app.session import finish as finish_module
from app.session.dds import prepare_handoff_queue, prepare_queue
from app.session.hub import hub
from app.session.state import SessionState
LIBRARY = Path(__file__).resolve().parents[2] / "scenarios"
@pytest.fixture(autouse=True)
def offline_finish(monkeypatch):
class NoCoaching:
def model_dump(self, **_kwargs):
return {}
async def no_coach(_metrics):
return NoCoaching()
monkeypatch.setattr(finish_module, "coach", no_coach)
monkeypatch.setattr(hub, "journal", None)
def accept_active_card(state: SessionState) -> None:
service = state.notified_services()[0]
state.set_service_status(
service, ServiceStatus.ACCEPTED,
"Основание: доклад старшего.\nСведения: карточка принята.",
)
def assert_active_card_kept(state: SessionState, active_id, active_status) -> None:
assert active_status is not CardStatus.REGISTERED
assert state.score["card_status"] == active_status.value
assert state.dds_active_card_id == active_id
assert state.dispatched_card.card_id == active_id
assert len(state.score["card_results"]) == 2
def test_dds_finish_reports_status_of_active_card_not_last_scored():
first = load_file(LIBRARY / "fire-apartment-l2.yaml", LIBRARY)
second = load_file(LIBRARY / "tickets" / "t20-2-stroke.yaml", LIBRARY)
state = SessionState(
session_id=uuid4(), scenario_id=first.id, scenario_title=first.title,
level=first.level.value, mode=SessionMode.TRAINING, exercise=Exercise.DDS,
)
prepare_queue(state, [first, second])
assert len(state.dds_live_cards) == 2
active_id = state.dds_active_card_id
accept_active_card(state)
active_status = state.station_snapshot().card
asyncio.run(finish_module.finish(state.session_id, state))
assert_active_card_kept(state, active_id, active_status)
def test_handoff_finish_reports_status_of_active_card_not_last_scored():
first = load_file(LIBRARY / "fire-apartment-l2.yaml", LIBRARY)
second = load_file(LIBRARY / "tickets" / "t01-1-fire-container.yaml", LIBRARY)
state = SessionState(
session_id=uuid4(), scenario_id=first.id, scenario_title=first.title,
level=first.level.value, mode=SessionMode.TRAINING,
exercise=Exercise.CARD, handoff_to_dds=True, scenario=first,
pending_dds_scenarios=[second],
)
state.kio = KIO(
address="улица Ленина, 14", description="горит балкон", notify=["Служба 101"],
)
state.dispatch()
prepare_handoff_queue(state, state.pending_dds_scenarios)
assert len(state.dds_live_cards) == 2
active_id = state.dds_active_card_id
accept_active_card(state)
active_status = state.station_snapshot().card
asyncio.run(finish_module.finish(state.session_id, state))
assert_active_card_kept(state, active_id, active_status)