185 lines
6.8 KiB
Python
185 lines
6.8 KiB
Python
"""Маршруты работы с профилем."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||
from sqlalchemy.orm import Session, selectinload
|
||
|
||
from app.api.deps import get_current_user
|
||
from app.db.session import get_db
|
||
from app.models.rank import Rank
|
||
from app.models.user import User, UserRole, UserCompetency
|
||
from app.models.mission import SubmissionStatus
|
||
from app.schemas.progress import ProgressSnapshot
|
||
from app.schemas.rank import RankBase
|
||
from app.schemas.user import (
|
||
LeaderboardEntry,
|
||
ProfilePhotoResponse,
|
||
UserCompetencyRead,
|
||
UserProfile,
|
||
)
|
||
from app.services.rank import build_progress_snapshot
|
||
from app.services.storage import (
|
||
build_photo_data_url,
|
||
delete_profile_photo,
|
||
save_profile_photo,
|
||
)
|
||
|
||
router = APIRouter(prefix="/api", tags=["profile"])
|
||
|
||
|
||
@router.get("/me", response_model=UserProfile, summary="Профиль пилота")
|
||
def get_profile(
|
||
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||
) -> UserProfile:
|
||
"""Возвращаем профиль и связанные сущности."""
|
||
|
||
db.refresh(current_user)
|
||
for item in current_user.competencies:
|
||
_ = item.competency
|
||
for artifact in current_user.artifacts:
|
||
_ = artifact.artifact
|
||
|
||
profile = UserProfile.model_validate(current_user)
|
||
profile.profile_photo_uploaded = bool(current_user.profile_photo_path)
|
||
profile.profile_photo_updated_at = (
|
||
current_user.updated_at if current_user.profile_photo_path else None
|
||
)
|
||
return profile
|
||
|
||
|
||
@router.get(
|
||
"/me/photo",
|
||
response_model=ProfilePhotoResponse,
|
||
summary="Получаем фото профиля кандидата",
|
||
)
|
||
def get_profile_photo(
|
||
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||
) -> ProfilePhotoResponse:
|
||
"""Читаем сохранённое изображение и возвращаем его в виде data URL."""
|
||
|
||
db.refresh(current_user)
|
||
if not current_user.profile_photo_path:
|
||
return ProfilePhotoResponse(photo=None, detail="Фотография не загружена")
|
||
|
||
try:
|
||
photo = build_photo_data_url(current_user.profile_photo_path)
|
||
except FileNotFoundError:
|
||
# Если файл удалили вручную, сбрасываем ссылку в базе, чтобы не мешать пользователю загрузить новую.
|
||
current_user.profile_photo_path = None
|
||
db.add(current_user)
|
||
db.commit()
|
||
return ProfilePhotoResponse(photo=None, detail="Файл не найден")
|
||
|
||
return ProfilePhotoResponse(photo=photo)
|
||
|
||
|
||
@router.post(
|
||
"/me/photo",
|
||
response_model=ProfilePhotoResponse,
|
||
status_code=status.HTTP_200_OK,
|
||
summary="Загружаем фото профиля",
|
||
)
|
||
def upload_profile_photo(
|
||
photo: UploadFile = File(...),
|
||
*,
|
||
db: Session = Depends(get_db),
|
||
current_user: User = Depends(get_current_user),
|
||
) -> ProfilePhotoResponse:
|
||
"""Сохраняем изображение и возвращаем обновлённый data URL."""
|
||
|
||
try:
|
||
relative_path = save_profile_photo(upload=photo, user_id=current_user.id)
|
||
except ValueError as error:
|
||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(error)) from error
|
||
|
||
delete_profile_photo(current_user.profile_photo_path)
|
||
current_user.profile_photo_path = relative_path
|
||
db.add(current_user)
|
||
db.commit()
|
||
db.refresh(current_user)
|
||
|
||
photo_url = build_photo_data_url(relative_path)
|
||
return ProfilePhotoResponse(photo=photo_url, detail="Фотография обновлена")
|
||
|
||
|
||
@router.delete(
|
||
"/me/photo",
|
||
response_model=ProfilePhotoResponse,
|
||
summary="Удаляем фото профиля",
|
||
)
|
||
def delete_profile_photo_endpoint(
|
||
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||
) -> ProfilePhotoResponse:
|
||
"""Удаляем сохранённое фото и очищаем ссылку в профиле."""
|
||
|
||
if not current_user.profile_photo_path:
|
||
return ProfilePhotoResponse(photo=None, detail="Фотография уже удалена")
|
||
|
||
delete_profile_photo(current_user.profile_photo_path)
|
||
current_user.profile_photo_path = None
|
||
db.add(current_user)
|
||
db.commit()
|
||
|
||
return ProfilePhotoResponse(photo=None, detail="Фотография удалена")
|
||
|
||
|
||
@router.get("/ranks", response_model=list[RankBase], summary="Перечень рангов")
|
||
def list_ranks(
|
||
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||
) -> list[RankBase]:
|
||
"""Возвращаем ранги по возрастанию требований."""
|
||
|
||
ranks = db.query(Rank).order_by(Rank.required_xp).all()
|
||
return [RankBase.model_validate(rank) for rank in ranks]
|
||
|
||
|
||
@router.get("/progress", response_model=ProgressSnapshot, summary="Прогресс до следующего ранга")
|
||
def get_progress(
|
||
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||
) -> ProgressSnapshot:
|
||
"""Возвращаем агрегированную информацию о выполненных условиях следующего ранга."""
|
||
|
||
db.refresh(current_user)
|
||
_ = current_user.submissions
|
||
_ = current_user.competencies
|
||
snapshot = build_progress_snapshot(current_user, db)
|
||
return snapshot
|
||
|
||
|
||
@router.get("/leaderboard", response_model=list[LeaderboardEntry], summary="Лидерборд пилотов")
|
||
def leaderboard(
|
||
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||
) -> list[LeaderboardEntry]:
|
||
"""Возвращаем пилотов, отсортированных по опыту с перечислением компетенций."""
|
||
|
||
users = (
|
||
db.query(User)
|
||
.filter(User.role == UserRole.PILOT)
|
||
.options(
|
||
selectinload(User.current_rank),
|
||
selectinload(User.competencies).selectinload(UserCompetency.competency),
|
||
selectinload(User.submissions),
|
||
)
|
||
.order_by(User.xp.desc(), User.created_at)
|
||
.all()
|
||
)
|
||
|
||
leaderboard: list[LeaderboardEntry] = []
|
||
for user in users:
|
||
completed = sum(1 for submission in user.submissions if submission.status == SubmissionStatus.APPROVED)
|
||
competencies = [UserCompetencyRead.model_validate(entry) for entry in user.competencies]
|
||
leaderboard.append(
|
||
LeaderboardEntry(
|
||
user_id=user.id,
|
||
full_name=user.full_name,
|
||
rank_title=user.current_rank.title if user.current_rank else None,
|
||
xp=user.xp,
|
||
mana=user.mana,
|
||
completed_missions=completed,
|
||
competencies=competencies,
|
||
)
|
||
)
|
||
|
||
return leaderboard
|