teecup/app/routers/players.py

265 lines
11 KiB
Python
Raw Normal View History

"""
Spillerpool organisasjonsnivå (ADR-001: gjenbrukbar tvers av turneringer).
Kun org-medlemskap kreves (get_authorized_org) å opprette en spiller i poolen
er organisator-arbeid, ikke noe som forutsetter rostring et lag.
Utvidet profil (ADR-017): kontakt-/identitetsfelt brukt til å matche en
selvregistrerende spiller mot en allerede organisator-opprettet rad
e-post (se app/routers/registration.py), og senere til å koble
app_user.id (app/routers/auth.py sin verify_magic_link).
"""
from datetime import date
from fastapi import APIRouter, Depends
from pydantic import BaseModel, EmailStr, Field
from ..auth import get_authorized_org
from ..db import org_connection
Update Todos Diagnostisere dashboard-dato-bug (allerede kjent, bekreftet på nytt) Fiks: utled turnering-datospenn fra øktenes scheduled_at i list_tournaments Bygg PATCH /orgs/{id}/players/{id} (rediger spillerpool) Bygg frontend: Rediger spiller-skjema i tournament-detail.tsx Scratch-verifisere begge fiksene Typesjekket frontend-build Oppdatere .md-filer + be om utrullingsbekreftelse Logikken bak "Ingen datoer satt": dashbord-kortet leser et helt eget felt på selve turneringen (start_date/end_date), atskilt fra øktenes scheduled_at som Program-fanen viser. Ingen UI har noensinne satt det feltet, så det var alltid tomt — uavhengig av at "11. juli" faktisk var satt på øktene. Fikset: kortet henter nå datospennet fra øktene i stedet (11.–12. juli her, siden du har to økter samme dag). Rediger spiller: bygget. Ny "Rediger spiller" i "⋮"-menyen på hver spiller — navn, HCP og kjønn kan endres. Én viktig presisering vist rett i skjemaet: dette endrer spilleren i poolen (gjelder fremtidig rostring), men endrer ikke HCP-tallet som allerede er frosset på et lag i en turnering (det er bevisst, for at et allerede spilt/pågående oppgjør ikke skal endre seg i etterkant). Skal du rette et tall som allerede vises på et lag, må du fortsatt fjerne og legge til spilleren på nytt — si fra om du vil at jeg bygger en direkte "rett dette laget sitt tall her og nå"-vei også. Begge deler er scratch-verifisert (9 automatiserte sjekker, inkludert at frysing-regelen fortsatt holder) og typesjekket bygget. Ingen migrasjon.
2026-07-20 10:01:44 +02:00
from ..errors import app_error, translate_db_errors
router = APIRouter()
class PlayerCreate(BaseModel):
display_name: str = Field(min_length=1, max_length=200)
# Samme grense som ProfileUpdate.handicap_index i auth.py (WHS-maksimum).
handicap_index: float | None = Field(default=None, ge=-10, le=54)
gender: str | None = Field(default=None, pattern="^[mfx]$")
mobile: str | None = Field(default=None, max_length=30)
email: EmailStr | None = None
birth_date: date | None = None
nickname: str | None = Field(default=None, max_length=100)
country: str | None = Field(default=None, max_length=100)
club: str | None = Field(default=None, max_length=200)
club_member_number: str | None = Field(default=None, max_length=50)
# Utvidet registreringsskjema (2026-08-14, migrasjon 074) -- for-/
# etternavn er en VALGFRI, separat kilde ved siden av display_name
# (som fortsatt er det som faktisk vises overalt ellers, uendret) --
# se migrasjonens header. paid/comment er ren registreringsadministrasjon.
first_name: str | None = Field(default=None, max_length=100)
last_name: str | None = Field(default=None, max_length=100)
paid: bool = False
comment: str | None = Field(default=None, max_length=2000)
class Player(BaseModel):
id: str
display_name: str
handicap_index: float | None
gender: str | None
mobile: str | None
email: str | None
birth_date: date | None
nickname: str | None
country: str | None
club: str | None
club_member_number: str | None
first_name: str | None
last_name: str | None
paid: bool
comment: str | None
_PLAYER_COLUMNS = """
id::text, display_name, handicap_index::float AS handicap_index, gender,
mobile, email, birth_date, nickname, country, club, club_member_number,
first_name, last_name, paid, comment
"""
@router.get("/orgs/{organization_id}/players", response_model=list[Player])
async def list_players(
organization_id: str = Depends(get_authorized_org),
) -> list[Player]:
async with org_connection(organization_id) as conn:
rows = await conn.fetch(
f"SELECT {_PLAYER_COLUMNS} FROM player ORDER BY display_name"
)
return [Player(**dict(r)) for r in rows]
@router.post("/orgs/{organization_id}/players", response_model=Player, status_code=201)
async def create_player(
body: PlayerCreate,
organization_id: str = Depends(get_authorized_org),
) -> Player:
async with org_connection(organization_id) as conn, translate_db_errors():
row = await conn.fetchrow(
f"""
INSERT INTO player
(organization_id, display_name, handicap_index, gender,
mobile, email, birth_date, nickname, country, club, club_member_number,
first_name, last_name, paid, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING {_PLAYER_COLUMNS}
""",
organization_id,
body.display_name,
body.handicap_index,
body.gender,
body.mobile,
body.email.lower() if body.email else None,
body.birth_date,
body.nickname,
body.country,
body.club,
body.club_member_number,
body.first_name,
body.last_name,
body.paid,
body.comment,
)
return Player(**dict(row))
Update Todos Diagnostisere dashboard-dato-bug (allerede kjent, bekreftet på nytt) Fiks: utled turnering-datospenn fra øktenes scheduled_at i list_tournaments Bygg PATCH /orgs/{id}/players/{id} (rediger spillerpool) Bygg frontend: Rediger spiller-skjema i tournament-detail.tsx Scratch-verifisere begge fiksene Typesjekket frontend-build Oppdatere .md-filer + be om utrullingsbekreftelse Logikken bak "Ingen datoer satt": dashbord-kortet leser et helt eget felt på selve turneringen (start_date/end_date), atskilt fra øktenes scheduled_at som Program-fanen viser. Ingen UI har noensinne satt det feltet, så det var alltid tomt — uavhengig av at "11. juli" faktisk var satt på øktene. Fikset: kortet henter nå datospennet fra øktene i stedet (11.–12. juli her, siden du har to økter samme dag). Rediger spiller: bygget. Ny "Rediger spiller" i "⋮"-menyen på hver spiller — navn, HCP og kjønn kan endres. Én viktig presisering vist rett i skjemaet: dette endrer spilleren i poolen (gjelder fremtidig rostring), men endrer ikke HCP-tallet som allerede er frosset på et lag i en turnering (det er bevisst, for at et allerede spilt/pågående oppgjør ikke skal endre seg i etterkant). Skal du rette et tall som allerede vises på et lag, må du fortsatt fjerne og legge til spilleren på nytt — si fra om du vil at jeg bygger en direkte "rett dette laget sitt tall her og nå"-vei også. Begge deler er scratch-verifisert (9 automatiserte sjekker, inkludert at frysing-regelen fortsatt holder) og typesjekket bygget. Ingen migrasjon.
2026-07-20 10:01:44 +02:00
class PlayerBulkResult(BaseModel):
player: Player
status: str # "created" | "updated"
@router.post("/orgs/{organization_id}/players/bulk", response_model=list[PlayerBulkResult], status_code=201)
async def create_players_bulk(
body: list[PlayerCreate],
organization_id: str = Depends(get_authorized_org),
) -> list[PlayerBulkResult]:
"""Massimport (CSV-basert, se FEATURE_BACKLOG.md). Samme "match på
e-post, fyll KUN tomme felt, ellers opprett ny"-mønster som
`registration.py` sin selvregistrering (ADR-017 Beslutning B) -- her
utvidet til ALLE `PlayerCreate`-felt (registration.py sin variant
mangler first_name/last_name/paid/comment, som ikke fantes da den ble
skrevet). `display_name` og `paid` røres ALDRI en eksisterende
match -- førstnevnte er organisators kanoniske navn, sistnevnte er en
ikke-nullbar boolean (COALESCE mot NULL er da alltid en no-op, som er
riktig oppførsel: en import skal ikke stille kunne endre en allerede
satt betalt-status). Hele lista i én transaksjon (org_connection sin
egen) -- ingen delvis-suksess-håndtering, brukeren har allerede
validert/rettet radene i tabellen før "Lagre" trykkes."""
results: list[PlayerBulkResult] = []
async with org_connection(organization_id) as conn, translate_db_errors():
for p in body:
existing = None
if p.email:
existing = await conn.fetchrow(
"SELECT id FROM player WHERE lower(email) = lower($1)", p.email
)
if existing is not None:
row = await conn.fetchrow(
f"""
UPDATE player SET
handicap_index = COALESCE(handicap_index, $2),
gender = COALESCE(gender, $3),
mobile = COALESCE(mobile, $4),
birth_date = COALESCE(birth_date, $5),
nickname = COALESCE(nickname, $6),
country = COALESCE(country, $7),
club = COALESCE(club, $8),
club_member_number = COALESCE(club_member_number, $9),
first_name = COALESCE(first_name, $10),
last_name = COALESCE(last_name, $11),
comment = COALESCE(comment, $12)
WHERE id = $1
RETURNING {_PLAYER_COLUMNS}
""",
existing["id"],
p.handicap_index,
p.gender,
p.mobile,
p.birth_date,
p.nickname,
p.country,
p.club,
p.club_member_number,
p.first_name,
p.last_name,
p.comment,
)
results.append(PlayerBulkResult(player=Player(**dict(row)), status="updated"))
else:
row = await conn.fetchrow(
f"""
INSERT INTO player
(organization_id, display_name, handicap_index, gender,
mobile, email, birth_date, nickname, country, club, club_member_number,
first_name, last_name, paid, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING {_PLAYER_COLUMNS}
""",
organization_id,
p.display_name,
p.handicap_index,
p.gender,
p.mobile,
p.email.lower() if p.email else None,
p.birth_date,
p.nickname,
p.country,
p.club,
p.club_member_number,
p.first_name,
p.last_name,
p.paid,
p.comment,
)
results.append(PlayerBulkResult(player=Player(**dict(row)), status="created"))
return results
Update Todos Diagnostisere dashboard-dato-bug (allerede kjent, bekreftet på nytt) Fiks: utled turnering-datospenn fra øktenes scheduled_at i list_tournaments Bygg PATCH /orgs/{id}/players/{id} (rediger spillerpool) Bygg frontend: Rediger spiller-skjema i tournament-detail.tsx Scratch-verifisere begge fiksene Typesjekket frontend-build Oppdatere .md-filer + be om utrullingsbekreftelse Logikken bak "Ingen datoer satt": dashbord-kortet leser et helt eget felt på selve turneringen (start_date/end_date), atskilt fra øktenes scheduled_at som Program-fanen viser. Ingen UI har noensinne satt det feltet, så det var alltid tomt — uavhengig av at "11. juli" faktisk var satt på øktene. Fikset: kortet henter nå datospennet fra øktene i stedet (11.–12. juli her, siden du har to økter samme dag). Rediger spiller: bygget. Ny "Rediger spiller" i "⋮"-menyen på hver spiller — navn, HCP og kjønn kan endres. Én viktig presisering vist rett i skjemaet: dette endrer spilleren i poolen (gjelder fremtidig rostring), men endrer ikke HCP-tallet som allerede er frosset på et lag i en turnering (det er bevisst, for at et allerede spilt/pågående oppgjør ikke skal endre seg i etterkant). Skal du rette et tall som allerede vises på et lag, må du fortsatt fjerne og legge til spilleren på nytt — si fra om du vil at jeg bygger en direkte "rett dette laget sitt tall her og nå"-vei også. Begge deler er scratch-verifisert (9 automatiserte sjekker, inkludert at frysing-regelen fortsatt holder) og typesjekket bygget. Ingen migrasjon.
2026-07-20 10:01:44 +02:00
class PlayerUpdate(BaseModel):
"""Alle felt valgfrie -- kun de som faktisk sendes med blir endret
(PATCH-semantikk via Pydantic sin exclude_unset, se update_player).
Endrer KUN player-raden (spillerpoolen) -- rører ALDRI en allerede
frosset team_roster.handicap_index_snapshot (ADR-007): en HCP-
korrigering her slår derfor ikke inn et lag spilleren allerede er
rostret i en turnering, kun ved FREMTIDIG rostring. Fjern og legg
til spilleren nytt laget for å oppdatere en allerede frosset
snapshot-verdi.
"""
display_name: str | None = Field(default=None, min_length=1, max_length=200)
handicap_index: float | None = Field(default=None, ge=-10, le=54)
Update Todos Diagnostisere dashboard-dato-bug (allerede kjent, bekreftet på nytt) Fiks: utled turnering-datospenn fra øktenes scheduled_at i list_tournaments Bygg PATCH /orgs/{id}/players/{id} (rediger spillerpool) Bygg frontend: Rediger spiller-skjema i tournament-detail.tsx Scratch-verifisere begge fiksene Typesjekket frontend-build Oppdatere .md-filer + be om utrullingsbekreftelse Logikken bak "Ingen datoer satt": dashbord-kortet leser et helt eget felt på selve turneringen (start_date/end_date), atskilt fra øktenes scheduled_at som Program-fanen viser. Ingen UI har noensinne satt det feltet, så det var alltid tomt — uavhengig av at "11. juli" faktisk var satt på øktene. Fikset: kortet henter nå datospennet fra øktene i stedet (11.–12. juli her, siden du har to økter samme dag). Rediger spiller: bygget. Ny "Rediger spiller" i "⋮"-menyen på hver spiller — navn, HCP og kjønn kan endres. Én viktig presisering vist rett i skjemaet: dette endrer spilleren i poolen (gjelder fremtidig rostring), men endrer ikke HCP-tallet som allerede er frosset på et lag i en turnering (det er bevisst, for at et allerede spilt/pågående oppgjør ikke skal endre seg i etterkant). Skal du rette et tall som allerede vises på et lag, må du fortsatt fjerne og legge til spilleren på nytt — si fra om du vil at jeg bygger en direkte "rett dette laget sitt tall her og nå"-vei også. Begge deler er scratch-verifisert (9 automatiserte sjekker, inkludert at frysing-regelen fortsatt holder) og typesjekket bygget. Ingen migrasjon.
2026-07-20 10:01:44 +02:00
gender: str | None = Field(default=None, pattern="^[mfx]$")
mobile: str | None = Field(default=None, max_length=30)
Update Todos Diagnostisere dashboard-dato-bug (allerede kjent, bekreftet på nytt) Fiks: utled turnering-datospenn fra øktenes scheduled_at i list_tournaments Bygg PATCH /orgs/{id}/players/{id} (rediger spillerpool) Bygg frontend: Rediger spiller-skjema i tournament-detail.tsx Scratch-verifisere begge fiksene Typesjekket frontend-build Oppdatere .md-filer + be om utrullingsbekreftelse Logikken bak "Ingen datoer satt": dashbord-kortet leser et helt eget felt på selve turneringen (start_date/end_date), atskilt fra øktenes scheduled_at som Program-fanen viser. Ingen UI har noensinne satt det feltet, så det var alltid tomt — uavhengig av at "11. juli" faktisk var satt på øktene. Fikset: kortet henter nå datospennet fra øktene i stedet (11.–12. juli her, siden du har to økter samme dag). Rediger spiller: bygget. Ny "Rediger spiller" i "⋮"-menyen på hver spiller — navn, HCP og kjønn kan endres. Én viktig presisering vist rett i skjemaet: dette endrer spilleren i poolen (gjelder fremtidig rostring), men endrer ikke HCP-tallet som allerede er frosset på et lag i en turnering (det er bevisst, for at et allerede spilt/pågående oppgjør ikke skal endre seg i etterkant). Skal du rette et tall som allerede vises på et lag, må du fortsatt fjerne og legge til spilleren på nytt — si fra om du vil at jeg bygger en direkte "rett dette laget sitt tall her og nå"-vei også. Begge deler er scratch-verifisert (9 automatiserte sjekker, inkludert at frysing-regelen fortsatt holder) og typesjekket bygget. Ingen migrasjon.
2026-07-20 10:01:44 +02:00
email: EmailStr | None = None
birth_date: date | None = None
nickname: str | None = Field(default=None, max_length=100)
country: str | None = Field(default=None, max_length=100)
club: str | None = Field(default=None, max_length=200)
club_member_number: str | None = Field(default=None, max_length=50)
first_name: str | None = Field(default=None, max_length=100)
last_name: str | None = Field(default=None, max_length=100)
paid: bool | None = None
comment: str | None = Field(default=None, max_length=2000)
Update Todos Diagnostisere dashboard-dato-bug (allerede kjent, bekreftet på nytt) Fiks: utled turnering-datospenn fra øktenes scheduled_at i list_tournaments Bygg PATCH /orgs/{id}/players/{id} (rediger spillerpool) Bygg frontend: Rediger spiller-skjema i tournament-detail.tsx Scratch-verifisere begge fiksene Typesjekket frontend-build Oppdatere .md-filer + be om utrullingsbekreftelse Logikken bak "Ingen datoer satt": dashbord-kortet leser et helt eget felt på selve turneringen (start_date/end_date), atskilt fra øktenes scheduled_at som Program-fanen viser. Ingen UI har noensinne satt det feltet, så det var alltid tomt — uavhengig av at "11. juli" faktisk var satt på øktene. Fikset: kortet henter nå datospennet fra øktene i stedet (11.–12. juli her, siden du har to økter samme dag). Rediger spiller: bygget. Ny "Rediger spiller" i "⋮"-menyen på hver spiller — navn, HCP og kjønn kan endres. Én viktig presisering vist rett i skjemaet: dette endrer spilleren i poolen (gjelder fremtidig rostring), men endrer ikke HCP-tallet som allerede er frosset på et lag i en turnering (det er bevisst, for at et allerede spilt/pågående oppgjør ikke skal endre seg i etterkant). Skal du rette et tall som allerede vises på et lag, må du fortsatt fjerne og legge til spilleren på nytt — si fra om du vil at jeg bygger en direkte "rett dette laget sitt tall her og nå"-vei også. Begge deler er scratch-verifisert (9 automatiserte sjekker, inkludert at frysing-regelen fortsatt holder) og typesjekket bygget. Ingen migrasjon.
2026-07-20 10:01:44 +02:00
@router.patch("/orgs/{organization_id}/players/{player_id}", response_model=Player)
async def update_player(
player_id: str,
body: PlayerUpdate,
organization_id: str = Depends(get_authorized_org),
) -> Player:
updates = body.model_dump(exclude_unset=True)
if not updates:
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
if updates.get("email") is not None:
updates["email"] = updates["email"].lower()
set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)]
values = list(updates.values())
values.append(player_id)
async with org_connection(organization_id) as conn, translate_db_errors():
row = await conn.fetchrow(
f"""
UPDATE player SET {", ".join(set_clauses)}
WHERE id = ${len(values)}
RETURNING {_PLAYER_COLUMNS}
""",
*values,
)
if row is None:
raise app_error(404, "NOT_FOUND", "Spilleren finnes ikke.")
return Player(**dict(row))