"""Библиотека сценариев по HTTP. `GET /api/scenarios/{id}` **не отдаёт** `facts` и `ground_truth`: иначе курсант откроет DevTools и прочитает адрес до того, как его спросит. `checklist` скрыт по той же причине и даже более веской: чек-лист — это содержимое подсказок. Отдать его целиком значит выдать в контрольном режиме то, чего там не должно быть вовсе, и обойти выдачу по одному пункту (docs/product/MODES.md#подсказка-по-запросу). Подсказки идут только событием `hint.shown` из живой сессии, эталонные вопросы — только в разборе. """ from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.api.auth import audit, require from app.domain import ekp from app.db.base import get_session from app.domain.roles import Role from app.scenarios import store from app.scenarios.editor import validate from app.scenarios.loader import ScenarioError router = APIRouter(prefix="/api/scenarios", tags=["scenarios"]) HIDDEN_FROM_TRAINEE = {"facts", "ground_truth", "tree", "checklist"} class TemplateDraftIn(BaseModel): source_id: str = Field(min_length=1) title: str | None = Field(default=None, min_length=1, max_length=200) def _draft_out(row) -> dict: return { "id": row.id, "status": row.status, "generation": "template_copy", "body": row.body, } @router.post("/drafts/from-template", status_code=201) async def create_template_draft( body: TemplateDraftIn, request: Request, db: AsyncSession = Depends(get_session) ) -> dict: who = require(request, Role.INSTRUCTOR) source = store.get(body.source_id) if source is None: raise HTTPException(status_code=404, detail="published_source_not_found") row = await store.create_draft(db, source=source, title=body.title) await audit(who.login, who.role.value, "scenario.draft.create", row.id, f"template:{source.id}") return _draft_out(row) @router.get("/drafts/{scenario_id}") async def read_draft( scenario_id: str, request: Request, db: AsyncSession = Depends(get_session) ) -> dict: require(request, Role.INSTRUCTOR) row = await store.draft(db, scenario_id) if row is None: raise HTTPException(status_code=404, detail="draft_not_found") return _draft_out(row) @router.patch("/drafts/{scenario_id}") async def patch_draft( scenario_id: str, body: dict[str, Any], request: Request, db: AsyncSession = Depends(get_session), ) -> dict: who = require(request, Role.INSTRUCTOR) row = await store.draft(db, scenario_id) if row is None: raise HTTPException(status_code=404, detail="draft_not_found") try: row = await store.update_draft(db, row, body) except ScenarioError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc await audit(who.login, who.role.value, "scenario.draft.update", row.id) return _draft_out(row) @router.post("/drafts/{scenario_id}/validate") async def validate_draft( scenario_id: str, request: Request, db: AsyncSession = Depends(get_session) ) -> dict: require(request, Role.INSTRUCTOR) row = await store.draft(db, scenario_id) if row is None: raise HTTPException(status_code=404, detail="draft_not_found") try: scenario = validate(row.body) except ScenarioError as exc: return {"valid": False, "errors": [str(exc)]} return { "valid": True, "errors": [], "ground_truth": scenario.ground_truth.model_dump(mode="json"), } @router.post("/drafts/{scenario_id}/approve") async def approve_draft( scenario_id: str, request: Request, db: AsyncSession = Depends(get_session) ) -> dict: who = require(request, Role.INSTRUCTOR) row = await store.draft(db, scenario_id) if row is None: raise HTTPException(status_code=404, detail="draft_not_found") try: scenario = await store.approve_draft(db, row) except ScenarioError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc await audit(who.login, who.role.value, "scenario.approve", scenario.id) return {"id": scenario.id, "status": "published", "title": scenario.title} @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, "ticket": scenario.ticket, "position": scenario.position, "ekp_group": (ekp.incident(scenario.ground_truth.incident_code).group if scenario.ground_truth.incident_code and ekp.incident(scenario.ground_truth.incident_code) 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