Import gender tees + front/back split ratings; front/back/9 hole selection

Course import now fetches both male and female tees, each with their own
per-tee stroke indices extracted from the holes array. Front and back 9
slope/rating are stored per tee for more accurate WHS handicap calculation.

New game creation replaces the 9/18 radio with a course-aware selector:
18-hole courses offer 18 holes, Front 9, or Back 9; 9-hole courses auto-
select. start_hole is stored on the game and used throughout scoring,
handicap calculation, and scorecard rendering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rolf
2026-03-23 22:37:20 +01:00
parent 5432734240
commit 3e98f5ae54
6 changed files with 211 additions and 78 deletions
+40 -16
View File
@@ -1,3 +1,4 @@
import json
import os
import secrets
from datetime import datetime, timezone
@@ -43,7 +44,7 @@ async def _get_course_detail(cid: str, db: aiosqlite.Connection):
) as c:
holes = [dict(r) for r in await c.fetchall()]
async with db.execute(
"SELECT id, name, slope, rating FROM course_tees WHERE course_id = ? ORDER BY name",
"SELECT id, name, slope, rating, gender, front_slope, front_rating, back_slope, back_rating FROM course_tees WHERE course_id = ? ORDER BY gender, name",
(cid,),
) as c:
tees = [dict(r) for r in await c.fetchall()]
@@ -197,10 +198,10 @@ async def import_course(api_id: int, request: Request,
holes_count = n
break
# Hole par + stroke index from first tee that has hole data
# Hole par + course-level stroke index from first male tee with hole data (fallback for display)
hole_pars: dict[int, int] = {}
hole_sis: dict[int, int] = {}
for t in all_tees:
for t in tees_male:
hole_data = t.get("holes") or []
if hole_data:
for i, h in enumerate(hole_data[:holes_count], 1):
@@ -220,15 +221,30 @@ async def import_course(api_id: int, request: Request,
"INSERT INTO course_holes (id, course_id, hole_number, par, stroke_index) VALUES (?,?,?,?,?)",
(secrets.token_urlsafe(10), cid, i, hole_pars.get(i, 4), hole_sis.get(i)),
)
for t in tees_male:
tname = (t.get("tee_name") or "").strip()
slope = t.get("slope_rating")
rating = t.get("course_rating")
if tname and slope and rating:
await db.execute(
"INSERT INTO course_tees (id, course_id, name, slope, rating) VALUES (?,?,?,?,?)",
(secrets.token_urlsafe(10), cid, tname, slope, rating),
)
def _tee_stroke_indices(t: dict) -> str | None:
"""Extract per-hole stroke indices from a tee's holes array as JSON."""
si_map = {}
for i, h in enumerate((t.get("holes") or [])[:holes_count], 1):
if h.get("handicap"):
si_map[i] = h["handicap"]
return json.dumps(si_map) if si_map else None
for gender, tee_list in (("male", tees_male), ("female", tees_female)):
for t in tee_list:
tname = (t.get("tee_name") or "").strip()
slope = t.get("slope_rating")
rating = t.get("course_rating")
if tname and slope and rating:
await db.execute(
"""INSERT INTO course_tees
(id, course_id, name, slope, rating, gender, stroke_indices,
front_slope, front_rating, back_slope, back_rating)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(secrets.token_urlsafe(10), cid, tname, slope, rating, gender, _tee_stroke_indices(t),
t.get("front_slope_rating"), t.get("front_course_rating"),
t.get("back_slope_rating"), t.get("back_course_rating")),
)
await db.commit()
return RedirectResponse(
f"/admin/courses/{cid}/edit?success=Course+imported+successfully.", status_code=302
@@ -339,11 +355,15 @@ async def delete_course(cid: str, user: dict = Depends(require_admin),
@router.post("/admin/courses/{cid}/tees/new")
async def add_tee(cid: str, name: str = Form(...),
slope: int = Form(...), rating: float = Form(...),
gender: str = Form("male"),
front_slope: int | None = Form(None), front_rating: float | None = Form(None),
back_slope: int | None = Form(None), back_rating: float | None = Form(None),
user: dict = Depends(require_admin),
db: aiosqlite.Connection = Depends(get_db)):
gender = gender if gender in ("male", "female") else "male"
await db.execute(
"INSERT INTO course_tees (id, course_id, name, slope, rating) VALUES (?,?,?,?,?)",
(secrets.token_urlsafe(10), cid, name.strip(), slope, rating),
"INSERT INTO course_tees (id, course_id, name, slope, rating, gender, front_slope, front_rating, back_slope, back_rating) VALUES (?,?,?,?,?,?,?,?,?,?)",
(secrets.token_urlsafe(10), cid, name.strip(), slope, rating, gender, front_slope, front_rating, back_slope, back_rating),
)
await db.commit()
return RedirectResponse(f"/admin/courses/{cid}/edit?success=Tee+added.", status_code=302)
@@ -352,11 +372,15 @@ async def add_tee(cid: str, name: str = Form(...),
@router.post("/admin/courses/{cid}/tees/{tid}/edit")
async def edit_tee(cid: str, tid: str, name: str = Form(...),
slope: int = Form(...), rating: float = Form(...),
gender: str = Form("male"),
front_slope: int | None = Form(None), front_rating: float | None = Form(None),
back_slope: int | None = Form(None), back_rating: float | None = Form(None),
user: dict = Depends(require_admin),
db: aiosqlite.Connection = Depends(get_db)):
gender = gender if gender in ("male", "female") else "male"
await db.execute(
"UPDATE course_tees SET name = ?, slope = ?, rating = ? WHERE id = ? AND course_id = ?",
(name.strip(), slope, rating, tid, cid),
"UPDATE course_tees SET name = ?, slope = ?, rating = ?, gender = ?, front_slope = ?, front_rating = ?, back_slope = ?, back_rating = ? WHERE id = ? AND course_id = ?",
(name.strip(), slope, rating, gender, front_slope, front_rating, back_slope, back_rating, tid, cid),
)
await db.commit()
return RedirectResponse(f"/admin/courses/{cid}/edit?success=Tee+updated.", status_code=302)