Files
wiki-tcg/backend/routers/decks.py
T
2026-07-29 16:30:14 +02:00

122 lines
4.7 KiB
Python

import uuid
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session, selectinload
from game.card import compute_deck_type
from core.database import get_db
from core.dependencies import get_current_user, limiter, parse_uuid
from core.models import Card as CardModel
from core.models import Deck as DeckModel
from core.models import DeckCard as DeckCardModel
from core.models import User as UserModel
router = APIRouter()
class DeckUpdate(BaseModel):
name: Optional[str] = Field(None, max_length=64)
# The real constraint is the 50-cost rule enforced at game start, but cost is
# per-card so an unbounded list is still a cheap way to make us do arbitrary
# work. Cheapest cards are 1 cost, so 60 can't hide a legal deck.
card_ids: Optional[List[str]] = Field(None, max_length=60)
@router.get("/decks")
def get_decks(user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
decks = db.query(DeckModel).options(
selectinload(DeckModel.deck_cards).selectinload(DeckCardModel.card)
).filter(
DeckModel.user_id == user.id,
DeckModel.deleted == False
).order_by(DeckModel.created_at).all()
result = []
for deck in decks:
cards = [dc.card for dc in deck.deck_cards]
result.append({
"id": str(deck.id),
"name": deck.name,
"card_count": len(cards),
"total_cost": sum(card.cost for card in cards),
"times_played": deck.times_played,
"wins": deck.wins,
"losses": deck.losses,
"deck_type": compute_deck_type(cards),
})
return result
@router.post("/decks")
@limiter.limit("30/minute")
def create_deck(request: Request, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
count = db.query(DeckModel).filter(DeckModel.user_id == user.id).count()
deck = DeckModel(id=uuid.uuid4(), user_id=user.id, name=f"Deck #{count + 1}")
db.add(deck)
db.commit()
return {"id": str(deck.id), "name": deck.name, "card_count": 0}
@router.patch("/decks/{deck_id}")
# Generous: the deck editor saves on every change.
@limiter.limit("120/minute")
def update_deck(request: Request, deck_id: str, body: DeckUpdate, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
deck = db.query(DeckModel).filter(DeckModel.id == parse_uuid(deck_id, "deck_id"), DeckModel.user_id == user.id).first()
if not deck:
raise HTTPException(status_code=404, detail="Deck not found")
if body.name is not None:
deck.name = body.name
if body.card_ids is not None:
try:
parsed_ids = [uuid.UUID(cid) for cid in body.card_ids]
except ValueError:
raise HTTPException(status_code=400, detail="Invalid card IDs")
# deck_cards is keyed on (deck_id, card_id), so a repeated id would blow up
# on flush. dict.fromkeys dedupes while keeping the submitted order.
unique_ids = list(dict.fromkeys(parsed_ids))
# Without this the only constraint is the FK, so any card UUID that exists
# is accepted — including another user's. Public profiles hand those out.
owned = db.query(CardModel.id).filter(
CardModel.id.in_(unique_ids),
CardModel.user_id == user.id,
).all()
if len(owned) != len(unique_ids):
raise HTTPException(status_code=400, detail="Some cards are not in your collection")
db.query(DeckCardModel).filter(DeckCardModel.deck_id == deck.id).delete()
for card_id in unique_ids:
db.add(DeckCardModel(deck_id=deck.id, card_id=card_id))
if deck.times_played > 0:
deck.wins = 0
deck.losses = 0
deck.times_played = 0
db.commit()
return {"id": str(deck.id), "name": deck.name}
@router.delete("/decks/{deck_id}")
def delete_deck(deck_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
deck = db.query(DeckModel).filter(DeckModel.id == parse_uuid(deck_id, "deck_id"), DeckModel.user_id == user.id).first()
if not deck:
raise HTTPException(status_code=404, detail="Deck not found")
if deck.times_played > 0:
deck.deleted = True
else:
db.delete(deck)
db.commit()
return {"message": "Deleted"}
@router.get("/decks/{deck_id}/cards")
def get_deck_cards(deck_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
deck = db.query(DeckModel).filter(DeckModel.id == parse_uuid(deck_id, "deck_id"), DeckModel.user_id == user.id).first()
if not deck:
raise HTTPException(status_code=404, detail="Deck not found")
deck_cards = db.query(DeckCardModel).options(
selectinload(DeckCardModel.card)
).filter(DeckCardModel.deck_id == deck.id).all()
return [{"id": str(dc.card_id), "cost": dc.card.cost} for dc in deck_cards]