Web-GUI: fixierte Gruppen anzeigen, Strafprofil waehlen, Farben nutzen

CSVConfig.HandleGaesteliste liefert zusaetzlich FixierteGruppen (Name ->
Tisch/Anzahl), damit fest platzierte Gaeste in der Gaesteliste sichtbar
(grau hinterlegt) erscheinen. Durchgaengig "VIP" -> "fixiert" umbenannt.

Backend: Strafprofile werden gecacht geladen, neuer Endpoint /strafprofile
und Query-Parameter strafen an /berechnen, damit sich Profile (default vs.
locker) in der GUI vergleichen lassen. SVG-Ausgabe nutzt das Farbschema aus
farben.cfg (Rueckfall auf Palette).

Frontend: tische.ini und CSV werden bei Auswahl sofort geladen, Auswahl der
fixierten Gruppen und des Optimierungsprofils in der Oberflaeche.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-27 10:10:41 +02:00
parent d19913a53d
commit c6e0b95d81
3 changed files with 124 additions and 29 deletions
+64 -9
View File
@@ -44,6 +44,7 @@ from pydantic import BaseModel, Field
from ga import Farm, Zyklus
from CSVConfig import CSVConfig
from Farben import Farbschema
from Strukturdaten import Sitzplatzverteilung, Strafliste, Tisch, Tische
app = FastAPI(
@@ -62,11 +63,43 @@ ZYKLUSDATEI = os.path.join( CFGDIR, 'zyklus.cfg' )
STRAFENDATEI = os.path.join( CFGDIR, 'strafen.cfg' )
ZYKLUSART = os.environ.get( 'PLATZ_ZYKLUSART', 'Adaptiv' )
STRAFENART = os.environ.get( 'PLATZ_STRAFENART', 'default' )
FARBENDATEI = os.path.join( CFGDIR, 'farben.cfg' )
FARBENART = os.environ.get( 'PLATZ_FARBENART', 'grosse_gruppen' )
GAZyklus = Zyklus()
GAZyklus.laden( ZYKLUSDATEI, ZYKLUSART )
Strafen = Strafliste()
Strafen.laden( STRAFENDATEI, STRAFENART )
# Strafprofile werden bei Bedarf geladen und gecacht, damit sich in der GUI
# verschiedene Profile (z.B. 'default' vs 'locker') auswaehlen und vergleichen
# lassen. Das Default-Profil wird schon beim Start geladen (faellt frueh auf,
# falls die Datei/Sektion kaputt ist).
StrafProfile = {}
def strafprofil(Name):
"""liefert (gecacht) die Strafliste einer Sektion aus strafen.cfg;
wirft ValueError bei unbekannter Sektion"""
if Name not in StrafProfile:
SL = Strafliste()
SL.laden( STRAFENDATEI, Name )
StrafProfile[Name] = SL
return StrafProfile[Name]
def strafprofil_namen():
"""alle in strafen.cfg vorhandenen Profilnamen (fuer die GUI-Auswahl)"""
cfg = configparser.ConfigParser()
cfg.read( STRAFENDATEI )
return cfg.sections()
strafprofil( STRAFENART ) # Default vorwaermen / frueh scheitern
# Farbprofil fuer die SVG-Ausgabe; faellt bei fehlender Datei/Sektion auf die
# eingebaute Palette zurueck (Farbschema=None), damit der Server trotzdem laeuft.
try:
Farben = Farbschema().laden( FARBENDATEI, FARBENART )
except (ValueError, OSError):
Farben = None
# ---------------------------------------------------------------- Sessions
SESSION_TTL_SEKUNDEN = 24 * 3600
@@ -160,7 +193,7 @@ def _VarianteJSON(SV, GruppenNamen, Nr):
G = SV.Gruppen.GruppeVonPid( P.Id )
return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id )
except KeyError:
return 'VIP'
return 'fixiert'
TischListe = []
for T in SV.Tische:
@@ -220,6 +253,7 @@ def _ErgebnisJSON(S):
GruppenNamen = S.get( 'gruppenNamen', {} )
return {
'lauf': S['laeufe'],
'strafen': S.get( 'strafen', STRAFENART ), # verwendetes Strafprofil
'varianten': [ _VarianteJSON( SV, GruppenNamen, Nr + 1 )
for Nr, SV in enumerate( S['ergebnisse'] ) ],
}
@@ -389,7 +423,7 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
Text = Rohdaten.decode( 'cp1252' ) # Excel/Windows-Fallback
try:
[ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = \
[ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ] = \
CSVConfig().HandleGaesteliste( Text )
except ValueError as Fehler:
raise HTTPException( status_code=400, detail=str(Fehler) )
@@ -407,12 +441,15 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
return {
'personen': GN.NLeute,
'vips': VIPGruppe.Anzahl(),
'fixiert': VIPGruppe.Anzahl(),
'gruppen': [ {
'id': G.Id,
'name': GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ),
'anzahl': G.Anzahl(),
} for G in GN ],
# fest platzierte Gruppen (fuer die grau hinterlegte Anzeige)
'fixierte': [ { 'name': Name, 'tisch': Info['tisch'], 'anzahl': Info['anzahl'] }
for Name, Info in FixierteGruppen.items() ],
}
@@ -420,9 +457,14 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
def Berechnen(SitzungsId: str,
varianten: int = Query( default=3, ge=1, le=5,
description='wieviele unterschiedliche Varianten der Lauf '
'zurueckgeben soll (aus Farm.TopN, dedupliziert)' )):
'zurueckgeben soll (aus Farm.TopN, dedupliziert)' ),
strafen: str = Query( default=STRAFENART,
description='Name des Strafpunkte-Profils (Sektion in strafen.cfg), '
'z.B. default oder locker - zum Vergleich waehlbar' )):
"""startet die GA-Optimierung; jeder Aufruf ist ein neuer Lauf und
liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen"""
liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen. Das
Strafpunkte-Profil ('strafen') steuert die Bewertung waehrend der
Optimierung - so lassen sich z.B. default und locker vergleichen."""
S = _GibSession( SitzungsId )
if S['tische'] is None:
raise HTTPException( status_code=400,
@@ -454,9 +496,14 @@ def Berechnen(SitzungsId: str,
detail='Tisch %d hat %d Plaetze, aber %d VIPs sind '
'dort fest gesetzt' % (Tid, T.Plaetze, AnzahlVIPs) )
try:
SL = strafprofil( strafen )
except ValueError as Fehler:
raise HTTPException( status_code=400, detail=str(Fehler) )
try:
GAFarm = Farm( GAZyklus, Sitzplatzverteilung,
Tische=TE, Gruppen=P['GN'], Strafen=Strafen,
Tische=TE, Gruppen=P['GN'], Strafen=SL,
VIPListe=P['VipHash'], VIPs=P['VIPGruppe'] )
except IndexError as Fehler:
raise HTTPException( status_code=400, detail=str(Fehler) )
@@ -481,9 +528,17 @@ def Berechnen(SitzungsId: str,
S['ergebnisse'] = Ergebnisse
S['laeufe'] = S['laeufe'] + 1
S['strafen'] = strafen # fuer die Anzeige, welches Profil den Lauf ergab
return _ErgebnisJSON( S )
@app.get( '/strafprofile' )
def Strafprofile():
"""verfuegbare Strafpunkte-Profile (Sektionen in strafen.cfg) fuer die
Auswahl in der GUI; 'vorgabe' ist das voreingestellte Profil."""
return { 'profile': strafprofil_namen(), 'vorgabe': STRAFENART }
@app.get( '/sitzung/{SitzungsId}/ergebnis' )
def Ergebnis(SitzungsId: str):
"""alle Varianten des letzten Laufs als JSON (beste zuerst)"""
@@ -513,7 +568,7 @@ def ErgebnisSVG(SitzungsId: str,
os.close( Handle )
try:
S['ergebnisse'][variante - 1].alsSVG( TempPfad,
GruppenNamen=S.get( 'gruppenNamen', {} ) )
GruppenNamen=S.get( 'gruppenNamen', {} ), Farbschema=Farben )
fsock = open( TempPfad, 'r' )
SVG = fsock.read()
fsock.close()