Backend er live og grundig verifisert med nøyaktig scenarioet du beskrev (10 hull spilt under feil bane, matchen avgjort, rettet til riktig bane — tee-er byttet, handicap regnet på nytt, matchstatus oppdatert, de faktiske slagene urørt). .md-filene er oppdatert.

Ikke bygget ennå: selve rediger-/slett-knappene i program-skjermen — det er ren backend-kapasitet nå. Vil du at jeg skriver en V0-prompt for det, eller er det noe annet du vil ta fatt på?
This commit is contained in:
Erol Haagenrud 2026-07-18 22:21:22 +02:00
parent d51c0153bb
commit 459f3f8e14
6 changed files with 322 additions and 6 deletions

View file

@ -265,7 +265,12 @@
"Bash(docker exec teeoff_db psql -U teeoff_admin -d teecup_scratch -c ' *)", "Bash(docker exec teeoff_db psql -U teeoff_admin -d teecup_scratch -c ' *)",
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/lb_cookies.txt)", "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/lb_cookies.txt)",
"Bash(mkdir -p /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/v0-zip-8)", "Bash(mkdir -p /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/v0-zip-8)",
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/lb2_cookies.txt)" "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/lb2_cookies.txt)",
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/idem_cookies.txt)",
"Bash(docker exec teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 -c ' *)",
"Bash(python3 -m py_compile /opt/teecup/app/routers/tournaments.py /opt/teecup/app/routers/scoring.py)",
"Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/patch_cookies.txt)",
"Bash(awk 'NR==499,/^---$/{print NR\": \"$0}' /opt/teecup/FEATURE_BACKLOG.md)"
], ],
"additionalDirectories": [ "additionalDirectories": [
"/opt/teeoff/deploy", "/opt/teeoff/deploy",

View file

@ -965,6 +965,81 @@ Ferdig og verifisert:
**Hele "bygg i rekkefølgen ting brukes"-serien for match-play-flyten er **Hele "bygg i rekkefølgen ting brukes"-serien for match-play-flyten er
dermed komplett:** oppsett (lag/roster) → program → blind draw → dermed komplett:** oppsett (lag/roster) → program → blind draw →
scorekort → leaderboard. scorekort → leaderboard.
- **Offisiell bane-import: idempotent + tydeligere navn, LIVE (2026-07-18),
rapportert av brukeren som faktisk brukte funksjonen på ekte
produksjonsdata:** to reelle problemer, begge funnet ved å lese (kun
lesing) de faktiske dataene for "De Gamle er Eldst" FØR noe ble antatt.
1. **`POST .../courses/official-import` var IKKE idempotent** — å
importere samme bane på nytt (helt vanlig: flere økter spilles ofte
på samme bane) ga en 409 `DUPLICATE`-feil (migrasjon 010 sin sperre)
i stedet for å bare gi tilbake den allerede importerte banen. Fikset:
sjekker nå `external_course_ref` FØR noe teeoff-kall gjøres — finnes
banen fra før, returneres den eksisterende raden direkte (også
raskere, og robust mot at teeoff er nede akkurat da).
2. **Lagret navn var kun selve banens navn ("Hovedbanen"), ikke hvilken
klubb** — ubrukelig til å skille baner fra hverandre, siden mange
klubber navngir hovedbanen sin identisk. Fikset: navnet kombineres nå
til "{anlegg} {bane}" (f.eks. "Tjøme Golfklubb Hovedbanen") ved
import.
**Reell konsekvens av hull #1 funnet i produksjonsdata:** brukeren hadde,
mens hen forsøkte å søke opp Tjøme via det ØVERSTE banefeltet (som søker
organisasjonens EGNE baner, ikke teeoff), ved et uhell trigget «Opprett
ny bane: «Tj»»-snarveien og fått en tom, søppel `custom`-bane hengende på
Foursome-økten -- en ekte forvekslingsfelle mellom de to adskilte
bane-søkeflatene (eget vs. teeoff), ikke en kodefeil i seg selv.
**Data ryddet opp i EKTE `teecup_db`, bruker bekreftet eksplisitt:**
Foursome-økten pekt om til den allerede importerte, ekte Tjøme-banen
(samme bane som Fourball-økten allerede brukte -- nøyaktig det
organisatoren egentlig ønsket), søppel-«Tj»-banen slettet (bekreftet
ingen matcher/tee-er/hull hang på den FØR sletting), og den ekte banens
navn oppdatert til "Tjøme Golfklubb Hovedbanen". Verifisert i etterkant
at begge økter nå peker til samme, korrekt navngitte bane.
**Verifisert mot ekte teeoff_api i scratch FØR utrulling:** importerte
Borregaard på nytt to ganger — andre kallet ga nøyaktig samme course-id
(ikke en duplikat-rad), navnet kom ut som "Borregaard Golfklubb
Hovedbanen". `test_isolation.sql` 12/12. Ingen migrasjon, kun `teecup_api`
redeployet, `teeoff.no` upåvirket.
- **`PATCH`/`DELETE` for økter, LIVE (2026-07-18), samme dag:** brukeren
spurte rett etter opprydningen om det i det hele tatt var mulig å
rette/slette en feiloppsatt økt — det var det ikke (kun `POST`/`GET`
fantes). Ny `PATCH /orgs/{id}/sessions/{id}` (`app/routers/
tournaments.py`) for enkle felt (navn, klokkeslett/intervall, starthull,
poeng, handicap-brytere) via vanlig `exclude_unset`-mønster, PLUSS en egen
gren for bane-bytte. Ny `DELETE /orgs/{id}/sessions/{id}` — kun tomme
økter (ingen matcher), avviser med 409 ellers (bruk PATCH til å korrigere
i stedet).
**Banebytte-scenarioet brukeren selv reiste** ("5 hull spilt, oppdager
feil bane — slagene er ekte, utregningen er trolig feil") krevde egen
design: `match_participant.tee_id` peker til en tee som HØRER til den
gamle banen. Løst med `_remap_course()` — finner en tee med samme
navn+kjønn på den nye banen for hver allerede tillagte deltaker, flytter
dem dit, og avviser HELE bane-byttet tydelig (400, ingenting skrevet,
bekreftet transaksjonell rollback) hvis den nye banen mangler en
tilsvarende tee. Etter et vellykket bytte: handicap regnes om for alle
berørte deltakere, og matchstatus/poeng regnes om for HVER match i
økten — **bevisst uavhengig av om matchen allerede er avgjort** (brukeren
bekreftet eksplisitt at en bane-korrigering skal kunne endre et allerede
cachet resultat). `recompute_and_cache_match_state` i `app/routers/
scoring.py` gjort delt (fjernet ledende understrek) for gjenbruk fra
tournaments.py.
**Reelt, urelatert funn underveis i scratch-testingen, IKKE fikset:**
`tee_rating` lages i dag ALLTID kun med `full_18`-omfang (både ved
teeoff-import og manuell tee-opprettelse) — en økt satt til `front_9`/
`back_9` i `stroke`-modus kan derfor ALDRI få handicap beregnet
(`compute_and_store_side_handicaps` sin `tee_rating`-join finner aldri
noen rad), og dermed aldri avgjøre noen hull. `hole_result`-modus
upåvirket. Flagget til bruker, bevisst latt urørt denne runden.
**Verifisert grundig i scratch:** enkelt feltbytte (kun navn), fullt
banebytte-scenario bygget nøyaktig som brukerens eksempel (10 hull spilt
under feil bane, matchen allerede avgjort 10&8, PATCH til riktig bane →
tee-er ombyttet korrekt, handicap endret fra 7/9 til 10/13 under den nye
banens rating, matchstatus regnet på nytt med UENDREDE rå slagtall),
avvist bane-bytte ved manglende tee-match (bekreftet full rollback,
også av det urelaterte navnefeltet i samme kall), DELETE avvist på økt
med match (409) og godtatt på tom økt (204). `test_isolation.sql` 12/12.
Ingen migrasjon, kun `teecup_api` redeployet, `teeoff.no` upåvirket.
Frontend (rediger-/slett-knapper i program-skjermen) ikke bygget ennå —
kun backend-kapasiteten denne runden.
Neste steg: Neste steg:
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet. 1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.

View file

@ -564,6 +564,19 @@ andre skjermen når den ble generert i en egen prompt.
(`teecup_api`, `teecup_frontend`) bygget og redeployet, live sjekker OK (`teecup_api`, `teecup_frontend`) bygget og redeployet, live sjekker OK
(`/health`, `/dashboard` → 200), `teeoff.no` upåvirket. (`/health`, `/dashboard` → 200), `teeoff.no` upåvirket.
**Nytt 2026-07-18, ✅ BACKEND LIVE (frontend gjenstår):** `PATCH`/`DELETE`
for økter (`app/routers/tournaments.py`) — kunne tidligere verken rettes
eller slettes etter opprettelse. `DELETE` kun for tomme økter (409 hvis den
har matcher). `PATCH` dekker enkle felt fritt, pluss en egen, forsiktig
gren for bane-bytte (finner/flytter tilsvarende tee per allerede tillagt
deltaker, regner om handicap+matchstatus for hele økten etterpå — også for
allerede AVGJORTE matcher, bekreftet eksplisitt av bruker). Se
CLAUDE.md-status for det fulle scenarioet (verifisert med et 10-hulls
avgjort-match-eksempel) og et urelatert funn (`front_9`/`back_9` +
`stroke`-modus kan aldri få handicap i dag, siden `tee_rating` alltid kun
lages med `full_18`-omfang). Rediger-/slett-UI i program-skjermen ikke
bygget ennå.
--- ---
### Offisiell banedata fra teeoff — ✅ BYGGET OG LIVE 2026-07-18 (ADR-019) ### Offisiell banedata fra teeoff — ✅ BYGGET OG LIVE 2026-07-18 (ADR-019)
@ -610,6 +623,22 @@ vasket bort `?org=...`-parameteren fra URL-en. Fikset ved å fjerne det
indre `<form>`-elementet (vanlig `<div>` + Enter-tast/knapp-klikk i indre `<form>`-elementet (vanlig `<div>` + Enter-tast/knapp-klikk i
stedet). Se CLAUDE.md-status for full root cause. stedet). Se CLAUDE.md-status for full root cause.
**Nok en reell bug funnet og fikset samme uke, rapportert fra ekte bruk mot
`teecup.teeoff.no`:** import av samme teeoff-bane til flere økter (helt
normalt — flere runder spilles ofte på samme bane) ga en 409-feil i stedet
for å bare gjenbruke banen, og det lagrede navnet ("Hovedbanen" alene) ga
ingen måte å se hvilken klubb det gjaldt. Fikset: `POST .../courses/
official-import` er nå idempotent (gir tilbake eksisterende rad ved
reimport, sjekket FØR teeoff-kallet), og navnet lagres nå som
"{anlegg} {bane}". Ekte produksjonsdata for "De Gamle er Eldst" ryddet
opp (en økt hadde ved et uhell fått en tom søppel-`custom`-bane — se
CLAUDE.md-status for full hendelse og rotårsak).
**Åpent, ikke løst i denne runden:** de to bane-søkeflatene (øverste felt
= organisasjonens egne baner + "opprett ny"-snarvei, "Hent bane fra
teeoff"-knappen lenger ned = offisielt søk) er lette å forveksle — det var
nettopp dette som forårsaket søppel-banen. Vurder en tydeligere UI-
sammenslåing eller rekkefølge-endring i en senere runde.
--- ---
## UX / frontend (senere fase) ## UX / frontend (senere fase)

View file

@ -326,6 +326,23 @@ async def import_official_course(
body: OfficialCourseImport, body: OfficialCourseImport,
organization_id: str = Depends(get_authorized_org), organization_id: str = Depends(get_authorized_org),
) -> Course: ) -> Course:
external_ref = f"{body.facility_slug}:{body.teeoff_course_id}"
# Idempotent: importerer organisatoren samme bane på nytt (vanlig --
# flere økter spilles ofte på samme bane), gi tilbake den EKSISTERENDE
# raden i stedet for en 409-feil. Sjekket FØR noe teeoff-kall gjøres,
# både raskere og mer robust mot at teeoff er nede akkurat da. Fant
# og fikset 2026-07-18 etter en reell brukerrapport: duplikat-sperren
# (migrasjon 010) var riktig i prinsippet, men ga ingen vei videre for
# den helt vanlige "bruk samme bane på runde 2" siden av saken.
async with org_connection(organization_id) as conn:
existing = await conn.fetchrow(
f"SELECT {_COURSE_COLUMNS} FROM course WHERE external_course_ref = $1",
external_ref,
)
if existing is not None:
return Course(**dict(existing))
try: try:
facility = await teeoff_client.get_facility(body.facility_slug) facility = await teeoff_client.get_facility(body.facility_slug)
except teeoff_client.TeeoffNotFoundError: except teeoff_client.TeeoffNotFoundError:
@ -372,9 +389,13 @@ async def import_official_course(
"Banen har ingen tee med registrert rating/slope i teeoff ennå.", "Banen har ingen tee med registrert rating/slope i teeoff ennå.",
) )
external_ref = f"{body.facility_slug}:{body.teeoff_course_id}"
par_total = sum(h["par"] for h in holes) par_total = sum(h["par"] for h in holes)
# Navn kombinerer anlegg + bane -- "Hovedbanen" alene er tvetydig (mange
# klubber navngir hovedbanen sin nøyaktig likt). Fant og fikset samme
# runde som idempotent-fiksen over, samme bakenforliggende brukerrapport.
course_name = f"{facility.get('name')} {course_data.get('name') or 'Bane'}"
async with org_connection(organization_id) as conn, translate_db_errors(): async with org_connection(organization_id) as conn, translate_db_errors():
course_row = await conn.fetchrow( course_row = await conn.fetchrow(
f""" f"""
@ -383,7 +404,7 @@ async def import_official_course(
RETURNING {_COURSE_COLUMNS} RETURNING {_COURSE_COLUMNS}
""", """,
organization_id, organization_id,
course_data.get("name") or facility.get("name"), course_name,
external_ref, external_ref,
) )
course_id = course_row["id"] course_id = course_row["id"]

View file

@ -148,7 +148,7 @@ async def _compute_hole_results(conn, match_id: str, match) -> list[HoleResult]:
return results return results
async def _recompute_and_cache_match_state(conn, match_id: str) -> None: async def recompute_and_cache_match_state(conn, match_id: str) -> None:
match = await conn.fetchrow( match = await conn.fetchrow(
""" """
SELECT m.id::text, s.format, s.scoring_mode, SELECT m.id::text, s.format, s.scoring_mode,
@ -311,7 +311,7 @@ async def submit_hole_score(
body.gross_strokes, body.gross_strokes,
) )
await _recompute_and_cache_match_state(conn, match_id) await recompute_and_cache_match_state(conn, match_id)
return HoleScoreOut(**dict(row)) return HoleScoreOut(**dict(row))
@ -386,7 +386,7 @@ async def submit_hole_result(
body.winning_side, body.winning_side,
) )
await _recompute_and_cache_match_state(conn, match_id) await recompute_and_cache_match_state(conn, match_id)
return HoleResultOut(**dict(row)) return HoleResultOut(**dict(row))

View file

@ -17,6 +17,8 @@ from ..auth import get_authorized_org
from ..blind_draw import locked_team_ids from ..blind_draw import locked_team_ids
from ..db import org_connection from ..db import org_connection
from ..errors import app_error, translate_db_errors from ..errors import app_error, translate_db_errors
from ..handicap import compute_and_store_side_handicaps, parse_allowance_config
from .scoring import recompute_and_cache_match_state
from .. import storage from .. import storage
router = APIRouter() router = APIRouter()
@ -542,6 +544,190 @@ async def create_session(
return _session_out(row, set()) return _session_out(row, set())
class SessionUpdate(BaseModel):
"""Bevisst IKKE format/scoring_mode/hole_config ennå -- disse har dypere
strukturelle konsekvenser for allerede registrerte scores (bytter man
scoring_mode havner gamle oppføringer i feil tabell; bytter man format
endres reglene for match_participant) som fortjener egen, senere design.
course_id er den ENESTE feltet som krever spesialhåndtering her (se
_remap_course) -- resten er en vanlig exclude_unset-oppdatering."""
name: str | None = None
scheduled_at: datetime | None = None
tee_interval_minutes: int | None = None
start_hole: int | None = Field(default=None, ge=1, le=18)
points_per_match: float | None = None
allowance_override: dict | None = None
course_id: str | None = None
async def _remap_course(conn, session_id: str, new_course_id: str) -> None:
"""Reist av brukeren (2026-07-18): en organisator oppdager MIDT I RUNDEN
at feil bane var satt opp -- de faktisk registrerte slagene er ekte og
skal IKKE røres, men `match_participant.tee_id` peker til en tee som
hører til DEN GAMLE banen, og flyttes til en tilsvarende tee den
nye for at handicap-beregningen skal bli riktig videre.
Matcher tee-navn + kjønn (case-insensitive). Finnes ingen tilsvarende
tee den nye banen for én eller flere spillere, avvises HELE
bane-byttet tydelig FØR noe skrives -- heller stoppe enn å gjette feil
(samme "fail loudly" prinsipp som ADR-019 sin importvalidering).
"""
course_exists = await conn.fetchval("SELECT id FROM course WHERE id = $1", new_course_id)
if course_exists is None:
raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
participants = await conn.fetch(
"""
SELECT mp.id::text AS id, t.name AS tee_name, t.gender AS tee_gender
FROM match_participant mp
JOIN match m ON m.id = mp.match_id
JOIN tee t ON t.id = mp.tee_id
WHERE m.session_id = $1
""",
session_id,
)
remap: dict[str, str] = {}
missing: set[str] = set()
for p in participants:
new_tee_id = await conn.fetchval(
"""
SELECT id::text FROM tee
WHERE course_id = $1 AND lower(name) = lower($2) AND gender IS NOT DISTINCT FROM $3
""",
new_course_id,
p["tee_name"],
p["tee_gender"],
)
if new_tee_id is None:
missing.add(p["tee_name"])
else:
remap[p["id"]] = new_tee_id
if missing:
raise app_error(
400,
"VALIDATION_FAILED",
"Den nye banen mangler en tee som matcher: "
+ ", ".join(sorted(missing))
+ ". Legg til tilsvarende tee på den nye banen før du bytter.",
)
for participant_id, new_tee_id in remap.items():
await conn.execute("UPDATE match_participant SET tee_id = $1 WHERE id = $2", new_tee_id, participant_id)
await conn.execute("UPDATE session SET course_id = $1 WHERE id = $2", new_course_id, session_id)
async def _recompute_session_matches(
conn, session_id: str, format_: str, hole_config: str, allowance_override: dict | None
) -> None:
"""Regner om handicap + matchstatus for HVER match i økten -- brukt etter
et bane- eller allowance-bytte. Bevisst UAVHENGIG av om en match allerede
er avgjort (brukeren bekreftet eksplisitt: en bane-korrigering skal kunne
endre et allerede cachet resultat, ikke bare blokkeres av den vanlige
ALREADY_DECIDED-sperren som gjelder NYE slag-innsendinger)."""
config = parse_allowance_config(format_, allowance_override)
matches = await conn.fetch("SELECT id::text AS id FROM match WHERE session_id = $1", session_id)
for m in matches:
for side in ("a", "b"):
await compute_and_store_side_handicaps(conn, m["id"], side, format_, hole_config, config)
await recompute_and_cache_match_state(conn, m["id"])
@router.patch("/orgs/{organization_id}/sessions/{session_id}", response_model=SessionOut)
async def update_session(
session_id: str,
body: SessionUpdate,
organization_id: str = Depends(get_authorized_org),
) -> SessionOut:
updates = body.model_dump(exclude_unset=True)
if not updates:
raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.")
async with org_connection(organization_id) as conn, translate_db_errors():
session = await conn.fetchrow(
"SELECT format, hole_config::text AS hole_config, course_id::text AS course_id FROM session WHERE id = $1",
session_id,
)
if session is None:
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
new_course_id = updates.pop("course_id", None)
allowance_or_points_changed = "allowance_override" in updates or "points_per_match" in updates
if updates:
set_clauses = []
values: list = []
for i, (key, value) in enumerate(updates.items(), start=1):
if key == "allowance_override":
set_clauses.append(f"allowance_override = ${i}::jsonb")
values.append(json.dumps(value) if value is not None else None)
else:
set_clauses.append(f"{key} = ${i}")
values.append(value)
values.append(session_id)
await conn.execute(
f"UPDATE session SET {', '.join(set_clauses)} WHERE id = ${len(values)}",
*values,
)
if new_course_id is not None and new_course_id != session["course_id"]:
await _remap_course(conn, session_id, new_course_id)
allowance_or_points_changed = True # banebytte krever alltid omregning
if allowance_or_points_changed:
current = await conn.fetchrow(
"SELECT allowance_override::text AS allowance_override FROM session WHERE id = $1",
session_id,
)
current_override = json.loads(current["allowance_override"]) if current["allowance_override"] else None
await _recompute_session_matches(
conn, session_id, session["format"], session["hole_config"], current_override
)
row = await conn.fetchrow(
"""
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
FROM session WHERE id = $1
""",
session_id,
)
locked = await locked_team_ids(conn, session_id)
return _session_out(row, locked)
@router.delete("/orgs/{organization_id}/sessions/{session_id}", status_code=204)
async def delete_session(
session_id: str,
organization_id: str = Depends(get_authorized_org),
) -> None:
"""Kun tomme økter kan slettes (ingen matcher opprettet ennå) -- dekker
"jeg satte opp feil økt, fjern den" FØR den er tatt i bruk. En økt med
matcher (og dermed potensielt registrerte scores/poeng) rettes med
PATCH (bane-/felt-korrigering) i stedet for å kunne slettes -- hindrer
at noen ved et uhell sletter ekte kamper/resultater."""
async with org_connection(organization_id) as conn:
session = await conn.fetchrow("SELECT id FROM session WHERE id = $1", session_id)
if session is None:
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
match_count = await conn.fetchval("SELECT count(*) FROM match WHERE session_id = $1", session_id)
if match_count > 0:
raise app_error(
409,
"VALIDATION_FAILED",
"Økten har allerede matcher og kan ikke slettes -- bruk PATCH for å korrigere den i stedet.",
)
await conn.execute("DELETE FROM session WHERE id = $1", session_id)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Sponsorer (ADR-018) -- navn+lenke+logo, alle aktive fra MinIO-runden. # Sponsorer (ADR-018) -- navn+lenke+logo, alle aktive fra MinIO-runden.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------