Remove manual course creation; import-only with per-tee stroke indices

Courses can now only be added via Golf Course API import, which
provides the full data model: per-tee slope/rating/gender/SI
(stroke_indices JSON) and front/back split ratings. Manual creation
required admins to enter SI by hand and didn't match the API model.

Changes:
- Remove GET/POST /admin/courses/new routes and new_course.html
- Remove _parse_hole_form helper (no longer needed)
- Edit course: par-only editing; stroke_index preserved from import,
  not exposed or overwritten in the admin form
- courses.html: replace "+ New course" with "+ Import course" button
- course_search.html: remove "+ Manual" shortcut
- edit_course.html: remove SI column from hole grid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rolf
2026-03-24 20:03:19 +01:00
parent a4ce8a9d50
commit 06995c5668
5 changed files with 16 additions and 182 deletions
+14 -91
View File
@@ -51,36 +51,6 @@ async def _get_course_detail(cid: str, db: aiosqlite.Connection):
return dict(course), holes, tees
def _parse_hole_form(form, hole_count: int) -> tuple[dict, dict, dict]:
"""Parse par/SI fields from a form. Returns (pars, stroke_indices, errors)."""
pars: dict[int, int] = {}
stroke_indices: dict[int, int | None] = {}
errors: dict[str, str] = {}
for i in range(1, hole_count + 1):
raw = form.get(f"par_{i}", "4")
try:
p = int(raw)
if p < 3 or p > 5:
errors[f"par_{i}"] = f"Hole {i}: par must be 35."
else:
pars[i] = p
except (ValueError, TypeError):
errors[f"par_{i}"] = f"Hole {i}: invalid par."
raw_si = form.get(f"si_{i}", "")
if raw_si:
try:
si = int(raw_si)
if si < 1 or si > hole_count:
errors[f"si_{i}"] = f"Hole {i}: SI must be 1{hole_count}."
else:
stroke_indices[i] = si
except (ValueError, TypeError):
errors[f"si_{i}"] = f"Hole {i}: invalid SI."
else:
stroke_indices[i] = None
return pars, stroke_indices, errors
# ── List ──────────────────────────────────────────────────────────────────────
@router.get("/admin/courses", response_class=HTMLResponse)
@@ -96,61 +66,6 @@ async def courses_page(request: Request, user: dict = Depends(require_admin),
)
# ── Create ────────────────────────────────────────────────────────────────────
@router.get("/admin/courses/new", response_class=HTMLResponse)
async def new_course_page(request: Request, user: dict = Depends(require_admin)):
return templates.TemplateResponse(
"admin/new_course.html", {"request": request, "user": user}
)
@router.post("/admin/courses/new")
async def create_course(request: Request,
name: str = Form(...), holes: int = Form(...),
location: str = Form(""),
user: dict = Depends(require_admin),
db: aiosqlite.Connection = Depends(get_db)):
form = await request.form()
name = name.strip()
location = location.strip()
errors = {}
if not name:
errors["name"] = "Name is required."
if holes not in (9, 18):
errors["holes"] = "Holes must be 9 or 18."
hole_count = holes if holes in (9, 18) else 18
pars, stroke_indices, hole_errors = _parse_hole_form(form, hole_count)
errors.update(hole_errors)
if errors:
return templates.TemplateResponse(
"admin/new_course.html",
{"request": request, "user": user, "errors": errors,
"form": {"name": name, "holes": holes, "location": location,
"pars": pars, "stroke_indices": stroke_indices}},
status_code=422,
)
cid = secrets.token_urlsafe(10)
now = datetime.now(timezone.utc).isoformat()
await db.execute(
"INSERT INTO courses (id, name, holes, location, created_at) VALUES (?,?,?,?,?)",
(cid, name, holes, location or None, now),
)
for hole_num, par in pars.items():
await db.execute(
"INSERT INTO course_holes (id, course_id, hole_number, par, stroke_index) VALUES (?,?,?,?,?)",
(secrets.token_urlsafe(10), cid, hole_num, par, stroke_indices.get(hole_num)),
)
await db.commit()
return RedirectResponse(
f"/admin/courses/{cid}/edit?success=Course+created.+Add+tees+below.",
status_code=302,
)
# ── API Search & Import ───────────────────────────────────────────────────────
@router.get("/admin/courses/search", response_class=HTMLResponse)
@@ -290,8 +205,17 @@ async def edit_course_submit(cid: str, request: Request,
errors["holes"] = "Holes must be 9 or 18."
hole_count = holes if holes in (9, 18) else 18
pars, stroke_indices, hole_errors = _parse_hole_form(form, hole_count)
errors.update(hole_errors)
pars: dict[int, int] = {}
for i in range(1, hole_count + 1):
raw = form.get(f"par_{i}", "4")
try:
p = int(raw)
if p < 3 or p > 5:
errors[f"par_{i}"] = f"Hole {i}: par must be 35."
else:
pars[i] = p
except (ValueError, TypeError):
errors[f"par_{i}"] = f"Hole {i}: invalid par."
if errors:
return templates.TemplateResponse(
@@ -307,10 +231,9 @@ async def edit_course_submit(cid: str, request: Request,
)
for hole_num, par in pars.items():
await db.execute(
"""INSERT INTO course_holes (id, course_id, hole_number, par, stroke_index) VALUES (?,?,?,?,?)
ON CONFLICT(course_id, hole_number) DO UPDATE SET
par=excluded.par, stroke_index=excluded.stroke_index""",
(secrets.token_urlsafe(10), cid, hole_num, par, stroke_indices.get(hole_num)),
"""INSERT INTO course_holes (id, course_id, hole_number, par) VALUES (?,?,?,?)
ON CONFLICT(course_id, hole_number) DO UPDATE SET par = excluded.par""",
(secrets.token_urlsafe(10), cid, hole_num, par),
)
# Remove holes beyond new count if holes was reduced
await db.execute(