Fix: eagerload series.sets i admin og API (MissingGreenlet i async mode)

This commit is contained in:
2026-07-01 22:15:14 +02:00
parent c0952a3bd3
commit 8361066f2f
20 changed files with 1075 additions and 554 deletions
+316 -93
View File
@@ -1,155 +1,378 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
import csv
import io
import os
import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File, Form, status
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from pydantic import BaseModel
from ..database import get_db
from ..models import Set, Card, User
from ..auth import require_admin
from ..models import User, Serie, Set, Card
from ..auth import require_admin, hash_password
router = APIRouter()
async def require_admin_html(request: Request) -> User:
"""Admin check from cookie-based auth (HTML pages)."""
user = request.state.user
if not user or user.role != "admin":
raise HTTPException(status_code=401, detail="Admin login required")
return user
templates = Jinja2Templates(directory="app/templates")
UPLOAD_DIR = Path("app/static/images")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
class SetForm(BaseModel):
# ─── Pydantic schemas ───────────────────────────────────────────────
class SerieCreate(BaseModel):
name: str
description: str | None = None
class SetCreate(BaseModel):
code: str
name: str
release_date: str | None = None
description: str | None = None
class CardForm(BaseModel):
class CardCreate(BaseModel):
number: str
name: str
type: str | None = None
rarity: str | None = None
hp: int | None = None
stage: str | None = None
description: str | None = None
image_url: str | None = None
image: str | None = None # URL
class NewUserForm(BaseModel):
username: str
password: str
role: str = "user"
# --- Admin pages ---
# ─── Admin HTML pages ───────────────────────────────────────────────
@router.get("/admin", response_class=HTMLResponse)
async def admin_dashboard(request: Request, user: User = Depends(require_admin_html)):
return templates.TemplateResponse("admin/dashboard.html", {"request": request, "user": user})
async def admin_dashboard(request: Request, user: User = Depends(require_admin)):
return templates.TemplateResponse("admin/dashboard.html", {"request": request})
@router.get("/admin/series", response_class=HTMLResponse)
async def admin_series(request: Request, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin)):
result = await db.execute(select(Serie).options(selectinload(Serie.sets)).order_by(Serie.name))
series = result.scalars().all()
return templates.TemplateResponse("admin/series.html", {"request": request, "series": series})
@router.get("/admin/sets", response_class=HTMLResponse)
async def admin_sets(request: Request, user: User = Depends(require_admin_html)):
return templates.TemplateResponse("admin/sets.html", {"request": request, "user": user})
async def admin_sets_page(request: Request, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin)):
result = await db.execute(
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
)
sets = result.scalars().all()
series_result = await db.execute(select(Serie).order_by(Serie.name))
series = series_result.scalars().all()
return templates.TemplateResponse("admin/sets.html", {"request": request, "sets": sets, "series": series})
@router.get("/admin/cards/{set_code}", response_class=HTMLResponse)
async def admin_cards(request: Request, set_code: str, user: User = Depends(require_admin_html)):
return templates.TemplateResponse("admin/cards.html", {"request": request, "user": user, "set_code": set_code})
async def admin_cards_page(request: Request, set_code: str, db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin)):
result = await db.execute(
select(Set).where(Set.code == set_code.upper()).options(
selectinload(Set.cards), selectinload(Set.serie)
)
)
s = result.scalar_one_or_none()
if not s:
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
return templates.TemplateResponse("admin/cards.html", {"request": request, "set": s})
# ─── Users (admin) ─────────────────────────────────────────────────
@router.post("/api/admin/users")
async def create_user(data: NewUserForm, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
exists = await db.execute(select(User).where(User.username == data.username))
if exists.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Bruger findes allerede")
user = User(username=data.username, password_hash=hash_password(data.password), role=data.role)
db.add(user)
await db.commit()
return {"ok": True, "username": user.username, "role": user.role}
@router.get("/api/admin/users")
async def list_users(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
result = await db.execute(select(User).order_by(User.username))
return [{"id": u.id, "username": u.username, "role": u.role, "created_at": str(u.created_at)}
for u in result.scalars().all()]
# --- CRUD: Sets ---
# ─── API: Series ────────────────────────────────────────────────────
@router.post("/api/admin/sets")
async def create_set(
data: SetForm,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
@router.post("/api/admin/series")
async def create_serie(data: SerieCreate, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Serie).where(Serie.name == data.name))
if result.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Serie findes allerede")
serie = Serie(name=data.name, description=data.description)
db.add(serie)
await db.commit()
return {"ok": True, "id": serie.id, "name": serie.name}
@router.get("/api/admin/series")
async def list_series(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
result = await db.execute(select(Serie).options(selectinload(Serie.sets)).order_by(Serie.name))
return [{"id": s.id, "name": s.name, "description": s.description or "",
"set_count": len(s.sets)} for s in result.scalars().all()]
@router.delete("/api/admin/series/{serie_id}")
async def delete_serie(serie_id: int, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Serie).where(Serie.id == serie_id))
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail="Serie ikke fundet")
await db.delete(s)
await db.commit()
return {"ok": True}
@router.post("/api/admin/series/import")
async def import_series(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
content = await file.read()
text = content.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
created = 0
errors = []
for i, row in enumerate(reader, start=2):
name = row.get("name", "").strip()
if not name:
errors.append(f"Linje {i}: mangler navn")
continue
try:
exists = await db.execute(select(Serie).where(Serie.name == name))
if exists.scalar_one_or_none():
errors.append(f"Linje {i}: '{name}' findes allerede")
continue
serie = Serie(name=name, description=row.get("description", "").strip() or None)
db.add(serie)
created += 1
except Exception as e:
errors.append(f"Linje {i}: {str(e)}")
await db.commit()
return {"ok": True, "created": created, "errors": errors}
# ─── API: Sets ──────────────────────────────────────────────────────
@router.post("/api/admin/series/{serie_name}/sets")
async def create_set(serie_name: str, data: SetCreate, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Serie).where(Serie.name == serie_name))
serie = result.scalar_one_or_none()
if not serie:
raise HTTPException(status_code=404, detail="Serie ikke fundet")
exists = await db.execute(select(Set).where(Set.code == data.code.upper()))
if exists.scalar_one_or_none():
raise HTTPException(status_code=400, detail=f"Set '{data.code.upper()}' findes allerede")
s = Set(code=data.code.upper(), name=data.name, release_date=data.release_date, description=data.description)
raise HTTPException(status_code=400, detail="Set-kode findes allerede")
s = Set(serie_id=serie.id, code=data.code.upper(), name=data.name,
release_date=data.release_date, description=data.description)
db.add(s)
await db.commit()
await db.refresh(s)
return {"ok": True, "id": s.id, "code": s.code}
return {"ok": True, "id": s.id, "code": s.code, "name": s.name}
@router.get("/api/admin/series/{serie_name}/sets")
async def list_sets(serie_name: str, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(
select(Serie).where(Serie.name == serie_name).options(selectinload(Serie.sets))
)
serie = result.scalar_one_or_none()
if not serie:
raise HTTPException(status_code=404, detail="Serie ikke fundet")
return [{"id": s.id, "code": s.code, "name": s.name, "card_count": len(s.cards)}
for s in serie.sets]
@router.get("/api/admin/sets")
async def list_sets_admin(
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
result = await db.execute(select(Set).order_by(Set.code))
async def list_all_sets(db: AsyncSession = Depends(get_db), _: User = Depends(require_admin)):
result = await db.execute(
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
)
sets = result.scalars().all()
return [{"id": s.id, "code": s.code, "name": s.name, "release_date": s.release_date or ""} for s in sets]
return [{"id": s.id, "code": s.code, "name": s.name, "serie": s.serie.name,
"card_count": len(s.cards), "release_date": s.release_date or ""}
for s in sets]
@router.delete("/api/admin/sets/{set_id}")
async def delete_set(
set_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
s = await db.get(Set, set_id)
async def delete_set(set_id: int, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Set).where(Set.id == set_id))
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail="Set ikke fundet")
await db.delete(s)
await db.commit()
return {"ok": True}
@router.post("/api/admin/sets/import")
async def import_sets(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
content = await file.read()
text = content.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
created = 0
errors = []
for i, row in enumerate(reader, start=2):
code = row.get("code", "").strip().upper()
name = row.get("name", "").strip()
serie_name = row.get("serie", "").strip()
if not code or not name or not serie_name:
errors.append(f"Linje {i}: mangler kode, navn eller serie")
continue
try:
serie = await db.execute(select(Serie).where(Serie.name == serie_name))
serie = serie.scalar_one_or_none()
if not serie:
errors.append(f"Linje {i}: serie '{serie_name}' ikke fundet")
continue
exists = await db.execute(select(Set).where(Set.code == code))
if exists.scalar_one_or_none():
errors.append(f"Linje {i}: kode '{code}' findes allerede")
continue
s = Set(serie_id=serie.id, code=code, name=name,
release_date=row.get("release_date", "").strip() or None,
description=row.get("description", "").strip() or None)
db.add(s)
created += 1
except Exception as e:
errors.append(f"Linje {i}: {str(e)}")
await db.commit()
return {"ok": True, "created": created, "errors": errors}
# --- CRUD: Cards ---
# ─── API: Cards ─────────────────────────────────────────────────────
@router.post("/api/admin/sets/{set_code}/cards")
async def create_card(
set_code: str,
data: CardForm,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
result = await db.execute(select(Set).where(Set.code == set_code.upper()))
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail=f"Set '{set_code.upper()}' ikke fundet")
card = Card(
set_id=s.id, number=data.number, name=data.name,
type=data.type, rarity=data.rarity, hp=data.hp,
stage=data.stage, description=data.description, image_url=data.image_url,
)
db.add(card)
await db.commit()
await db.refresh(card)
return {"ok": True, "id": card.id, "full_code": card.full_code}
@router.get("/api/admin/sets/{set_code}/cards")
async def list_cards_admin(
set_code: str,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
async def create_card(set_code: str, data: CardCreate, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Set).where(Set.code == set_code.upper()))
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail="Set ikke fundet")
return [
{"id": c.id, "number": c.number, "name": c.name, "type": c.type or "",
"rarity": c.rarity or "", "hp": c.hp, "full_code": c.full_code}
for c in s.cards
]
card = Card(set_id=s.id, number=data.number, name=data.name)
if data.image:
card.image_path = data.image
db.add(card)
await db.commit()
return {"ok": True, "id": card.id, "full_code": card.full_code, "name": card.name}
@router.get("/api/admin/sets/{set_code}/cards")
async def list_cards(set_code: str, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(
select(Set).where(Set.code == set_code.upper()).options(
selectinload(Set.cards), selectinload(Set.serie)
)
)
s = result.scalar_one_or_none()
if not s:
raise HTTPException(status_code=404, detail="Set ikke fundet")
return [{"id": c.id, "number": c.number, "name": c.name, "full_code": c.full_code,
"image_url": c.image_url} for c in s.cards]
@router.delete("/api/admin/cards/{card_id}")
async def delete_card(
card_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
card = await db.get(Card, card_id)
async def delete_card(card_id: int, db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(select(Card).where(Card.id == card_id))
card = result.scalar_one_or_none()
if not card:
raise HTTPException(status_code=404, detail="Kort ikke fundet")
# Slet billedfil hvis lokal
if card.image_path and not card.image_path.startswith(("http://", "https://")):
fp = Path("app/static") / card.image_path
if fp.exists():
fp.unlink()
await db.delete(card)
await db.commit()
return {"ok": True}
@router.post("/api/admin/cards/import")
async def import_cards(file: UploadFile = File(...), db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
content = await file.read()
text = content.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
created = 0
errors = []
for i, row in enumerate(reader, start=2):
set_code = row.get("set_code", "").strip().upper()
number = row.get("number", "").strip()
name = row.get("name", "").strip()
if not set_code or not number or not name:
errors.append(f"Linje {i}: mangler set_code, number eller name")
continue
try:
result = await db.execute(select(Set).where(Set.code == set_code))
s = result.scalar_one_or_none()
if not s:
errors.append(f"Linje {i}: set '{set_code}' ikke fundet")
continue
image = row.get("image", "").strip() or None
card = Card(set_id=s.id, number=number, name=name, image_path=image)
db.add(card)
created += 1
except Exception as e:
errors.append(f"Linje {i}: {str(e)}")
await db.commit()
return {"ok": True, "created": created, "errors": errors}
@router.post("/api/admin/cards/{card_id}/upload")
async def upload_card_image(card_id: int, file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin)):
result = await db.execute(
select(Card).where(Card.id == card_id).options(
selectinload(Card.set).selectinload(Set.serie)
)
)
card = result.scalar_one_or_none()
if not card:
raise HTTPException(status_code=404, detail="Kort ikke fundet")
# Bestem filtype
ext = os.path.splitext(file.filename or "image.png")[1].lower()
if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"):
raise HTTPException(status_code=400, detail="Kun .jpg, .png, .gif, .webp tilladt")
serie_name = card.set.serie.name.replace(" ", "_").replace("/", "_")
set_code = card.set.code
number = card.number
folder = UPLOAD_DIR / serie_name / set_code
folder.mkdir(parents=True, exist_ok=True)
filename = f"{number}{ext}"
filepath = folder / filename
content = await file.read()
filepath.write_bytes(content)
# Gem relativ sti
rel_path = f"images/{serie_name}/{set_code}/{filename}"
card.image_path = rel_path
await db.commit()
return {"ok": True, "image_url": f"/static/{rel_path}", "path": rel_path}
# ─── CSV Templates ──────────────────────────────────────────────────
@router.get("/api/admin/templates/series.csv", response_class=PlainTextResponse)
async def series_csv_template(_: User = Depends(require_admin)):
return "name,description\nSword & Shield,The Sword & Shield series\n"
@router.get("/api/admin/templates/sets.csv", response_class=PlainTextResponse)
async def sets_csv_template(_: User = Depends(require_admin)):
return "serie,code,name,release_date,description\nSword & Shield,EVS,Evolving Skies,2021-08-27,\n"
@router.get("/api/admin/templates/cards.csv", response_class=PlainTextResponse)
async def cards_csv_template(_: User = Depends(require_admin)):
return "set_code,number,name,image\nEVS,050,Raichu,\n"
+4 -39
View File
@@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -7,7 +7,7 @@ from pydantic import BaseModel
from ..database import get_db
from ..models import User
from ..auth import verify_password, create_token, hash_password, require_admin
from ..auth import verify_password, create_token, hash_password
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@@ -37,7 +37,7 @@ async def login(data: LoginForm, db: AsyncSession = Depends(get_db)):
raise HTTPException(status_code=401, detail="Forkert brugernavn eller adgangskode")
token = create_token(user.username, user.role)
resp = RedirectResponse(url="/", status_code=302)
resp = JSONResponse(content={"ok": True, "redirect": "/"})
resp.set_cookie(
key="token", value=token, httponly=True, max_age=30 * 24 * 3600,
samesite="lax"
@@ -47,41 +47,6 @@ async def login(data: LoginForm, db: AsyncSession = Depends(get_db)):
@router.post("/api/logout")
async def logout():
resp = RedirectResponse(url="/login", status_code=302)
resp = JSONResponse(content={"ok": True, "redirect": "/login"})
resp.delete_cookie("token")
return resp
# --- Admin-only: user management ---
@router.post("/api/admin/users")
async def create_user(
data: CreateUserForm,
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
exists = await db.execute(select(User).where(User.username == data.username))
if exists.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Bruger findes allerede")
user = User(
username=data.username,
password_hash=hash_password(data.password),
role=data.role,
)
db.add(user)
await db.commit()
return {"ok": True, "username": user.username, "role": user.role}
@router.get("/api/admin/users")
async def list_users(
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
):
result = await db.execute(select(User).order_by(User.username))
users = result.scalars().all()
return [
{"id": u.id, "username": u.username, "role": u.role, "created_at": str(u.created_at)}
for u in users
]
+116 -59
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from ..database import get_db
from ..models import Set, Card
from ..models import Serie, Set, Card
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@@ -14,14 +14,40 @@ templates = Jinja2Templates(directory="app/templates")
@router.get("/", response_class=HTMLResponse)
async def index(request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Set).order_by(Set.code))
sets = result.scalars().all()
return templates.TemplateResponse("index.html", {"request": request, "sets": sets})
result = await db.execute(
select(Serie).options(selectinload(Serie.sets).selectinload(Set.cards)).order_by(Serie.name)
)
series = result.scalars().all()
return templates.TemplateResponse("index.html", {"request": request, "series": series})
@router.get("/series", response_class=HTMLResponse)
async def list_series(request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Serie).options(selectinload(Serie.sets).selectinload(Set.cards)).order_by(Serie.name)
)
series = result.scalars().all()
return templates.TemplateResponse("series.html", {"request": request, "series": series})
@router.get("/series/{serie_name}", response_class=HTMLResponse)
async def serie_detail(request: Request, serie_name: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Serie).where(Serie.name == serie_name).options(
selectinload(Serie.sets).selectinload(Set.cards)
)
)
serie = result.scalar_one_or_none()
if not serie:
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
return templates.TemplateResponse("serie_detail.html", {"request": request, "serie": serie})
@router.get("/sets", response_class=HTMLResponse)
async def list_sets(request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Set).order_by(Set.code))
result = await db.execute(
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
)
sets = result.scalars().all()
return templates.TemplateResponse("sets.html", {"request": request, "sets": sets})
@@ -29,7 +55,9 @@ async def list_sets(request: Request, db: AsyncSession = Depends(get_db)):
@router.get("/sets/{set_code}", response_class=HTMLResponse)
async def set_detail(request: Request, set_code: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Set).where(Set.code == set_code.upper()).options(selectinload(Set.cards))
select(Set).where(Set.code == set_code.upper()).options(
selectinload(Set.cards), selectinload(Set.serie)
)
)
s = result.scalar_one_or_none()
if not s:
@@ -37,65 +65,61 @@ async def set_detail(request: Request, set_code: str, db: AsyncSession = Depends
return templates.TemplateResponse("set_detail.html", {"request": request, "set": s})
@router.get("/card/{set_code}/{number}", response_class=HTMLResponse)
async def card_detail(request: Request, set_code: str, number: str,
db: AsyncSession = Depends(get_db)):
# Prøv med og uden leading zero (robust overfor "50" vs "050")
candidates = set([number, number.zfill(3), number.lstrip("0")])
for num in candidates:
result = await db.execute(
select(Card)
.join(Card.set)
.where(Set.code == set_code.upper(), Card.number == num)
.options(selectinload(Card.set).selectinload(Set.serie))
)
card = result.scalar_one_or_none()
if card:
return templates.TemplateResponse("card_detail.html", {"request": request, "card": card})
return templates.TemplateResponse("404.html", {"request": request}, status_code=404)
@router.get("/search", response_class=HTMLResponse)
async def search_page(request: Request):
return templates.TemplateResponse("search.html", {"request": request})
@router.get("/api/search")
async def search_cards(
q: str = Query(""),
db: AsyncSession = Depends(get_db),
):
"""Search cards by full code (e.g. CRI-068) or by name."""
q = q.strip()
# ─── JSON API ───────────────────────────────────────────────────────
# Try full-code search: CODE-NUMBER
if "-" in q:
parts = q.split("-", 1)
code_part = parts[0].strip().upper()
num_part = parts[1].strip().lstrip("0") # remove leading zeros
stmt = (
select(Card)
.join(Card.set)
.where(Set.code == code_part, Card.number == num_part)
.options(selectinload(Card.set))
@router.get("/api/series")
async def api_list_series(db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Serie).options(selectinload(Serie.sets)).order_by(Serie.name)
)
return [{"id": s.id, "name": s.name, "description": s.description or "",
"set_count": len(s.sets)} for s in result.scalars().all()]
@router.get("/api/series/{serie_name}/sets")
async def api_series_sets(serie_name: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Serie).where(Serie.name == serie_name).options(
selectinload(Serie.sets).selectinload(Set.cards)
)
else:
# Search by name
stmt = (
select(Card)
.join(Card.set)
.where(Card.name.ilike(f"%{q}%"))
.options(selectinload(Card.set))
.limit(50)
)
result = await db.execute(stmt)
cards = result.scalars().all()
return [
{
"id": c.id,
"full_code": c.full_code,
"name": c.name,
"type": c.type or "",
"rarity": c.rarity or "",
"set_name": c.set.name,
}
for c in cards
]
)
serie = result.scalar_one_or_none()
if not serie:
return {"error": "Serie ikke fundet"}
return [{"code": s.code, "name": s.name, "card_count": len(s.cards)} for s in serie.sets]
@router.get("/api/sets")
async def api_list_sets(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Set).order_by(Set.code))
sets = result.scalars().all()
return [
{"code": s.code, "name": s.name, "release_date": s.release_date or "",
"card_count": len(s.cards)}
for s in sets
]
result = await db.execute(
select(Set).options(selectinload(Set.serie), selectinload(Set.cards)).order_by(Set.code)
)
return [{"code": s.code, "name": s.name, "serie": s.serie.name,
"card_count": len(s.cards)} for s in result.scalars().all()]
@router.get("/api/sets/{set_code}/cards")
@@ -106,8 +130,41 @@ async def api_set_cards(set_code: str, db: AsyncSession = Depends(get_db)):
s = result.scalar_one_or_none()
if not s:
return []
return [
{"id": c.id, "number": c.number, "name": c.name, "type": c.type or "",
"rarity": c.rarity or "", "hp": c.hp, "full_code": c.full_code}
for c in s.cards
]
return [{"id": c.id, "number": c.number, "name": c.name,
"full_code": c.full_code, "image_url": c.image_url} for c in s.cards]
@router.get("/api/search")
async def search_cards(q: str = Query(""), db: AsyncSession = Depends(get_db)):
"""Search cards by full code (CRI-068) or by name."""
q = q.strip()
if not q:
return []
if "\u002D" in q or " " in q or "=" in q:
# Split på bindestreg, mellemrum eller =: "CRI-068" eller "CRI 068"
sep = "\u002D" if "\u002D" in q else (" " if " " in q else "=")
parts = q.split(sep, 1)
code_part = parts[0].strip().upper()
num_part = parts[1].strip()
candidates = set([num_part, num_part.zfill(3), num_part.lstrip("0")])
stmt = select(Card).join(Card.set).where(
Set.code == code_part, Card.number.in_(list(candidates))
).options(selectinload(Card.set).selectinload(Set.serie))
else:
stmt = (
select(Card)
.join(Card.set)
.where(Card.name.ilike(f"%{q}%"))
.options(selectinload(Card.set).selectinload(Set.serie))
.limit(50)
)
result = await db.execute(stmt)
cards = result.scalars().all()
return [{
"id": c.id, "full_code": c.full_code, "name": c.name,
"set_name": c.set.name, "set_code": c.set.code,
"serie_name": c.set.serie.name,
"image_url": c.image_url,
} for c in cards]