Bland org-turnering-runder inn i personlig statistikk-dashboard (ADR-105)

Punkt 3 i scoring-paritet-gap-listen: GET /rounds/stats/summary leste
tidligere kun frittstående runder. Ny _fetch_tournament_stats_rows()
speiler hole_history.py sin N+1-per-org-henting og blander resultatet
inn i samme by_round/stat_level_by_round/played_at_by_round-dict-er som
personlige rader -- ingen endring i selve aggregeringslogikken
(_resolve_stats_window/_build_round_series/_summarize_rounds er allerede
generiske). Kilde-antall vises separat i UI (samme presedens som
hole_history.py sin times_personal/times_tournament).

5 nye pytest-tester + full regresjon (193/193), tsc rent, full-stack
scratch-verifisert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-22 17:15:05 +02:00
parent 5ef8d155e7
commit c5b5f441da
8 changed files with 464 additions and 17 deletions

View file

@ -539,7 +539,10 @@
"Bash(python3 -m py_compile tests/test_account_merge.py)",
"Bash(mkdir -p \"/opt/teecup/frontend/app/kontosammenslaing/[token]\")",
"Bash(grep -n 'href={`/tournaments/\\\\|Link.*tournaments/\\\\${' components/tournament-detail.tsx)",
"Bash(grep -n \"fetch\\(\\\\|getJson\\(\\\\|baseUrl}\\\\`\\\\|\\\\`/rounds/\\\\${roundId}\" /opt/teecup/frontend/components/round-detail.tsx)"
"Bash(grep -n \"fetch\\(\\\\|getJson\\(\\\\|baseUrl}\\\\`\\\\|\\\\`/rounds/\\\\${roundId}\" /opt/teecup/frontend/components/round-detail.tsx)",
"Bash(TEECUP_API_ORIGIN=\"http://127.0.0.1:8102\" npx next build)",
"Bash(cp -r public .next/standalone/public)",
"Bash(cp -r .next/static .next/standalone/.next/static)"
],
"additionalDirectories": [
"/opt/teeoff/deploy",

View file

@ -10524,6 +10524,98 @@ Alle scratch-ressurser ryddet opp.
---
## ADR-105: Personlig statistikk-dashboard blander inn org-turnering-runder (2026-08-22)
**Kontekst:** punkt 3 i "Scoring-paritet: frittstående runde vs.
org-turnering"-gap-listen (FEATURE_BACKLOG.md), bedt om av bruker rett
etter ADR-104. `GET /rounds/stats/summary` (`/my-rounds/stats`)
aggregerte tidligere KUN `round_hole`/`round_participant` -- en spillers
GIR%/putting/fairway-trender fra org-turneringer vistes aldri der.
**Beslutning A -- bland tallene, vis kilde-antall separat (bruker-
bekreftet via AskUserQuestion).** Org-turnering-hull blandes rett inn i
de SAMME trend-graf-/snitt-tallene som frittstående runder (ingen
visuell markering i selve grafene, ikke filtrerbart), men
`RoundStatsSummary` fikk to nye felt --
`rounds_completed_personal`/`rounds_completed_tournament` -- vist som en
parentes i "Oversikt"-seksjonens ingress ("N fullførte runder (X
frittstående + Y turnering)"). Speiler eksakt presedensen i
`app/hole_history.py` sin `times_personal`/`times_tournament` (ADR-072).
**Beslutning B -- ingen endring i selve aggregeringslogikken.**
`_resolve_stats_window`/`_build_round_series`/`_summarize_rounds`
(`rounds.py`) er allerede fullt generiske over dict-nøklede runde-data
(aksesserer felt via `h["felt"]`, likegyldig om raden kom fra
`round_hole` eller `tournament_round_hole`) -- kun datahenting/
sammenslåing i selve endepunktet ble utvidet. `_summarize_rounds` fikk
ett nytt parameter, `source_by_round`, brukt UTELUKKENDE til å telle de
to nye kilde-antall-feltene.
**Beslutning C -- ny `_fetch_tournament_stats_rows()`, speiler
`hole_history.py` sin N+1-per-org-henting.** Samme mønster som
`fetch_tournament_hole_history` (`plain_connection()` →
`player_organizations_for_user``org_connection()` per org), men uten
course-key-filtreringen (ikke nødvendig -- stats trenger ikke
cross-system bane-identitet, kun ALLE en brukers turnering-runder).
Namespacet nøkkel (`f"t:{trp_id}"`) slås inn i de SAMME
`by_round`/`stat_level_by_round`/`played_at_by_round`-dict-ene som de
personlige radene, ingen egen kodesti for turnering-siden i selve
aggregeringen.
**Beslutning D -- "fullført runde" avgjøres ved radtelling, ikke en
kolonne.** `tournament_round` har INGEN `completed_at`-kolonne (bekreftet
mot `040_individual_tournaments.sql`). En turneringsrunde regnes som
fullført når antall `tournament_round_hole`-rader for deltakeren er lik
`played_hole_numbers(hole_config)` sin lengde (9/9/18) -- samme
radtellings-prinsipp `individual_tournaments.py` allerede bruker andre
steder (flaggturnering-resultatet). En rad uten `tr.scheduled_at`
(nullable) ekskluderes helt fra stats (kan ikke plasseres kronologisk
eller i et vindu uten dato) -- ikke bare fra sortering slik
`hole_history.py` sin `NULLS LAST` gjør, siden vindu-logikken her
faktisk krever en dato. `hole.par` (gjeldende, ikke et snapshot) leses
via samme join som `hole_history.py` bruker -- akseptert, allerede
presedert begrensning.
**Beslutning E -- sortering må gjøres eksplisitt etter sammenslåing.**
`_resolve_stats_window` krever `ordered_rounds` sortert NYEST FØRST.
Før denne endringen kunne koden stole på at SQL-ens `ORDER BY
r.played_at DESC` bevarte riktig innsettingsrekkefølge i
Python-dict-en. Etter sammenslåing av to kilder stemmer ikke det lenger
-- `ordered_rounds` sorteres nå eksplisitt (`sorted(..., reverse=True)`)
i stedet for å stole på innsettingsrekkefølge.
**Uttrykkelig utenfor omfang:** ingen ny per-turnering-runde
stats-side (`round-stats.tsx`-ekvivalent for én `tournament_round`) --
punkt 3 slik gap-listen beskriver det er spesifikt AGGREGAT-dashbordet.
Ingen migrasjon -- alle kolonner fantes fra før (migrasjon 075).
**Status:** 5 nye pytest-tester (blanding av kilder, strokes_only-
turneringsrunde ekskludert fra putt-snittet, ufullført turneringsrunde
ekskludert, runde uten `scheduled_at` ekskludert, RLS -- runde i en org
brukeren ikke er spiller i ekskludert) + full regresjonssuite, 193/193
grønt mot en scratch-database. `tsc --noEmit` rent.
Full-stack scratch-verifisert (egen db + to one-off-containere for
teecup_api/teecup_frontend, ekte innlogging, Chrome DevTools MCP) --
funnet OG rettet underveis: frontend-imagets `next.config.mjs`
`rewrites()` leses ved BUILD-tid for standalone-output (dokumentert i
`frontend/Dockerfile`s egen kommentar), IKKE ved container-oppstart --
en første one-off frontend-container bygget uten riktig
`--build-arg TEECUP_API_ORIGIN` proxyet derfor tre innloggingsforsøk til
den EKTE, kjørende `teecup_api` (2×422 ugyldig e-postdomene, 1×401
ukjent bruker) før feilen ble oppdaget. Ingen skriving skjedde (begge
feilkodene er før/uten DB-skriving, rate-limiteren er kun en in-memory-
teller), men containeren ble stanset umiddelbart og brukeren varslet.
Rettet ved å bygge frontend-imaget på nytt med riktig
`--build-arg TEECUP_API_ORIGIN=http://<scratch-api-container>:8000`.
Etter fiksen: bruker med runder i to ULIKE organisasjoner + én
frittstående runde -- dashbordet viste riktig "3 fullførte runder (1
frittstående + 2 turnering)", riktig blandet snitt/GIR/putt-tall, ingen
konsollfeil. Mørk modus + smal mobilskjerm (375px) bekreftet lesbart.
Alle scratch-ressurser (db, rolle, containere, images) ryddet opp.
---
## Utviklingsplan (rekkefølge)
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)

