Kaskader cut-resultatet til alle senere runder (ADR-095)
Overlevere auto-legges til runder etter cut (tee kopiert fra cut-runden), kuttede spilleres rundedeltakelse fjernes -- med mindre de allerede har scorer der, da beskyttes dataene av eksisterende cut-sperre i stedet. Bruker bekreftet eksplisitt: kaskade til ALLE runder, ikke bare neste. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7e41daf1de
commit
544c952cab
4 changed files with 361 additions and 5 deletions
|
|
@ -9652,6 +9652,79 @@ skjerm faller korrekt tilbake til sidescroll.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ADR-095: Cut-overlevere kaskaderes automatisk til ALLE senere runder (2026-08-20)
|
||||||
|
|
||||||
|
Bruker, i en oppfølging til cut-mekanismen (ADR-083): "I eksempel-
|
||||||
|
turneringen vår opererer vi med cut etter runde 1. På et vis burde de
|
||||||
|
som har klart cut'en autoselekteres på runde 2? Gir ikke det mening?"
|
||||||
|
Undersøkt (read-only) før bygging: `tournament_round_participant`-rader
|
||||||
|
opprettes KUN på forespørsel (aldri opprettet på forhånd for alle
|
||||||
|
runder), så "overlevere spiller videre" krever en eksplisitt
|
||||||
|
INSERT-mekanisme et sted -- fantes ikke fra før.
|
||||||
|
|
||||||
|
To eksplisitte beslutninger bekreftet av bruker, etter at jeg først
|
||||||
|
foreslo en snevrere variant (kun neste runde, aldri fjern noen):
|
||||||
|
1. **Kaskade til ALLE runder etter cut, ikke bare den neste.** "Kaskade
|
||||||
|
er det eneste riktige. Har du klart cut'en spiller du alle
|
||||||
|
påfølgende runder."
|
||||||
|
2. **Fjern kuttede spilleres rundedeltakelse i senere runder, ikke bare
|
||||||
|
la dem stå (blokkert av eksisterende 403-sperre).** "Jo, fjern de.
|
||||||
|
Dette får vi heller reversere om det viser seg å være en dårlig
|
||||||
|
beslutning." -- bruker informert eksplisitt om at sletting av
|
||||||
|
`tournament_round_participant` KASKADERER til `tournament_round_hole`
|
||||||
|
(migrasjon 040, `ON DELETE CASCADE`) FØR beslutningen ble tatt.
|
||||||
|
|
||||||
|
**Implementasjon** (`_sync_post_cut_round_participation`, kalt fra
|
||||||
|
`apply_cut` i `app/routers/individual_tournaments.py`, rett etter
|
||||||
|
`tournament_participant.cut`-oppdateringen, samme transaksjon):
|
||||||
|
- For hver runde med `sequence > cut_after_round`: overlevere som
|
||||||
|
MANGLER en rad der, får én INSERT-et automatisk, med samme `tee_id`
|
||||||
|
som de spilte cut-runden med (eneste faktiske data å basere det på --
|
||||||
|
ingen "kopier fra forrige runde"-mekanisme fantes). Handicap regnes ut
|
||||||
|
med samme `_compute_round_participant_handicap`-funksjon som
|
||||||
|
`add_round_participant` allerede bruker.
|
||||||
|
- Kuttede spillere med en EKSISTERENDE rad i en slik runde: raden
|
||||||
|
SLETTES -- MEN kun dersom den ikke allerede har registrert score
|
||||||
|
(`EXISTS (SELECT 1 FROM tournament_round_hole WHERE
|
||||||
|
tournament_round_participant_id = ...)`). Har raden scorer, la den stå
|
||||||
|
urørt -- den eksisterende cut-sperren (403, basert på
|
||||||
|
`tournament_participant.cut`) blokkerer likevel videre
|
||||||
|
scoreregistrering derfra.
|
||||||
|
- IDEMPOTENT, som resten av `apply_cut` -- trygt å trykke "Anvend cut"
|
||||||
|
på nytt etter en score-rettelse.
|
||||||
|
- Manuelt organisator-arbeid overskrives IKKE med mindre det kolliderer
|
||||||
|
direkte: en spiller organisator allerede har lagt til manuelt i en
|
||||||
|
senere runde (uansett om de overlever cutten) berøres ikke av
|
||||||
|
INSERT-logikken (kjører kun når raden MANGLER).
|
||||||
|
|
||||||
|
**Frontend:** `CutResult` fikk et nytt felt, `added_to_later_rounds`
|
||||||
|
(antall nye rader), vist i "Anvend cut"-bekreftelsen
|
||||||
|
(`individual-tournament-detail.tsx`). `SetupTab` nullstiller nå
|
||||||
|
`roundParticipantsByRound` til `null` etter "Anvend cut" (i stedet for
|
||||||
|
kun å refreshe én runde) siden FLERE runder kan ha fått rader
|
||||||
|
lagt til/fjernet samtidig -- utløser eksisterende lazy-load-effekt for
|
||||||
|
alle runder på nytt.
|
||||||
|
|
||||||
|
**Testet** i `tests/test_cut.py` (scratch-database, `./scripts/
|
||||||
|
run_backend_tests.sh`, alle 156 tester grønne): auto-tillegg når raden
|
||||||
|
mangler (riktig tee kopiert), fjerning av kuttede spilleres rad,
|
||||||
|
IKKE-fjerning når scorer allerede finnes (+ bekreftet at 403-sperren
|
||||||
|
fortsatt fungerer i det tilfellet), kaskade til runde 3 uten at runde 2
|
||||||
|
finnes i mellom. Én eksisterende test
|
||||||
|
(`test_cut_player_blocked_from_scoring_in_later_round`) fikk sin
|
||||||
|
forventning oppdatert fra 403 til 404 -- riktig og forventet konsekvens
|
||||||
|
av at raden nå faktisk slettes, ikke lenger bare blokkeres.
|
||||||
|
|
||||||
|
Frontend-delen (sortable-kolonner i samme tabell, se
|
||||||
|
`v0-prompt-players-table-sort.md`) er en SEPARAT, urelatert endring
|
||||||
|
levert samme dag -- V0-generert sorterings-mønster (stigende/synkende/
|
||||||
|
opprinnelig, `aria-sort`, kun de syv ikke-runde-kolonnene) integrert i
|
||||||
|
`tournament-players-table.tsx`, verifisert i scratch mot den ekte
|
||||||
|
komponenten (ikke V0-mocken) med presise `evaluate_script`-sjekker av
|
||||||
|
faktisk radrekkefølge og `aria-sort`-tilstand for alle tre sykluser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Utviklingsplan (rekkefølge)
|
## Utviklingsplan (rekkefølge)
|
||||||
|
|
||||||
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
||||||
|
|
|
||||||
|
|
@ -1840,6 +1840,108 @@ class CutResult(BaseModel):
|
||||||
survivors: int
|
survivors: int
|
||||||
cut_count: int
|
cut_count: int
|
||||||
cut_participant_ids: list[str]
|
cut_participant_ids: list[str]
|
||||||
|
added_to_later_rounds: int
|
||||||
|
|
||||||
|
|
||||||
|
async def _sync_post_cut_round_participation(
|
||||||
|
conn, organization_id: str, tournament_id: str, cut_after_round: int, cut_ids: list[str]
|
||||||
|
) -> int:
|
||||||
|
"""Kaskader cut-resultatet til ALLE runder etter cut_after_round
|
||||||
|
(2026-08-20, bruker bekreftet eksplisitt: "Har du klart cut'en
|
||||||
|
spiller du alle påfølgende runder" -- ikke bare den aller neste).
|
||||||
|
|
||||||
|
Overlevere: legges automatisk til enhver senere runde de ikke
|
||||||
|
allerede er meldt på i, med samme utslagssted som de spilte
|
||||||
|
cut-runden med (eneste faktiske data å basere det på -- ingen
|
||||||
|
"kopier fra forrige runde"-mekanisme fantes fra før).
|
||||||
|
|
||||||
|
Kuttede: fjernes fra enhver senere runde de er meldt på i -- MEN
|
||||||
|
KUN dersom de ikke allerede har registrert score der (sletting av
|
||||||
|
tournament_round_participant kaskader til tournament_round_hole,
|
||||||
|
se migrasjon 040 -- ville slettet ekte scorer). Dersom scorer
|
||||||
|
allerede finnes, la raden stå urørt; den eksisterende cut-sperren
|
||||||
|
blokkerer likevel videre scoreregistrering for dem.
|
||||||
|
|
||||||
|
IDEMPOTENT, som resten av apply-cut -- trygt å kjøre på nytt etter
|
||||||
|
en score-rettelse. Returnerer antall rader lagt til (for
|
||||||
|
tilbakemelding til organisator)."""
|
||||||
|
later_rounds = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT id::text AS id
|
||||||
|
FROM tournament_round
|
||||||
|
WHERE tournament_id = $1 AND sequence > $2
|
||||||
|
ORDER BY sequence
|
||||||
|
""",
|
||||||
|
tournament_id,
|
||||||
|
cut_after_round,
|
||||||
|
)
|
||||||
|
if not later_rounds:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
cut_round = await conn.fetchrow(
|
||||||
|
"SELECT id::text AS id FROM tournament_round WHERE tournament_id = $1 AND sequence = $2",
|
||||||
|
tournament_id,
|
||||||
|
cut_after_round,
|
||||||
|
)
|
||||||
|
survivor_tees: dict[str, str] = {}
|
||||||
|
if cut_round is not None:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"SELECT tournament_participant_id::text AS pid, tee_id::text AS tee_id "
|
||||||
|
"FROM tournament_round_participant WHERE tournament_round_id = $1",
|
||||||
|
cut_round["id"],
|
||||||
|
)
|
||||||
|
survivor_tees = {r["pid"]: r["tee_id"] for r in rows}
|
||||||
|
|
||||||
|
all_participants = await conn.fetch(
|
||||||
|
"SELECT id::text AS id FROM tournament_participant WHERE tournament_id = $1",
|
||||||
|
tournament_id,
|
||||||
|
)
|
||||||
|
survivor_ids = [r["id"] for r in all_participants if r["id"] not in cut_ids]
|
||||||
|
|
||||||
|
added = 0
|
||||||
|
for round_row in later_rounds:
|
||||||
|
round_id = round_row["id"]
|
||||||
|
|
||||||
|
existing = await conn.fetch(
|
||||||
|
"SELECT id::text AS id, tournament_participant_id::text AS pid "
|
||||||
|
"FROM tournament_round_participant WHERE tournament_round_id = $1",
|
||||||
|
round_id,
|
||||||
|
)
|
||||||
|
existing_by_pid = {r["pid"]: r["id"] for r in existing}
|
||||||
|
|
||||||
|
for pid in survivor_ids:
|
||||||
|
if pid in existing_by_pid:
|
||||||
|
continue
|
||||||
|
tee_id = survivor_tees.get(pid)
|
||||||
|
if tee_id is None:
|
||||||
|
continue # ingen kjent tee å bruke -- hopp over, ikke gjett
|
||||||
|
new_id = await conn.fetchval(
|
||||||
|
"""
|
||||||
|
INSERT INTO tournament_round_participant
|
||||||
|
(organization_id, tournament_round_id, tournament_participant_id, tee_id)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
organization_id,
|
||||||
|
round_id,
|
||||||
|
pid,
|
||||||
|
tee_id,
|
||||||
|
)
|
||||||
|
await _compute_round_participant_handicap(conn, str(new_id))
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
for pid, rp_id in existing_by_pid.items():
|
||||||
|
if pid not in cut_ids:
|
||||||
|
continue
|
||||||
|
has_scores = await conn.fetchval(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM tournament_round_hole WHERE tournament_round_participant_id = $1)",
|
||||||
|
rp_id,
|
||||||
|
)
|
||||||
|
if has_scores:
|
||||||
|
continue
|
||||||
|
await conn.execute("DELETE FROM tournament_round_participant WHERE id = $1", rp_id)
|
||||||
|
|
||||||
|
return added
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|
@ -1897,6 +1999,9 @@ async def apply_cut(
|
||||||
tournament_id,
|
tournament_id,
|
||||||
cut_ids,
|
cut_ids,
|
||||||
)
|
)
|
||||||
|
added_to_later_rounds = await _sync_post_cut_round_participation(
|
||||||
|
conn, organization_id, tournament_id, cut_after_round, cut_ids
|
||||||
|
)
|
||||||
await conn.execute("UPDATE tournament SET cut_applied_at = now() WHERE id = $1", tournament_id)
|
await conn.execute("UPDATE tournament SET cut_applied_at = now() WHERE id = $1", tournament_id)
|
||||||
|
|
||||||
return CutResult(
|
return CutResult(
|
||||||
|
|
@ -1905,6 +2010,7 @@ async def apply_cut(
|
||||||
survivors=len(entries) - len(cut_ids),
|
survivors=len(entries) - len(cut_ids),
|
||||||
cut_count=len(cut_ids),
|
cut_count=len(cut_ids),
|
||||||
cut_participant_ids=cut_ids,
|
cut_participant_ids=cut_ids,
|
||||||
|
added_to_later_rounds=added_to_later_rounds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -695,11 +695,15 @@ export function IndividualTournamentDetail({
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error()
|
if (!res.ok) throw new Error()
|
||||||
const result: { survivors: number; cut_count: number } = await res.json()
|
const result: { survivors: number; cut_count: number; added_to_later_rounds: number } = await res.json()
|
||||||
setTournament({ ...tournament, cut_applied_at: new Date().toISOString() })
|
setTournament({ ...tournament, cut_applied_at: new Date().toISOString() })
|
||||||
await refreshParticipants()
|
await refreshParticipants()
|
||||||
setError(null)
|
setError(null)
|
||||||
alert(`Cut anvendt: ${result.survivors} går videre, ${result.cut_count} er kuttet.`)
|
const extra =
|
||||||
|
result.added_to_later_rounds > 0
|
||||||
|
? ` ${result.added_to_later_rounds} rundedeltakelser lagt til automatisk i senere runder (kuttede spillere er samtidig fjernet der de ikke allerede har scorer).`
|
||||||
|
: ""
|
||||||
|
alert(`Cut anvendt: ${result.survivors} går videre, ${result.cut_count} er kuttet.${extra}`)
|
||||||
} catch {
|
} catch {
|
||||||
setError("Klarte ikke å anvende cutten. Prøv igjen.")
|
setError("Klarte ikke å anvende cutten. Prøv igjen.")
|
||||||
}
|
}
|
||||||
|
|
@ -1321,6 +1325,15 @@ function SetupTab({
|
||||||
setRoundParticipantsByRound((prev) => ({ ...(prev ?? {}), [roundId]: data ?? [] }))
|
setRoundParticipantsByRound((prev) => ({ ...(prev ?? {}), [roundId]: data ?? [] }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "Anvend cut" (ADR-095) kaskader nå til rundedeltakelse i alle senere
|
||||||
|
// runder på backend -- tvinger derfor et fullt reload av
|
||||||
|
// roundParticipantsByRound (i stedet for kun ett enkelt refreshRoundParticipants-
|
||||||
|
// kall) siden FLERE runder kan ha fått rader lagt til/fjernet samtidig.
|
||||||
|
async function applyCutAndRefreshRounds() {
|
||||||
|
await onApplyCut()
|
||||||
|
setRoundParticipantsByRound(null)
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleRoundParticipant(
|
async function toggleRoundParticipant(
|
||||||
roundId: string,
|
roundId: string,
|
||||||
tournamentParticipantId: string,
|
tournamentParticipantId: string,
|
||||||
|
|
@ -1728,7 +1741,7 @@ function SetupTab({
|
||||||
{tournament.scoring_method != null &&
|
{tournament.scoring_method != null &&
|
||||||
["stroke_gross", "stroke_net", "stableford"].includes(tournament.scoring_method) && (
|
["stroke_gross", "stroke_net", "stableford"].includes(tournament.scoring_method) && (
|
||||||
<>
|
<>
|
||||||
<CutCard tournament={tournament} rounds={rounds} onUpdateConfig={onUpdateCutConfig} onApplyCut={onApplyCut} />
|
<CutCard tournament={tournament} rounds={rounds} onUpdateConfig={onUpdateCutConfig} onApplyCut={applyCutAndRefreshRounds} />
|
||||||
<TiebreakCard tournament={tournament} onUpdateConfig={onUpdateTiebreakConfig} />
|
<TiebreakCard tournament={tournament} onUpdateConfig={onUpdateTiebreakConfig} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,13 @@ async def test_apply_cut_requires_config_set_first(pool):
|
||||||
|
|
||||||
|
|
||||||
async def test_cut_player_blocked_from_scoring_in_later_round(pool):
|
async def test_cut_player_blocked_from_scoring_in_later_round(pool):
|
||||||
|
"""Siden 2026-08-20 fjerner apply_cut kuttede spilleres runde-2-rad
|
||||||
|
HELT (kaskade-vedtaket, se test_apply_cut_removes_cut_players_...) --
|
||||||
|
forsøk på å score for dem gir derfor 404 (raden finnes ikke), ikke
|
||||||
|
lenger 403. Den gamle cut-sperren (403) lever videre som forsvar i
|
||||||
|
dybden for det sjeldne tilfellet raden IKKE ble fjernet fordi den
|
||||||
|
allerede hadde scorer -- se test_apply_cut_does_not_delete_round_
|
||||||
|
participation_with_existing_scores for det tilfellet."""
|
||||||
org_id = await create_org()
|
org_id = await create_org()
|
||||||
owner_id = await create_user()
|
owner_id = await create_user()
|
||||||
await add_membership(org_id, owner_id, role="owner")
|
await add_membership(org_id, owner_id, role="owner")
|
||||||
|
|
@ -160,14 +167,15 @@ async def test_cut_player_blocked_from_scoring_in_later_round(pool):
|
||||||
await apply_cut(tournament_id, organization_id=org_id)
|
await apply_cut(tournament_id, organization_id=org_id)
|
||||||
|
|
||||||
user = CurrentUser(user_id=owner_id)
|
user = CurrentUser(user_id=owner_id)
|
||||||
# P4 er kuttet -- runde 2 (etter cut_after_round=1) skal avvises.
|
# P4 er kuttet -- runde-2-raden er fjernet av apply_cut, så forsøket
|
||||||
|
# treffer "finnes ikke", ikke "blokkert".
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
await tournament_update_hole(
|
await tournament_update_hole(
|
||||||
tournament_id, round2_id, rp2["P4"], 1,
|
tournament_id, round2_id, rp2["P4"], 1,
|
||||||
TournamentHoleUpdate(gross_strokes=4, expected_version=None),
|
TournamentHoleUpdate(gross_strokes=4, expected_version=None),
|
||||||
organization_id=org_id, user=user,
|
organization_id=org_id, user=user,
|
||||||
)
|
)
|
||||||
assert exc_info.value.status_code == 403
|
assert exc_info.value.status_code == 404
|
||||||
|
|
||||||
# P1 overlevde cutten -- runde 2 skal fortsatt fungere som normalt.
|
# P1 overlevde cutten -- runde 2 skal fortsatt fungere som normalt.
|
||||||
result = await tournament_update_hole(
|
result = await tournament_update_hole(
|
||||||
|
|
@ -202,6 +210,162 @@ async def test_cut_player_can_still_edit_score_in_round_at_or_before_cut_point(p
|
||||||
assert result.gross_strokes == 5
|
assert result.gross_strokes == 5
|
||||||
|
|
||||||
|
|
||||||
|
async def test_apply_cut_auto_adds_survivor_to_round_after_cut_if_missing(pool):
|
||||||
|
"""P1 overlevde, men har (i motsetning til _setup sitt vanlige rp2-
|
||||||
|
oppsett) INGEN runde 2-rad ennå -- apply_cut skal opprette den, med
|
||||||
|
samme tee som P1 spilte runde 1 med (2026-08-20, bruker bekreftet
|
||||||
|
kaskade-oppførsel)."""
|
||||||
|
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="Cut-turnering")
|
||||||
|
course_id = await create_course(org_id, name="Cut 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 = 'stroke_gross' WHERE id = $1",
|
||||||
|
tournament_id,
|
||||||
|
)
|
||||||
|
round1_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
||||||
|
round2_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=2)
|
||||||
|
|
||||||
|
p1 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P1"))
|
||||||
|
p2 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P2"))
|
||||||
|
rp1_p1 = await create_tournament_round_participant(org_id, round1_id, p1, tee_id)
|
||||||
|
rp1_p2 = await create_tournament_round_participant(org_id, round1_id, p2, tee_id)
|
||||||
|
await _score_holes(tournament_id, round1_id, rp1_p1, org_id, owner_id, [3, 3, 3, 3] + [4] * 14) # -4
|
||||||
|
await _score_holes(tournament_id, round1_id, rp1_p2, org_id, owner_id, [5] * 8 + [4] * 10) # +8
|
||||||
|
|
||||||
|
# Ingen runde 2-rad for NOEN opprettet på forhånd -- P1 skal likevel
|
||||||
|
# dukke opp der etter cut, siden P1 overlever.
|
||||||
|
await _set_cut_config(org_id, tournament_id, cut_after_round=1, cut_size=1)
|
||||||
|
|
||||||
|
result = await apply_cut(tournament_id, organization_id=org_id)
|
||||||
|
assert result.added_to_later_rounds == 1
|
||||||
|
|
||||||
|
async with app_db.org_connection(org_id) as conn:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"SELECT tee_id::text AS tee_id, course_handicap FROM tournament_round_participant "
|
||||||
|
"WHERE tournament_round_id = $1 AND tournament_participant_id = $2",
|
||||||
|
round2_id, p1,
|
||||||
|
)
|
||||||
|
p2_row = await conn.fetchval(
|
||||||
|
"SELECT id FROM tournament_round_participant WHERE tournament_round_id = $1 AND tournament_participant_id = $2",
|
||||||
|
round2_id, p2,
|
||||||
|
)
|
||||||
|
assert row is not None
|
||||||
|
assert row["tee_id"] == tee_id
|
||||||
|
assert p2_row is None # P2 ble kuttet -- ingen runde 2-rad opprettet for dem
|
||||||
|
|
||||||
|
|
||||||
|
async def test_apply_cut_removes_cut_players_round_participation_in_later_rounds(pool):
|
||||||
|
"""_setup() oppretter runde 2-rader for ALLE (unntatt P6) på forhånd
|
||||||
|
-- apply_cut skal fjerne raden for de som blir kuttet (P4, P5, P6),
|
||||||
|
men la P1/P2/P3 sine stå."""
|
||||||
|
org_id = await create_org()
|
||||||
|
owner_id = await create_user()
|
||||||
|
await add_membership(org_id, owner_id, role="owner")
|
||||||
|
tournament_id, round1_id, round2_id, players, rp2 = await _setup(org_id, owner_id)
|
||||||
|
await _set_cut_config(org_id, tournament_id, cut_after_round=1, cut_size=2)
|
||||||
|
|
||||||
|
await apply_cut(tournament_id, organization_id=org_id)
|
||||||
|
|
||||||
|
async with app_db.org_connection(org_id) as conn:
|
||||||
|
remaining = {
|
||||||
|
name: await conn.fetchval(
|
||||||
|
"SELECT id FROM tournament_round_participant WHERE tournament_round_id = $1 AND tournament_participant_id = $2",
|
||||||
|
round2_id, tp_id,
|
||||||
|
)
|
||||||
|
for name, tp_id in players.items()
|
||||||
|
}
|
||||||
|
assert remaining["P1"] is not None
|
||||||
|
assert remaining["P2"] is not None
|
||||||
|
assert remaining["P3"] is not None
|
||||||
|
assert remaining["P4"] is None # kuttet, ingen scorer i runde 2 -- fjernet
|
||||||
|
assert remaining["P5"] is None # kuttet, ingen scorer i runde 2 -- fjernet
|
||||||
|
|
||||||
|
|
||||||
|
async def test_apply_cut_does_not_delete_round_participation_with_existing_scores(pool):
|
||||||
|
"""Dersom en kuttet spiller likevel har scorer registrert i en senere
|
||||||
|
runde (kant-tilfelle -- burde normalt blokkeres av score-sperren, men
|
||||||
|
testes eksplisitt siden sletting av tournament_round_participant
|
||||||
|
kaskader til tournament_round_hole), skal raden IKKE fjernes."""
|
||||||
|
org_id = await create_org()
|
||||||
|
owner_id = await create_user()
|
||||||
|
await add_membership(org_id, owner_id, role="owner")
|
||||||
|
tournament_id, round1_id, round2_id, players, rp2 = await _setup(org_id, owner_id)
|
||||||
|
|
||||||
|
# Score inn ett hull for P4 i runde 2 FØR cutten anvendes (simulerer at
|
||||||
|
# data allerede finnes -- score-sperren gjelder kun EFTER cut er anvendt).
|
||||||
|
await _score_holes(tournament_id, round2_id, rp2["P4"], org_id, owner_id, [4])
|
||||||
|
|
||||||
|
await _set_cut_config(org_id, tournament_id, cut_after_round=1, cut_size=2)
|
||||||
|
await apply_cut(tournament_id, organization_id=org_id)
|
||||||
|
|
||||||
|
async with app_db.org_connection(org_id) as conn:
|
||||||
|
still_there = await conn.fetchval(
|
||||||
|
"SELECT id FROM tournament_round_participant WHERE id = $1", rp2["P4"],
|
||||||
|
)
|
||||||
|
score_still_there = await conn.fetchval(
|
||||||
|
"SELECT COUNT(*) FROM tournament_round_hole WHERE tournament_round_participant_id = $1", rp2["P4"],
|
||||||
|
)
|
||||||
|
assert still_there is not None # IKKE slettet -- ville kaskadert bort scoren
|
||||||
|
assert score_still_there == 1
|
||||||
|
|
||||||
|
# Raden lever videre, men den eksisterende cut-sperren (403, basert på
|
||||||
|
# tournament_participant.cut) blokkerer likevel forsøk på Å FORTSETTE
|
||||||
|
# å registrere flere hull for denne kuttede spilleren i runden.
|
||||||
|
user = CurrentUser(user_id=owner_id)
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await tournament_update_hole(
|
||||||
|
tournament_id, round2_id, rp2["P4"], 2,
|
||||||
|
TournamentHoleUpdate(gross_strokes=4, expected_version=None),
|
||||||
|
organization_id=org_id, user=user,
|
||||||
|
)
|
||||||
|
assert exc_info.value.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
async def test_apply_cut_cascades_to_all_rounds_after_cut_not_just_the_next_one(pool):
|
||||||
|
"""Bruker bekreftet eksplisitt: har du klart cutten spiller du ALLE
|
||||||
|
påfølgende runder, ikke bare den aller neste. Turnering med 3 runder,
|
||||||
|
cut etter runde 1 -- overlever skal auto-legges til BÅDE runde 2 og 3."""
|
||||||
|
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="Cut-turnering, 3 runder")
|
||||||
|
course_id = await create_course(org_id, name="Cut 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 = 'stroke_gross' WHERE id = $1",
|
||||||
|
tournament_id,
|
||||||
|
)
|
||||||
|
round1_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1)
|
||||||
|
round3_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=3)
|
||||||
|
|
||||||
|
p1 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P1"))
|
||||||
|
p2 = await create_tournament_participant(org_id, tournament_id, await create_player(org_id, display_name="P2"))
|
||||||
|
rp1_p1 = await create_tournament_round_participant(org_id, round1_id, p1, tee_id)
|
||||||
|
rp1_p2 = await create_tournament_round_participant(org_id, round1_id, p2, tee_id)
|
||||||
|
await _score_holes(tournament_id, round1_id, rp1_p1, org_id, owner_id, [3, 3, 3, 3] + [4] * 14) # -4
|
||||||
|
await _score_holes(tournament_id, round1_id, rp1_p2, org_id, owner_id, [5] * 8 + [4] * 10) # +8
|
||||||
|
|
||||||
|
await _set_cut_config(org_id, tournament_id, cut_after_round=1, cut_size=1)
|
||||||
|
result = await apply_cut(tournament_id, organization_id=org_id)
|
||||||
|
assert result.added_to_later_rounds == 1 # kun round3 finnes (ingen round2 opprettet i dette oppsettet)
|
||||||
|
|
||||||
|
async with app_db.org_connection(org_id) as conn:
|
||||||
|
row = await conn.fetchval(
|
||||||
|
"SELECT id FROM tournament_round_participant WHERE tournament_round_id = $1 AND tournament_participant_id = $2",
|
||||||
|
round3_id, p1,
|
||||||
|
)
|
||||||
|
assert row is not None # P1 (overlever) auto-lagt til runde 3, selv om runde 2 ikke fantes i mellom
|
||||||
|
|
||||||
|
|
||||||
async def test_leaderboard_marks_cut_players_with_status_and_sorts_them_last(pool):
|
async def test_leaderboard_marks_cut_players_with_status_and_sorts_them_last(pool):
|
||||||
org_id = await create_org()
|
org_id = await create_org()
|
||||||
owner_id = await create_user()
|
owner_id = await create_user()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue