Complete DDS training workflow and delivery package
This commit is contained in:
parent
68dd83c7c2
commit
4c4b91064f
229 changed files with 11969 additions and 1024 deletions
|
|
@ -8,11 +8,14 @@ import logging
|
|||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import update
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from app.db import repo
|
||||
from app.db.models import Score, SelfAssessment, Session
|
||||
from app.db.models import Score, SelfAssessment, Session, User, Utterance
|
||||
from app.domain.events import Mood, Speaker, TranscriptEntry
|
||||
from app.session.checkpoint import dump_state, load_state
|
||||
from app.session.state import SessionState, now_utc
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -32,8 +35,8 @@ class DbJournal:
|
|||
|
||||
async def start_lesson(
|
||||
self, session_id: UUID, scenario_id: str, mode: str, trainee_name: str | None,
|
||||
trainee_id: UUID | None = None,
|
||||
) -> tuple[int, UUID | None]:
|
||||
trainee_id: UUID | None = None, owner_login: str | None = None,
|
||||
) -> tuple[int, UUID | None, str | None]:
|
||||
"""Завести сессию в журнале и вернуть номер попытки и ID курсанта.
|
||||
|
||||
Если база недоступна, занятие всё равно идёт: номер попытки
|
||||
|
|
@ -48,11 +51,75 @@ class DbJournal:
|
|||
mode=mode,
|
||||
trainee_name=trainee_name,
|
||||
trainee_id=trainee_id,
|
||||
owner_login=owner_login,
|
||||
)
|
||||
return row.attempt, row.trainee_id
|
||||
service = None
|
||||
if row.trainee_id is not None:
|
||||
service = await db.scalar(
|
||||
select(User.service)
|
||||
.where(User.trainee_id == row.trainee_id, User.blocked.is_(False))
|
||||
.limit(1)
|
||||
)
|
||||
return row.attempt, row.trainee_id, service
|
||||
except PermissionError:
|
||||
raise
|
||||
except Exception: # noqa: BLE001 — журнал не должен ронять живую сессию
|
||||
log.exception("журнал: сессию завести не удалось")
|
||||
return 1, trainee_id
|
||||
return 1, trainee_id, None
|
||||
|
||||
async def checkpoint(self, state: SessionState) -> None:
|
||||
"""Сохранить снимок после подтверждённого действия пользователя."""
|
||||
async def action(db):
|
||||
values = (
|
||||
{"live_state": None, "checkpoint_at": None}
|
||||
if state.ended
|
||||
else {"live_state": dump_state(state), "checkpoint_at": now_utc()}
|
||||
)
|
||||
await db.execute(update(Session).where(Session.id == state.session_id).values(**values))
|
||||
await db.commit()
|
||||
|
||||
await self._write(lambda db: action(db))
|
||||
|
||||
async def restore_active(self) -> list[SessionState]:
|
||||
"""Восстановить только незавершённые сессии с валидным снимком."""
|
||||
restored: list[SessionState] = []
|
||||
async with self._sessionmaker() as db:
|
||||
rows = (await db.scalars(
|
||||
select(Session).where(
|
||||
Session.ended_at.is_(None),
|
||||
Session.live_state.is_not(None),
|
||||
Session.checkpoint_at.is_not(None),
|
||||
)
|
||||
)).all()
|
||||
for row in rows:
|
||||
try:
|
||||
state = load_state(row.live_state, row.checkpoint_at)
|
||||
if state.session_id != row.id:
|
||||
raise ValueError("ID снимка не совпадает с записью занятия")
|
||||
state.owner_login = row.owner_login
|
||||
# Реплики пишутся отдельно сразу после появления. Если
|
||||
# процесс умер между репликой и общим снимком, отдельный
|
||||
# журнал не даёт потерять последний фрагмент диалога.
|
||||
utterances = (await db.scalars(
|
||||
select(Utterance)
|
||||
.where(Utterance.session_id == row.id)
|
||||
.order_by(Utterance.at, Utterance.ref)
|
||||
)).all()
|
||||
if utterances:
|
||||
state.transcript = [
|
||||
TranscriptEntry(
|
||||
ref=item.ref,
|
||||
speaker=Speaker(item.speaker),
|
||||
text=item.text,
|
||||
at=item.at,
|
||||
mood=Mood(item.mood) if item.mood else None,
|
||||
)
|
||||
for item in utterances
|
||||
]
|
||||
restored.append(state)
|
||||
except Exception: # noqa: BLE001 — один снимок не блокирует весь стенд
|
||||
log.exception("журнал: снимок занятия %s повреждён", row.id)
|
||||
return restored
|
||||
|
||||
async def utterance(self, session_id: UUID, entry) -> None:
|
||||
await self._write(
|
||||
|
|
@ -111,6 +178,16 @@ class DbJournal:
|
|||
|
||||
await self._write(lambda db: action(db))
|
||||
|
||||
async def score_snapshot(self, session_id: UUID, report: dict) -> None:
|
||||
"""Обновить полный архивный разбор после самооценки курсанта."""
|
||||
async def action(db):
|
||||
await db.execute(
|
||||
update(Score).where(Score.session_id == session_id).values(report=report)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
await self._write(lambda db: action(db))
|
||||
|
||||
async def session_started(self, session_id: UUID, at: datetime) -> None:
|
||||
async def action(db):
|
||||
await db.execute(update(Session).where(Session.id == session_id).values(started_at=at))
|
||||
|
|
@ -123,7 +200,12 @@ class DbJournal:
|
|||
await db.execute(
|
||||
update(Session)
|
||||
.where(Session.id == session_id)
|
||||
.values(ended_at=at, end_reason=reason)
|
||||
.values(
|
||||
ended_at=at,
|
||||
end_reason=reason,
|
||||
live_state=None,
|
||||
checkpoint_at=None,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue