Files
platz/libs/CSVConfig.py
T
Michael StanglandClaude Opus 4.8 c6e0b95d81 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>
2026-07-27 10:10:41 +02:00

202 lines
6.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Liest eine Gaesteliste im CSV-Format ein — der Web-Ersatz fuer bestellung.json
(Meilenstein 1 der Roadmap, Adapter analog zu JSONConfig in Strukturdaten.py).
CSV-Format (Kopfzeile noetig, Spaltenreihenfolge egal, Trennzeichen ; oder ,):
Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl
- Vorname, Nachname: Pflicht
- Titel: optional (z.B. "Dr.")
- Gruppe: Gruppenname — Personen mit demselben Gruppennamen gehoeren
zusammen und sollen moeglichst zusammen sitzen.
Leer = Einzelperson (bildet eine eigene Ein-Personen-Gruppe).
- Tisch: optionale Tischnummer — die Person ist VIP und wird fest an
diesen Tisch gesetzt (wie "tisch" in bestellung.json).
VIPs laufen nicht durch die GA-Platzierung.
- Anzahl: optionale Reservierungsanzahl — expandiert die Zeile in so
viele identische Buchungen (wie "anzahl" in bestellung.json,
z.B. fuer anonyme Sammelbuchungen). Leer = 1.
Zusaetzlich werden die Spalten "MagNicht" und "MagGern" akzeptiert,
aber noch ignoriert (reserviert fuer Personenpaar-Regeln, siehe
Roadmap-Konzeptentscheidung Nr. 10) — so entsteht spaeter kein
Formatbruch.
"""
__author__ = "Michael Stangl"
__copyright__ = "Copyright (c) 2026 Michael Stangl"
__license__ = "Python"
import csv
import io
from Strukturdaten import Person, Personen, Gruppe, Gruppen
class CSVConfig:
"""aus CSV Text/Datei alles einlesen (Rueckgabe wie JSONConfig.laden,
plus Gruppennamen fuer die Anzeige im Web-Frontend)"""
PflichtSpalten = ['vorname', 'nachname']
BekannteSpalten = ['vorname', 'nachname', 'titel', 'gruppe',
'tisch', 'anzahl', 'magnicht', 'maggern']
def HandleGaesteliste(self, Text):
"""parst den CSV-Text und baut Personen/Gruppen/VIPs auf
Rueckgabe: [ PN, GN, VIPGruppe, VipHash, GruppenNamen ]
- PN: Personen-Container (ohne VIPs, wie JSONConfig)
- GN: Gruppen-Container (ohne VIP-Gruppe, wie JSONConfig)
- VIPGruppe: Gruppe(0) mit allen fest gesetzten Personen
- VipHash: { PersonId: Tischnummer } fuer VIPsSetzen()
- GruppenNamen: { GruppenId: Anzeigename }
"""
# Trennzeichen automatisch bestimmen: die Kopfzeile entscheidet
Kopfzeile = Text.lstrip('').splitlines()[0] if Text.strip() else ''
Trenner = ';' if Kopfzeile.count(';') >= Kopfzeile.count(',') else ','
Leser = csv.DictReader( io.StringIO(Text.lstrip('')), delimiter=Trenner )
if Leser.fieldnames is None:
raise ValueError("CSV ist leer — Kopfzeile mit mindestens "
"Vorname/Nachname erwartet")
# Spaltennamen normalisieren (Gross/Klein egal, Leerzeichen weg)
SpaltenMap = {}
for Feld in Leser.fieldnames:
if Feld is None:
continue
SpaltenMap[Feld] = Feld.strip().lower()
for Pflicht in self.PflichtSpalten:
if Pflicht not in SpaltenMap.values():
raise ValueError("CSV-Kopfzeile: Pflichtspalte '%s' fehlt "
"(gefunden: %s)" % (Pflicht, Leser.fieldnames))
def Wert(Zeile, Name):
for Original, Normiert in SpaltenMap.items():
if Normiert == Name:
Inhalt = Zeile.get(Original)
return (Inhalt or '').strip()
return ''
PN = Personen()
GN = Gruppen()
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
GruppeVonName = {}
PID = 0
GID = 0
ZeilenNr = 1
for Zeile in Leser:
ZeilenNr = ZeilenNr + 1
Vorname = Wert(Zeile, 'vorname')
Nachname = Wert(Zeile, 'nachname')
if Vorname == '' and Nachname == '':
continue # Leerzeile
if Vorname == '' or Nachname == '':
raise ValueError("Zeile %d: Vorname und Nachname sind Pflicht"
% ZeilenNr)
Titel = Wert(Zeile, 'titel')
GruppenName = Wert(Zeile, 'gruppe')
TischFeld = Wert(Zeile, 'tisch')
AnzahlFeld = Wert(Zeile, 'anzahl')
try:
AnzahlReservierungen = int(AnzahlFeld) if AnzahlFeld else 1
except ValueError:
raise ValueError("Zeile %d: Anzahl '%s' ist keine Zahl"
% (ZeilenNr, AnzahlFeld))
if AnzahlReservierungen < 1:
raise ValueError("Zeile %d: Anzahl muss >= 1 sein" % ZeilenNr)
TischNr = None
if TischFeld != '':
try:
TischNr = int(TischFeld)
except ValueError:
raise ValueError("Zeile %d: Tisch '%s' ist keine "
"Tischnummer" % (ZeilenNr, TischFeld))
for i in range(AnzahlReservierungen):
PID = PID + 1
P = Person( PID, Vorname, Nachname, Titel )
if TischNr is not None:
# 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 == '':
# Einzelperson: eigene Ein-Personen-Gruppe,
# komplett befuellen und dann registrieren
GID = GID + 1
G = Gruppe( GID )
G.Person_dazu( P )
GruppenNamen[ GID ] = '%s %s' % (Vorname, Nachname)
GN.Gruppe_dazu( G )
else:
if GruppenName not in GruppeVonName:
GID = GID + 1
G = Gruppe( GID )
GruppeVonName[ GruppenName ] = G
GruppenNamen[ GID ] = GruppenName
else:
G = GruppeVonName[ GruppenName ]
G.Person_dazu( P )
# benannte Gruppen erst jetzt registrieren, wenn sie komplett sind
# (Gruppen.Gruppe_dazu uebernimmt Mitglieder und zaehlt NLeute)
for GruppenName, G in GruppeVonName.items():
if G.Anzahl() > 0:
GN.Gruppe_dazu( G )
return [ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ]
def laden(self, FileName):
"""lese alle Eingangsdaten aus einer CSV-Datei ein"""
# utf-8-sig schluckt das BOM, das Excel beim CSV-Export voranstellt
fsock = open( FileName, 'r', encoding='utf-8-sig' )
Text = fsock.read()
fsock.close()
return self.HandleGaesteliste( Text )
if __name__ == '__main__':
Beispiel = """Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl
Britta;Beispiel;;Brautpaar;1;
Bernd;Beispiel;Dr.;Brautpaar;1;
Anna;Huber;;Familie Huber;;
Berta;Huber;;Familie Huber;;
Carl;Huber;;Familie Huber;;
Gast;Kollege;;Kollegen;;4
Einzel;Gast;;;;
"""
print("\n-- Parse Beispiel-CSV:")
C = CSVConfig()
[ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ] = \
C.HandleGaesteliste( Beispiel )
print(PN)
print(GN)
print(VIPGruppe)
print(VipHash)
print(GruppenNamen)
print(FixierteGruppen)
else:
pass