Legg til Jakter Jesper som visningsmodus for netto-scoring (ADR-106)
Bruker beskrev en turneringsform (start på -spillehandicap, brutto mot
par løpende, lavest sluttsum vinner) og spurte om dette egentlig var en
presentasjonsform fremfor en ny turneringsform. Riktig observasjon:
sluttsummen er matematisk identisk med stroke_net sitt netto-til-par.
Ny tournament.net_display_style ('standard'/'hunt', migrasjon 092), kun
gyldig sammen med scoring_method='stroke_net'. _attach_stroke_play_columns
fikk én stroke_val()-hjelper som bytter net_total (gradvis slagindeks-
basert) med gross_total - playing_handicap (hele handicapet trukket fra
med én gang) når hunt er aktiv -- ingen ny motorfunksjon. Ny, generelt
tilgjengelig tiebreak-verdi lowest_playing_hcp.
5 nye pytest-tester + full regresjon (198/198), tsc rent, full-stack
scratch-verifisert (hånd-utregnede tall matchet eksakt, offentlig
følg-live + utskrift arvet visningen uten kodeendring der).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c8bfe96eb6
commit
7614c4b83a
8 changed files with 584 additions and 10 deletions
36
092_net_hunt_display.sql
Normal file
36
092_net_hunt_display.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-- =====================================================================
|
||||
-- TeeCup — migrasjon 092
|
||||
-- Visningsmodus for netto-scoring ("Jakter Jesper"): start på
|
||||
-- -spillehandicap, brutto-mot-par løpende -- IKKE en ny scoring_method,
|
||||
-- se ADR (neste ledige nummer) for hele resonnementet. Pluss en ny,
|
||||
-- generelt tilgjengelig tiebreak-verdi (lavest spillehandicap).
|
||||
-- =====================================================================
|
||||
-- `net_display_style`: 'standard' (uendret oppførsel -- dagens
|
||||
-- gradvise, slagindeks-baserte netto-visning, default, ingen regresjon
|
||||
-- for eksisterende turneringer) eller 'hunt' (hele spillehandicapet
|
||||
-- trukket fra med én gang, brutto-til-par-differansen vist løpende).
|
||||
-- Kun meningsfullt for scoring_method='stroke_net' -- håndheves i
|
||||
-- app-laget (samme presedens som _SCORING_METHODS_REQUIRING_HANDICAP),
|
||||
-- ikke en tverr-kolonne CHECK her.
|
||||
--
|
||||
-- `lowest_playing_hcp`: ny tiebreak-verdi (utvider migrasjon 085/086
|
||||
-- sitt mønster) -- spilleren med lavest SPILLEHANDICAP (tee-spesifikt
|
||||
-- tall, IKKE WHS-indeksen `lowest_hcp` bruker) i turneringens siste
|
||||
-- runde vinner uavgjortheten. Generelt tilgjengelig for alle
|
||||
-- stroke-play-turneringer, ikke bare hunt-visningen -- tiebreak er
|
||||
-- allerede uavhengig av scoring_method.
|
||||
-- =====================================================================
|
||||
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
ALTER TABLE tournament
|
||||
ADD COLUMN net_display_style text NOT NULL DEFAULT 'standard'
|
||||
CHECK (net_display_style IN ('standard', 'hunt'));
|
||||
|
||||
ALTER TABLE tournament
|
||||
DROP CONSTRAINT tournament_tiebreak_winner_method_check,
|
||||
ADD CONSTRAINT tournament_tiebreak_winner_method_check
|
||||
CHECK (tiebreak_winner_method IN ('none', 'countback', 'lowest_hcp', 'playoff', 'lowest_playing_hcp')),
|
||||
DROP CONSTRAINT tournament_tiebreak_field_method_check,
|
||||
ADD CONSTRAINT tournament_tiebreak_field_method_check
|
||||
CHECK (tiebreak_field_method IN ('none', 'countback', 'lowest_hcp', 'lowest_playing_hcp'));
|
||||
|
|
@ -10616,6 +10616,89 @@ Alle scratch-ressurser (db, rolle, containere, images) ryddet opp.
|
|||
|
||||
---
|
||||
|
||||
## ADR-106: "Jakter Jesper" er en visningsmodus for netto-scoring, ikke en ny turneringsform (migrasjon 092, 2026-08-23)
|
||||
|
||||
**Kontekst:** bruker beskrev en turneringsform: start på -spillehandicap,
|
||||
hullscore måles brutto mot par gjennom runden, lavest sluttsum vinner,
|
||||
tiebreak på lavest spillehandicap ved likt resultat. "Kun interessant
|
||||
dersom man har et leaderboard å forholde seg til."
|
||||
|
||||
**Beslutning A -- IKKE en ny scoring_method, en visningsmodus på
|
||||
eksisterende `stroke_net`.** Sluttsummen er matematisk identisk med
|
||||
`stroke_net` sitt allerede eksisterende netto-til-par
|
||||
(`net_total - par_played`): `-handicap + Σ(brutto-par) = (brutto-
|
||||
handicap)-par = netto-til-par`. Det eneste som faktisk er nytt er
|
||||
hvordan tallet vises LØPENDE midt i runden -- dagens netto "tjener inn"
|
||||
handicap-slag gradvis per slagindeks (`allocate_over_played_holes`,
|
||||
partial allocation basert på hvilke hull som faktisk er spilt), mens
|
||||
Jakter Jesper trekker fra HELE spillehandicapet med én gang og viser
|
||||
brutto-til-par-differansen løpende. Vurdert og avvist som egen
|
||||
scoring_method (ville krevd berøring av ~15 filer -- CHECK-constraint,
|
||||
to Pydantic-regex-steder, dispatch-gren, cut-gate, frontend-labelkart --
|
||||
for null reell ny beregningslogikk).
|
||||
|
||||
**Beslutning B -- ny kolonne `tournament.net_display_style`
|
||||
('standard'/'hunt', default 'standard'), KUN gyldig sammen med
|
||||
scoring_method='stroke_net'** (fail-loudly 400 begge veier: kan ikke
|
||||
settes til 'hunt' på en annen metode, OG kan ikke bytte bort fra
|
||||
stroke_net mens 'hunt' står -- se `update_tournament` i
|
||||
`app/routers/tournaments.py`). `_attach_stroke_play_columns`
|
||||
(`individual_tournaments.py`) fikk en `stroke_val(row)`-hjelper som
|
||||
bytter ut `row["net_total"]` med `row["gross_total"] -
|
||||
row["playing_handicap"]` når hunt er aktiv -- ett sted, ingen duplisert
|
||||
beregningslogikk. `playing_handicap` hentes nå per (runde, deltaker) i
|
||||
samme spørring (ny JOIN mot `tournament_round_participant`) siden den
|
||||
kan variere per runde/tee i en flerrunde-turnering.
|
||||
|
||||
**Beslutning C -- ny tiebreak-verdi `lowest_playing_hcp`, GENERELT
|
||||
tilgjengelig (ikke bundet til hunt-visningen).** Bruker presiserte
|
||||
"spillehandicap" (det tee-spesifikke tallet som faktisk ga
|
||||
startverdien), ikke WHS-indeksen den eksisterende `lowest_hcp`-
|
||||
tiebreaken bruker (`tournament_participant.handicap_index_snapshot`).
|
||||
Ny `_lowest_playing_hcp_sort_keys` leser
|
||||
`tournament_round_participant.playing_handicap` for turneringens
|
||||
ABSOLUTT SISTE runde (samme presedens som `_countback_sort_keys`).
|
||||
Tiebreak forblir organisator-valgfritt som i dag (bruker bekreftet
|
||||
eksplisitt: IKKE tvunget på for formatet, samme fleksible to-
|
||||
uavhengige-innstillinger-mønster som `countback`/`lowest_hcp`/
|
||||
`playoff`).
|
||||
|
||||
**Beslutning D -- omfang: kun org-individuelle turneringer denne
|
||||
runden.** Bruker bekreftet sekvensiell bygging -- frittstående runder
|
||||
(`round.play_format`) og en kolonne på lagturnering-øktens individuelle
|
||||
leaderboard (`fetch_individual_leaderboard`) er bevisst UTENFOR,
|
||||
egne senere oppfølgingsrunder. Org-LAG-turneringer (Ryder Cup-
|
||||
modellen) har strukturelt ikke noe `scoring_method`-begrep i det hele
|
||||
tatt (ADR-011) -- kan aldri hoste denne visningsmodusen direkte, kun
|
||||
potensielt som en tilleggskolonne på den separate økt-leaderboard-
|
||||
mekanismen.
|
||||
|
||||
**Status:** 5 nye pytest-tester (løpende tall skiller seg fra standard
|
||||
midt i runden men konvergerer nøyaktig ved 18 fullførte hull -- selve
|
||||
beviset på ekvivalensen; validering begge veier; `lowest_playing_hcp`
|
||||
løser en reell uavgjort; manglende spillehandicap sorteres sist) + full
|
||||
regresjonssuite, 198/198 grønt. `tsc --noEmit` rent.
|
||||
|
||||
Full-stack scratch-verifisert (egen db + to one-off-containere, ekte
|
||||
innlogging inkl. et ekte 2FA-e-post-oppsett for org-eier-tilgang): en
|
||||
stroke_net-turnering med to spillere (spillehandicap 12/4), hunt PÅ --
|
||||
leaderboardet viste -12/-2 (hånd-utregnet på forhånd, matchet eksakt).
|
||||
Slått AV live -- tallene endret seg umiddelbart til standard-visningens
|
||||
-5/-2 (samme underliggende data, kun presentasjonen endret), ingen
|
||||
sideoppfriskning nødvendig. Bekreftet at BÅDE den offentlige "Følg
|
||||
live"-siden OG utskrifts-resultatlisten viste hunt-tallene korrekt UTEN
|
||||
noen kodeendring der -- beviser at de kun konsumerer `total_label`/
|
||||
`position` som opake strenger, akkurat som antatt i planen. Mørk modus
|
||||
bekreftet lesbart. Alle scratch-ressurser ryddet opp.
|
||||
|
||||
**Ikke rullet ut ennå** -- venter på brukerens bekreftelse før migrasjon
|
||||
092 kjøres mot ekte `teecup_db` (additiv, ingen eksisterende data
|
||||
berøres -- default 'standard'/uendret tiebreak-oppførsel for alle
|
||||
eksisterende turneringer, men skal likevel bekreftes eksplisitt per
|
||||
standard sikkerhetsregel).
|
||||
|
||||
---
|
||||
|
||||
## Utviklingsplan (rekkefølge)
|
||||
|
||||
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
||||
|
|
|
|||
38
CHANGELOG.md
38
CHANGELOG.md
|
|
@ -14703,3 +14703,41 @@ Neste steg:
|
|||
også `teecup_api` som vanlig bivirkning, ingen backend-kode rørt).
|
||||
Begge containere friske, rene logger. `https://teecup.golf/health`
|
||||
200 etter utrulling.
|
||||
|
||||
166. **"Jakter Jesper" bygget som visningsmodus for netto-scoring, ikke
|
||||
en ny turneringsform (ADR-106), 2026-08-23.** Bruker beskrev en
|
||||
turneringsform (start på -spillehandicap, brutto mot par løpende,
|
||||
lavest sluttsum vinner, tiebreak på lavest spillehandicap) og spurte
|
||||
om det egentlig var en ny presentasjonsform fremfor en ny
|
||||
turneringsform -- riktig observasjon: sluttsummen er matematisk
|
||||
identisk med `stroke_net` sitt netto-til-par.
|
||||
|
||||
**Ny kolonne `tournament.net_display_style`** ('standard'/'hunt',
|
||||
migrasjon 092), kun gyldig sammen med `scoring_method='stroke_net'`
|
||||
(fail-loudly 400 begge veier). `_attach_stroke_play_columns` fikk én
|
||||
`stroke_val()`-hjelper som bytter `net_total` (gradvis slagindeks-
|
||||
basert) med `gross_total - playing_handicap` (hele handicapet trukket
|
||||
fra med én gang) når hunt er aktiv -- ingen ny motorfunksjon, ingen
|
||||
duplisert beregning. Ny, GENERELT tilgjengelig tiebreak-verdi
|
||||
`lowest_playing_hcp` (spillehandicap, ikke WHS-indeksen `lowest_hcp`
|
||||
bruker) -- organisator-valgfri som de andre, ikke tvunget på for
|
||||
formatet (brukerbekreftet). `LeaderboardRow`-kontrakten (ADR-101)
|
||||
RØRT IKKE -- alle felt var allerede opake, ferdig-formaterte strenger.
|
||||
|
||||
**Verifisert:** 5 nye pytest-tester (løpende tall skiller seg fra
|
||||
standard midt i runden, konvergerer eksakt ved 18 fullførte hull;
|
||||
validering begge veier; `lowest_playing_hcp` løser en reell
|
||||
uavgjort; manglende spillehandicap sorteres sist) + full
|
||||
regresjonssuite, 198/198 grønt. `tsc --noEmit` rent. Full-stack
|
||||
scratch (egen db + to one-off-containere, ekte innlogging inkl. et
|
||||
ekte 2FA-e-post-oppsett): stroke_net-turnering med to spillere
|
||||
(spillehandicap 12/4), hunt PÅ -- leaderboardet viste -12/-2,
|
||||
hånd-utregnet på forhånd og matchet eksakt. Slått av live -- tallene
|
||||
endret seg umiddelbart til standard-visningens -5/-2 uten
|
||||
sideoppfriskning. Offentlig "Følg live"-side OG utskrifts-
|
||||
resultatlisten viste hunt-tallene korrekt UTEN kodeendring der,
|
||||
bekrefter at de kun konsumerer opake strenger. Mørk modus bekreftet
|
||||
lesbart. Alle scratch-ressurser ryddet opp.
|
||||
|
||||
**Ikke rullet ut ennå** -- venter på brukerens bekreftelse før
|
||||
migrasjon 092 kjøres mot ekte `teecup_db`.
|
||||
|
|
|
|||
|
|
@ -1785,7 +1785,7 @@ async def _compute_individual_standings(
|
|||
hver entry (`gross_total`/`net_total`/`stableford_total`/`position`)
|
||||
som faktisk er meningsfullt å lese videre."""
|
||||
tournament = await conn.fetchrow(
|
||||
"SELECT format_type, scoring_method, tiebreak_winner_method, tiebreak_field_method "
|
||||
"SELECT format_type, scoring_method, tiebreak_winner_method, tiebreak_field_method, net_display_style "
|
||||
"FROM tournament WHERE id = $1",
|
||||
tournament_id,
|
||||
)
|
||||
|
|
@ -1842,6 +1842,7 @@ async def _compute_individual_standings(
|
|||
max_sequence=max_sequence,
|
||||
tiebreak_winner_method=tournament["tiebreak_winner_method"],
|
||||
tiebreak_field_method=tournament["tiebreak_field_method"],
|
||||
net_display_style=tournament["net_display_style"],
|
||||
)
|
||||
# Cut (2026-08-18): kuttede spillere vises samlet BAKERST, som en
|
||||
# egen seksjon -- ALDRI innimellom fortsatt-med-spillere basert på
|
||||
|
|
@ -2182,6 +2183,31 @@ async def _lowest_hcp_sort_keys(conn, participant_ids: list[str]) -> dict[str, t
|
|||
return {r["id"]: (r["hcp"] if r["hcp"] is not None else float("inf"),) for r in rows}
|
||||
|
||||
|
||||
async def _lowest_playing_hcp_sort_keys(conn, tournament_id: str, participant_ids: list[str]) -> dict[str, tuple]:
|
||||
"""Tiebreak 'lowest_playing_hcp' (migrasjon 092) -- laveste
|
||||
SPILLEHANDICAP (tee-spesifikt tall, IKKE WHS-indeksen `lowest_hcp`
|
||||
bruker) vinner. Leser turneringens ABSOLUTT SISTE runde (samme
|
||||
presedens som `_countback_sort_keys` -- en reell uavgjort forutsetter
|
||||
normalt likt antall runder spilt). Manglende verdi (runde ikke spilt
|
||||
av denne deltakeren, eller handicap ikke beregnet ennå) sorteres
|
||||
SIST, samme mønster som `_lowest_hcp_sort_keys`."""
|
||||
last_round = await conn.fetchrow(
|
||||
"SELECT id FROM tournament_round WHERE tournament_id = $1 ORDER BY sequence DESC LIMIT 1",
|
||||
tournament_id,
|
||||
)
|
||||
if last_round is None:
|
||||
return {pid: (float("inf"),) for pid in participant_ids}
|
||||
rows = await conn.fetch(
|
||||
"SELECT tournament_participant_id::text AS id, playing_handicap::float AS hcp "
|
||||
"FROM tournament_round_participant "
|
||||
"WHERE tournament_round_id = $1 AND tournament_participant_id = ANY($2::uuid[])",
|
||||
last_round["id"],
|
||||
participant_ids,
|
||||
)
|
||||
hcp_by_id = {r["id"]: r["hcp"] for r in rows}
|
||||
return {pid: (hcp_by_id.get(pid) if hcp_by_id.get(pid) is not None else float("inf"),) for pid in participant_ids}
|
||||
|
||||
|
||||
async def _countback_sort_keys(conn, tournament_id: str, method: str, participant_ids: list[str]) -> dict[str, tuple]:
|
||||
"""Tiebreak 'countback' (migrasjon 085) -- kaskaderende "matching
|
||||
cards": siste 9 -> siste 6 -> siste 3 -> siste hull i turneringens
|
||||
|
|
@ -2292,6 +2318,7 @@ async def _attach_stroke_play_columns(
|
|||
max_sequence: int | None = None,
|
||||
tiebreak_winner_method: str = "none",
|
||||
tiebreak_field_method: str = "none",
|
||||
net_display_style: str = "standard",
|
||||
) -> None:
|
||||
"""Augusta-stil resultattavle (2026-08-04, stroke-play-leaderboard.tsx):
|
||||
POS/TODAY/THRU/TOTAL/R1-Rn. Muterer `entries` (allerede riktig sortert av
|
||||
|
|
@ -2312,7 +2339,21 @@ async def _attach_stroke_play_columns(
|
|||
aldri max_sequence, så tiebreak gjelder der -- inkludert midt i
|
||||
turneringen, ikke bare ved siste runde ferdigspilt (samme "vis det
|
||||
beste vi vet akkurat nå"-filosofi som resten av leaderboardet).
|
||||
|
||||
`net_display_style` (migrasjon 092, 2026-08-23) -- 'hunt' er IKKE en
|
||||
ny scoring-motor, kun en annen presentasjon av samme underliggende
|
||||
data for `stroke_net`-turneringer ("Jakter Jesper", se ADR): i
|
||||
stedet for å "tjene inn" handicap-slag gradvis per slagindeks
|
||||
(dagens `net_total`, partial allocation via
|
||||
`allocate_over_played_holes`), trekkes HELE spillehandicapet fra med
|
||||
én gang og brutto-til-par-differansen vises løpende -- dramatisk
|
||||
forskjellig tall MIDT i en runde, men konvergerer til NØYAKTIG samme
|
||||
sluttall som standard-visningen ved 18 fullførte hull (siden summen
|
||||
av per-hull-tildelte slag over alle 18 hull uansett er lik hele
|
||||
spillehandicapet). Kun virksom når `method == "stroke_net"` --
|
||||
stableford/gross har ingen netto-til-par-visning å endre på.
|
||||
"""
|
||||
hunt_active = net_display_style == "hunt" and method == "stroke_net"
|
||||
rounds_meta = await conn.fetch(
|
||||
"""
|
||||
SELECT sequence, hole_config::text AS hole_config, scheduled_at::text AS scheduled_at, start_hole
|
||||
|
|
@ -2338,9 +2379,12 @@ async def _attach_stroke_play_columns(
|
|||
SELECT tr.sequence, tr.id::text AS tournament_round_id,
|
||||
trs.tournament_participant_id::text AS tournament_participant_id,
|
||||
trs.holes_played, trs.gross_total, trs.net_total, trs.stableford_points,
|
||||
trp.playing_handicap,
|
||||
COALESCE(pp.par_played, 0)::int AS par_played
|
||||
FROM tournament_round tr
|
||||
JOIN tournament_round_score trs ON trs.tournament_round_id = tr.id
|
||||
LEFT JOIN tournament_round_participant trp
|
||||
ON trp.tournament_round_id = tr.id AND trp.tournament_participant_id = trs.tournament_participant_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT SUM(h.par) AS par_played
|
||||
FROM tournament_round_participant trp
|
||||
|
|
@ -2373,6 +2417,18 @@ async def _attach_stroke_play_columns(
|
|||
"net_total" if method == "stroke_net" else "gross_total"
|
||||
)
|
||||
|
||||
def stroke_val(row) -> int | None:
|
||||
"""Netto/brutto-tallet for én runde-rad -- `hunt_active` bytter
|
||||
ut den gradvise `net_total` med "trekk fra HELE spillehandicapet
|
||||
med én gang" (se `net_display_style`-docstringen over). `None`
|
||||
hvis spillehandicap ikke er beregnet ennå for raden (samme
|
||||
nøytrale "ingen data"-håndtering som `value_col` allerede har)."""
|
||||
if not hunt_active:
|
||||
return row[value_col]
|
||||
if row["playing_handicap"] is None or row["gross_total"] is None:
|
||||
return None
|
||||
return row["gross_total"] - row["playing_handicap"]
|
||||
|
||||
# Rangeringsnøkkel PER DELTAKER, til-par-normalisert (lavere = bedre,
|
||||
# uniformt for alle tre metodene -- stableford negeres siden flere poeng
|
||||
# er bedre der). Kan IKKE bruke rå gross_total/stableford_total: i en
|
||||
|
|
@ -2387,6 +2443,7 @@ async def _attach_stroke_play_columns(
|
|||
rounds_out: list[RoundCellOut] = []
|
||||
total_par_played = 0
|
||||
total_holes_played = 0
|
||||
total_stroke_val = 0
|
||||
for seq, row in sorted(pmap.items()):
|
||||
if row["holes_played"] == 0:
|
||||
continue
|
||||
|
|
@ -2404,9 +2461,10 @@ async def _attach_stroke_play_columns(
|
|||
)
|
||||
)
|
||||
else:
|
||||
val = row[value_col]
|
||||
val = stroke_val(row)
|
||||
if val is None:
|
||||
continue
|
||||
total_stroke_val += val
|
||||
rounds_out.append(
|
||||
RoundCellOut(
|
||||
round_number=seq,
|
||||
|
|
@ -2425,7 +2483,7 @@ async def _attach_stroke_play_columns(
|
|||
if method == "stableford":
|
||||
e.today_label = f"{today_row['stableford_points'] or 0} p"
|
||||
else:
|
||||
val = today_row[value_col]
|
||||
val = stroke_val(today_row)
|
||||
e.today_label = _to_par_label(val - today_row["par_played"]) if val is not None else None
|
||||
else:
|
||||
e.thru_label = "-"
|
||||
|
|
@ -2449,6 +2507,12 @@ async def _attach_stroke_play_columns(
|
|||
# runde spilt ennå (total_holes_played == 0) -> total_label forblir
|
||||
# None (nøytral -- IKKE "E"/"0 p", som ville sett ut som en ekte,
|
||||
# jevn/poengløs score i stedet for "ikke startet").
|
||||
#
|
||||
# `hunt_active`: TOTAL kan IKKE gjenbruke `e.net_total` direkte --
|
||||
# `playing_handicap` kan variere per runde (ulik tee), så
|
||||
# hunt-summen må bygges runde for runde (`total_stroke_val`,
|
||||
# akkumulert i løkken over) i stedet for kalleren sin ferdig-
|
||||
# summerte `net_total`-kolonne.
|
||||
started_any = total_holes_played > 0
|
||||
if method == "stableford":
|
||||
e.total_label = f"{e.stableford_total or 0} p" if started_any else None
|
||||
|
|
@ -2457,7 +2521,7 @@ async def _attach_stroke_play_columns(
|
|||
else -(e.stableford_total - 2 * total_holes_played)
|
||||
)
|
||||
else:
|
||||
total_val = e.net_total if method == "stroke_net" else e.gross_total
|
||||
total_val = total_stroke_val if hunt_active else (e.net_total if method == "stroke_net" else e.gross_total)
|
||||
e.total_label = (
|
||||
_to_par_label(total_val - total_par_played) if (total_val is not None and started_any) else None
|
||||
)
|
||||
|
|
@ -2497,10 +2561,12 @@ async def _attach_stroke_play_columns(
|
|||
# gruppe faller gjennom til else-grenen under akkurat som 'none':
|
||||
# delt plassering, ingen automatisk oppløsning. Forskjellen er
|
||||
# ren informasjon (tiebreak_playoff_rule), ikke rangeringslogikk.
|
||||
if tiebreak_method in ("countback", "lowest_hcp"):
|
||||
if tiebreak_method in ("countback", "lowest_hcp", "lowest_playing_hcp"):
|
||||
group_ids = [entries[k].tournament_participant_id for k in range(i, j)]
|
||||
if tiebreak_method == "countback":
|
||||
sort_keys = await _countback_sort_keys(conn, tournament_id, method, group_ids)
|
||||
elif tiebreak_method == "lowest_playing_hcp":
|
||||
sort_keys = await _lowest_playing_hcp_sort_keys(conn, tournament_id, group_ids)
|
||||
else:
|
||||
sort_keys = await _lowest_hcp_sort_keys(conn, group_ids)
|
||||
_assign_ranks_within_group(entries, list(range(i, j)), sort_keys, base_rank=rank)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ _TOURNAMENT_COLUMNS = """
|
|||
registration_overflow_policy, registration_requires_approval, hero_image_key,
|
||||
join_code, format_type, scoring_method, bbb_sweep_bonus_enabled, flag_map_visible,
|
||||
cut_after_round, cut_size, cut_applied_at, tiebreak_winner_method, tiebreak_field_method,
|
||||
tiebreak_playoff_rule
|
||||
tiebreak_playoff_rule, net_display_style
|
||||
"""
|
||||
|
||||
# Uten 0/O/1/I -- lett å lese/skrive/si høyt (ADR-020 Beslutning A).
|
||||
|
|
@ -118,6 +118,14 @@ class Tournament(BaseModel):
|
|||
# setter (f.eks. "Sudden death fra hull 18") -- vist proaktivt i
|
||||
# Oppsett OG på leaderboardet, ikke bare når en uavgjort faktisk skjer.
|
||||
tiebreak_playoff_rule: str | None
|
||||
# Visningsmodus for netto-scoring (migrasjon 092, 2026-08-23) -- "Jakter
|
||||
# Jesper": IKKE en ny scoring_method (sluttallet er matematisk identisk
|
||||
# med stroke_net sitt netto-til-par), kun en annen presentasjon av
|
||||
# LØPENDE tall midt i runden -- se individual_tournaments.py sin
|
||||
# _attach_stroke_play_columns. KUN gyldig sammen med
|
||||
# scoring_method='stroke_net', håndhevet i update_tournament/
|
||||
# create_tournament under.
|
||||
net_display_style: str
|
||||
|
||||
|
||||
def _tournament_from_row(row) -> Tournament:
|
||||
|
|
@ -151,7 +159,8 @@ async def list_tournaments(
|
|||
t.registration_requires_approval, t.hero_image_key,
|
||||
t.join_code, t.format_type, t.scoring_method, t.bbb_sweep_bonus_enabled,
|
||||
t.flag_map_visible, t.cut_after_round, t.cut_size, t.cut_applied_at,
|
||||
t.tiebreak_winner_method, t.tiebreak_field_method, t.tiebreak_playoff_rule
|
||||
t.tiebreak_winner_method, t.tiebreak_field_method, t.tiebreak_playoff_rule,
|
||||
t.net_display_style
|
||||
FROM tournament t
|
||||
LEFT JOIN (
|
||||
SELECT tournament_id,
|
||||
|
|
@ -247,9 +256,16 @@ class TournamentUpdate(BaseModel):
|
|||
# Tiebreak (migrasjon 085, 2026-08-18) -- fritt valgbar av organisator,
|
||||
# se Tournament sin kommentar. 'playoff' (migrasjon 086) er KUN gyldig
|
||||
# for vinner-metoden, ikke felt-metoden.
|
||||
tiebreak_winner_method: str | None = Field(default=None, pattern="^(none|countback|lowest_hcp|playoff)$")
|
||||
tiebreak_field_method: str | None = Field(default=None, pattern="^(none|countback|lowest_hcp)$")
|
||||
tiebreak_winner_method: str | None = Field(
|
||||
default=None, pattern="^(none|countback|lowest_hcp|playoff|lowest_playing_hcp)$"
|
||||
)
|
||||
tiebreak_field_method: str | None = Field(default=None, pattern="^(none|countback|lowest_hcp|lowest_playing_hcp)$")
|
||||
tiebreak_playoff_rule: str | None = Field(default=None, max_length=2000)
|
||||
# Visningsmodus for netto-scoring (migrasjon 092, 2026-08-23) -- se
|
||||
# Tournament sin kommentar. Gyldigheten mot scoring_method='stroke_net'
|
||||
# håndheves i update_tournament under (ikke her -- denne modellen kjenner
|
||||
# ikke turneringens gjeldende scoring_method).
|
||||
net_display_style: str | None = Field(default=None, pattern="^(standard|hunt)$")
|
||||
# hero_image_key er BEVISST IKKE med her -- den settes kun via
|
||||
# upload_hero_image under, aldri som en fritekst-streng klienten sender,
|
||||
# siden den nå alltid kommer fra en ekte, server-konvertert opplasting.
|
||||
|
|
@ -282,6 +298,25 @@ async def update_tournament(
|
|||
values.append(tournament_id)
|
||||
|
||||
async with org_connection(organization_id) as conn, translate_db_errors():
|
||||
# Fail-loudly (migrasjon 092): 'hunt' gir ingen mening utenfor
|
||||
# stroke_net -- de andre metodene har ingen netto-til-par-visning å
|
||||
# endre presentasjonen av. Sjekker den EFFEKTIVE tilstanden ETTER
|
||||
# denne PATCH-en (kan selv sette scoring_method/net_display_style i
|
||||
# samme kall) -- dekker BEGGE retninger: (a) satt til 'hunt' på en
|
||||
# ikke-stroke_net-turnering, (b) scoring_method byttet bort fra
|
||||
# stroke_net mens 'hunt' henger igjen fra før, uendret i denne
|
||||
# PATCH-en. Unngår en stille, meningsløs kombinasjon i databasen.
|
||||
if "scoring_method" in updates or "net_display_style" in updates:
|
||||
current = await conn.fetchrow(
|
||||
"SELECT scoring_method, net_display_style FROM tournament WHERE id = $1", tournament_id
|
||||
)
|
||||
effective_method = updates.get("scoring_method", current["scoring_method"] if current else None)
|
||||
effective_style = updates.get("net_display_style", current["net_display_style"] if current else "standard")
|
||||
if effective_style == "hunt" and effective_method != "stroke_net":
|
||||
raise app_error(
|
||||
400, "VALIDATION_FAILED",
|
||||
"Jakter Jesper-visningen krever scoring_method='stroke_net'.",
|
||||
)
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
UPDATE tournament SET {', '.join(set_clauses)}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
Plus,
|
||||
RefreshCw,
|
||||
Settings2,
|
||||
Target,
|
||||
Trash2,
|
||||
Trophy,
|
||||
Upload,
|
||||
|
|
@ -153,6 +154,10 @@ type ApiTournamentInfo = {
|
|||
// tiebreak_winner_method. Fritekst-regel vist proaktivt i Oppsett OG
|
||||
// på leaderboardet, ikke bare når en uavgjort faktisk skjer.
|
||||
tiebreak_playoff_rule: string | null
|
||||
// Visningsmodus for netto-scoring (migrasjon 092, 2026-08-23) -- "Jakter
|
||||
// Jesper". IKKE en ny scoring_method -- kun gyldig sammen med
|
||||
// scoring_method="stroke_net". "standard" (default) = uendret oppførsel.
|
||||
net_display_style: string
|
||||
}
|
||||
|
||||
type ApiParticipant = {
|
||||
|
|
@ -659,6 +664,29 @@ export function IndividualTournamentDetail({
|
|||
}
|
||||
}
|
||||
|
||||
// Visningsmodus for netto-scoring (migrasjon 092, 2026-08-23) -- "Jakter
|
||||
// Jesper". Samme optimistiske PATCH-mønster som tiebreak/cut over. Ren
|
||||
// presentasjon -- endrer ALDRI selve rangeringen, kun hvilket tall som
|
||||
// vises løpende (se individual_tournaments.py sin
|
||||
// _attach_stroke_play_columns).
|
||||
async function updateNetDisplayStyle(style: string) {
|
||||
if (!tournament) return
|
||||
const previous = tournament
|
||||
setTournament({ ...tournament, net_display_style: style })
|
||||
try {
|
||||
const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ net_display_style: style }),
|
||||
})
|
||||
if (!res.ok) throw new Error()
|
||||
} catch {
|
||||
setTournament(previous)
|
||||
setError("Klarte ikke å lagre visningsmodus. Prøv igjen.")
|
||||
}
|
||||
}
|
||||
|
||||
// Turneringsinnstillinger (2026-08-18, TournamentSettingsCard --
|
||||
// V0-eksportert, se ADR/CHANGELOG #118). Komponenten viser sin EGEN
|
||||
// lagre-/feil-tilstand rundt onSave-løftet -- IKKE fang feilen her
|
||||
|
|
@ -1093,6 +1121,7 @@ export function IndividualTournamentDetail({
|
|||
onUpdateFlagMapVisible={updateFlagMapVisible}
|
||||
onUpdateCutConfig={updateCutConfig}
|
||||
onUpdateTiebreakConfig={updateTiebreakConfig}
|
||||
onUpdateNetDisplayStyle={updateNetDisplayStyle}
|
||||
onUpdateSettings={updateSettings}
|
||||
onApplyCut={applyCut}
|
||||
onAddParticipant={addParticipant}
|
||||
|
|
@ -1216,6 +1245,7 @@ function SetupTab({
|
|||
onUpdateCutConfig,
|
||||
onApplyCut,
|
||||
onUpdateTiebreakConfig,
|
||||
onUpdateNetDisplayStyle,
|
||||
onUpdateSettings,
|
||||
onAddParticipant,
|
||||
onAddNewPlayer,
|
||||
|
|
@ -1246,6 +1276,7 @@ function SetupTab({
|
|||
onUpdateCutConfig: (afterRound: number | null, size: number | null) => Promise<void>
|
||||
onApplyCut: () => Promise<void>
|
||||
onUpdateTiebreakConfig: (winner: string, field: string, playoffRule: string | null) => Promise<void>
|
||||
onUpdateNetDisplayStyle: (style: string) => Promise<void>
|
||||
onUpdateSettings: (next: TournamentSettingsValues) => Promise<void>
|
||||
onAddParticipant: (playerId: string) => Promise<void>
|
||||
onAddNewPlayer: (input: NewPlayerInput) => Promise<void>
|
||||
|
|
@ -1778,6 +1809,9 @@ function SetupTab({
|
|||
<TiebreakCard tournament={tournament} onUpdateConfig={onUpdateTiebreakConfig} />
|
||||
</>
|
||||
)}
|
||||
{tournament.scoring_method === "stroke_net" && (
|
||||
<NetDisplayStyleCard tournament={tournament} onUpdateConfig={onUpdateNetDisplayStyle} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TournamentSetupNav>
|
||||
|
|
@ -1905,6 +1939,11 @@ const TIEBREAK_OPTIONS_WINNER: { value: string; label: string; hint: string }[]
|
|||
{ value: "none", label: "Delt plassering", hint: "Ingen automatisk avgjørelse -- vises som «T1»." },
|
||||
{ value: "countback", label: "Countback", hint: "Beste siste 9 -> 6 -> 3 -> siste hull i siste runde" },
|
||||
{ value: "lowest_hcp", label: "Laveste HCP", hint: "Spilleren med lavest handicap-indeks vinner uavgjortheten" },
|
||||
{
|
||||
value: "lowest_playing_hcp",
|
||||
label: "Lavest spillehandicap",
|
||||
hint: "Spilleren med lavest spillehandicap i siste runde vinner uavgjortheten",
|
||||
},
|
||||
{ value: "playoff", label: "Omspill", hint: "Avgjøres på banen -- beskriv formatet under, vises til alle før turneringen" },
|
||||
]
|
||||
|
||||
|
|
@ -1912,6 +1951,11 @@ const TIEBREAK_OPTIONS_FIELD: { value: string; label: string; hint: string }[] =
|
|||
{ value: "none", label: "Delt plassering", hint: "Ingen automatisk avgjørelse -- vises som «T5» osv." },
|
||||
{ value: "countback", label: "Countback", hint: "Beste siste 9 -> 6 -> 3 -> siste hull i siste runde" },
|
||||
{ value: "lowest_hcp", label: "Laveste HCP", hint: "Spilleren med lavest handicap-indeks vinner uavgjortheten" },
|
||||
{
|
||||
value: "lowest_playing_hcp",
|
||||
label: "Lavest spillehandicap",
|
||||
hint: "Spilleren med lavest spillehandicap i siste runde vinner uavgjortheten",
|
||||
},
|
||||
]
|
||||
|
||||
function TiebreakCard({
|
||||
|
|
@ -2010,6 +2054,43 @@ function TiebreakCard({
|
|||
)
|
||||
}
|
||||
|
||||
// Visningsmodus for netto-scoring ("Jakter Jesper", migrasjon 092,
|
||||
// 2026-08-23) -- IKKE en ny scoring_method, kun en annen presentasjon av
|
||||
// LØPENDE tall for stroke_net-turneringer (se ADR/CHANGELOG). Egen,
|
||||
// enkel bryter-card, samme mønster som CutCard/TiebreakCard rett over.
|
||||
function NetDisplayStyleCard({
|
||||
tournament,
|
||||
onUpdateConfig,
|
||||
}: {
|
||||
tournament: ApiTournamentInfo
|
||||
onUpdateConfig: (style: string) => Promise<void>
|
||||
}) {
|
||||
return (
|
||||
<section className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8">
|
||||
<h2 className="text-base font-bold text-foreground">Visningsmodus for netto-score</h2>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Sluttresultatet blir identisk uansett -- dette endrer kun hvordan tallet vises LØPENDE
|
||||
gjennom runden.
|
||||
</p>
|
||||
<label className="flex min-h-11 items-start gap-3 rounded-xl border border-border bg-background px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tournament.net_display_style === "hunt"}
|
||||
onChange={(e) => void onUpdateConfig(e.target.checked ? "hunt" : "standard")}
|
||||
className="mt-1 size-5 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-bold text-foreground">Vis som Jakter Jesper</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Alle starter på minus spillehandicapet sitt, og hullscore vises brutto mot par
|
||||
løpende -- i stedet for at slag "tjenes inn" gradvis gjennom runden. Av som standard.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// Konkurranseklasser (2026-08-03) -- se tournament-detail.tsx sin
|
||||
// ClassesCard for lagturnering-varianten (samme mønster, men uten
|
||||
// leaderboard-koblingen -- her deler klasse OGSÅ opp resultatlisten).
|
||||
|
|
@ -5491,6 +5572,12 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou
|
|||
? tournament.tiebreak_playoff_rule
|
||||
: null
|
||||
|
||||
// Jakter Jesper-varsel (migrasjon 092, 2026-08-23) -- ren presentasjon,
|
||||
// samme "informer proaktivt over selve tavlen"-mønster som playoffNote
|
||||
// over. Forklarer HVORFOR tallene starter dypt i minus, for spillere som
|
||||
// ikke kjenner formatet fra før.
|
||||
const huntModeNote = isStrokePlay && tournament.scoring_method === "stroke_net" && tournament.net_display_style === "hunt"
|
||||
|
||||
// base = /orgs/{organizationId}/tournaments/{tournamentId} -- utledet i
|
||||
// stedet for å utvide LeaderboardTab sitt propsett kun for utskrifts-lenken.
|
||||
const [, , printOrgId, , printTournamentId] = base.split("/")
|
||||
|
|
@ -5513,6 +5600,14 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou
|
|||
</p>
|
||||
</div>
|
||||
)}
|
||||
{huntModeNote && (
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-primary/30 bg-primary/10 px-4 py-3">
|
||||
<Target aria-hidden="true" className="mt-0.5 size-5 shrink-0 text-primary" />
|
||||
<p className="text-sm font-semibold leading-relaxed text-foreground text-pretty">
|
||||
Jakter Jesper: alle starter på minus spillehandicapet sitt -- lavest sluttsum vinner.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<div key={group.key} className="flex flex-col gap-2">
|
||||
{showSectionHeaders && (
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
221
tests/test_net_hunt_display.py
Normal file
221
tests/test_net_hunt_display.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
"""
|
||||
Visningsmodus for netto-scoring ("Jakter Jesper", migrasjon 092,
|
||||
2026-08-23). IKKE en ny scoring_method -- sluttallet er matematisk
|
||||
identisk med stroke_net sitt netto-til-par (`net_total - par_played`).
|
||||
Det eneste som faktisk er nytt er den LØPENDE visningen: standard
|
||||
"tjener inn" handicap-slag gradvis per slagindeks etter hvert som hull
|
||||
spilles, mens 'hunt' trekker fra HELE spillehandicapet med én gang og
|
||||
viser brutto-til-par-differansen løpende. Se
|
||||
`_attach_stroke_play_columns` i individual_tournaments.py.
|
||||
"""
|
||||
|
||||
from fastapi import HTTPException
|
||||
import pytest
|
||||
|
||||
from app.routers.individual_tournaments import (
|
||||
HoleUpdate as TournamentHoleUpdate,
|
||||
individual_leaderboard,
|
||||
update_hole as tournament_update_hole,
|
||||
)
|
||||
from app.routers.tournaments import TournamentUpdate, update_tournament
|
||||
from app.auth import CurrentUser
|
||||
|
||||
from tests.conftest import (
|
||||
add_membership,
|
||||
create_org,
|
||||
create_org_hole,
|
||||
create_course,
|
||||
create_player,
|
||||
create_tee,
|
||||
create_tournament,
|
||||
create_tournament_participant,
|
||||
create_tournament_round,
|
||||
create_tournament_round_participant,
|
||||
create_user,
|
||||
)
|
||||
|
||||
import app.db as app_db
|
||||
|
||||
|
||||
async def _score_holes(tournament_id, round_id, rp_id, org_id, user_id, scores: list[int]) -> None:
|
||||
user = CurrentUser(user_id=user_id)
|
||||
for n, gross in enumerate(scores, start=1):
|
||||
await tournament_update_hole(
|
||||
tournament_id, round_id, rp_id, n,
|
||||
TournamentHoleUpdate(gross_strokes=gross, expected_version=None),
|
||||
organization_id=org_id, user=user,
|
||||
)
|
||||
|
||||
|
||||
async def _setup_stroke_net_round(org_id, method="stroke_net"):
|
||||
tournament_id = await create_tournament(org_id, name="Jakter Jesper-turnering")
|
||||
course_id = await create_course(org_id, name="Hunt Links")
|
||||
for n in range(1, 19):
|
||||
await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n)
|
||||
tee_id = await create_tee(org_id, course_id)
|
||||
async with app_db.org_connection(org_id) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE tournament SET format_type = 'individual', scoring_method = $2 WHERE id = $1",
|
||||
tournament_id, method,
|
||||
)
|
||||
round_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
||||
return tournament_id, course_id, tee_id, round_id
|
||||
|
||||
|
||||
async def _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, name, playing_handicap):
|
||||
player_id = await create_player(org_id, display_name=name)
|
||||
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
|
||||
rp_id = await create_tournament_round_participant(org_id, round_id, tp_id, tee_id)
|
||||
async with app_db.org_connection(org_id) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE tournament_round_participant SET course_handicap = $1, playing_handicap = $1 WHERE id = $2",
|
||||
playing_handicap, rp_id,
|
||||
)
|
||||
return tp_id, rp_id
|
||||
|
||||
|
||||
async def test_hunt_differs_mid_round_but_converges_at_completion(pool):
|
||||
org_id = await create_org()
|
||||
owner_id = await create_user()
|
||||
await add_membership(org_id, owner_id, role="owner")
|
||||
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id)
|
||||
p1, rp1 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P1", 12)
|
||||
|
||||
# Hull 1, par -- stroke_index 1 (<=12) -> mottar 1 slag under standard.
|
||||
await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4])
|
||||
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
by_id = {e.tournament_participant_id: e for e in entries}
|
||||
# Standard (default): netto = 4-1=3, til par = 3-4 = -1 -- slaget er
|
||||
# kun DELVIS "tjent inn" ennå (1 av 12 spilte hull).
|
||||
assert by_id[p1].total_label == "-1"
|
||||
|
||||
await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id)
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
by_id = {e.tournament_participant_id: e for e in entries}
|
||||
# Hunt: gross(4) - FULLT spillehandicap(12) = -8, til par = -8-4 = -12
|
||||
# -- hele handicapet trukket fra med én gang, mye "dypere" tall.
|
||||
assert by_id[p1].total_label == "-12"
|
||||
|
||||
# Fullfør runden med par på alle resterende hull.
|
||||
await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4] * 17)
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
by_id = {e.tournament_participant_id: e for e in entries}
|
||||
# Fortsatt -12 under hunt -- flatt hele veien når hvert hull er par.
|
||||
assert by_id[p1].total_label == "-12"
|
||||
|
||||
# Bytt tilbake til standard nå som runden er fullført -- KONVERGERER
|
||||
# til nøyaktig samme tall som hunt (selve beviset på ekvivalensen).
|
||||
await update_tournament(tournament_id, TournamentUpdate(net_display_style="standard"), organization_id=org_id)
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
by_id = {e.tournament_participant_id: e for e in entries}
|
||||
assert by_id[p1].total_label == "-12"
|
||||
|
||||
|
||||
async def test_hunt_display_style_rejected_for_non_stroke_net(pool):
|
||||
org_id = await create_org()
|
||||
owner_id = await create_user()
|
||||
await add_membership(org_id, owner_id, role="owner")
|
||||
tournament_id = await create_tournament(org_id)
|
||||
async with app_db.org_connection(org_id) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE tournament SET format_type = 'individual', scoring_method = 'stableford' WHERE id = $1",
|
||||
tournament_id,
|
||||
)
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
async def test_switching_away_from_stroke_net_rejected_while_hunt_active(pool):
|
||||
org_id = await create_org()
|
||||
owner_id = await create_user()
|
||||
await add_membership(org_id, owner_id, role="owner")
|
||||
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id)
|
||||
await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await update_tournament(
|
||||
tournament_id, TournamentUpdate(scoring_method="stableford"), organization_id=org_id
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
# Men lov når begge felt endres sammen i samme kall.
|
||||
out = await update_tournament(
|
||||
tournament_id,
|
||||
TournamentUpdate(scoring_method="stableford", net_display_style="standard"),
|
||||
organization_id=org_id,
|
||||
)
|
||||
assert out.scoring_method == "stableford"
|
||||
assert out.net_display_style == "standard"
|
||||
|
||||
|
||||
async def test_lowest_playing_hcp_tiebreak_resolves_field_tie(pool):
|
||||
"""Speiler `test_lowest_hcp_resolves_field_tie_but_not_winner`
|
||||
(test_tiebreak.py) sitt oppsett nøyaktig, men med `stroke_gross`
|
||||
(handicap påvirker IKKE selve rangeringen der -- kun tiebreaken --
|
||||
slik at identiske bruttoscorer faktisk BLIR en reell uavgjort å
|
||||
løse). `stroke_net` ville gjort dette meningsløst: ulikt
|
||||
spillehandicap med identisk brutto gir ulik netto, altså ingen
|
||||
uavgjort i det hele tatt å teste tiebreaken på."""
|
||||
org_id = await create_org()
|
||||
owner_id = await create_user()
|
||||
await add_membership(org_id, owner_id, role="owner")
|
||||
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id, method="stroke_gross")
|
||||
await update_tournament(
|
||||
tournament_id,
|
||||
TournamentUpdate(tiebreak_winner_method="none", tiebreak_field_method="lowest_playing_hcp"),
|
||||
organization_id=org_id,
|
||||
)
|
||||
|
||||
leader, rp_leader = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "Leader", 5)
|
||||
p2, rp2 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P2", 8)
|
||||
p3, rp3 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P3", 20)
|
||||
|
||||
await _score_holes(tournament_id, round_id, rp_leader, org_id, owner_id, [3] * 18) # klar leder
|
||||
await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, [4] * 18) # uavgjort brutto med p3
|
||||
await _score_holes(tournament_id, round_id, rp3, org_id, owner_id, [4] * 18) # uavgjort brutto med p2
|
||||
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
by_id = {e.tournament_participant_id: e for e in entries}
|
||||
|
||||
assert by_id[leader].position == "1"
|
||||
assert by_id[p2].position == "2" # lavere spillehandicap (8 < 20) -> foran
|
||||
assert by_id[p3].position == "3"
|
||||
|
||||
|
||||
async def test_lowest_playing_hcp_missing_value_sorts_last(pool):
|
||||
"""Speiler `lowest_hcp` sin regel: manglende data kan ALDRI vinne en
|
||||
tiebreak ved å mangle -- sorteres sist. `stroke_gross`, samme
|
||||
begrunnelse som testen over -- spillehandicap er ikke påkrevd for
|
||||
brutto-scoring (`_SCORING_METHODS_REQUIRING_HANDICAP` gjelder ikke
|
||||
her), så et reelt NULL-scenario for tiebreak-metrikken kan faktisk
|
||||
oppstå (ulikt stroke_net, der manglende handicap gjør at netto-
|
||||
tallet aldri blir beregnet i det hele tatt -- deltakeren havner da
|
||||
helt utenfor rangeringen, ikke i en tiebreak-gruppe)."""
|
||||
org_id = await create_org()
|
||||
owner_id = await create_user()
|
||||
await add_membership(org_id, owner_id, role="owner")
|
||||
tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id, method="stroke_gross")
|
||||
# Kun to deltakere, begge uavgjort for 1.-plass -- tiebreak_winner_method
|
||||
# er den relevante innstillingen her (rank==1), ikke feltmetoden.
|
||||
await update_tournament(
|
||||
tournament_id,
|
||||
TournamentUpdate(tiebreak_winner_method="lowest_playing_hcp", tiebreak_field_method="none"),
|
||||
organization_id=org_id,
|
||||
)
|
||||
|
||||
p1, rp1 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P1", 10)
|
||||
# P2 -- ingen spillehandicap beregnet (NULL, samme som "aldri kjørt
|
||||
# gjennom _compute_round_participant_handicap").
|
||||
player2 = await create_player(org_id, display_name="P2")
|
||||
p2 = await create_tournament_participant(org_id, tournament_id, player2)
|
||||
rp2 = await create_tournament_round_participant(org_id, round_id, p2, tee_id)
|
||||
|
||||
await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4] * 18)
|
||||
await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, [4] * 18)
|
||||
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
by_id = {e.tournament_participant_id: e for e in entries}
|
||||
assert by_id[p1].position == "1" # har en verdi -> vinner uavgjortheten
|
||||
assert by_id[p2].position == "2" # mangler verdi -> sist
|
||||
Loading…
Reference in a new issue