Auf Vorgabe des Auftraggebers umgestellt:
* genau eine Meisterschaft je Saison (unique auf saison_jahr)
* turnier.meisterschaft_id (nullable FK) statt bool zaehlt_meisterschaft;
NULL = "o. M.". Kompat-Property turnier.zaehlt_meisterschaft bleibt.
* Teilwertungen (Erwachsene-Gesamt, Jugend J1-J5) sind (Kategorie, Klasse)-
Sichten auf die eine Meisterschaft, keine eigenen Zeilen mehr
* meisterschaft_stand: kategorie + klasse_id + Unique je Wertung/Fahrer
* berechne_stand(kategorie=, klasse_id=), wertungen_fuer(), berechne_alle_wertungen()
* Read-API: /meisterschaften/{id}/stand[?wertung=], .../endlauf-qualifikation?wertung=
* Ingest: POST /turniere haengt zaehlende Turniere an die Jahres-Meisterschaft
* Initialmigration neu erzeugt (Projekt noch nicht ausgerollt); Seed: eine
"HTH-Meisterschaft <jahr>"
* Tests angepasst/ergaenzt (61 gruen)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
112 lines
4.3 KiB
Python
112 lines
4.3 KiB
Python
"""Meisterschaftsberechnung (AP 3) — eine Meisterschaft pro Saison, mehrere
|
|
Wertungen darin. PLATZHALTER-Punktetabelle (O-1 offen)."""
|
|
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
|
|
from oamc.meisterschaft.berechnung import (
|
|
berechne_stand,
|
|
gewertete_laeufe,
|
|
punkte_fuer_platz,
|
|
wertungen_fuer,
|
|
)
|
|
from oamc.models.enums import Kategorie, VeranstaltungStatus
|
|
from tests import factories as f
|
|
|
|
|
|
def test_punkte_fuer_platz_platzhalter():
|
|
assert punkte_fuer_platz(1) == Decimal(25)
|
|
assert punkte_fuer_platz(10) == Decimal(6)
|
|
assert punkte_fuer_platz(99) == Decimal(1)
|
|
assert punkte_fuer_platz(None) == Decimal(0)
|
|
|
|
|
|
def test_streichresultate_tabelle():
|
|
tab = {"12": 8, "9": 7, "4": 2}
|
|
assert gewertete_laeufe(12, tab) == 8
|
|
assert gewertete_laeufe(10, tab) == 7 # naechstkleinerer Schluessel (9)
|
|
assert gewertete_laeufe(3, tab) == 3 # unterhalb der Tabelle: kein Streichresultat
|
|
|
|
|
|
def test_eine_meisterschaft_pro_saison_mit_wertungen(seed_2026):
|
|
db = seed_2026
|
|
m = f.meisterschaft(db)
|
|
wertungen = wertungen_fuer(db, m.id)
|
|
codes = {w.code for w in wertungen}
|
|
assert "erwachsene" in codes
|
|
assert {"J 1", "J 2", "J 3", "J 4", "J 5"} <= codes
|
|
|
|
|
|
def _lauf(db, m, k, ergebnisse, *, datum, zaehlt=True):
|
|
"""ergebnisse = [(fahrer, platz), ...]"""
|
|
v = f.veranstaltung(db, datum=datum, status=VeranstaltungStatus.FINAL)
|
|
t = f.turnier(db, v, meisterschaft=m if zaehlt else None)
|
|
for i, (fahrer, platz) in enumerate(ergebnisse, start=1):
|
|
s = f.start(db, t, fahrer, k, startnummer=i)
|
|
e = f.ergebnis(db, s, zeit=Decimal("100"), fehler=0)
|
|
e.platzierung = platz
|
|
db.flush()
|
|
return t
|
|
|
|
|
|
def test_stand_summiert_punkte_und_zaehlt_siege(seed_2026):
|
|
db = seed_2026
|
|
m = f.meisterschaft(db)
|
|
k = f.klasse(db, "J 3")
|
|
a = f.fahrer(db, "Sieger", "Sina", 2014)
|
|
b = f.fahrer(db, "Zweite", "Zoe", 2014)
|
|
_lauf(db, m, k, [(a, 1), (b, 2)], datum=date(2026, 4, 1))
|
|
_lauf(db, m, k, [(a, 1), (b, 2)], datum=date(2026, 5, 1))
|
|
bericht = berechne_stand(db, m.id, kategorie=Kategorie.JUGEND, klasse_id=k.id)
|
|
zeilen = {z.fahrer_id: z for z in bericht.zeilen}
|
|
assert zeilen[a.id].platz == 1
|
|
assert zeilen[a.id].punkte_gesamt == Decimal(50) # 2x Platz 1 = 2x25
|
|
assert zeilen[a.id].anzahl_siege == 2
|
|
assert bericht.hinweise # O-1 Platzhalter-Warnung
|
|
assert bericht.wertung == "J 3"
|
|
|
|
|
|
def test_wertungen_sind_getrennt(seed_2026):
|
|
db = seed_2026
|
|
m = f.meisterschaft(db)
|
|
j3, j4 = f.klasse(db, "J 3"), f.klasse(db, "J 4")
|
|
kind_j3 = f.fahrer(db, "Drei", "D", 2014)
|
|
kind_j4 = f.fahrer(db, "Vier", "V", 2012)
|
|
v = f.veranstaltung(db, status=VeranstaltungStatus.FINAL)
|
|
t = f.turnier(db, v, meisterschaft=m)
|
|
for i, (fahrer, kl) in enumerate([(kind_j3, j3), (kind_j4, j4)], start=1):
|
|
s = f.start(db, t, fahrer, kl, startnummer=i)
|
|
e = f.ergebnis(db, s, zeit=Decimal("100"), fehler=0)
|
|
e.platzierung = 1
|
|
db.flush()
|
|
stand_j3 = berechne_stand(db, m.id, kategorie=Kategorie.JUGEND, klasse_id=j3.id)
|
|
stand_j4 = berechne_stand(db, m.id, kategorie=Kategorie.JUGEND, klasse_id=j4.id)
|
|
assert [z.fahrer_id for z in stand_j3.zeilen] == [kind_j3.id]
|
|
assert [z.fahrer_id for z in stand_j4.zeilen] == [kind_j4.id]
|
|
|
|
|
|
def test_tie_break_majoritaet_der_siege(seed_2026):
|
|
db = seed_2026
|
|
m = f.meisterschaft(db)
|
|
k = f.klasse(db, "J 3")
|
|
a = f.fahrer(db, "Aaa", "A", 2014)
|
|
b = f.fahrer(db, "Bbb", "B", 2014)
|
|
_lauf(db, m, k, [(a, 1), (b, 6)], datum=date(2026, 4, 1)) # A:25 B:10
|
|
_lauf(db, m, k, [(b, 1), (a, 6)], datum=date(2026, 5, 1)) # B:25 A:10
|
|
_lauf(db, m, k, [(a, 1), (b, 1)], datum=date(2026, 6, 1)) # beide 25, beide Sieg
|
|
_lauf(db, m, k, [(a, 1), (b, 4)], datum=date(2026, 6, 15)) # A:25 (Sieg) B:13
|
|
bericht = berechne_stand(db, m.id, kategorie=Kategorie.JUGEND, klasse_id=k.id)
|
|
assert bericht.zeilen[0].fahrer_id == a.id
|
|
assert bericht.zeilen[0].anzahl_siege > bericht.zeilen[1].anzahl_siege
|
|
|
|
|
|
def test_laeufe_ohne_meisterschaft_zaehlen_nicht(seed_2026):
|
|
db = seed_2026
|
|
m = f.meisterschaft(db)
|
|
k = f.klasse(db, "J 3")
|
|
a = f.fahrer(db, "Solo", "S", 2014)
|
|
_lauf(db, m, k, [(a, 1)], datum=date(2026, 4, 1), zaehlt=False)
|
|
bericht = berechne_stand(db, m.id, kategorie=Kategorie.JUGEND, klasse_id=k.id)
|
|
assert bericht.laeufe_gesamt == 0
|
|
assert bericht.zeilen == []
|