Shotgun-start: backend (migrasjon 088, ADR-096)
Ny start_mode ('consecutive'/'shotgun') på tournament_round + session,
og valgfri start_hole på tournament_round_group + match. Shotgun: alle
grupper/matcher går ut samtidig, sortert etter starthull i stedet for
klokkeslett. Reverserer/utvider migrasjon 084s bevisste "ikke shotgun"
fra 2026-08-18 -- nytt behov oppdaget under utskrift-planlegging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
0792205c64
commit
f1575c9b42
6 changed files with 318 additions and 24 deletions
48
088_shotgun_start.sql
Normal file
48
088_shotgun_start.sql
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
-- =====================================================================
|
||||
-- TeeCup — migrasjon 088
|
||||
-- Shotgun-start: alternativ til den tradisjonelle, staggerte starten
|
||||
-- =====================================================================
|
||||
-- Reverserer/utvider EKSPLISITT migrasjon 084 sitt bevisste "IKKE
|
||||
-- shotgun"-valg fra 2026-08-18 -- se ADR-096 for full begrunnelse for
|
||||
-- hvorfor dette nå bygges. Samme arkitektoniske hull fantes BEGGE
|
||||
-- steder rundeplanlegging finnes i appen: individuelle turneringer
|
||||
-- (tournament_round/tournament_round_group) og lag/Cup-format
|
||||
-- (session/match) -- ett delt starthull for HELE runden/økten, kun
|
||||
-- ulikt KLOKKESLETT per gruppe/match, aldri ulikt HULL. Rettet
|
||||
-- identisk begge steder.
|
||||
--
|
||||
-- start_mode = 'consecutive' (standard, uendret oppførsel -- INGEN
|
||||
-- regresjon): alle grupper/matcher deler samme starthull
|
||||
-- (tournament_round.start_hole / session.start_hole, uendret), staggert
|
||||
-- KUN på klokkeslett (eksisterende tee_time_override-mønster, uendret).
|
||||
--
|
||||
-- start_mode = 'shotgun': ALLE grupper/matcher i runden/økten går ut
|
||||
-- SAMTIDIG (intervallet blir meningsløst og ignoreres av app-laget i
|
||||
-- denne modusen) -- fra HVER SIN starthull i stedet. Nytt, valgfritt
|
||||
-- per-gruppe/per-match starthull-felt dekker dette -- NULL når runden/
|
||||
-- økten uansett er 'consecutive' (bruker da forelderens delte
|
||||
-- starthull som før), settes eksplisitt av organisator per gruppe/
|
||||
-- match kun i shotgun-modus.
|
||||
--
|
||||
-- Samme text+CHECK-mønster som migrasjon 087 (ikke en ny
|
||||
-- CREATE TYPE ... AS ENUM, konsistent med nyere migrasjoner i denne
|
||||
-- filserien).
|
||||
-- =====================================================================
|
||||
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
ALTER TABLE tournament_round
|
||||
ADD COLUMN start_mode text NOT NULL DEFAULT 'consecutive'
|
||||
CHECK (start_mode IN ('consecutive', 'shotgun'));
|
||||
|
||||
ALTER TABLE tournament_round_group
|
||||
ADD COLUMN start_hole smallint
|
||||
CHECK (start_hole IS NULL OR start_hole BETWEEN 1 AND 18);
|
||||
|
||||
ALTER TABLE session
|
||||
ADD COLUMN start_mode text NOT NULL DEFAULT 'consecutive'
|
||||
CHECK (start_mode IN ('consecutive', 'shotgun'));
|
||||
|
||||
ALTER TABLE match
|
||||
ADD COLUMN start_hole smallint
|
||||
CHECK (start_hole IS NULL OR start_hole BETWEEN 1 AND 18);
|
||||
|
|
@ -100,6 +100,12 @@ class TournamentRoundCreate(BaseModel):
|
|||
scheduled_at: datetime | None = None
|
||||
tee_interval_minutes: int | None = Field(default=None, gt=0)
|
||||
start_hole: int = Field(default=1, ge=1, le=18)
|
||||
# Shotgun-start (migrasjon 088, ADR-096) -- 'consecutive' (standard)
|
||||
# er dagens oppførsel uendret: alle grupper deler DETTE feltet,
|
||||
# staggert kun på klokkeslett. 'shotgun': alle grupper går ut
|
||||
# SAMTIDIG, fra hvert sitt starthull (tournament_round_group.
|
||||
# start_hole) i stedet -- se _compute_group_tee_time.
|
||||
start_mode: str = Field(default="consecutive", pattern="^(consecutive|shotgun)$")
|
||||
|
||||
|
||||
class TournamentRound(BaseModel):
|
||||
|
|
@ -113,12 +119,13 @@ class TournamentRound(BaseModel):
|
|||
scheduled_at: str | None
|
||||
tee_interval_minutes: int | None
|
||||
start_hole: int
|
||||
start_mode: str
|
||||
|
||||
|
||||
_ROUND_COLUMNS = """
|
||||
tr.id::text, tr.tournament_id::text, tr.sequence, tr.name,
|
||||
tr.hole_config::text, tr.course_id::text, c.name AS course_name,
|
||||
tr.scheduled_at::text, tr.tee_interval_minutes, tr.start_hole
|
||||
tr.scheduled_at::text, tr.tee_interval_minutes, tr.start_hole, tr.start_mode
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -162,8 +169,8 @@ async def create_round(
|
|||
WITH inserted AS (
|
||||
INSERT INTO tournament_round
|
||||
(organization_id, tournament_id, sequence, name, hole_config,
|
||||
course_id, scheduled_at, tee_interval_minutes, start_hole)
|
||||
VALUES ($1, $2, $3, $4, $5::hole_scope, $6, $7, $8, $9)
|
||||
course_id, scheduled_at, tee_interval_minutes, start_hole, start_mode)
|
||||
VALUES ($1, $2, $3, $4, $5::hole_scope, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
)
|
||||
SELECT {_ROUND_COLUMNS} FROM inserted tr JOIN course c ON c.id = tr.course_id
|
||||
|
|
@ -177,6 +184,7 @@ async def create_round(
|
|||
body.scheduled_at,
|
||||
body.tee_interval_minutes,
|
||||
body.start_hole,
|
||||
body.start_mode,
|
||||
)
|
||||
return TournamentRound(**dict(row))
|
||||
|
||||
|
|
@ -199,6 +207,7 @@ class TournamentRoundUpdate(BaseModel):
|
|||
scheduled_at: datetime | None = None # se TournamentRoundCreate for hvorfor datetime, ikke str
|
||||
tee_interval_minutes: int | None = Field(default=None, gt=0)
|
||||
start_hole: int | None = Field(default=None, ge=1, le=18)
|
||||
start_mode: str | None = Field(default=None, pattern="^(consecutive|shotgun)$")
|
||||
|
||||
|
||||
@router.patch(
|
||||
|
|
@ -777,12 +786,21 @@ def _compute_group_tee_time(
|
|||
tee_interval_minutes: int | None,
|
||||
sequence: int,
|
||||
override: datetime | None,
|
||||
start_mode: str = "consecutive",
|
||||
) -> datetime | None:
|
||||
"""Samme formel/filosofi som matches.py sin `_compute_tee_time` for
|
||||
lagturneringers økter -- utledet, ikke lagret, med en valgfri
|
||||
override som vinner (f.eks. én gruppe forsinket)."""
|
||||
override som vinner (f.eks. én gruppe forsinket).
|
||||
|
||||
Shotgun (migrasjon 088, ADR-096): ALLE grupper går ut SAMTIDIG --
|
||||
intervallet er meningsløst i denne modusen (ulikt starthull, ikke
|
||||
ulikt klokkeslett), så `sequence` ignoreres helt. Kun `scheduled_at`
|
||||
(felles for hele runden) eller en eksplisitt per-gruppe override
|
||||
gjelder."""
|
||||
if override is not None:
|
||||
return override
|
||||
if start_mode == "shotgun":
|
||||
return scheduled_at
|
||||
if scheduled_at is None or tee_interval_minutes is None:
|
||||
return None
|
||||
return scheduled_at + timedelta(minutes=(sequence - 1) * tee_interval_minutes)
|
||||
|
|
@ -799,6 +817,10 @@ class RoundGroupOut(BaseModel):
|
|||
sequence: int
|
||||
tee_time: str | None # utledet (eller override), ISO-tekst
|
||||
tee_time_override: str | None
|
||||
# Kun meningsfullt når runden er 'shotgun' (migrasjon 088, ADR-096) --
|
||||
# `None` for en 'consecutive'-runde, som fortsatt bruker rundens EGET
|
||||
# delte start_hole (uendret siden migrasjon 040).
|
||||
start_hole: int | None
|
||||
participants: list[RoundGroupParticipant]
|
||||
|
||||
|
||||
|
|
@ -811,7 +833,8 @@ class RoundGroupsOut(BaseModel):
|
|||
|
||||
async def _round_scheduling(conn, tournament_id: str, round_id: str) -> asyncpg.Record:
|
||||
round_row = await conn.fetchrow(
|
||||
"SELECT scheduled_at, tee_interval_minutes FROM tournament_round WHERE id = $1 AND tournament_id = $2",
|
||||
"SELECT scheduled_at, tee_interval_minutes, start_mode FROM tournament_round "
|
||||
"WHERE id = $1 AND tournament_id = $2",
|
||||
round_id,
|
||||
tournament_id,
|
||||
)
|
||||
|
|
@ -822,7 +845,7 @@ async def _round_scheduling(conn, tournament_id: str, round_id: str) -> asyncpg.
|
|||
|
||||
async def _fetch_round_groups(conn, round_id: str, round_row) -> RoundGroupsOut:
|
||||
group_rows = await conn.fetch(
|
||||
"SELECT id::text, sequence, tee_time_override "
|
||||
"SELECT id::text, sequence, tee_time_override, start_hole "
|
||||
"FROM tournament_round_group WHERE tournament_round_id = $1 ORDER BY sequence",
|
||||
round_id,
|
||||
)
|
||||
|
|
@ -850,7 +873,11 @@ async def _fetch_round_groups(conn, round_id: str, round_row) -> RoundGroupsOut:
|
|||
groups = []
|
||||
for g in group_rows:
|
||||
tee_time = _compute_group_tee_time(
|
||||
round_row["scheduled_at"], round_row["tee_interval_minutes"], g["sequence"], g["tee_time_override"]
|
||||
round_row["scheduled_at"],
|
||||
round_row["tee_interval_minutes"],
|
||||
g["sequence"],
|
||||
g["tee_time_override"],
|
||||
round_row["start_mode"],
|
||||
)
|
||||
groups.append(
|
||||
RoundGroupOut(
|
||||
|
|
@ -858,9 +885,18 @@ async def _fetch_round_groups(conn, round_id: str, round_row) -> RoundGroupsOut:
|
|||
sequence=g["sequence"],
|
||||
tee_time=tee_time.isoformat() if tee_time is not None else None,
|
||||
tee_time_override=g["tee_time_override"].isoformat() if g["tee_time_override"] else None,
|
||||
start_hole=g["start_hole"],
|
||||
participants=by_group.get(g["id"], []),
|
||||
)
|
||||
)
|
||||
# Visningsrekkefølge (bruker, 2026-08-21): løpende start sorteres etter
|
||||
# klokkeslett (allerede = sequence-rekkefølgen for 'consecutive', siden
|
||||
# sequence 1 alltid er tidligst -- ingen endring der), shotgun sorteres
|
||||
# etter starthull i stedet. Ikke tildelt starthull ennå (None) sorteres
|
||||
# sist, ikke først -- en uferdig oppsatt gruppe skal ikke fortrenge
|
||||
# ferdig plasserte grupper i visningen.
|
||||
if round_row["start_mode"] == "shotgun":
|
||||
groups.sort(key=lambda gr: (gr.start_hole is None, gr.start_hole))
|
||||
return RoundGroupsOut(groups=groups, ungrouped=ungrouped)
|
||||
|
||||
|
||||
|
|
@ -920,7 +956,7 @@ async def suggest_round_groups(
|
|||
sequence = i // body.group_size + 1
|
||||
chunk = entries[i : i + body.group_size]
|
||||
tee_time = _compute_group_tee_time(
|
||||
round_row["scheduled_at"], round_row["tee_interval_minutes"], sequence, None
|
||||
round_row["scheduled_at"], round_row["tee_interval_minutes"], sequence, None, round_row["start_mode"]
|
||||
)
|
||||
groups.append(
|
||||
RoundGroupOut(
|
||||
|
|
@ -928,6 +964,12 @@ async def suggest_round_groups(
|
|||
sequence=sequence,
|
||||
tee_time=tee_time.isoformat() if tee_time is not None else None,
|
||||
tee_time_override=None,
|
||||
# Forslaget dekker KUN sammensetning (hvem spiller med
|
||||
# hvem) -- starthull for en shotgun-runde er fortsatt et
|
||||
# eget, manuelt valg organisator gjør etterpå, samme
|
||||
# "forslag å justere fra, ikke helautomatisk"-prinsipp
|
||||
# som selve gruppesammensetningen.
|
||||
start_hole=None,
|
||||
participants=chunk,
|
||||
)
|
||||
)
|
||||
|
|
@ -937,6 +979,7 @@ async def suggest_round_groups(
|
|||
class RoundGroupIn(BaseModel):
|
||||
sequence: int = Field(ge=1)
|
||||
tee_time_override: datetime | None = None
|
||||
start_hole: int | None = Field(default=None, ge=1, le=18)
|
||||
round_participant_ids: list[str]
|
||||
|
||||
|
||||
|
|
@ -978,14 +1021,15 @@ async def save_round_groups(
|
|||
group_row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO tournament_round_group
|
||||
(organization_id, tournament_round_id, sequence, tee_time_override)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
(organization_id, tournament_round_id, sequence, tee_time_override, start_hole)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
""",
|
||||
organization_id,
|
||||
round_id,
|
||||
g.sequence,
|
||||
g.tee_time_override,
|
||||
g.start_hole,
|
||||
)
|
||||
if g.round_participant_ids:
|
||||
await conn.execute(
|
||||
|
|
|
|||
|
|
@ -40,9 +40,15 @@ def _compute_tee_time(
|
|||
tee_interval_minutes: int | None,
|
||||
sequence: int,
|
||||
override: datetime | None,
|
||||
start_mode: str = "consecutive",
|
||||
) -> datetime | None:
|
||||
"""Shotgun (migrasjon 088, ADR-096): ALLE matcher i økten går ut
|
||||
SAMTIDIG -- intervallet/sequence er meningsløst (ulikt starthull,
|
||||
ikke ulikt klokkeslett), se match.start_hole i stedet."""
|
||||
if override is not None:
|
||||
return override
|
||||
if start_mode == "shotgun":
|
||||
return scheduled_at
|
||||
if scheduled_at is None or tee_interval_minutes is None:
|
||||
return None
|
||||
return scheduled_at + timedelta(minutes=(sequence - 1) * tee_interval_minutes)
|
||||
|
|
@ -64,6 +70,10 @@ class MatchCreate(BaseModel):
|
|||
sequence: int
|
||||
team_a_id: str
|
||||
team_b_id: str
|
||||
# Kun meningsfullt (og satt av organisator) når økten er 'shotgun' --
|
||||
# se _compute_tee_time. `None` for en 'consecutive'-økt, som fortsatt
|
||||
# bruker øktens EGET delte start_hole (uendret).
|
||||
start_hole: int | None = Field(default=None, ge=1, le=18)
|
||||
|
||||
|
||||
class MatchOut(BaseModel):
|
||||
|
|
@ -76,6 +86,7 @@ class MatchOut(BaseModel):
|
|||
points_side_b: float | None
|
||||
leading_side: str | None
|
||||
tee_time: datetime | None
|
||||
start_hole: int | None
|
||||
participants: list[MatchParticipantOut]
|
||||
|
||||
|
||||
|
|
@ -92,7 +103,7 @@ async def create_match(
|
|||
async with org_connection(organization_id) as conn, translate_db_errors():
|
||||
session = await conn.fetchrow(
|
||||
"""
|
||||
SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes
|
||||
SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes, start_mode
|
||||
FROM session WHERE id = $1
|
||||
""",
|
||||
session_id,
|
||||
|
|
@ -113,20 +124,25 @@ async def create_match(
|
|||
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO match (organization_id, session_id, sequence, team_a_id, team_b_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
INSERT INTO match (organization_id, session_id, sequence, team_a_id, team_b_id, start_hole)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id::text, sequence, team_a_id::text, team_b_id::text,
|
||||
status_text, points_side_a::float AS points_side_a,
|
||||
points_side_b::float AS points_side_b, leading_side, tee_time_override
|
||||
points_side_b::float AS points_side_b, leading_side, tee_time_override, start_hole
|
||||
""",
|
||||
organization_id,
|
||||
session_id,
|
||||
body.sequence,
|
||||
body.team_a_id,
|
||||
body.team_b_id,
|
||||
body.start_hole,
|
||||
)
|
||||
tee_time = _compute_tee_time(
|
||||
session["scheduled_at"], session["tee_interval_minutes"], row["sequence"], row["tee_time_override"]
|
||||
session["scheduled_at"],
|
||||
session["tee_interval_minutes"],
|
||||
row["sequence"],
|
||||
row["tee_time_override"],
|
||||
session["start_mode"],
|
||||
)
|
||||
return MatchOut(**dict(row), tee_time=tee_time, participants=[])
|
||||
|
||||
|
|
@ -141,7 +157,7 @@ async def fetch_matches(
|
|||
avslørte matcher, se blind_draw.py)."""
|
||||
session = await conn.fetchrow(
|
||||
"""
|
||||
SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes
|
||||
SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes, start_mode
|
||||
FROM session WHERE id = $1
|
||||
""",
|
||||
session_id,
|
||||
|
|
@ -153,7 +169,7 @@ async def fetch_matches(
|
|||
"""
|
||||
SELECT id::text, sequence, team_a_id::text, team_b_id::text,
|
||||
status_text, points_side_a::float AS points_side_a,
|
||||
points_side_b::float AS points_side_b, leading_side, tee_time_override
|
||||
points_side_b::float AS points_side_b, leading_side, tee_time_override, start_hole
|
||||
FROM match
|
||||
WHERE session_id = $1
|
||||
ORDER BY sequence
|
||||
|
|
@ -203,7 +219,7 @@ async def fetch_matches(
|
|||
)
|
||||
)
|
||||
|
||||
return [
|
||||
out = [
|
||||
MatchOut(
|
||||
id=m["id"],
|
||||
sequence=m["sequence"],
|
||||
|
|
@ -214,12 +230,23 @@ async def fetch_matches(
|
|||
points_side_b=m["points_side_b"],
|
||||
leading_side=m["leading_side"],
|
||||
tee_time=_compute_tee_time(
|
||||
session["scheduled_at"], session["tee_interval_minutes"], m["sequence"], m["tee_time_override"]
|
||||
session["scheduled_at"],
|
||||
session["tee_interval_minutes"],
|
||||
m["sequence"],
|
||||
m["tee_time_override"],
|
||||
session["start_mode"],
|
||||
),
|
||||
start_hole=m["start_hole"],
|
||||
participants=by_match[m["id"]],
|
||||
)
|
||||
for m in matches
|
||||
]
|
||||
# Visningsrekkefølge (bruker, 2026-08-21, ADR-096): løpende sorteres
|
||||
# etter klokkeslett (allerede = sequence-rekkefølgen for 'consecutive'),
|
||||
# shotgun sorteres etter starthull -- ikke tildelt (None) sist.
|
||||
if session["start_mode"] == "shotgun":
|
||||
out.sort(key=lambda mo: (mo.start_hole is None, mo.start_hole))
|
||||
return out
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
|
|||
|
|
@ -899,6 +899,12 @@ class SessionCreate(BaseModel):
|
|||
scheduled_at: datetime | None = None
|
||||
tee_interval_minutes: int | None = None
|
||||
start_hole: int = 1
|
||||
# Shotgun-start (migrasjon 088, ADR-096) -- samme mønster som
|
||||
# tournament_round for individuelle turneringer. 'consecutive'
|
||||
# (standard) = uendret oppførsel. 'shotgun' = alle matcher i økten
|
||||
# går ut SAMTIDIG, fra hvert sitt starthull (match.start_hole) i
|
||||
# stedet for staggert klokkeslett.
|
||||
start_mode: str = Field(default="consecutive", pattern="^(consecutive|shotgun)$")
|
||||
# KUN meningsfullt (og påkrevd) når format='shamble' -- antall av
|
||||
# sidens individuelle resultater som teller per hull ("beste N av M").
|
||||
shamble_best_n: int | None = Field(default=None, ge=1)
|
||||
|
|
@ -917,6 +923,7 @@ class SessionOut(BaseModel):
|
|||
scheduled_at: datetime | None
|
||||
tee_interval_minutes: int | None
|
||||
start_hole: int
|
||||
start_mode: str
|
||||
shamble_best_n: int | None
|
||||
locked_team_ids: list[str]
|
||||
revealed: bool
|
||||
|
|
@ -936,6 +943,7 @@ def _session_out(row, locked: set[str]) -> SessionOut:
|
|||
scheduled_at=row["scheduled_at"],
|
||||
tee_interval_minutes=row["tee_interval_minutes"],
|
||||
start_hole=row["start_hole"],
|
||||
start_mode=row["start_mode"],
|
||||
shamble_best_n=row["shamble_best_n"],
|
||||
locked_team_ids=sorted(locked),
|
||||
revealed=len(locked) >= 2,
|
||||
|
|
@ -951,7 +959,7 @@ async def _fetch_sessions(conn, tournament_id: str) -> list[SessionOut]:
|
|||
SELECT id::text, sequence, name, format, hole_config::text AS hole_config,
|
||||
course_id::text, points_per_match::float AS points_per_match,
|
||||
allowance_override::text AS allowance_override, scoring_mode,
|
||||
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n
|
||||
scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n
|
||||
FROM session
|
||||
WHERE tournament_id = $1
|
||||
ORDER BY sequence
|
||||
|
|
@ -1008,12 +1016,12 @@ async def create_session(
|
|||
INSERT INTO session
|
||||
(organization_id, tournament_id, sequence, name, format,
|
||||
hole_config, course_id, points_per_match, allowance_override, scoring_mode,
|
||||
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $13, $14)
|
||||
scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING id::text, sequence, name, format, hole_config::text AS hole_config,
|
||||
course_id::text, points_per_match::float AS points_per_match,
|
||||
allowance_override::text AS allowance_override, scoring_mode,
|
||||
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n
|
||||
scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n
|
||||
""",
|
||||
organization_id,
|
||||
tournament_id,
|
||||
|
|
@ -1028,6 +1036,7 @@ async def create_session(
|
|||
body.scheduled_at,
|
||||
body.tee_interval_minutes,
|
||||
body.start_hole,
|
||||
body.start_mode,
|
||||
body.shamble_best_n,
|
||||
)
|
||||
return _session_out(row, set())
|
||||
|
|
@ -1045,6 +1054,7 @@ class SessionUpdate(BaseModel):
|
|||
scheduled_at: datetime | None = None
|
||||
tee_interval_minutes: int | None = None
|
||||
start_hole: int | None = Field(default=None, ge=1, le=18)
|
||||
start_mode: str | None = Field(default=None, pattern="^(consecutive|shotgun)$")
|
||||
points_per_match: float | None = None
|
||||
allowance_override: dict | None = None
|
||||
course_id: str | None = None
|
||||
|
|
@ -1188,7 +1198,7 @@ async def update_session(
|
|||
SELECT id::text, sequence, name, format, hole_config::text AS hole_config,
|
||||
course_id::text, points_per_match::float AS points_per_match,
|
||||
allowance_override::text AS allowance_override, scoring_mode,
|
||||
scheduled_at, tee_interval_minutes, start_hole, shamble_best_n
|
||||
scheduled_at, tee_interval_minutes, start_hole, start_mode, shamble_best_n
|
||||
FROM session WHERE id = $1
|
||||
""",
|
||||
session_id,
|
||||
|
|
|
|||
|
|
@ -162,3 +162,56 @@ async def test_group_tee_time_override_wins_over_computed(pool):
|
|||
)
|
||||
assert result.groups[0].tee_time is not None
|
||||
assert "09:15" in result.groups[0].tee_time # override, IKKE den utledede 10:00
|
||||
|
||||
|
||||
async def test_shotgun_mode_gives_all_groups_the_same_tee_time(pool):
|
||||
"""Migrasjon 088, ADR-096 -- intervallet er meningsløst i shotgun,
|
||||
alle grupper går ut SAMTIDIG uansett sequence."""
|
||||
org_id = await create_org()
|
||||
tournament_id, round_id, rp_ids = await _setup(org_id, n_players=4)
|
||||
await update_round(
|
||||
tournament_id, round_id,
|
||||
TournamentRoundUpdate(
|
||||
scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10, start_mode="shotgun",
|
||||
),
|
||||
organization_id=org_id,
|
||||
)
|
||||
|
||||
result = await save_round_groups(
|
||||
tournament_id, round_id,
|
||||
SaveRoundGroupsIn(groups=[
|
||||
RoundGroupIn(sequence=1, start_hole=5, round_participant_ids=[rp_ids[0], rp_ids[1]]),
|
||||
RoundGroupIn(sequence=2, start_hole=1, round_participant_ids=[rp_ids[2], rp_ids[3]]),
|
||||
]),
|
||||
organization_id=org_id,
|
||||
)
|
||||
# Begge grupper går ut kl 08:00, uavhengig av sequence/intervall.
|
||||
for g in result.groups:
|
||||
assert g.tee_time is not None and "08:00" in g.tee_time
|
||||
|
||||
# Vises sortert etter STARTHULL (1 før 5), ikke etter sequence (1 før 2).
|
||||
assert [g.start_hole for g in result.groups] == [1, 5]
|
||||
|
||||
|
||||
async def test_consecutive_mode_still_sorts_by_sequence_not_start_hole(pool):
|
||||
"""Regresjonsvern -- start_hole (uansett verdi, normalt None) skal
|
||||
IKKE påvirke rekkefølgen når runden fortsatt er 'consecutive'."""
|
||||
org_id = await create_org()
|
||||
tournament_id, round_id, rp_ids = await _setup(org_id, n_players=4)
|
||||
await update_round(
|
||||
tournament_id, round_id,
|
||||
TournamentRoundUpdate(scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10),
|
||||
organization_id=org_id,
|
||||
)
|
||||
|
||||
result = await save_round_groups(
|
||||
tournament_id, round_id,
|
||||
SaveRoundGroupsIn(groups=[
|
||||
RoundGroupIn(sequence=1, round_participant_ids=[rp_ids[0], rp_ids[1]]),
|
||||
RoundGroupIn(sequence=2, round_participant_ids=[rp_ids[2], rp_ids[3]]),
|
||||
]),
|
||||
organization_id=org_id,
|
||||
)
|
||||
assert [g.sequence for g in result.groups] == [1, 2]
|
||||
assert "08:00" in result.groups[0].tee_time
|
||||
assert "08:10" in result.groups[1].tee_time
|
||||
|
|
|
|||
112
tests/test_shotgun_start_cup.py
Normal file
112
tests/test_shotgun_start_cup.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
Shotgun-start for Cup-/lagformatet (migrasjon 088, ADR-096) -- samme
|
||||
mønster som individuelle turneringers utslagsgrupper
|
||||
(test_round_groups.py), men for session/match. Bruker: "Det må komme
|
||||
frem om det er Shotgun eller løpende start... Sorteringen av
|
||||
startlisten gjøres etter utslagsform."
|
||||
"""
|
||||
|
||||
from app.routers.matches import MatchCreate, create_match, list_matches
|
||||
from app.routers.tournaments import SessionUpdate, update_session
|
||||
from app.auth import CurrentUser
|
||||
|
||||
import app.db as app_db
|
||||
|
||||
from tests.conftest import (
|
||||
add_membership,
|
||||
create_course,
|
||||
create_org,
|
||||
create_session,
|
||||
create_team,
|
||||
create_tournament,
|
||||
create_user,
|
||||
)
|
||||
|
||||
|
||||
async def _setup(org_id):
|
||||
owner_id = await create_user()
|
||||
await add_membership(org_id, owner_id, role="owner")
|
||||
tournament_id = await create_tournament(org_id, name="Cup-turnering")
|
||||
course_id = await create_course(org_id, name="Cup Links")
|
||||
session_id = await create_session(org_id, tournament_id, course_id, format="foursome")
|
||||
team_a = await create_team(org_id, tournament_id, name="Team Nord")
|
||||
team_b = await create_team(org_id, tournament_id, name="Team Sør")
|
||||
return owner_id, tournament_id, session_id, team_a, team_b
|
||||
|
||||
|
||||
async def test_shotgun_session_gives_all_matches_the_same_tee_time(pool):
|
||||
org_id = await create_org()
|
||||
owner_id, tournament_id, session_id, team_a, team_b = await _setup(org_id)
|
||||
|
||||
await update_session(
|
||||
session_id,
|
||||
SessionUpdate(scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10, start_mode="shotgun"),
|
||||
organization_id=org_id,
|
||||
)
|
||||
|
||||
m1 = (await create_match(
|
||||
session_id, MatchCreate(sequence=1, team_a_id=team_a, team_b_id=team_b, start_hole=5),
|
||||
organization_id=org_id,
|
||||
)).id
|
||||
m2 = (await create_match(
|
||||
session_id, MatchCreate(sequence=2, team_a_id=team_a, team_b_id=team_b, start_hole=1),
|
||||
organization_id=org_id,
|
||||
)).id
|
||||
|
||||
user = CurrentUser(user_id=owner_id)
|
||||
matches = await list_matches(session_id, organization_id=org_id, user=user)
|
||||
# Begge matcher går ut kl 08:00, uansett sequence/intervall.
|
||||
for m in matches:
|
||||
assert m.tee_time is not None and m.tee_time.isoformat().startswith("2026-08-19T08:00:00")
|
||||
# Vist sortert etter STARTHULL (1 før 5), ikke sequence (m1 før m2).
|
||||
assert [m.start_hole for m in matches] == [1, 5]
|
||||
assert [m.id for m in matches] == [m2, m1]
|
||||
|
||||
|
||||
async def test_consecutive_session_still_sorts_by_sequence(pool):
|
||||
org_id = await create_org()
|
||||
owner_id, tournament_id, session_id, team_a, team_b = await _setup(org_id)
|
||||
|
||||
await update_session(
|
||||
session_id,
|
||||
SessionUpdate(scheduled_at="2026-08-19T08:00:00", tee_interval_minutes=10),
|
||||
organization_id=org_id,
|
||||
)
|
||||
|
||||
m1 = (await create_match(
|
||||
session_id, MatchCreate(sequence=1, team_a_id=team_a, team_b_id=team_b),
|
||||
organization_id=org_id,
|
||||
)).id
|
||||
m2 = (await create_match(
|
||||
session_id, MatchCreate(sequence=2, team_a_id=team_a, team_b_id=team_b),
|
||||
organization_id=org_id,
|
||||
)).id
|
||||
|
||||
user = CurrentUser(user_id=owner_id)
|
||||
matches = await list_matches(session_id, organization_id=org_id, user=user)
|
||||
assert [m.id for m in matches] == [m1, m2]
|
||||
assert matches[0].tee_time.isoformat().startswith("2026-08-19T08:00:00")
|
||||
assert matches[1].tee_time.isoformat().startswith("2026-08-19T08:10:00")
|
||||
|
||||
|
||||
async def test_match_tee_time_override_wins_even_in_shotgun_mode(pool):
|
||||
org_id = await create_org()
|
||||
owner_id, tournament_id, session_id, team_a, team_b = await _setup(org_id)
|
||||
|
||||
await update_session(
|
||||
session_id,
|
||||
SessionUpdate(scheduled_at="2026-08-19T08:00:00", start_mode="shotgun"),
|
||||
organization_id=org_id,
|
||||
)
|
||||
match_id = (await create_match(
|
||||
session_id, MatchCreate(sequence=1, team_a_id=team_a, team_b_id=team_b, start_hole=9),
|
||||
organization_id=org_id,
|
||||
)).id
|
||||
async with app_db.org_connection(org_id) as conn:
|
||||
await conn.execute(
|
||||
"UPDATE match SET tee_time_override = '2026-08-19T09:30:00' WHERE id = $1", match_id
|
||||
)
|
||||
|
||||
user = CurrentUser(user_id=owner_id)
|
||||
matches = await list_matches(session_id, organization_id=org_id, user=user)
|
||||
assert matches[0].tee_time.isoformat().startswith("2026-08-19T09:30:00")
|
||||
Loading…
Reference in a new issue