Files
nuzlocke-tracker/backend/src/app/api/runs.py

162 lines
5.4 KiB
Python
Raw Normal View History

from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Response
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload
from app.core.database import get_session
from app.models.encounter import Encounter
from app.models.game import Game
from app.models.genlocke import Genlocke, GenlockeLeg
from app.models.nuzlocke_run import NuzlockeRun
from app.schemas.run import RunCreate, RunDetailResponse, RunGenlockeContext, RunResponse, RunUpdate
router = APIRouter()
@router.post("", response_model=RunResponse, status_code=201)
async def create_run(
data: RunCreate, session: AsyncSession = Depends(get_session)
):
# Validate game exists
game = await session.get(Game, data.game_id)
if game is None:
raise HTTPException(status_code=404, detail="Game not found")
run = NuzlockeRun(
game_id=data.game_id,
name=data.name,
status="active",
rules=data.rules,
)
session.add(run)
await session.commit()
await session.refresh(run)
return run
@router.get("", response_model=list[RunResponse])
async def list_runs(session: AsyncSession = Depends(get_session)):
result = await session.execute(
select(NuzlockeRun).order_by(NuzlockeRun.started_at.desc())
)
return result.scalars().all()
@router.get("/{run_id}", response_model=RunDetailResponse)
async def get_run(run_id: int, session: AsyncSession = Depends(get_session)):
result = await session.execute(
select(NuzlockeRun)
.where(NuzlockeRun.id == run_id)
.options(
joinedload(NuzlockeRun.game),
selectinload(NuzlockeRun.encounters)
.joinedload(Encounter.pokemon),
selectinload(NuzlockeRun.encounters)
.joinedload(Encounter.current_pokemon),
selectinload(NuzlockeRun.encounters)
.joinedload(Encounter.route),
)
)
run = result.scalar_one_or_none()
if run is None:
raise HTTPException(status_code=404, detail="Run not found")
# Check if this run belongs to a genlocke
genlocke_context = None
leg_result = await session.execute(
select(GenlockeLeg)
.where(GenlockeLeg.run_id == run_id)
.options(joinedload(GenlockeLeg.genlocke))
)
leg = leg_result.scalar_one_or_none()
if leg:
total_legs_result = await session.execute(
select(func.count())
.select_from(GenlockeLeg)
.where(GenlockeLeg.genlocke_id == leg.genlocke_id)
)
total_legs = total_legs_result.scalar_one()
genlocke_context = RunGenlockeContext(
genlocke_id=leg.genlocke_id,
genlocke_name=leg.genlocke.name,
leg_order=leg.leg_order,
total_legs=total_legs,
is_final_leg=leg.leg_order == total_legs,
)
response = RunDetailResponse.model_validate(run)
response.genlocke = genlocke_context
return response
@router.patch("/{run_id}", response_model=RunResponse)
async def update_run(
run_id: int,
data: RunUpdate,
session: AsyncSession = Depends(get_session),
):
run = await session.get(NuzlockeRun, run_id)
if run is None:
raise HTTPException(status_code=404, detail="Run not found")
update_data = data.model_dump(exclude_unset=True)
# Auto-set completed_at when ending a run
if "status" in update_data and update_data["status"] in ("completed", "failed"):
if run.status != "active":
raise HTTPException(
status_code=400, detail="Only active runs can be ended"
)
update_data["completed_at"] = datetime.now(timezone.utc)
for field, value in update_data.items():
setattr(run, field, value)
# Genlocke side effects when run status changes
if "status" in update_data and update_data["status"] in ("completed", "failed"):
leg_result = await session.execute(
select(GenlockeLeg)
.where(GenlockeLeg.run_id == run_id)
.options(joinedload(GenlockeLeg.genlocke))
)
leg = leg_result.scalar_one_or_none()
if leg:
genlocke = leg.genlocke
if update_data["status"] == "failed":
genlocke.status = "failed"
elif update_data["status"] == "completed":
total_legs_result = await session.execute(
select(func.count())
.select_from(GenlockeLeg)
.where(GenlockeLeg.genlocke_id == genlocke.id)
)
total_legs = total_legs_result.scalar_one()
if leg.leg_order == total_legs:
genlocke.status = "completed"
await session.commit()
await session.refresh(run)
return run
@router.delete("/{run_id}", status_code=204)
async def delete_run(
run_id: int, session: AsyncSession = Depends(get_session)
):
run = await session.get(NuzlockeRun, run_id)
if run is None:
raise HTTPException(status_code=404, detail="Run not found")
# Delete associated encounters first
encounters = await session.execute(
select(Encounter).where(Encounter.run_id == run_id)
)
for enc in encounters.scalars():
await session.delete(enc)
await session.delete(run)
await session.commit()
return Response(status_code=204)