From ed117301f2ddddc02e1daf70703b90a71d1694da Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sat, 19 Sep 2026 04:10:38 +0800 Subject: [PATCH] feat: expose bracket results and discovery feed --- src/pk_lab_server/api.py | 41 +++++++++++++++++++++++++++++++++--- src/pk_lab_server/schemas.py | 17 +++++++++++++-- tests/test_api.py | 4 ++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/pk_lab_server/api.py b/src/pk_lab_server/api.py index 111d4a3..e131797 100644 --- a/src/pk_lab_server/api.py +++ b/src/pk_lab_server/api.py @@ -16,6 +16,7 @@ from .schemas import ( LinkStartOut, LoginRequest, LoginResponse, + PublicRunOut, RunOut, TestCard, TestDetail, @@ -88,7 +89,8 @@ def list_tests(db: Session = Depends(get_db)) -> list[TestCard]: slug=test.slug, title=test.title, subtitle=test.subtitle, - emoji=test.emoji, + description=test.subtitle, + cover_url=f"/assets/test-covers/{test.slug}.jpg", accent=test.accent, candidate_count=len(test.candidates), run_count=counts.get(test.id, 0), @@ -111,12 +113,19 @@ def get_test(slug: str, db: Session = Depends(get_db)) -> TestDetail: slug=test.slug, title=test.title, subtitle=test.subtitle, - emoji=test.emoji, + description=test.subtitle, + cover_url=f"/assets/test-covers/{test.slug}.jpg", accent=test.accent, candidate_count=len(test.candidates), run_count=run_count, candidates=[ - {"id": c.id, "name": c.name, "emoji": c.emoji, "seed_order": c.seed_order} + { + "id": c.id, + "name": c.name, + "description": None, + "image_url": None, + "seed_order": c.seed_order, + } for c in test.candidates ], ) @@ -132,10 +141,36 @@ def _run_out(run: TestRun) -> RunOut: champion_name=run.champion.name, runner_up_name=run.runner_up.name if run.runner_up else None, ranking_names=[candidate_by_id[item] for item in run.ranking_ids if item in candidate_by_id], + decisions=run.decisions, + bracket_size=run.bracket_size, + candidate_names=candidate_by_id, completed_at=run.completed_at, ) +@router.get("/results/discover", response_model=list[PublicRunOut]) +def discover_results( + limit: int = Query(default=30, ge=1, le=100), + db: Session = Depends(get_db), +) -> list[PublicRunOut]: + runs = db.scalars( + select(TestRun) + .options(selectinload(TestRun.test), selectinload(TestRun.champion)) + .order_by(TestRun.completed_at.desc()) + .limit(limit) + ).all() + return [ + PublicRunOut( + id=run.id, + test_slug=run.test.slug, + test_title=run.test.title, + champion_name=run.champion.name, + completed_at=run.completed_at, + ) + for run in runs + ] + + @router.post("/tests/{slug}/runs", response_model=RunOut, status_code=201) def complete_run( slug: str, diff --git a/src/pk_lab_server/schemas.py b/src/pk_lab_server/schemas.py index e656932..53d5a94 100644 --- a/src/pk_lab_server/schemas.py +++ b/src/pk_lab_server/schemas.py @@ -25,7 +25,8 @@ class LoginResponse(BaseModel): class CandidateOut(BaseModel): id: str name: str - emoji: str | None + description: str | None = None + image_url: str | None = None seed_order: int @@ -33,7 +34,8 @@ class TestCard(BaseModel): slug: str title: str subtitle: str - emoji: str + description: str + cover_url: str accent: str candidate_count: int run_count: int @@ -66,12 +68,23 @@ class RunOut(BaseModel): champion_name: str runner_up_name: str | None ranking_names: list[str] + decisions: list[DecisionIn] + bracket_size: int + candidate_names: dict[str, str] completed_at: datetime share_templates: list[str] = Field( default_factory=lambda: ["celebration", "editorial", "sticker"] ) +class PublicRunOut(BaseModel): + id: str + test_slug: str + test_title: str + champion_name: str + completed_at: datetime + + class LinkStartOut(BaseModel): code: str expires_at: datetime diff --git a/tests/test_api.py b/tests/test_api.py index 15f8f1d..81dc079 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -57,6 +57,10 @@ def test_login_complete_and_read_result(): }, ) assert created.status_code == 201, created.text + assert created.json()["decisions"][0]["winner_id"] == left["id"] + assert created.json()["candidate_names"][left["id"]] == "蛋炒饭" + public = client.get("/api/v1/results/discover").json() + assert public[0]["champion_name"] == "蛋炒饭" history = client.get("/api/v1/me/results?test_slug=rice", headers=headers).json() assert history[0]["champion_name"] == "蛋炒饭" assert client.post("/api/v1/auth/logout", headers=headers).status_code == 204