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,24 +8,77 @@
|
|||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import exists, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.auth import DEMO_TRAINEE_ID, require
|
||||
from app.config import get_settings
|
||||
from app.domain.roles import Role
|
||||
from app.db.base import get_session, get_sessionmaker
|
||||
from app.db.models import Group, Score, Session, Trainee
|
||||
from app.db.models import Group, Score, Session, Trainee, User
|
||||
from app.scoring.export import certificate_pdf
|
||||
from app.voice.recording import recording_path
|
||||
|
||||
router = APIRouter(prefix="/api/trainees", tags=["trainees"])
|
||||
|
||||
|
||||
@router.get("/{trainee_id}/certificate.pdf")
|
||||
async def certificate(
|
||||
trainee_id: UUID, request: Request, db: AsyncSession = Depends(get_session)
|
||||
) -> Response:
|
||||
who = require(request)
|
||||
if who.role is Role.TRAINEE and who.trainee_id != trainee_id:
|
||||
raise HTTPException(status_code=403, detail="not_your_certificate")
|
||||
if who.role is Role.INSTRUCTOR:
|
||||
owns_trainee = await db.scalar(
|
||||
select(Session.id)
|
||||
.where(Session.trainee_id == trainee_id, Session.owner_login == who.login)
|
||||
.limit(1)
|
||||
)
|
||||
if owns_trainee is None:
|
||||
raise HTTPException(status_code=404, detail="trainee_not_found")
|
||||
trainee = await db.get(Trainee, trainee_id)
|
||||
if trainee is None:
|
||||
raise HTTPException(status_code=404, detail="trainee_not_found")
|
||||
group = await db.get(Group, trainee.group_id) if trainee.group_id else None
|
||||
if who.role is Role.INSTRUCTOR and group is not None and group.owner_login != who.login:
|
||||
group = None
|
||||
scores_query = (
|
||||
select(func.count(Score.id), func.avg(Score.score_final), func.max(Score.created_at))
|
||||
.join(Session, Session.id == Score.session_id)
|
||||
.where(Session.trainee_id == trainee_id)
|
||||
)
|
||||
if who.role is Role.INSTRUCTOR:
|
||||
scores_query = scores_query.where(Session.owner_login == who.login)
|
||||
result = await db.execute(scores_query)
|
||||
attempts, average_score, completed_at = result.one()
|
||||
if not attempts:
|
||||
raise HTTPException(status_code=409, detail="no_scored_attempts")
|
||||
try:
|
||||
content = certificate_pdf(
|
||||
trainee_name=trainee.name,
|
||||
trainee_id=trainee.id,
|
||||
group_name=group.name if group else None,
|
||||
attempts=int(attempts),
|
||||
average_score=float(average_score),
|
||||
issued_at=completed_at.date().isoformat(),
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||
return Response(
|
||||
content=content,
|
||||
media_type="application/pdf",
|
||||
headers={"Content-Disposition": f'attachment; filename="trainee-{trainee_id}-certificate.pdf"'},
|
||||
)
|
||||
|
||||
|
||||
class TraineeOut(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
group: str | None = None
|
||||
service: str | None = None
|
||||
|
||||
|
||||
class AttemptOut(BaseModel):
|
||||
|
|
@ -40,6 +93,7 @@ class AttemptOut(BaseModel):
|
|||
facts_required: int | None = None
|
||||
hints: int | None = None
|
||||
codes: dict[str, int] = {}
|
||||
recording_available: bool = False
|
||||
|
||||
|
||||
class DeltaOut(BaseModel):
|
||||
|
|
@ -65,14 +119,39 @@ class ProfileOut(BaseModel):
|
|||
async def listing(request: Request) -> list[TraineeOut]:
|
||||
"""Список курсантов — преподавателю и администратору: обучающемуся он
|
||||
не нужен, а чужие фамилии из него видны."""
|
||||
require(request, Role.INSTRUCTOR, Role.ADMIN)
|
||||
who = require(request, Role.INSTRUCTOR, Role.ADMIN)
|
||||
if get_settings().demo_no_db:
|
||||
return [TraineeOut(id=DEMO_TRAINEE_ID, name="Демо-курсант")]
|
||||
return [TraineeOut(id=DEMO_TRAINEE_ID, name="Демо-курсант", service="Служба 101")]
|
||||
async with get_sessionmaker()() as db:
|
||||
rows = await db.execute(
|
||||
select(Trainee, Group.name).join(Group, Group.id == Trainee.group_id, isouter=True)
|
||||
query = (
|
||||
select(Trainee, Group.name, Group.owner_login, User.service)
|
||||
.join(Group, Group.id == Trainee.group_id, isouter=True)
|
||||
.join(User, User.trainee_id == Trainee.id, isouter=True)
|
||||
)
|
||||
return [TraineeOut(id=trainee.id, name=trainee.name, group=group) for trainee, group in rows]
|
||||
if who.role is Role.INSTRUCTOR:
|
||||
has_owned_attempt = exists(
|
||||
select(Session.id).where(
|
||||
Session.trainee_id == Trainee.id,
|
||||
Session.owner_login == who.login,
|
||||
)
|
||||
)
|
||||
query = query.where(
|
||||
or_(
|
||||
Trainee.group_id.is_(None),
|
||||
Group.owner_login == who.login,
|
||||
has_owned_attempt,
|
||||
)
|
||||
)
|
||||
rows = await db.execute(query)
|
||||
return [
|
||||
TraineeOut(
|
||||
id=trainee.id,
|
||||
name=trainee.name,
|
||||
group=(group if who.role is not Role.INSTRUCTOR or group_owner == who.login else None),
|
||||
service=service,
|
||||
)
|
||||
for trainee, group, group_owner, service in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/{trainee_id}/profile", response_model=ProfileOut)
|
||||
|
|
@ -82,17 +161,30 @@ async def profile(
|
|||
who = require(request)
|
||||
if who.role is Role.TRAINEE and who.trainee_id != trainee_id:
|
||||
raise HTTPException(status_code=403, detail="not_your_profile")
|
||||
if who.role is Role.INSTRUCTOR:
|
||||
owns_trainee = await db.scalar(
|
||||
select(Session.id)
|
||||
.where(Session.trainee_id == trainee_id, Session.owner_login == who.login)
|
||||
.limit(1)
|
||||
)
|
||||
if owns_trainee is None:
|
||||
raise HTTPException(status_code=404, detail="trainee_not_found")
|
||||
trainee = await db.get(Trainee, trainee_id)
|
||||
if trainee is None:
|
||||
raise HTTPException(status_code=404, detail="trainee_not_found")
|
||||
group = await db.get(Group, trainee.group_id) if trainee.group_id else None
|
||||
if who.role is Role.INSTRUCTOR and group is not None and group.owner_login != who.login:
|
||||
group = None
|
||||
|
||||
rows = await db.execute(
|
||||
attempts_query = (
|
||||
select(Session, Score)
|
||||
.join(Score, Score.session_id == Session.id, isouter=True)
|
||||
.where(Session.trainee_id == trainee_id)
|
||||
.order_by(Session.created_at)
|
||||
)
|
||||
if who.role is Role.INSTRUCTOR:
|
||||
attempts_query = attempts_query.where(Session.owner_login == who.login)
|
||||
rows = await db.execute(attempts_query)
|
||||
attempts: list[AttemptOut] = []
|
||||
competency_sums: dict[str, list[float]] = {}
|
||||
for session, score in rows:
|
||||
|
|
@ -110,6 +202,7 @@ async def profile(
|
|||
facts_required=summary.get("facts_required"),
|
||||
hints=summary.get("hints"),
|
||||
codes=summary.get("codes", {}),
|
||||
recording_available=recording_path(session.id).is_file(),
|
||||
)
|
||||
)
|
||||
for item in (score.report or {}).get("competencies", []) if score else []:
|
||||
|
|
|
|||
Loading…
Reference in a new issue