Full-stack mobile app for pole dance championship management. Backend: FastAPI + SQLAlchemy 2 (async) + SQLite (dev) / PostgreSQL (prod) - JWT auth with refresh token rotation - Championship CRUD with Instagram Graph API sync (APScheduler) - Registration flow with status management - Participant list publish with Expo push notifications - Alembic migrations, pytest test suite Mobile: React Native + Expo (TypeScript) - Auth gate: pending approval screen for new members - Championships list & detail screens - Registration form with status tracking - React Query + Zustand + React Navigation v6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
4.5 KiB
Python
124 lines
4.5 KiB
Python
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.crud import crud_championship, crud_registration
|
|
from app.database import get_db
|
|
from app.dependencies import get_approved_user, get_organizer
|
|
from app.models.user import User
|
|
from app.schemas.registration import RegistrationCreate, RegistrationOut, RegistrationStatusUpdate
|
|
|
|
router = APIRouter(prefix="/registrations", tags=["registrations"])
|
|
|
|
|
|
@router.post("", response_model=RegistrationOut, status_code=status.HTTP_201_CREATED)
|
|
async def submit_registration(
|
|
body: RegistrationCreate,
|
|
current_user: User = Depends(get_approved_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
champ = await crud_championship.get(db, body.championship_id)
|
|
if not champ:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Championship not found")
|
|
if champ.status != "open":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Registration is not open"
|
|
)
|
|
if champ.registration_close_at and champ.registration_close_at < datetime.now(timezone.utc):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Registration deadline has passed"
|
|
)
|
|
existing = await crud_registration.get_by_champ_and_user(
|
|
db, body.championship_id, current_user.id
|
|
)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="You have already registered for this championship",
|
|
)
|
|
return await crud_registration.create(
|
|
db,
|
|
championship_id=uuid.UUID(body.championship_id),
|
|
user_id=current_user.id,
|
|
category=body.category,
|
|
level=body.level,
|
|
notes=body.notes,
|
|
)
|
|
|
|
|
|
@router.get("/my", response_model=list[RegistrationOut])
|
|
async def my_registrations(
|
|
current_user: User = Depends(get_approved_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await crud_registration.list_by_user(db, current_user.id)
|
|
|
|
|
|
@router.get("/{registration_id}", response_model=RegistrationOut)
|
|
async def get_registration(
|
|
registration_id: str,
|
|
current_user: User = Depends(get_approved_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
reg = await crud_registration.get(db, registration_id)
|
|
if not reg:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
if str(reg.user_id) != str(current_user.id) and current_user.role not in (
|
|
"organizer",
|
|
"admin",
|
|
):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
|
return reg
|
|
|
|
|
|
@router.get("/championship/{championship_id}", response_model=list[RegistrationOut])
|
|
async def championship_registrations(
|
|
championship_id: str,
|
|
_organizer: User = Depends(get_organizer),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
champ = await crud_championship.get(db, championship_id)
|
|
if not champ:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
return await crud_registration.list_by_championship(db, championship_id)
|
|
|
|
|
|
@router.patch("/{registration_id}/status", response_model=RegistrationOut)
|
|
async def update_registration_status(
|
|
registration_id: str,
|
|
body: RegistrationStatusUpdate,
|
|
_organizer: User = Depends(get_organizer),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
allowed = {"accepted", "rejected", "waitlisted"}
|
|
if body.status not in allowed:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Status must be one of: {', '.join(allowed)}",
|
|
)
|
|
reg = await crud_registration.get(db, registration_id)
|
|
if not reg:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
return await crud_registration.update_status(db, reg, body.status)
|
|
|
|
|
|
@router.delete("/{registration_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def withdraw_registration(
|
|
registration_id: str,
|
|
current_user: User = Depends(get_approved_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
reg = await crud_registration.get(db, registration_id)
|
|
if not reg:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
if str(reg.user_id) != str(current_user.id):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
|
if reg.status != "submitted":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Only submitted registrations can be withdrawn",
|
|
)
|
|
await crud_registration.delete(db, reg)
|