Erstumsetzung: OAMC Turnierauswertung (AP 0–7, 9)
* Datenmodell + Alembic-Initialmigration (Plan §4), Seed Saison 2026 * Wertungs-Engine (Plan §3.3), regelbasiert je Kategorie/Saison - Abnahmetest gegen die Ergebnisliste 2026-05-31 * Meisterschaftsberechnung mit Streichresultaten + Tie-Break (PLATZHALTER-Punktetabelle, O-1 offen) * Ingest-API (idempotent, dry_run) inkl. Excel-Leser und Fahrer-Matching/Merge * Zeitmessungs-Endpoint (Batch, idempotent, unzugeordnet/unplausibel) + SSE-Anzeige - Mess-Pi-Client bewusst NICHT enthalten (docs/zeitmessung-endpoint.md) * Frontend: öffentliche Seiten + internes Backend (Jinja2/HTMX, kein Build) * Auslieferung: Docker (multi-arch) + docker-compose sowie Debian-Paket/APT-Repo * 57 Tests grün gegen echte PostgreSQL-Testdatenbank Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""Start (eine Teilnahme) und Ergebnis (1:1 zum Start).
|
||||
|
||||
Schluessel eines Ergebnisses ist (Turnier, Start), NICHT (Turnier, Fahrer) — ein
|
||||
Fahrer kann am selben Tag mehrfach starten (Plan §3.3 Mehrfachstarts).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
Numeric,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from oamc.models.base import Base, sa_enum
|
||||
from oamc.models.enums import StartStatus
|
||||
|
||||
|
||||
class Start(Base):
|
||||
__tablename__ = "start"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("turnier_id", "startnummer", name="start_turnier_startnummer"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
turnier_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("turnier.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
fahrer_id: Mapped[int] = mapped_column(ForeignKey("fahrer.id"), nullable=False, index=True)
|
||||
klasse_id: Mapped[int] = mapped_column(ForeignKey("klasse.id"), nullable=False)
|
||||
wertungsklasse_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("wertungsklasse.id", ondelete="SET NULL"), index=True
|
||||
)
|
||||
startnummer: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
fahrzeug: Mapped[str | None] = mapped_column(String(200))
|
||||
nenngeld_bezahlt: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
status: Mapped[StartStatus] = mapped_column(
|
||||
sa_enum(StartStatus, "start_status"), nullable=False, default=StartStatus.GENANNT
|
||||
)
|
||||
nennreihenfolge: Mapped[int | None] = mapped_column(Integer) # nummerierte Nennungsliste
|
||||
|
||||
turnier: Mapped[Turnier] = relationship(back_populates="starts") # noqa: F821
|
||||
fahrer: Mapped[Fahrer] = relationship(back_populates="starts") # noqa: F821
|
||||
klasse: Mapped[Klasse] = relationship() # noqa: F821
|
||||
wertungsklasse: Mapped[Wertungsklasse | None] = relationship( # noqa: F821
|
||||
back_populates="starts"
|
||||
)
|
||||
ergebnis: Mapped[Ergebnis | None] = relationship(
|
||||
back_populates="start", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
zeitmessungen: Mapped[list[Zeitmessung]] = relationship( # noqa: F821
|
||||
back_populates="start"
|
||||
)
|
||||
|
||||
@property
|
||||
def gewertet(self) -> bool:
|
||||
return self.status in (StartStatus.GESTARTET,)
|
||||
|
||||
|
||||
class Ergebnis(Base):
|
||||
"""1:1 zum Start — getrennte Tabelle fuer saubere Nachtraege/Korrekturen.
|
||||
|
||||
Zwei Quellen (Plan §3.6): ``summe_zeit`` aus ``zeitmessung`` summiert,
|
||||
``summe_fehler`` manuell erfasst. Ein "halbes" Ergebnis ist der Normalzustand
|
||||
waehrend der Veranstaltung. Gewertet wird erst, wenn beide ``*_vollstaendig``.
|
||||
"""
|
||||
|
||||
__tablename__ = "ergebnis"
|
||||
|
||||
start_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("start.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
summe_zeit: Mapped[Decimal | None] = mapped_column(Numeric(7, 2)) # aus zeitmessung summiert
|
||||
summe_fehler: Mapped[int | None] = mapped_column(Integer) # manuell erfasst
|
||||
gesamt: Mapped[Decimal | None] = mapped_column(Numeric(9, 2)) # berechnet
|
||||
platzierung: Mapped[int | None] = mapped_column(Integer) # berechnet
|
||||
|
||||
zeit_vollstaendig: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
fehler_vollstaendig: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
erfasst_von: Mapped[str | None] = mapped_column(String(120))
|
||||
erfasst_am: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
korrigiert_am: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
korrektur_grund: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
start: Mapped[Start] = relationship(back_populates="ergebnis")
|
||||
|
||||
@property
|
||||
def wertbar(self) -> bool:
|
||||
return bool(self.zeit_vollstaendig and self.fehler_vollstaendig)
|
||||
Reference in New Issue
Block a user