БД: 11 таблиц, первая миграция. Группы и связь trainee → group заложены
сразу, даже пустыми — размечать накопленные сессии задним числом значит
делать лишнюю миграцию. Номер попытки живёт в сессии, отдельной таблицы
попыток нет: дельта считается запросом по (trainee_id, scenario_id).
Сценарии: строгая схема — опечатка в имени поля падает на старте, а не
игнорируется молча. ground_truth собирается кодом, попытка задать
incident_type, dds или required_facts в YAML отвергается: иначе генератор
разведёт факты и эталон и курсанта оштрафуют за правильный ответ. Руками
задаются только нормализованные адрес и число пострадавших — из фразы
«улица Ленина, 14, квартира 47, 5-й этаж» кодом «улица Ленина, 14»
не достать.
GET /api/scenarios/{id} больше не отдаёт чек-лист. Это содержимое
подсказок: отдать его целиком значит выдать в контрольном режиме то,
чего там быть не должно, в обход выдачи по одному пункту.
Тесты базы поднимают свой движок на каждый тест: глобальный кэшируется
и привязывается к первому событийному циклу.
47 lines
1.9 KiB
Python
47 lines
1.9 KiB
Python
"""Библиотека сценариев по HTTP.
|
||
|
||
`GET /api/scenarios/{id}` **не отдаёт** `facts` и `ground_truth`: иначе курсант
|
||
откроет DevTools и прочитает адрес до того, как его спросит.
|
||
|
||
`checklist` скрыт по той же причине и даже более веской: чек-лист — это
|
||
содержимое подсказок. Отдать его целиком значит выдать в контрольном режиме
|
||
то, чего там не должно быть вовсе, и обойти выдачу по одному пункту
|
||
(docs/product/MODES.md#подсказка-по-запросу). Подсказки идут только событием
|
||
`hint.shown` из живой сессии, эталонные вопросы — только в разборе.
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
|
||
from app.scenarios import store
|
||
|
||
router = APIRouter(prefix="/api/scenarios", tags=["scenarios"])
|
||
|
||
HIDDEN_FROM_TRAINEE = {"facts", "ground_truth", "tree", "checklist"}
|
||
|
||
|
||
@router.get("")
|
||
async def listing() -> list[dict]:
|
||
return [
|
||
{
|
||
"id": scenario.id,
|
||
"title": scenario.title,
|
||
"type": scenario.type.value,
|
||
"level": scenario.level.value,
|
||
"topics": scenario.topics,
|
||
"modes": scenario.modes,
|
||
"dds": scenario.ground_truth.dds.value if scenario.ground_truth.dds else None,
|
||
}
|
||
for scenario in store.all_scenarios()
|
||
]
|
||
|
||
|
||
@router.get("/{scenario_id}")
|
||
async def read(scenario_id: str) -> dict:
|
||
scenario = store.get(scenario_id)
|
||
if scenario is None:
|
||
raise HTTPException(status_code=404, detail="scenario_not_found")
|
||
payload = scenario.model_dump(mode="json")
|
||
for key in HIDDEN_FROM_TRAINEE:
|
||
payload.pop(key, None)
|
||
payload["required_fields"] = scenario.required_fields
|
||
return payload
|