View file

@ -14555,3 +14555,72 @@ Neste steg:
kun `docker compose build teecup_frontend && up -d`, container
frisk, rene logger. `https://teecup.golf/health` 200 etter
utrulling.
163. **Personlig statistikk-dashboard blander inn org-turnering-runder
(ADR-105), 2026-08-22 -- punkt 3 i gap-listen, bedt om rett etter
ADR-104, med punkt 2 (live push) varslet som neste.** `GET
/rounds/stats/summary` (`/my-rounds/stats`) aggregerte tidligere KUN
frittstående runder -- en spillers GIR%/putting/fairway-trender fra
org-turneringer vistes aldri der.
**Design-avklaring (AskUserQuestion, bekreftet):** bland tallene inn
i de eksisterende trend-/snitt-tallene (ingen visuell markering),
men vis kilde-antall separat -- speiler `hole_history.py` (ADR-072)
sin `times_personal`/`times_tournament`. Ny `RoundStatsSummary`-felt
`rounds_completed_personal`/`rounds_completed_tournament`, vist som
"(X frittstående + Y turnering)" i Oversikt-ingressen.
**Backend:** ny `_fetch_tournament_stats_rows()` i `rounds.py`,
speiler `hole_history.py` sin N+1-per-org-henting
(`player_organizations_for_user` → `org_connection` per org), uten
course-key-filtreringen (ikke nødvendig her). Resultatet slås inn i
de SAMME `by_round`/`stat_level_by_round`/`played_at_by_round`-
dict-ene som personlige rader (namespacet nøkkel `f"t:{trp_id}"`) --
`_resolve_stats_window`/`_build_round_series`/`_summarize_rounds`
trengte INGEN endring i selve regnelogikken (kun ett nytt
`source_by_round`-parameter til `_summarize_rounds` for de to nye
kilde-antall-feltene). `tournament_round` har ingen `completed_at`
-- "fullført" avgjøres ved radtelling mot
`played_hole_numbers(hole_config)`, samme prinsipp
`individual_tournaments.py` bruker for flaggturnering-resultatet.
Rader uten `tr.scheduled_at` ekskluderes helt (kan ikke plasseres i
et tidsvindu uten dato). `ordered_rounds`-sorteringen måtte gjøres
eksplisitt (kunne før stole på SQL-ens `ORDER BY` som
innsettingsrekkefølge -- stemmer ikke lenger etter sammenslåing av
to kilder).
**Frontend:** `ApiStatsSummary`-typen fikk de to nye feltene,
`rounds-stats-summary.tsx` sin "Aggregert over N fullførte
runder"-setning utvidet med kilde-parentesen når
`rounds_completed_tournament > 0`. Ingen ny komponent.
**Fant og rettet underveis i full-stack scratch-verifiseringen:**
en første one-off scratch-frontend-container ble bygget UTEN riktig
`--build-arg TEECUP_API_ORIGIN` -- `next.config.mjs` sin
`rewrites()` leses ved BUILD-tid for standalone-output (dokumentert
i `frontend/Dockerfile`s egen kommentar), IKKE ved
container-oppstart som antatt. Containeren proxyet derfor tre
innloggingsforsøk til den EKTE, kjørende `teecup_api` (2×422 ugyldig
e-postdomene, 1×401 ukjent testbruker) før feilen ble oppdaget.
Ingen skriving til ekte `teecup_db` skjedde (begge feilkodene
inntreffer før/uten DB-skriving; rate-limiteren er en ren
in-memory-teller) -- containeren ble stanset umiddelbart og
brukeren varslet direkte i økten. Rettet ved å bygge
scratch-frontend-imaget på nytt med riktig
`--build-arg TEECUP_API_ORIGIN` pekt mot scratch-API-containeren.
**Verifisert:** 5 nye pytest-tester (kildeblanding,
strokes_only-turneringsrunde ekskludert fra putt-snittet, ufullført
turneringsrunde ekskludert, runde uten `scheduled_at` ekskludert,
RLS -- runde i en org brukeren ikke er spiller i ekskludert) + full
regresjonssuite, 193/193 grønt. `tsc --noEmit` rent. Full-stack
scratch (egen db + to one-off-containere for
teecup_api/teecup_frontend, ekte innlogging) med en bruker som har
runder i TO ulike organisasjoner + én frittstående runde: dashbordet
viste riktig "3 fullførte runder (1 frittstående + 2 turnering)",
riktig blandet snitt/GIR/putt, ingen konsollfeil. Mørk modus + smal
mobilskjerm (375px) bekreftet lesbart. Alle scratch-ressurser (db,
rolle, containere, images) ryddet opp.
**Ikke rullet ut ennå** -- venter på brukerens bekreftelse (kun
backend-endring denne gangen, i motsetning til ADR-102/103/104).

