alabuga/backend/app/api/routes/missions.py
2025-09-25 04:55:43 +02:00

331 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Маршруты для работы с миссиями."""
from __future__ import annotations
from collections import defaultdict
from fastapi import APIRouter, Depends, HTTPException, 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.branch import Branch, BranchMission
from app.models.mission import Mission, MissionSubmission, SubmissionStatus
from app.models.user import User
from app.schemas.branch import BranchMissionRead, BranchRead
from app.schemas.mission import (
MissionBase,
MissionDetail,
MissionSubmissionCreate,
MissionSubmissionRead,
)
from app.services.mission import submit_mission
router = APIRouter(prefix="/api/missions", tags=["missions"])
def _load_user_progress(user: User) -> set[int]:
"""Возвращаем идентификаторы успешно завершённых миссий."""
completed = {
submission.mission_id
for submission in user.submissions
if submission.status == SubmissionStatus.APPROVED
}
return completed
def _build_branch_dependencies(branches: list[Branch]) -> dict[int, set[int]]:
"""Строим карту зависимостей миссий по веткам."""
dependencies: dict[int, set[int]] = defaultdict(set)
for branch in branches:
ordered = sorted(branch.missions, key=lambda item: item.order)
previous: list[int] = []
for link in ordered:
if previous:
dependencies[link.mission_id].update(previous)
previous.append(link.mission_id)
return dependencies
def _mission_availability(
*,
mission: Mission,
user: User,
completed_missions: set[int],
branch_dependencies: dict[int, set[int]],
mission_titles: dict[int, str],
) -> tuple[bool, list[str]]:
"""Определяем, доступна ли миссия и формируем причины блокировки."""
reasons: list[str] = []
if mission.minimum_rank and user.xp < mission.minimum_rank.required_xp:
reasons.append(f"Требуется ранг «{mission.minimum_rank.title}»")
missing_explicit = [
req.required_mission_id
for req in mission.prerequisites
if req.required_mission_id not in completed_missions
]
for mission_id in missing_explicit:
reasons.append(f"Завершите миссию «{mission_titles.get(mission_id, '#'+str(mission_id))}»")
for mission_id in branch_dependencies.get(mission.id, set()):
if mission_id not in completed_missions:
reasons.append(
"Продолжение ветки откроется после миссии «"
f"{mission_titles.get(mission_id, '#'+str(mission_id))}»"
)
is_available = mission.is_active and not reasons
return is_available, reasons
@router.get("/branches", response_model=list[BranchRead], summary="Список веток миссий")
def list_branches(
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
) -> list[BranchRead]:
"""Возвращаем ветки с упорядоченными миссиями."""
db.refresh(current_user)
_ = current_user.submissions
branches = (
db.query(Branch)
.options(selectinload(Branch.missions).selectinload(BranchMission.mission))
.order_by(Branch.title)
.all()
)
completed_missions = _load_user_progress(current_user)
branch_dependencies = _build_branch_dependencies(branches)
mission_titles = {
item.mission_id: item.mission.title if item.mission else ""
for branch in branches
for item in branch.missions
}
mission_titles.update(dict(db.query(Mission.id, Mission.title).all()))
response: list[BranchRead] = []
for branch in branches:
ordered_links = sorted(branch.missions, key=lambda link: link.order)
completed_count = sum(1 for link in ordered_links if link.mission_id in completed_missions)
total = len(ordered_links)
missions_payload = []
for link in ordered_links:
mission_obj = link.mission
mission_title = mission_obj.title if mission_obj else ""
is_completed = link.mission_id in completed_missions
if mission_obj:
is_available, _ = _mission_availability(
mission=mission_obj,
user=current_user,
completed_missions=completed_missions,
branch_dependencies=branch_dependencies,
mission_titles=mission_titles,
)
else:
is_available = False
missions_payload.append(
BranchMissionRead(
mission_id=link.mission_id,
mission_title=mission_title,
order=link.order,
is_completed=is_completed,
is_available=is_available,
)
)
response.append(
BranchRead(
id=branch.id,
title=branch.title,
description=branch.description,
category=branch.category,
missions=missions_payload,
total_missions=total,
completed_missions=completed_count,
)
)
return response
@router.get("/", response_model=list[MissionBase], summary="Список миссий")
def list_missions(
*, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
) -> list[MissionBase]:
"""Возвращаем доступные миссии."""
db.refresh(current_user)
_ = current_user.submissions
branches = (
db.query(Branch)
.options(selectinload(Branch.missions))
.all()
)
branch_dependencies = _build_branch_dependencies(branches)
missions = (
db.query(Mission)
.options(
selectinload(Mission.prerequisites),
selectinload(Mission.minimum_rank),
)
.filter(Mission.is_active.is_(True))
.order_by(Mission.id)
.all()
)
mission_titles = {mission.id: mission.title for mission in missions}
completed_missions = _load_user_progress(current_user)
response: list[MissionBase] = []
for mission in missions:
is_available, reasons = _mission_availability(
mission=mission,
user=current_user,
completed_missions=completed_missions,
branch_dependencies=branch_dependencies,
mission_titles=mission_titles,
)
dto = MissionBase.model_validate(mission)
dto.is_available = is_available
dto.locked_reasons = reasons
response.append(dto)
return response
@router.get("/{mission_id}", response_model=MissionDetail, summary="Карточка миссии")
def get_mission(
mission_id: int,
*,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> MissionDetail:
"""Возвращаем подробную информацию о миссии."""
mission = db.query(Mission).filter(Mission.id == mission_id, Mission.is_active.is_(True)).first()
if not mission:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Миссия не найдена")
db.refresh(current_user)
_ = current_user.submissions
branches = (
db.query(Branch)
.options(selectinload(Branch.missions))
.all()
)
branch_dependencies = _build_branch_dependencies(branches)
completed_missions = _load_user_progress(current_user)
mission_titles = dict(db.query(Mission.id, Mission.title).all())
is_available, reasons = _mission_availability(
mission=mission,
user=current_user,
completed_missions=completed_missions,
branch_dependencies=branch_dependencies,
mission_titles=mission_titles,
)
prerequisites = [link.required_mission_id for link in mission.prerequisites]
rewards = [
{
"competency_id": reward.competency_id,
"competency_name": reward.competency.name,
"level_delta": reward.level_delta,
}
for reward in mission.competency_rewards
]
data = MissionDetail(
id=mission.id,
title=mission.title,
description=mission.description,
xp_reward=mission.xp_reward,
mana_reward=mission.mana_reward,
difficulty=mission.difficulty,
is_active=mission.is_active,
is_available=is_available,
locked_reasons=reasons,
minimum_rank_id=mission.minimum_rank_id,
artifact_id=mission.artifact_id,
prerequisites=prerequisites,
competency_rewards=rewards,
created_at=mission.created_at,
updated_at=mission.updated_at,
)
return data
@router.post("/{mission_id}/submit", response_model=MissionSubmissionRead, summary="Отправляем отчёт")
def submit(
mission_id: int,
submission_in: MissionSubmissionCreate,
*,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> MissionSubmissionRead:
"""Пилот отправляет доказательство выполнения миссии."""
mission = db.query(Mission).filter(Mission.id == mission_id, Mission.is_active.is_(True)).first()
if not mission:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Миссия не найдена")
db.refresh(current_user)
_ = current_user.submissions
branches = (
db.query(Branch)
.options(selectinload(Branch.missions))
.all()
)
branch_dependencies = _build_branch_dependencies(branches)
completed_missions = _load_user_progress(current_user)
mission_titles = dict(db.query(Mission.id, Mission.title).all())
is_available, reasons = _mission_availability(
mission=mission,
user=current_user,
completed_missions=completed_missions,
branch_dependencies=branch_dependencies,
mission_titles=mission_titles,
)
if not is_available:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="; ".join(reasons))
submission = submit_mission(
db=db,
user=current_user,
mission=mission,
comment=submission_in.comment,
proof_url=submission_in.proof_url,
)
return MissionSubmissionRead.model_validate(submission)
@router.get(
"/{mission_id}/submission",
response_model=MissionSubmissionRead | None,
summary="Получаем текущую отправку",
)
def get_submission(
mission_id: int,
*,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> MissionSubmissionRead | None:
"""Возвращаем статус отправленной миссии."""
submission = (
db.query(MissionSubmission)
.filter(MissionSubmission.user_id == current_user.id, MissionSubmission.mission_id == mission_id)
.first()
)
if not submission:
return None
return MissionSubmissionRead.model_validate(submission)