2026-02-05 15:09:05 +01:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
|
|
|
|
|
|
from app.core.database import get_session
|
|
|
|
|
from app.models.game import Game
|
|
|
|
|
from app.models.route import Route
|
2026-02-05 18:36:19 +01:00
|
|
|
from app.schemas.game import (
|
|
|
|
|
GameCreate,
|
|
|
|
|
GameDetailResponse,
|
|
|
|
|
GameResponse,
|
|
|
|
|
GameUpdate,
|
|
|
|
|
RouteCreate,
|
|
|
|
|
RouteReorderRequest,
|
|
|
|
|
RouteResponse,
|
|
|
|
|
RouteUpdate,
|
|
|
|
|
)
|
2026-02-05 15:09:05 +01:00
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=list[GameResponse])
|
|
|
|
|
async def list_games(session: AsyncSession = Depends(get_session)):
|
|
|
|
|
result = await session.execute(select(Game).order_by(Game.id))
|
|
|
|
|
return result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{game_id}", response_model=GameDetailResponse)
|
|
|
|
|
async def get_game(game_id: int, session: AsyncSession = Depends(get_session)):
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(Game)
|
|
|
|
|
.where(Game.id == game_id)
|
|
|
|
|
.options(selectinload(Game.routes))
|
|
|
|
|
)
|
|
|
|
|
game = result.scalar_one_or_none()
|
|
|
|
|
if game is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
|
|
|
|
|
|
# Sort routes by order for the response
|
|
|
|
|
game.routes.sort(key=lambda r: r.order)
|
|
|
|
|
return game
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{game_id}/routes", response_model=list[RouteResponse])
|
|
|
|
|
async def list_game_routes(
|
|
|
|
|
game_id: int, session: AsyncSession = Depends(get_session)
|
|
|
|
|
):
|
|
|
|
|
# Verify game exists
|
|
|
|
|
game = await session.get(Game, game_id)
|
|
|
|
|
if game is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
|
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(Route)
|
|
|
|
|
.where(Route.game_id == game_id)
|
|
|
|
|
.order_by(Route.order)
|
|
|
|
|
)
|
|
|
|
|
return result.scalars().all()
|
2026-02-05 18:36:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Admin endpoints ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("", response_model=GameResponse, status_code=201)
|
|
|
|
|
async def create_game(
|
|
|
|
|
data: GameCreate, session: AsyncSession = Depends(get_session)
|
|
|
|
|
):
|
|
|
|
|
existing = await session.execute(
|
|
|
|
|
select(Game).where(Game.slug == data.slug)
|
|
|
|
|
)
|
|
|
|
|
if existing.scalar_one_or_none() is not None:
|
|
|
|
|
raise HTTPException(status_code=409, detail="Game with this slug already exists")
|
|
|
|
|
|
|
|
|
|
game = Game(**data.model_dump())
|
|
|
|
|
session.add(game)
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(game)
|
|
|
|
|
return game
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{game_id}", response_model=GameResponse)
|
|
|
|
|
async def update_game(
|
|
|
|
|
game_id: int, data: GameUpdate, session: AsyncSession = Depends(get_session)
|
|
|
|
|
):
|
|
|
|
|
game = await session.get(Game, game_id)
|
|
|
|
|
if game is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
|
|
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
|
|
|
if "slug" in update_data:
|
|
|
|
|
existing = await session.execute(
|
|
|
|
|
select(Game).where(Game.slug == update_data["slug"], Game.id != game_id)
|
|
|
|
|
)
|
|
|
|
|
if existing.scalar_one_or_none() is not None:
|
|
|
|
|
raise HTTPException(status_code=409, detail="Game with this slug already exists")
|
|
|
|
|
|
|
|
|
|
for field, value in update_data.items():
|
|
|
|
|
setattr(game, field, value)
|
|
|
|
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(game)
|
|
|
|
|
return game
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{game_id}", status_code=204)
|
|
|
|
|
async def delete_game(
|
|
|
|
|
game_id: int, session: AsyncSession = Depends(get_session)
|
|
|
|
|
):
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(Game).where(Game.id == game_id).options(selectinload(Game.runs))
|
|
|
|
|
)
|
|
|
|
|
game = result.scalar_one_or_none()
|
|
|
|
|
if game is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
|
|
|
|
|
|
if game.runs:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=409,
|
|
|
|
|
detail="Cannot delete game with existing runs. Delete the runs first.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Delete routes (and their route_encounters via cascade)
|
|
|
|
|
routes = await session.execute(
|
|
|
|
|
select(Route).where(Route.game_id == game_id)
|
|
|
|
|
)
|
|
|
|
|
for route in routes.scalars().all():
|
|
|
|
|
await session.delete(route)
|
|
|
|
|
|
|
|
|
|
await session.delete(game)
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{game_id}/routes", response_model=RouteResponse, status_code=201)
|
|
|
|
|
async def create_route(
|
|
|
|
|
game_id: int, data: RouteCreate, session: AsyncSession = Depends(get_session)
|
|
|
|
|
):
|
|
|
|
|
game = await session.get(Game, game_id)
|
|
|
|
|
if game is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
|
|
|
|
|
|
route = Route(game_id=game_id, **data.model_dump())
|
|
|
|
|
session.add(route)
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(route)
|
|
|
|
|
return route
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{game_id}/routes/reorder", response_model=list[RouteResponse])
|
|
|
|
|
async def reorder_routes(
|
|
|
|
|
game_id: int,
|
|
|
|
|
data: RouteReorderRequest,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
):
|
|
|
|
|
game = await session.get(Game, game_id)
|
|
|
|
|
if game is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
|
|
|
|
|
|
for item in data.routes:
|
|
|
|
|
route = await session.get(Route, item.id)
|
|
|
|
|
if route is None or route.game_id != game_id:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail=f"Route {item.id} not found in this game",
|
|
|
|
|
)
|
|
|
|
|
route.order = item.order
|
|
|
|
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(Route).where(Route.game_id == game_id).order_by(Route.order)
|
|
|
|
|
)
|
|
|
|
|
return result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{game_id}/routes/{route_id}", response_model=RouteResponse)
|
|
|
|
|
async def update_route(
|
|
|
|
|
game_id: int,
|
|
|
|
|
route_id: int,
|
|
|
|
|
data: RouteUpdate,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
):
|
|
|
|
|
route = await session.get(Route, route_id)
|
|
|
|
|
if route is None or route.game_id != game_id:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Route not found in this game")
|
|
|
|
|
|
|
|
|
|
for field, value in data.model_dump(exclude_unset=True).items():
|
|
|
|
|
setattr(route, field, value)
|
|
|
|
|
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(route)
|
|
|
|
|
return route
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{game_id}/routes/{route_id}", status_code=204)
|
|
|
|
|
async def delete_route(
|
|
|
|
|
game_id: int,
|
|
|
|
|
route_id: int,
|
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
|
):
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(Route)
|
|
|
|
|
.where(Route.id == route_id, Route.game_id == game_id)
|
|
|
|
|
.options(selectinload(Route.encounters))
|
|
|
|
|
)
|
|
|
|
|
route = result.scalar_one_or_none()
|
|
|
|
|
if route is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Route not found in this game")
|
|
|
|
|
|
|
|
|
|
if route.encounters:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=409,
|
|
|
|
|
detail="Cannot delete route with existing encounters. Delete the encounters first.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await session.delete(route)
|
|
|
|
|
await session.commit()
|