125 lines
4.2 KiB
Python
125 lines
4.2 KiB
Python
"""Сводка ошибок и рекомендаций учебной группы для преподавателя."""
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import and_, func, or_, select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.auth import audit, require
|
|
from app.db.base import get_session
|
|
from app.db.models import Group, Score, Session, Trainee
|
|
from app.domain.roles import Role
|
|
from app.scoring.group import ScoredAttempt, summarize
|
|
|
|
router = APIRouter(prefix="/api/groups", tags=["groups"])
|
|
|
|
|
|
class GroupOut(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
|
|
|
|
class GroupCreate(BaseModel):
|
|
name: str = Field(min_length=1, max_length=120)
|
|
|
|
|
|
class GroupErrorOut(BaseModel):
|
|
code: str
|
|
title: str
|
|
affected_trainees: int
|
|
occurrences: int
|
|
rate_percent: float
|
|
recommendation: str
|
|
|
|
|
|
class GroupAnalyticsOut(BaseModel):
|
|
group: GroupOut
|
|
enrolled_trainees: int
|
|
active_trainees: int
|
|
scored_attempts: int
|
|
average_score: float | None
|
|
errors: list[GroupErrorOut]
|
|
|
|
|
|
@router.get("", response_model=list[GroupOut])
|
|
async def listing(request: Request, db: AsyncSession = Depends(get_session)) -> list[GroupOut]:
|
|
require(request, Role.INSTRUCTOR, Role.ADMIN)
|
|
groups = await db.scalars(select(Group).order_by(Group.name))
|
|
return [GroupOut(id=group.id, name=group.name) for group in groups]
|
|
|
|
|
|
@router.post("", response_model=GroupOut, status_code=201)
|
|
async def create(
|
|
body: GroupCreate, request: Request, db: AsyncSession = Depends(get_session)
|
|
) -> GroupOut:
|
|
who = require(request, Role.INSTRUCTOR, Role.ADMIN)
|
|
name = body.name.strip()
|
|
if not name:
|
|
raise HTTPException(status_code=422, detail="group_name_required")
|
|
group = Group(name=name)
|
|
db.add(group)
|
|
try:
|
|
await db.commit()
|
|
except IntegrityError as exc:
|
|
await db.rollback()
|
|
raise HTTPException(status_code=409, detail="group_exists") from exc
|
|
await audit(who.login, who.role.value, "group.create", str(group.id), group.name)
|
|
return GroupOut(id=group.id, name=group.name)
|
|
|
|
|
|
@router.put("/{group_id}/trainees/{trainee_id}", response_model=GroupOut)
|
|
async def assign_trainee(
|
|
group_id: UUID, trainee_id: UUID, request: Request,
|
|
db: AsyncSession = Depends(get_session),
|
|
) -> GroupOut:
|
|
who = require(request, Role.INSTRUCTOR, Role.ADMIN)
|
|
group = await db.get(Group, group_id)
|
|
if group is None:
|
|
raise HTTPException(status_code=404, detail="group_not_found")
|
|
trainee = await db.get(Trainee, trainee_id)
|
|
if trainee is None:
|
|
raise HTTPException(status_code=404, detail="trainee_not_found")
|
|
trainee.group_id = group_id
|
|
await db.commit()
|
|
await audit(who.login, who.role.value, "group.assign", str(group.id), str(trainee_id))
|
|
return GroupOut(id=group.id, name=group.name)
|
|
|
|
|
|
@router.get("/{group_id}/analytics", response_model=GroupAnalyticsOut)
|
|
async def analytics(
|
|
group_id: UUID, request: Request, db: AsyncSession = Depends(get_session)
|
|
) -> GroupAnalyticsOut:
|
|
require(request, Role.INSTRUCTOR, Role.ADMIN)
|
|
group = await db.get(Group, group_id)
|
|
if group is None:
|
|
raise HTTPException(status_code=404, detail="group_not_found")
|
|
enrolled = await db.scalar(
|
|
select(func.count()).select_from(Trainee).where(Trainee.group_id == group_id)
|
|
)
|
|
rows = await db.execute(
|
|
select(Session.trainee_id, Score.score_final, Score.report)
|
|
.join(Score, Score.session_id == Session.id)
|
|
.join(Trainee, Trainee.id == Session.trainee_id, isouter=True)
|
|
.where(
|
|
or_(
|
|
Session.group_id == group_id,
|
|
and_(Session.group_id.is_(None), Trainee.group_id == group_id),
|
|
),
|
|
Session.ended_at.is_not(None),
|
|
)
|
|
)
|
|
attempts = [
|
|
ScoredAttempt(
|
|
trainee_id=trainee_id,
|
|
score=score,
|
|
codes=(report or {}).get("summary", {}).get("codes", {}),
|
|
)
|
|
for trainee_id, score, report in rows
|
|
]
|
|
return GroupAnalyticsOut(
|
|
group=GroupOut(id=group.id, name=group.name),
|
|
**summarize(attempts, int(enrolled or 0)),
|
|
)
|