Files
walkup/backend/app/routes/games.py
2026-04-24 08:09:31 -05:00

205 lines
8.3 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import Request, Response
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..auth import require_session
from ..database import get_db
from ..http_cache import build_etag, is_matching_etag, set_private_revalidate
from ..models import AudioClip, GameAssignment, UserSession
from ..schemas import (
GameAssignmentCreate,
GameAssignmentResponse,
GamePrepResponse,
)
router = APIRouter(prefix="/games", tags=["games"])
def resolve_session_player_id(session: UserSession, requested_player_id: str | None = None) -> str:
if not session.external_team_id or not session.external_player_id:
raise HTTPException(status_code=422, detail="Select a team and player before using gameday")
if requested_player_id and requested_player_id != session.external_player_id:
raise HTTPException(status_code=403, detail="This player does not match your selected session")
return session.external_player_id
def assignment_to_response(assignment: GameAssignment) -> GameAssignmentResponse:
normalized_url = f"/media/files/{assignment.clip.normalized_path}" if assignment.clip.normalized_path else None
return GameAssignmentResponse(
id=assignment.id,
external_team_id=assignment.external_team_id,
external_game_id=assignment.external_game_id,
external_player_id=assignment.external_player_id,
clip_id=assignment.clip_id,
clip_label=assignment.clip.label,
asset_title=assignment.clip.asset.title,
start_ms=assignment.clip.start_ms,
end_ms=assignment.clip.end_ms,
batting_slot=assignment.batting_slot,
status=assignment.status,
normalized_url=normalized_url,
updated_at=assignment.updated_at,
)
def prepare_conditional_response(
request: Request,
payload: object,
*,
exclude_keys: set[str] | None = None,
) -> tuple[str, bool]:
etag = build_etag(payload, exclude_keys=exclude_keys)
return etag, is_matching_etag(request, etag)
@router.get("/pins", response_model=list[GameAssignmentResponse])
def list_pins(
request: Request,
response: Response,
external_player_id: str | None = Query(default=None),
session: UserSession = Depends(require_session),
db: Session = Depends(get_db),
) -> list[GameAssignmentResponse]:
player_id = resolve_session_player_id(session, external_player_id)
query = select(GameAssignment).join(GameAssignment.clip).where(
GameAssignment.external_team_id == session.external_team_id,
GameAssignment.external_player_id == player_id,
)
pins = db.scalars(query.order_by(GameAssignment.external_game_id.asc(), AudioClip.sort_order.asc())).all()
payload = [assignment_to_response(assignment) for assignment in pins]
etag, not_modified = prepare_conditional_response(request, payload)
set_private_revalidate(response, etag=etag)
if not_modified:
return Response(status_code=304, headers=dict(response.headers))
return payload
@router.get("/{external_game_id}/assignments", response_model=list[GameAssignmentResponse])
def list_assignments(
request: Request,
response: Response,
external_game_id: str,
external_player_id: str | None = Query(default=None),
session: UserSession = Depends(require_session),
db: Session = Depends(get_db),
) -> list[GameAssignmentResponse]:
resolve_session_player_id(session, external_player_id)
query = select(GameAssignment).join(GameAssignment.clip).where(
GameAssignment.external_team_id == session.external_team_id,
GameAssignment.external_game_id == external_game_id,
AudioClip.hidden.is_(False),
)
assignments = db.scalars(query.order_by(AudioClip.sort_order.asc(), GameAssignment.updated_at.desc())).all()
payload = [assignment_to_response(assignment) for assignment in assignments]
etag, not_modified = prepare_conditional_response(request, payload)
set_private_revalidate(response, etag=etag)
if not_modified:
return Response(status_code=304, headers=dict(response.headers))
return payload
@router.post("/{external_game_id}/assignments", response_model=GameAssignmentResponse)
def create_assignment(
external_game_id: str,
payload: GameAssignmentCreate,
session: UserSession = Depends(require_session),
db: Session = Depends(get_db),
) -> GameAssignmentResponse:
player_id = resolve_session_player_id(session, payload.external_player_id)
if payload.external_team_id != session.external_team_id:
raise HTTPException(status_code=403, detail="This team does not match your selected session")
clip = db.get(AudioClip, payload.clip_id)
if clip is None or clip.normalization_status != "ready":
raise HTTPException(status_code=422, detail="Clip is not ready")
if clip.hidden:
raise HTTPException(status_code=404, detail="Clip not found")
if clip.asset.external_team_id != session.external_team_id:
raise HTTPException(status_code=422, detail="Clip does not belong to this team")
if clip.asset.owner_external_player_id != player_id:
raise HTTPException(status_code=403, detail="You can only pin clips owned by that player")
assignment = db.scalar(
select(GameAssignment).where(
GameAssignment.external_game_id == external_game_id,
GameAssignment.clip_id == payload.clip_id,
GameAssignment.external_team_id == session.external_team_id,
)
)
if assignment is None:
assignment = GameAssignment(
external_team_id=session.external_team_id,
external_game_id=external_game_id,
external_player_id=player_id,
clip_id=payload.clip_id,
batting_slot=payload.batting_slot,
status=payload.status,
)
db.add(assignment)
else:
assignment.external_team_id = session.external_team_id
assignment.external_player_id = player_id
assignment.clip_id = payload.clip_id
assignment.batting_slot = payload.batting_slot
assignment.status = payload.status
db.commit()
db.refresh(assignment)
return assignment_to_response(assignment)
@router.delete("/{external_game_id}/assignments/{assignment_id}", status_code=204)
def delete_assignment(
external_game_id: str,
assignment_id: int,
external_player_id: str | None = Query(default=None),
session: UserSession = Depends(require_session),
db: Session = Depends(get_db),
) -> None:
player_id = resolve_session_player_id(session, external_player_id)
assignment = db.get(GameAssignment, assignment_id)
if assignment is None or assignment.external_game_id != external_game_id:
raise HTTPException(status_code=404, detail="Pin not found")
if assignment.external_team_id != session.external_team_id or assignment.external_player_id != player_id:
raise HTTPException(status_code=403, detail="Pin does not belong to your selected session")
db.delete(assignment)
db.commit()
@router.get("/{external_game_id}/prep", response_model=GamePrepResponse)
def prepare_game(
request: Request,
response: Response,
external_game_id: str,
session: UserSession = Depends(require_session),
db: Session = Depends(get_db),
) -> GamePrepResponse:
resolve_session_player_id(session, None)
assignments = db.scalars(
select(GameAssignment)
.join(GameAssignment.clip)
.where(
GameAssignment.external_team_id == session.external_team_id,
GameAssignment.external_game_id == external_game_id,
AudioClip.hidden.is_(False),
)
.order_by(AudioClip.sort_order.asc(), GameAssignment.updated_at.desc())
).all()
external_team_id = assignments[0].external_team_id if assignments else ""
payload = GamePrepResponse(
external_game_id=external_game_id,
external_team_id=external_team_id,
prepared_at=datetime.now(timezone.utc),
assignments=[assignment_to_response(assignment) for assignment in assignments],
)
etag, not_modified = prepare_conditional_response(request, payload, exclude_keys={"prepared_at"})
set_private_revalidate(response, etag=etag)
if not_modified:
return Response(status_code=304, headers=dict(response.headers))
return payload