View file

@ -4959,14 +4959,15 @@ runde HAR, org-turnering MANGLER, om ikke annet er sagt):**
kringkaster allerede (`broadcast_live_update` i
`individual_tournaments.py`), men kun til den OFFENTLIGE
tilskuersiden, ikke den autentiserte. Antatt **liten/middels**.
3. **Personlig stats-dashboard ekskluderer turneringsrunder helt.**
`/rounds/stats/summary`-spørringen (`rounds.py`) leser KUN
`round_participant`/`round_hole` -- `tournament_round_participant`/
`tournament_round_hole` er aldri med. En spillers GIR%/putting/
fairway-trender fra org-turneringer vises aldri i eget stats-bilde,
og org-turneringer har ingen egen tilsvarende endepunkt/side i det
hele tatt. Antatt **middels/stor** -- ekte datamodell-brobygging
(avstemme `stat_level`-forskjeller), ikke bare gjenbruk av UI.
3. ✅ **Personlig stats-dashboard blander nå inn turneringsrunder --
FERDIG BYGGET 2026-08-22 (ADR-105), se CHANGELOG punkt 163.**
`GET /rounds/stats/summary` henter nå ny `_fetch_tournament_stats_rows()`
(speiler `hole_history.py` sin N+1-per-org-henting) og blander inn i
de samme trend-/snitt-tallene som frittstående runder, med kilde-
antall vist separat ("N fullførte runder (X frittstående + Y
turnering)"), samme presedens som `hole_history.py` sin
`times_personal`/`times_tournament`. Ikke rullet ut ennå -- venter på
brukerens bekreftelse.
4. ✅ **Fullt flerspiller-scorekort-grid -- FERDIG BYGGET OG RULLET UT
2026-08-22 (ADR-104), se CHANGELOG punkt 162.** Ny `TournamentScorecardGrid`
(ekte alle-spillere×alle-hull-matrise, samme struktur som

View file

@ -49,7 +49,7 @@ from pydantic import BaseModel, EmailStr, Field
from .. import golfapi_cache, golfapi_client, storage, teeoff_client
from ..auth import CurrentUser, get_current_user, get_current_user_from_websocket, get_current_user_optional
from ..config import settings
from ..db import plain_connection
from ..db import org_connection, plain_connection
from ..email import RoundSummaryHole, send_round_summary_email
from ..errors import app_error, translate_db_errors
from ..handicap import SIDE_IS_UNIT, parse_allowance_config
@ -62,6 +62,7 @@ from ..realtime import broadcast_round_update, live_sockets_for_round
from ..target_points import TargetPoint, get_target_points
from .auth import _hash_secret
from .notifications import create_notification
from .scoring import played_hole_numbers
from handicap_engine import (
HoleResult,
PerPlayerPercentage,
@ -1870,6 +1871,12 @@ class RoundStatsSeriesPoint(BaseModel):
class RoundStatsSummary(BaseModel):
rounds_completed: int
# Kilde-nedbrytning av `rounds_completed` -- org-turnering-runder
# blandes inn i alle tallene under (ADR: se CHANGELOG), men antallet
# per kilde vises separat, samme prinsipp som `hole_history.py` sin
# `times_personal`/`times_tournament`.
rounds_completed_personal: int
rounds_completed_tournament: int
avg_score_to_par: float | None
avg_putts_per_18: float | None
putts_tracked_rounds: int
@ -2038,6 +2045,7 @@ def _summarize_rounds(
by_round: dict[str, list],
stat_level_by_round: dict[str, str],
played_at_by_round: dict[str, date],
source_by_round: dict[str, str],
) -> RoundStatsSummary:
"""Samme formler som `round-stats.tsx` sin `computeStats()` bruker per
enkeltrunde, pooler kun holdene fra `round_ids` -- gjenbrukt for BÅDE
@ -2125,6 +2133,8 @@ def _summarize_rounds(
return RoundStatsSummary(
rounds_completed=len(round_ids),
rounds_completed_personal=sum(1 for rid in round_ids if source_by_round.get(rid) == "personal"),
rounds_completed_tournament=sum(1 for rid in round_ids if source_by_round.get(rid) == "tournament"),
avg_score_to_par=avg(to_par_values),
avg_putts_per_18=avg(putt_totals),
putts_tracked_rounds=len(putt_totals),
@ -2160,6 +2170,83 @@ def _summarize_rounds(
)
_TOURNAMENT_HOLE_COUNT_BY_CONFIG = {"full_18": 18, "front_9": 9, "back_9": 9}
async def _fetch_tournament_stats_rows(user_id: str) -> list[dict]:
"""Speiler `hole_history.py` sin `fetch_tournament_hole_history`
N+1-per-org-mønster (`player_organizations_for_user` ->
`org_connection` per org), men henter ALLE en brukers turnering-hull
tvers av org-er i stedet for ett hullnummer for én bane.
`tournament_round` har ingen `completed_at`-kolonne (bekreftet mot
040_individual_tournaments.sql) -- "fullført runde" avgjøres i
stedet ved rad-telling per `tournament_round_participant_id` mot
`played_hole_numbers(hole_config)`, samme prinsipp som
`individual_tournaments.py` sin `holes_completed`/flag-resultat
bruker andre steder i kodebasen.
"""
async with plain_connection() as conn:
org_rows = await conn.fetch(
"SELECT organization_id::text AS organization_id FROM player_organizations_for_user($1)",
user_id,
)
rows: list[dict] = []
for org_row in org_rows:
async with org_connection(org_row["organization_id"]) as conn:
trh_rows = await conn.fetch(
"""
SELECT trp.id::text AS trp_id, tr.hole_config::text AS hole_config,
tp.stat_level, tr.scheduled_at,
h.par, trh.gross_strokes AS score, trh.putts,
trh.tee_shot_result, trh.approach_result, trh.chip_count,
trh.bunker_shot_count, trh.penalty_strokes, trh.anyway_strokes
FROM tournament_round_hole trh
JOIN tournament_round_participant trp ON trp.id = trh.tournament_round_participant_id
JOIN tournament_round tr ON tr.id = trp.tournament_round_id
JOIN tournament_participant tp ON tp.id = trp.tournament_participant_id
JOIN player p ON p.id = tp.player_id
JOIN hole h ON h.course_id = tr.course_id AND h.hole_number = trh.hole_number
WHERE p.user_id = $1
""",
user_id,
)
by_trp: dict[str, list] = {}
for r in trh_rows:
by_trp.setdefault(r["trp_id"], []).append(r)
for trp_id, holes in by_trp.items():
hole_config = holes[0]["hole_config"]
expected = _TOURNAMENT_HOLE_COUNT_BY_CONFIG.get(hole_config, 18)
if len(holes) < expected:
continue # runden er ikke fullført ennå
scheduled_at = holes[0]["scheduled_at"]
if scheduled_at is None:
continue # kan ikke plasseres kronologisk uten dato
for h in holes:
rows.append(
{
"round_id": f"t:{trp_id}",
"stat_level": h["stat_level"],
"played_at": scheduled_at.date(),
"source": "tournament",
"par": h["par"],
"played": True,
"score": h["score"],
"putts": h["putts"],
"tee_shot_result": h["tee_shot_result"],
"approach_result": h["approach_result"],
"chip_count": h["chip_count"],
"bunker_shot_count": h["bunker_shot_count"],
"penalty_strokes": h["penalty_strokes"],
"anyway_strokes": h["anyway_strokes"],
}
)
return rows
@router.get("/rounds/stats/summary", response_model=RoundStatsWindowSummary)
async def get_rounds_stats_summary(
window: StatsWindow = Query("all"),
@ -2202,22 +2289,40 @@ async def get_rounds_stats_summary(
by_round: dict[str, list] = {}
stat_level_by_round: dict[str, str] = {}
played_at_by_round: dict[str, date] = {}
source_by_round: dict[str, str] = {}
for r in rows:
by_round.setdefault(r["round_id"], []).append(r)
stat_level_by_round[r["round_id"]] = r["stat_level"]
played_at_by_round[r["round_id"]] = r["played_at"]
source_by_round[r["round_id"]] = "personal"
# Distinkte runder, nyest først -- rekkefølgen `rows` allerede har
# (ORDER BY r.played_at DESC) bevares siden Python-dicts er innsettings-
# ordnet, ingen ny sortering nødvendig.
ordered_rounds = [(rid, played_at_by_round[rid]) for rid in by_round]
# Org-turnering-runder (ADR: se CHANGELOG) blandes inn i de SAMME
# dict-ene -- `_resolve_stats_window`/`_summarize_rounds`/
# `_build_round_series` forblir uendret, de aksesserer felt via
# `h["felt"]` og bryr seg ikke om raden kom fra `round_hole` eller
# `tournament_round_hole`.
for r in await _fetch_tournament_stats_rows(user.user_id):
rid = r["round_id"]
by_round.setdefault(rid, []).append(r)
stat_level_by_round[rid] = r["stat_level"]
played_at_by_round[rid] = r["played_at"]
source_by_round[rid] = r["source"]
# Nyest først -- må sorteres eksplisitt nå (kunne før stole på at
# SQL-en sin ORDER BY ga riktig innsettingsrekkefølge, men det
# gjelder ikke lenger etter sammenslåingen over).
ordered_rounds = sorted(
((rid, played_at_by_round[rid]) for rid in by_round),
key=lambda pair: pair[1],
reverse=True,
)
current_ids, previous_ids = _resolve_stats_window(window, ordered_rounds)
return RoundStatsWindowSummary(
window=window,
current=_summarize_rounds(current_ids, by_round, stat_level_by_round, played_at_by_round),
previous=_summarize_rounds(previous_ids, by_round, stat_level_by_round, played_at_by_round),
current=_summarize_rounds(current_ids, by_round, stat_level_by_round, played_at_by_round, source_by_round),
previous=_summarize_rounds(previous_ids, by_round, stat_level_by_round, played_at_by_round, source_by_round),
round_series=_build_round_series(current_ids, by_round, stat_level_by_round, played_at_by_round),
)

View file

@ -40,6 +40,8 @@ type ApiRoundSeriesPoint = {
type ApiStatsSummary = {
rounds_completed: number
rounds_completed_personal: number
rounds_completed_tournament: number
avg_score_to_par: number | null
avg_putts_per_18: number | null
putts_tracked_rounds: number
@ -835,6 +837,9 @@ export function RoundsStatsSummary() {
<>
<p className="text-base leading-relaxed text-muted-foreground text-pretty">
Aggregert over {s.rounds_completed} {s.rounds_completed === 1 ? "fullført runde" : "fullførte runder"}
{s.rounds_completed_tournament > 0 && (
<> ({s.rounds_completed_personal} frittstående + {s.rounds_completed_tournament} turnering)</>
)}
{p.rounds_completed > 0 && (
<>
{" "}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,172 @@
"""
GET /rounds/stats/summary skal blande org-turnering-runder inn i de samme
trend-/snitt-tallene som frittstående runder (scoring-paritet-gap punkt 3,
se FEATURE_BACKLOG.md), med kilde-antall vist separat
(`rounds_completed_personal`/`rounds_completed_tournament`) -- speiler
`app/hole_history.py` sin `times_personal`/`times_tournament`-presedens.
Speiler `test_hole_history.py` sitt integrasjonsmønster (ekte router-
funksjoner, ikke SQL for selve scoringen) for personal/tournament-
scoring, men mot `get_rounds_stats_summary` i stedet for hull-historikk.
"""
from app.auth import CurrentUser
from app.routers.individual_tournaments import (
HoleUpdate as TournamentHoleUpdate,
update_hole as tournament_update_hole,
)
from app.routers.rounds import HoleUpdate as RoundHoleUpdate, get_rounds_stats_summary, update_hole as round_update_hole
from tests.conftest import (
add_membership,
create_hole,
create_org,
create_org_hole,
create_course,
create_participant,
create_player,
create_round,
create_tee,
create_tournament,
create_tournament_participant,
create_tournament_round,
create_tournament_round_participant,
create_user,
)
import app.db as app_db
async def _score_personal_hole(round_id, participant_id, user_id, hole_number, score, putts=None):
user = CurrentUser(user_id=user_id)
await round_update_hole(
round_id, participant_id, hole_number,
RoundHoleUpdate(score=score, putts=putts, expected_version=None),
user=user,
)
async def _score_tournament_hole(tournament_id, round_id, rp_id, org_id, user_id, hole_number, score, putts=None):
user = CurrentUser(user_id=user_id)
await tournament_update_hole(
tournament_id, round_id, rp_id, hole_number,
TournamentHoleUpdate(gross_strokes=score, putts=putts, expected_version=None),
organization_id=org_id, user=user,
)
async def _make_completed_personal_round(user_id, *, score_per_hole, putts_per_hole=None, par=4,
stat_level="strokes_and_putts"):
async with app_db.plain_connection() as conn:
round_id = await create_round(conn, user_id)
participant_id = await create_participant(conn, round_id, user_id=user_id, is_owner=True)
for n in range(1, 19):
await create_hole(conn, participant_id, hole_number=n, par=par, stroke_index=n)
await conn.execute("UPDATE round_participant SET stat_level = $1 WHERE id = $2", stat_level, participant_id)
for n in range(1, 19):
await _score_personal_hole(
round_id, participant_id, user_id, n, score_per_hole,
putts=putts_per_hole,
)
async with app_db.plain_connection() as conn:
await conn.execute("UPDATE round SET completed_at = now() WHERE id = $1", round_id)
return round_id, participant_id
async def _make_tournament_round(
user_id, *, score_per_hole, putts_per_hole=None, par=4, holes_scored=18,
stat_level="strokes_and_putts", set_scheduled_at=True, org_id=None,
):
if org_id is None:
org_id = await create_org()
await add_membership(org_id, user_id, role="member")
tournament_id = await create_tournament(org_id)
course_id = await create_course(org_id)
for n in range(1, 19):
await create_org_hole(org_id, course_id, hole_number=n, par=par, stroke_index=n)
tee_id = await create_tee(org_id, course_id)
tournament_round_id = await create_tournament_round(org_id, tournament_id, course_id)
if set_scheduled_at:
async with app_db.org_connection(org_id) as conn:
await conn.execute("UPDATE tournament_round SET scheduled_at = now() WHERE id = $1", tournament_round_id)
player_id = await create_player(org_id, user_id=user_id)
tp_id = await create_tournament_participant(org_id, tournament_id, player_id)
async with app_db.org_connection(org_id) as conn:
await conn.execute("UPDATE tournament_participant SET stat_level = $1 WHERE id = $2", stat_level, tp_id)
rp_id = await create_tournament_round_participant(org_id, tournament_round_id, tp_id, tee_id)
for n in range(1, holes_scored + 1):
await _score_tournament_hole(
tournament_id, tournament_round_id, rp_id, org_id, user_id, n, score_per_hole,
putts=putts_per_hole,
)
return org_id, tournament_round_id, rp_id
async def test_blends_personal_and_tournament_rounds(pool):
user_id = await create_user()
await _make_completed_personal_round(user_id, score_per_hole=5, putts_per_hole=2, par=4) # +18 til par
await _make_tournament_round(user_id, score_per_hole=4, putts_per_hole=2, par=4) # +0 til par
out = await get_rounds_stats_summary(window="all", user=CurrentUser(user_id=user_id))
assert out.current.rounds_completed == 2
assert out.current.rounds_completed_personal == 1
assert out.current.rounds_completed_tournament == 1
assert out.current.avg_score_to_par == 9.0 # (18 + 0) / 2
async def test_tournament_strokes_only_round_excluded_from_putts(pool):
user_id = await create_user()
await _make_completed_personal_round(user_id, score_per_hole=4, putts_per_hole=2, par=4)
await _make_tournament_round(
user_id, score_per_hole=4, putts_per_hole=None, par=4, stat_level="strokes_only",
)
out = await get_rounds_stats_summary(window="all", user=CurrentUser(user_id=user_id))
assert out.current.rounds_completed == 2
assert out.current.rounds_completed_tournament == 1
# Kun den frittstående runden sporet putter -- strokes_only-turnerings-
# runden skal IKKE forurense putt-snittet (samme regel som gjelder for
# en frittstående strokes_only-runde).
assert out.current.putts_tracked_rounds == 1
assert out.current.avg_putts_per_18 == 36.0
async def test_incomplete_tournament_round_is_excluded(pool):
user_id = await create_user()
await _make_completed_personal_round(user_id, score_per_hole=4, putts_per_hole=2, par=4)
await _make_tournament_round(user_id, score_per_hole=4, holes_scored=10) # kun 10 av 18 hull
out = await get_rounds_stats_summary(window="all", user=CurrentUser(user_id=user_id))
assert out.current.rounds_completed == 1
assert out.current.rounds_completed_personal == 1
assert out.current.rounds_completed_tournament == 0
async def test_tournament_round_without_scheduled_at_is_excluded(pool):
user_id = await create_user()
await _make_completed_personal_round(user_id, score_per_hole=4, putts_per_hole=2, par=4)
await _make_tournament_round(user_id, score_per_hole=4, set_scheduled_at=False)
out = await get_rounds_stats_summary(window="all", user=CurrentUser(user_id=user_id))
assert out.current.rounds_completed == 1
assert out.current.rounds_completed_tournament == 0
async def test_tournament_round_in_foreign_org_without_player_row_is_excluded(pool):
"""`player_organizations_for_user` returnerer kun org-er brukeren har en
`player`-rad i -- en fullført turneringsrunde i en org der brukeren
IKKE er spiller skal aldri kunne telles med i brukerens egen statistikk."""
user_id = await create_user()
other_user_id = await create_user()
await _make_completed_personal_round(user_id, score_per_hole=4, putts_per_hole=2, par=4)
# Turneringsrunde scoret for en ANNEN bruker, i en org `user_id` ikke er medlem/spiller i.
await _make_tournament_round(other_user_id, score_per_hole=4, putts_per_hole=2, par=4)
out = await get_rounds_stats_summary(window="all", user=CurrentUser(user_id=user_id))
assert out.current.rounds_completed == 1
assert out.current.rounds_completed_tournament == 0