diff --git a/libs/CSVConfig.py b/libs/CSVConfig.py
index a9e3978..2fc7791 100644
--- a/libs/CSVConfig.py
+++ b/libs/CSVConfig.py
@@ -84,6 +84,11 @@ class CSVConfig:
VIPGruppe = Gruppe( 0 )
VipHash = {}
GruppenNamen = {}
+ # fest platzierte ("fixierte") Gruppen fuer die Anzeige: Name ->
+ # {'tisch': Tischnummer, 'anzahl': Personenzahl}. VIPs laufen nicht
+ # durch die GA-Platzierung, sollen in der Gaesteliste aber sichtbar
+ # (grau hinterlegt) erscheinen.
+ FixierteGruppen = {}
# Gruppen entstehen beim ersten Auftreten ihres Namens;
# Einzelpersonen (leerer Gruppenname) bekommen eine eigene Gruppe
@@ -126,9 +131,13 @@ class CSVConfig:
PID = PID + 1
P = Person( PID, Vorname, Nachname, Titel )
if TischNr is not None:
- # VIP: fester Tisch, keine GA-Platzierung
+ # fixiert: fester Tisch, keine GA-Platzierung
VipHash[ PID ] = TischNr
VIPGruppe.Person_dazu( P )
+ Name = GruppenName if GruppenName else 'Fixiert Tisch %d' % TischNr
+ Eintrag = FixierteGruppen.setdefault(
+ Name, { 'tisch': TischNr, 'anzahl': 0 } )
+ Eintrag['anzahl'] = Eintrag['anzahl'] + 1
else:
PN.PersonAdd( P )
if GruppenName == '':
@@ -155,7 +164,7 @@ class CSVConfig:
if G.Anzahl() > 0:
GN.Gruppe_dazu( G )
- return [ PN, GN, VIPGruppe, VipHash, GruppenNamen ]
+ return [ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ]
def laden(self, FileName):
"""lese alle Eingangsdaten aus einer CSV-Datei ein"""
@@ -179,12 +188,14 @@ Einzel;Gast;;;;
"""
print("\n-- Parse Beispiel-CSV:")
C = CSVConfig()
- [ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = C.HandleGaesteliste( Beispiel )
+ [ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ] = \
+ C.HandleGaesteliste( Beispiel )
print(PN)
print(GN)
print(VIPGruppe)
print(VipHash)
print(GruppenNamen)
+ print(FixierteGruppen)
else:
pass
diff --git a/web/main.py b/web/main.py
index d01516d..a99f0f2 100644
--- a/web/main.py
+++ b/web/main.py
@@ -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()
diff --git a/web/static/index.html b/web/static/index.html
index b806e45..3519b8e 100644
--- a/web/static/index.html
+++ b/web/static/index.html
@@ -61,6 +61,7 @@
padding: .3rem .7rem; margin: .15rem .3rem .15rem 0; font-size: .9rem; }
.kennzahl b { color: var(--akzent); }
.hinweis { font-size: .8rem; color: #6b6557; }
+ #gruppenUebersicht tr.fixiert td { background: #e6e3dc; color: #555; }
.varianten { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }
.variante {
border: 1px solid var(--rand); border-radius: 6px; background: white;
@@ -97,10 +98,9 @@
-
-
-
+
+
Spalten: Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl —
Gruppe hält Personen zusammen (leer = Einzelgast),
- Tisch setzt VIPs fest (z. B. Brautpaar an Tisch 1),
+ Tisch fixiert Gäste an einem Tisch (z. B. Brautpaar an Tisch 1),
Anzahl expandiert eine Zeile in mehrere Buchungen
- (anonyme Sammelbuchung). Datei hochladen oder direkt hier bearbeiten:
default vs.
+ locker): Profil wählen und neu berechnen.
+
@@ -259,6 +264,7 @@ async function tischDateiLaden() {
'„Tische speichern“ ist nur nach Änderungen nötig.', 'ok');
berechnenFreigeben();
} catch (e) { status('tischStatus', 'Fehler: ' + e.message, 'fehler'); }
+ finally { eingabe.value = ''; } // damit dieselbe Datei erneut auswählbar ist
}
/* ------------------------------------------------ 2. Gäste */
@@ -293,11 +299,10 @@ async function csvDateiGewaehlt(ev) {
}
text = text.replace(/^/, '');
document.getElementById('csvText').value = text;
- // stale Ausgabe eines frueheren Uploads entfernen
- document.getElementById('gruppenUebersicht').innerHTML = '';
- status('gaesteStatus', 'Datei „' + datei.name + '“ geladen — zum Übernehmen ' +
- '„Gästeliste übernehmen“ klicken.', 'ok');
- ev.target.value = ''; // Textarea ist ab jetzt die alleinige Quelle
+ ev.target.value = ''; // damit dieselbe Datei erneut auswählbar ist
+ // direkt uebernehmen: die ausgewaehlte Datei wird sofort geladen (die
+ // Textarea ist bereits befuellt und dient gaesteHochladen als Quelle)
+ await gaesteHochladen();
}
async function gaesteHochladen() {
@@ -308,16 +313,35 @@ async function gaesteHochladen() {
try {
const r = await api('/sitzung/' + sitzung + '/personen', { method: 'POST', body: form });
status('gaesteStatus', '✓ ' + r.personen + ' Personen in ' + r.gruppen.length +
- ' Gruppen, dazu ' + r.vips + ' VIPs mit festem Tisch.', 'ok');
+ ' Gruppen' + (r.fixiert ? ', dazu ' + r.fixiert +
+ ' fixierte Personen an festen Tischen' : '') + '.', 'ok');
+ // regulaere Gruppen, danach die fest platzierten ("fixierten") Gruppen
+ // grau hinterlegt
+ const regulaer = r.gruppen.map(g =>
+ '| Gruppe | Personen |
|---|---|
| ' + g.name + ' | ' + g.anzahl + ' |