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:
parent
d51c0153bb
commit
459f3f8e14
6 changed files with 322 additions and 6 deletions
|
|
@ -265,7 +265,12 @@
|
|||
"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(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": [
|
||||
"/opt/teeoff/deploy",
|
||||
|
|
|
|||
75
CLAUDE.md
75
CLAUDE.md
|
|
@ -965,6 +965,81 @@ Ferdig og verifisert:
|
|||
**Hele "bygg i rekkefølgen ting brukes"-serien for match-play-flyten er
|
||||
dermed komplett:** oppsett (lag/roster) → program → blind draw →
|
||||
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:
|
||||
1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
(`/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)
|
||||
|
|
@ -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
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -326,6 +326,23 @@ async def import_official_course(
|
|||
body: OfficialCourseImport,
|
||||
organization_id: str = Depends(get_authorized_org),
|
||||
) -> 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:
|
||||
facility = await teeoff_client.get_facility(body.facility_slug)
|
||||
except teeoff_client.TeeoffNotFoundError:
|
||||
|
|
@ -372,9 +389,13 @@ async def import_official_course(
|
|||
"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)
|
||||
|
||||
# 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():
|
||||
course_row = await conn.fetchrow(
|
||||
f"""
|
||||
|
|
@ -383,7 +404,7 @@ async def import_official_course(
|
|||
RETURNING {_COURSE_COLUMNS}
|
||||
""",
|
||||
organization_id,
|
||||
course_data.get("name") or facility.get("name"),
|
||||
course_name,
|
||||
external_ref,
|
||||
)
|
||||
course_id = course_row["id"]
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ async def _compute_hole_results(conn, match_id: str, match) -> list[HoleResult]:
|
|||
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(
|
||||
"""
|
||||
SELECT m.id::text, s.format, s.scoring_mode,
|
||||
|
|
@ -311,7 +311,7 @@ async def submit_hole_score(
|
|||
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))
|
||||
|
||||
|
||||
|
|
@ -386,7 +386,7 @@ async def submit_hole_result(
|
|||
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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ from ..auth import get_authorized_org
|
|||
from ..blind_draw import locked_team_ids
|
||||
from ..db import org_connection
|
||||
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
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -542,6 +544,190 @@ async def create_session(
|
|||
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 må flyttes til en tilsvarende tee på den
|
||||
nye for at handicap-beregningen skal bli riktig videre.
|
||||
|
||||
Matcher på tee-navn + kjønn (case-insensitive). Finnes ingen tilsvarende
|
||||
tee på 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) må 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.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue