Nøytral "ikke startet"-rad + tee-tid/starthull i leaderboardet
Deltakere som ikke har spilt noe ennå fikk feilaktig totalLabel "E"/"0 p" (til forveksling likt en ekte, jevn score) og en tallfestet sisteplass. Vises nå nøytralt: total/posisjon null/None, og runde-nivåets tee-tid/ starthull (hvis satt) i stedet for score-cellen.
This commit is contained in:
parent
985dada178
commit
54795065ef
5 changed files with 309 additions and 14 deletions
69
CHANGELOG.md
69
CHANGELOG.md
|
|
@ -12488,4 +12488,73 @@ Neste steg:
|
|||
manuelt for at scratch-dataen skulle vise seg riktig -- verdt å
|
||||
huske for fremtidig scratch-seeding av individuelle turneringer.
|
||||
|
||||
**Rullet ut 2026-08-18** -- bruker bekreftet ("Ja, bygg og rull ut").
|
||||
`docker compose build teecup_frontend && docker compose up -d
|
||||
teecup_frontend`, rene logger ("Ready in 0ms"). `curl -I
|
||||
https://teecup.golf/logg-inn` -> 200 etter utrulling.
|
||||
|
||||
113. **Nøytral "ikke startet"-rad + tee-tid/starthull i leaderboardet —
|
||||
2026-08-18.** Bruker: "De som ikke har begynt å spille ennå må være
|
||||
nøytrale på et vis, i leaderboardet. Det bør kanskje komme frem
|
||||
teetime og starthull på de, dersom det er angitt?" Rot funnet i
|
||||
`_attach_stroke_play_columns` (`individual_tournaments.py`): en
|
||||
deltaker som ikke hadde spilt ETT eneste hull ennå (`total_par_played
|
||||
== 0`) fikk likevel `total_label = "E"` (brutto/netto) eller `"0 p"`
|
||||
(stableford) -- til forveksling likt en ekte, jevn/poengløs SCORE,
|
||||
ikke "ingen data ennå". Samme deltaker fikk også en tallfestet
|
||||
plassering (nederst i feltet, siden NO_SCORE-nøkkelen sorterer sist)
|
||||
-- så de så ut som en reelt rangert, dårlig presterende spiller i
|
||||
stedet for en som rett og slett ikke har startet.
|
||||
|
||||
**Backend (`app/routers/individual_tournaments.py`):** ny
|
||||
`LeaderboardEntry.next_tee_time`/`next_start_hole` (kun individuelle
|
||||
turneringer har en `tournament_round`-tabell å hente disse fra --
|
||||
ETT klokkeslett/starthull PER RUNDE, delt av hele feltet, ikke per
|
||||
deltaker -- ingen per-deltaker utslettidspunkt finnes noe sted i
|
||||
denne turneringsformen). Populeres når `thru_label == "-"` (samme
|
||||
betingelse som already brukes for "ikke startet gjeldende runde"):
|
||||
referanserunden er `today_sequence` (runden som er i gang) hvis
|
||||
turneringen er underveis, ellers runde 1 (ingen har startet noe
|
||||
ennå). `next_start_hole` vises alltid når referanserunden finnes
|
||||
(kolonnen har alltid en verdi, default 1); `next_tee_time` vises kun
|
||||
når runden faktisk har et satt `scheduled_at` (nullable -- ikke alle
|
||||
turneringer setter det). `total_label` er nå `None` (ikke "E"/"0 p")
|
||||
når deltakeren ikke har spilt noe -- **kun** når `total_par_played
|
||||
== 0`, IKKE når de bare venter på neste runde etter å ha fullført en
|
||||
tidligere (den deltakeren beholder sin ekte total, kun THRU viser
|
||||
"-", eksisterende oppførsel uendret). `position`/`is_leader` settes
|
||||
til `None`/`False` for hele NO_SCORE-gruppen i stedet for en
|
||||
tallfestet sisteplass -- inkl. kant-tilfellet der INGEN i feltet har
|
||||
spilt noe ennå (da ville rank 1 ellers blitt feilaktig kåret til
|
||||
"leder").
|
||||
|
||||
**Frontend (`stroke-play-leaderboard.tsx`, liten, veloverveid
|
||||
håndkodet utvidelse av en eksisterende V0-komponent -- IKKE en ny
|
||||
UI-flate, se [[feedback_frontend_via_v0]]):** `totalLabel` er nå
|
||||
`string | null`; når `null` (og raden ikke allerede er en WD/DQ-
|
||||
statusrad) rendres en ny `TeeInfoTd` i TOTALT-kolonnen i stedet for
|
||||
en `ValueTd` -- en nøytral pille (klokke-ikon, `bg-muted`) med
|
||||
`"{tee-tid} · Hull {starthull}"`, eller bare det ene hvis kun det er
|
||||
satt, eller en ren dash hvis ingen av delene er satt. Raden får
|
||||
samme dempede `opacity-70`-behandling som WD/DQ-rader (bevisst
|
||||
gjenbruk -- samme "dette er ikke et konkurransedyktig resultat ennå"-
|
||||
signal). Ny "Ikke startet"-forklaring lagt til i toppens Legend
|
||||
(klokke-ikon), siden pillen introduserer en ny visuell betydning
|
||||
Legend ikke dekket fra før.
|
||||
|
||||
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Backend:
|
||||
to nye pytest-tester i `tests/test_individual_leaderboard.py` (én
|
||||
med en reell deltaker som HAR spilt runde 1 ved siden av én som
|
||||
ikke har startet noe -- bekrefter begge veier samtidig -- og én for
|
||||
alle-ikke-startet-kant-tilfellet), kjørt via
|
||||
`scripts/run_backend_tests.sh` (egen scratch-database, rører aldri
|
||||
ekte `teecup_db`) -- 117/117 grønt (opp fra 115). Frontend: en
|
||||
midlertidig lokal `/leaderboardpreviewtmp`-side (kun i en `next dev`-
|
||||
prosess, ALDRI committet) rendret `StrokePlayLeaderboard` med
|
||||
`MOCK_ROWS` utvidet med to nye rader (én med både tee-tid og
|
||||
starthull, én med KUN starthull) -- skjermbilde + a11y-snapshot
|
||||
bekreftet riktig pille-tekst, riktig dempet radstil, og at
|
||||
posisjonen viser "-" i stedet for et tall. Midlertidig side slettet
|
||||
igjen etter verifisering.
|
||||
|
||||
**Rullet ut:** venter på bekreftelse.
|
||||
|
|
|
|||
|
|
@ -1230,6 +1230,13 @@ class LeaderboardEntry(BaseModel):
|
|||
thru_label: str | None = None
|
||||
total_label: str | None = None
|
||||
rounds: list[RoundCellOut] = []
|
||||
# Nøytral visning for spillere som ikke har startet gjeldende/neste runde
|
||||
# ennå (2026-08-18). next_start_hole settes alltid når det finnes en
|
||||
# referanse-runde (start_hole har alltid en verdi -- default 1).
|
||||
# next_tee_time settes kun når runden faktisk har et oppgitt klokkeslett
|
||||
# (scheduled_at er nullable -- ikke alle turneringer setter det).
|
||||
next_tee_time: str | None = None
|
||||
next_start_hole: int | None = None
|
||||
# Eclectic (ADR-067-tillegget "Del C", migrasjon 072) -- beste resultat
|
||||
# per hull på tvers av ALLE turneringens runder (samme bane, håndhevet
|
||||
# ved rundeopprettelse). Alltid None/tom liste for øvrige scoring_
|
||||
|
|
@ -1356,7 +1363,7 @@ async def _attach_stroke_play_columns(
|
|||
"""
|
||||
rounds_meta = await conn.fetch(
|
||||
"""
|
||||
SELECT sequence, hole_config::text AS hole_config
|
||||
SELECT sequence, hole_config::text AS hole_config, scheduled_at::text AS scheduled_at, start_hole
|
||||
FROM tournament_round
|
||||
WHERE tournament_id = $1
|
||||
ORDER BY sequence
|
||||
|
|
@ -1366,6 +1373,8 @@ async def _attach_stroke_play_columns(
|
|||
hole_count_by_seq = {
|
||||
r["sequence"]: _HOLE_COUNT_BY_CONFIG[r["hole_config"]] for r in rounds_meta
|
||||
}
|
||||
round_meta_by_seq = {r["sequence"]: r for r in rounds_meta}
|
||||
first_sequence = rounds_meta[0]["sequence"] if rounds_meta else None
|
||||
|
||||
# Én rad per (runde, deltaker) som faktisk er PÅBEGYNT (holes_played > 0).
|
||||
# par_played = summen av par for KUN de hullene deltakeren faktisk har
|
||||
|
|
@ -1462,18 +1471,36 @@ async def _attach_stroke_play_columns(
|
|||
e.thru_label = "-"
|
||||
e.today_label = None
|
||||
|
||||
# Ikke-startet-info (2026-08-18): så lenge deltakeren ikke har
|
||||
# startet den gjeldende/neste runden ("-" over), vis tee-tid/
|
||||
# starthull for DEN runden -- referanserunden er selve gjeldende
|
||||
# runden (today_sequence) hvis turneringen er i gang, ellers
|
||||
# runde 1 (ingen har startet noe ennå).
|
||||
if e.thru_label == "-":
|
||||
reference_sequence = today_sequence if today_sequence is not None else first_sequence
|
||||
meta = round_meta_by_seq.get(reference_sequence) if reference_sequence is not None else None
|
||||
if meta is not None:
|
||||
e.next_tee_time = meta["scheduled_at"]
|
||||
e.next_start_hole = meta["start_hole"]
|
||||
|
||||
# TOTAL -- gjenbruker den allerede korrekt summerte gross_total/
|
||||
# net_total/stableford_total (uendret av denne funksjonen), trekker
|
||||
# kun fra summert par-for-spilte-hull for til-par-formatet.
|
||||
# kun fra summert par-for-spilte-hull for til-par-formatet. Ingen
|
||||
# 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").
|
||||
started_any = total_holes_played > 0
|
||||
if method == "stableford":
|
||||
e.total_label = f"{e.stableford_total or 0} p"
|
||||
e.total_label = f"{e.stableford_total or 0} p" if started_any else None
|
||||
rank_key_by_id[e.tournament_participant_id] = (
|
||||
NO_SCORE if e.stableford_total is None or total_holes_played == 0
|
||||
NO_SCORE if e.stableford_total is None or not started_any
|
||||
else -(e.stableford_total - 2 * total_holes_played)
|
||||
)
|
||||
else:
|
||||
total_val = 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 else "E"
|
||||
e.total_label = (
|
||||
_to_par_label(total_val - total_par_played) if (total_val is not None and started_any) else None
|
||||
)
|
||||
rank_key_by_id[e.tournament_participant_id] = (
|
||||
NO_SCORE if total_val is None or total_par_played == 0 else total_val - total_par_played
|
||||
)
|
||||
|
|
@ -1485,6 +1512,9 @@ async def _attach_stroke_play_columns(
|
|||
|
||||
# POS -- vanlig konkurranse-rangering (uavgjort deler plass, neste
|
||||
# rangering hopper over like mange plasser som antall uavgjorte).
|
||||
# Deltakere som ikke har spilt ETT eneste hull ennå (NO_SCORE) får INGEN
|
||||
# posisjon -- de er ikke "sist rangert", de er ennå ikke en del av
|
||||
# rangeringen (nøytral, se next_tee_time/next_start_hole over).
|
||||
i, n = 0, len(entries)
|
||||
while i < n:
|
||||
j = i
|
||||
|
|
@ -1493,9 +1523,10 @@ async def _attach_stroke_play_columns(
|
|||
j += 1
|
||||
rank = i + 1
|
||||
tied = (j - i) > 1
|
||||
not_started_group = key_i == NO_SCORE
|
||||
for k in range(i, j):
|
||||
entries[k].position = f"T{rank}" if tied else str(rank)
|
||||
entries[k].is_leader = rank == 1
|
||||
entries[k].position = None if not_started_group else (f"T{rank}" if tied else str(rank))
|
||||
entries[k].is_leader = rank == 1 and not not_started_group
|
||||
i = j
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,11 @@ type ApiLeaderboardEntry = {
|
|||
today_label: string | null
|
||||
thru_label: string | null
|
||||
total_label: string | null
|
||||
// Nøytral "ikke startet"-info (2026-08-18) -- kun satt når deltakeren
|
||||
// ikke har startet gjeldende/neste runde (thru_label "-"), se
|
||||
// stroke-play-leaderboard.tsx.
|
||||
next_tee_time: string | null
|
||||
next_start_hole: number | null
|
||||
rounds: ApiRoundCell[]
|
||||
// Eclectic (ADR-067-tillegget "Del C", 2026-08-14) -- kun populert for
|
||||
// scoring_method eclectic_gross/eclectic_net/eclectic_stableford.
|
||||
|
|
@ -3979,9 +3984,15 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou
|
|||
todayLabel: entry.today_label,
|
||||
todayIsUnderPar: null,
|
||||
thruLabel: entry.thru_label ?? "-",
|
||||
totalLabel: entry.total_label ?? "E",
|
||||
// total_label er null når deltakeren ikke har spilt ett eneste hull
|
||||
// ennå -- nøytral tilstand, IKKE til forveksling med "E" (jevnt med
|
||||
// par, en ekte score). stroke-play-leaderboard.tsx viser tee-tid/
|
||||
// starthull i stedet når totalLabel er null.
|
||||
totalLabel: entry.total_label,
|
||||
totalIsUnderPar: false,
|
||||
isLeader: entry.is_leader,
|
||||
nextTeeTime: entry.next_tee_time,
|
||||
nextStartHole: entry.next_start_hole,
|
||||
rounds: entry.rounds.map((r) => ({ roundNumber: r.round_number, label: r.label, tone: r.tone })),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
||||
import { Flag, Pause, Play, Trophy } from "lucide-react"
|
||||
import { Clock, Flag, Pause, Play, Trophy } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// --- Types (exact contract — do not change) --------------------------------
|
||||
|
|
@ -18,9 +18,14 @@ export type LeaderboardRow = {
|
|||
todayLabel: string | null // "-2", "+4", "E", "38 p" — null = today's round not started yet
|
||||
todayIsUnderPar: boolean | null
|
||||
thruLabel: string // "F" (finished), "14" (holes played), "-" (not started)
|
||||
totalLabel: string // "-13", "+7", "146 p"
|
||||
totalLabel: string | null // "-13", "+7", "146 p" — null = hasn't played a single hole yet (neutral, not "E")
|
||||
totalIsUnderPar: boolean
|
||||
isLeader: boolean // true for every row sharing the current lead position (ties possible)
|
||||
// When totalLabel is null, the row is rendered neutrally and shows this
|
||||
// instead of a score: the tee time / start hole of the round they're
|
||||
// about to play, if the organizer set one. Either can be null/undefined.
|
||||
nextTeeTime?: string | null // ISO datetime, or null if no tee time was set
|
||||
nextStartHole?: number | null
|
||||
rounds: RoundCell[] // one entry per round played so far (1-4 entries, not always 4)
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +59,8 @@ export const MOCK_ROWS: LeaderboardRow[] = [
|
|||
{ position: "14", playerName: "Maja Lund", todayLabel: "+2", todayIsUnderPar: false, thruLabel: "F", totalLabel: "+9", totalIsUnderPar: false, isLeader: false, rounds: [rc(1, 79), rc(2, 74)] },
|
||||
{ position: "15", playerName: "Frida Ness", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: "+11", totalIsUnderPar: false, isLeader: false, rounds: [rc(1, 83)] },
|
||||
{ position: "–", playerName: "Kaia Strand", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: "WD", totalIsUnderPar: false, isLeader: false, rounds: [rc(1, 76)] },
|
||||
{ position: "-", playerName: "Julie Berget", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: null, totalIsUnderPar: false, isLeader: false, nextTeeTime: "2026-08-19T11:10:00", nextStartHole: 1, rounds: [] },
|
||||
{ position: "-", playerName: "Selma Vang", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: null, totalIsUnderPar: false, isLeader: false, nextTeeTime: null, nextStartHole: 10, rounds: [] },
|
||||
]
|
||||
|
||||
// --- Tone helpers ----------------------------------------------------------
|
||||
|
|
@ -371,6 +378,10 @@ function Legend() {
|
|||
</span>
|
||||
Par
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<Clock aria-hidden="true" className="size-4 xl:size-6" />
|
||||
Ikke startet
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
|
@ -461,17 +472,23 @@ function FieldRow({
|
|||
roundNumbers: number[]
|
||||
striped: boolean
|
||||
}) {
|
||||
const status = isStatus(row.totalLabel)
|
||||
const status = row.totalLabel != null && isStatus(row.totalLabel)
|
||||
// Hasn't played a single hole yet anywhere in the tournament (backend
|
||||
// sends totalLabel: null for this, never "E" — "E" is a real even-par
|
||||
// score). Rendered neutrally, same muted treatment as a WD/DQ row, with
|
||||
// the upcoming tee time / start hole shown in place of a score if the
|
||||
// organizer set one.
|
||||
const notStarted = !status && row.totalLabel == null
|
||||
const bg = striped ? "bg-clubhouse-field" : "bg-clubhouse-card"
|
||||
const border = "border-b border-clubhouse-border"
|
||||
|
||||
return (
|
||||
<tr className={cn(bg, status && "opacity-70")}>
|
||||
<tr className={cn(bg, (status || notStarted) && "opacity-70")}>
|
||||
<td className={cn(cellPad, bg, border, "sticky left-0 z-10 text-center")}>
|
||||
<span className="text-base font-bold text-clubhouse-ink xl:text-3xl">{row.position}</span>
|
||||
</td>
|
||||
<td className={cn(cellPad, bg, border, "sticky z-10 text-left", PLAYER_LEFT)}>
|
||||
<span className={cn("block truncate text-base font-semibold xl:text-2xl", status ? "text-clubhouse-muted" : "text-clubhouse-ink")}>
|
||||
<span className={cn("block truncate text-base font-semibold xl:text-2xl", (status || notStarted) ? "text-clubhouse-muted" : "text-clubhouse-ink")}>
|
||||
{row.playerName}
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -494,7 +511,11 @@ function FieldRow({
|
|||
<>
|
||||
<ValueTd bg={bg} border={border} label={row.todayLabel} tone={toneFrom(row.todayIsUnderPar, row.todayLabel)} size="today" />
|
||||
<ThruTd bg={bg} border={border} thru={row.thruLabel} />
|
||||
{notStarted ? (
|
||||
<TeeInfoTd bg={bg} border={border} teeTime={row.nextTeeTime ?? null} startHole={row.nextStartHole ?? null} />
|
||||
) : (
|
||||
<ValueTd bg={bg} border={border} label={row.totalLabel} tone={toneFrom(row.totalIsUnderPar, row.totalLabel)} size="total" />
|
||||
)}
|
||||
{roundNumbers.map((n) => {
|
||||
const cell = row.rounds.find((c) => c.roundNumber === n)
|
||||
return <RoundTd key={n} bg={bg} border={border} cell={cell} />
|
||||
|
|
@ -668,3 +689,45 @@ function DashTd({ bg, border }: { bg: string; border: string }) {
|
|||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Tee-info cell (not-started-yet rows) -----------------------------------
|
||||
// Replaces the TOTAL cell for a player who hasn't played a single hole yet:
|
||||
// their upcoming tee time / start hole instead of a score, so the row reads
|
||||
// as "not started" rather than as a real (if unlucky) result.
|
||||
|
||||
function formatTeeTime(iso: string) {
|
||||
const date = new Date(iso)
|
||||
if (Number.isNaN(date.getTime())) return null
|
||||
return date.toLocaleTimeString("no-NO", { hour: "2-digit", minute: "2-digit" })
|
||||
}
|
||||
|
||||
function TeeInfoTd({
|
||||
teeTime,
|
||||
startHole,
|
||||
bg,
|
||||
border,
|
||||
}: {
|
||||
teeTime: string | null
|
||||
startHole: number | null
|
||||
bg: string
|
||||
border: string
|
||||
}) {
|
||||
const time = teeTime ? formatTeeTime(teeTime) : null
|
||||
const parts = [time, startHole != null ? `Hull ${startHole}` : null].filter((v): v is string => v != null)
|
||||
|
||||
return (
|
||||
<td className={cn(cellPad, bg, border, "text-right")}>
|
||||
{parts.length > 0 ? (
|
||||
<span className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-md bg-muted px-2.5 py-1 text-sm font-bold text-clubhouse-muted xl:text-lg">
|
||||
<Clock aria-hidden="true" className="size-3.5 shrink-0 xl:size-5" />
|
||||
{parts.join(" · ")}
|
||||
<span className="sr-only"> (ikke startet)</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-base text-clubhouse-muted xl:text-2xl" aria-label="ikke startet">
|
||||
–
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
121
tests/test_individual_leaderboard.py
Normal file
121
tests/test_individual_leaderboard.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""
|
||||
Nøytral "ikke startet"-visning i det individuelle leaderboardet (2026-08-18):
|
||||
en deltaker som ikke har spilt ett eneste hull ennå skal IKKE vises med en
|
||||
falsk "E"/"0 p"-total eller en rangert plassering -- og skal, hvis runden
|
||||
har et oppgitt klokkeslett/starthull, vise det i stedet. Kaller den ekte
|
||||
router-funksjonen direkte (samme mønster som test_order_of_merit.py).
|
||||
"""
|
||||
|
||||
from app.auth import CurrentUser
|
||||
from app.routers.individual_tournaments import (
|
||||
HoleUpdate as TournamentHoleUpdate,
|
||||
individual_leaderboard,
|
||||
update_hole as tournament_update_hole,
|
||||
)
|
||||
|
||||
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_all_holes(tournament_id, round_id, rp_id, org_id, user_id, score):
|
||||
user = CurrentUser(user_id=user_id)
|
||||
for n in range(1, 19):
|
||||
await tournament_update_hole(
|
||||
tournament_id, round_id, rp_id, n,
|
||||
TournamentHoleUpdate(gross_strokes=score, expected_version=None),
|
||||
organization_id=org_id, user=user,
|
||||
)
|
||||
|
||||
|
||||
async def test_not_started_player_is_neutral_and_shows_tee_info(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, name="Slagspill-turnering")
|
||||
course_id = await create_course(org_id, name="Testbanen")
|
||||
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 = 'stroke_gross' WHERE id = $1",
|
||||
tournament_id,
|
||||
)
|
||||
|
||||
round1_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
||||
async with app_db.org_connection(org_id) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE tournament_round SET scheduled_at = '2026-08-19 10:20:00+02', start_hole = 5 WHERE id = $1",
|
||||
round1_id,
|
||||
)
|
||||
|
||||
started_player_id = await create_player(org_id, display_name="Har startet")
|
||||
started_tp_id = await create_tournament_participant(org_id, tournament_id, started_player_id)
|
||||
started_rp_id = await create_tournament_round_participant(org_id, round1_id, started_tp_id, tee_id)
|
||||
await _score_all_holes(tournament_id, round1_id, started_rp_id, org_id, owner_id, score=4)
|
||||
|
||||
not_started_player_id = await create_player(org_id, display_name="Ikke startet ennå")
|
||||
not_started_tp_id = await create_tournament_participant(org_id, tournament_id, not_started_player_id)
|
||||
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
by_id = {e.tournament_participant_id: e for e in entries}
|
||||
|
||||
started = by_id[started_tp_id]
|
||||
assert started.total_label == "E"
|
||||
assert started.position == "1"
|
||||
assert started.is_leader is True
|
||||
|
||||
not_started = by_id[not_started_tp_id]
|
||||
assert not_started.total_label is None
|
||||
assert not_started.position is None
|
||||
assert not_started.is_leader is False
|
||||
assert not_started.next_start_hole == 5
|
||||
# ::text-castet fra Postgres kommer ut i DB-sesjonens tidssone (ikke
|
||||
# nødvendigvis Europe/Oslo -- frontend gjør selve lokal-formateringen),
|
||||
# så testen sjekker kun at DATOEN er riktig, ikke et eksakt klokkeslett.
|
||||
assert not_started.next_tee_time is not None
|
||||
assert "2026-08-19" in not_started.next_tee_time
|
||||
|
||||
|
||||
async def test_all_not_started_has_no_leader(pool):
|
||||
"""Kant-tilfelle: INGEN har spilt noe ennå -- da skal ingen markeres som
|
||||
leder (alle havner i samme NO_SCORE-gruppe, som ellers ville blitt
|
||||
"rank 1" og dermed feilaktig kåret til leder)."""
|
||||
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, name="Turnering uten spill ennå")
|
||||
course_id = await create_course(org_id, name="Testbanen")
|
||||
for n in range(1, 19):
|
||||
await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n)
|
||||
async with app_db.org_connection(org_id) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE tournament SET format_type = 'individual', scoring_method = 'stroke_gross' WHERE id = $1",
|
||||
tournament_id,
|
||||
)
|
||||
round1_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
||||
|
||||
player_id = await create_player(org_id, display_name="Spiller")
|
||||
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
|
||||
|
||||
entries = await individual_leaderboard(tournament_id, organization_id=org_id)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].tournament_participant_id == tp_id
|
||||
assert entries[0].total_label is None
|
||||
assert entries[0].position is None
|
||||
assert entries[0].is_leader is False
|
||||
Loading…
Reference in a new issue