Files
timing/backend/race_db.py
T
steinhelge 5393e85a74
Build & Deploy / build-and-deploy (push) Successful in 2m18s
Add race and station management
- races table: name, date, description, is_active
- stations table: ordered checkpoints with GPS per race
- New /api/races and /api/races/{id}/stations endpoints
- Upload now requires race + station selection; uses station GPS
  so images without GPS EXIF are accepted
- passages filtered by active race throughout
- RacePage: create races, manage stations (add/edit/delete checkpoints)
- Navbar shows active race name
- Start and finish stations created automatically per race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:45 +01:00

203 lines
6.9 KiB
Python

"""
CRUD for løp (races) og stasjoner (stations).
"""
import uuid
from typing import Optional
import aiosqlite
async def init_race_tables(db: aiosqlite.Connection) -> None:
await db.executescript("""
CREATE TABLE IF NOT EXISTS races (
race_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
date TEXT,
description TEXT,
is_active INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS stations (
station_id TEXT PRIMARY KEY,
race_id TEXT NOT NULL REFERENCES races(race_id) ON DELETE CASCADE,
name TEXT NOT NULL,
display_name TEXT NOT NULL,
station_order INTEGER NOT NULL DEFAULT 0,
gps_lat REAL,
gps_lon REAL,
gps_alt REAL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(race_id, name)
);
CREATE INDEX IF NOT EXISTS idx_stations_race ON stations(race_id, station_order);
""")
await db.commit()
# ── Løp ──────────────────────────────────────────────────────────────────────
async def create_race(
db: aiosqlite.Connection,
name: str,
date: Optional[str] = None,
description: Optional[str] = None,
) -> dict:
race_id = str(uuid.uuid4())
await db.execute(
"INSERT INTO races (race_id, name, date, description) VALUES (?, ?, ?, ?)",
(race_id, name, date, description),
)
# Sett som aktivt hvis det er første løpet
async with db.execute("SELECT COUNT(*) FROM races") as cur:
count = (await cur.fetchone())[0]
if count == 1:
await db.execute("UPDATE races SET is_active = 1 WHERE race_id = ?", (race_id,))
# Opprett standard start- og målstasjon
for order, (sname, display) in enumerate([("start", "Start"), ("finish", "Mål")]):
await db.execute(
"""INSERT INTO stations (station_id, race_id, name, display_name, station_order)
VALUES (?, ?, ?, ?, ?)""",
(str(uuid.uuid4()), race_id, sname, display, order * 1000),
)
await db.commit()
return await get_race(db, race_id)
async def get_race(db: aiosqlite.Connection, race_id: str) -> Optional[dict]:
async with db.execute("SELECT * FROM races WHERE race_id = ?", (race_id,)) as cur:
row = await cur.fetchone()
return dict(row) if row else None
async def list_races(db: aiosqlite.Connection) -> list[dict]:
async with db.execute("SELECT * FROM races ORDER BY date DESC, created_at DESC") as cur:
rows = await cur.fetchall()
return [dict(r) for r in rows]
async def update_race(
db: aiosqlite.Connection,
race_id: str,
name: str,
date: Optional[str],
description: Optional[str],
) -> bool:
cur = await db.execute(
"UPDATE races SET name = ?, date = ?, description = ? WHERE race_id = ?",
(name, date, description, race_id),
)
await db.commit()
return cur.rowcount > 0
async def delete_race(db: aiosqlite.Connection, race_id: str) -> bool:
cur = await db.execute("DELETE FROM races WHERE race_id = ?", (race_id,))
await db.commit()
return cur.rowcount > 0
async def set_active_race(db: aiosqlite.Connection, race_id: str) -> bool:
await db.execute("UPDATE races SET is_active = 0")
cur = await db.execute(
"UPDATE races SET is_active = 1 WHERE race_id = ?", (race_id,)
)
await db.commit()
return cur.rowcount > 0
async def get_active_race(db: aiosqlite.Connection) -> Optional[dict]:
async with db.execute("SELECT * FROM races WHERE is_active = 1 LIMIT 1") as cur:
row = await cur.fetchone()
return dict(row) if row else None
# ── Stasjoner ─────────────────────────────────────────────────────────────────
async def list_stations(db: aiosqlite.Connection, race_id: str) -> list[dict]:
async with db.execute(
"SELECT * FROM stations WHERE race_id = ? ORDER BY station_order",
(race_id,),
) as cur:
rows = await cur.fetchall()
return [dict(r) for r in rows]
async def get_station(db: aiosqlite.Connection, station_id: str) -> Optional[dict]:
async with db.execute(
"SELECT * FROM stations WHERE station_id = ?", (station_id,)
) as cur:
row = await cur.fetchone()
return dict(row) if row else None
async def create_station(
db: aiosqlite.Connection,
race_id: str,
name: str,
display_name: str,
station_order: int,
gps_lat: Optional[float] = None,
gps_lon: Optional[float] = None,
gps_alt: Optional[float] = None,
) -> dict:
station_id = str(uuid.uuid4())
await db.execute(
"""INSERT INTO stations
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt),
)
await db.commit()
return dict((await db.execute(
"SELECT * FROM stations WHERE station_id = ?", (station_id,)
)).get_cursor() or {})
async def upsert_station(
db: aiosqlite.Connection,
race_id: str,
name: str,
display_name: str,
station_order: int,
gps_lat: Optional[float] = None,
gps_lon: Optional[float] = None,
gps_alt: Optional[float] = None,
station_id: Optional[str] = None,
) -> dict:
if station_id:
await db.execute(
"""UPDATE stations SET name=?, display_name=?, station_order=?,
gps_lat=?, gps_lon=?, gps_alt=? WHERE station_id=?""",
(name, display_name, station_order, gps_lat, gps_lon, gps_alt, station_id),
)
else:
station_id = str(uuid.uuid4())
await db.execute(
"""INSERT INTO stations
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(station_id, race_id, name, display_name, station_order, gps_lat, gps_lon, gps_alt),
)
await db.commit()
async with db.execute("SELECT * FROM stations WHERE station_id = ?", (station_id,)) as cur:
row = await cur.fetchone()
return dict(row)
async def delete_station(db: aiosqlite.Connection, station_id: str) -> bool:
# Ikke tillat sletting av start/finish
async with db.execute(
"SELECT name FROM stations WHERE station_id = ?", (station_id,)
) as cur:
row = await cur.fetchone()
if row and row["name"] in ("start", "finish"):
return False
cur = await db.execute("DELETE FROM stations WHERE station_id = ?", (station_id,))
await db.commit()
return cur.rowcount > 0