9de80d2f2a
- bestellung.xml -> bestellung.json: neuer JSONConfig (loest XMLConfig ab), alle work-Beispiele konvertiert, Doku (CLAUDE/README/dateiformate) umgestellt - ga.py: Beobachtungsobjekt Entwicklung zeichnet den Loesungsverlauf auf, neuer Schalter --show-development erzeugt Entwicklung.svg (beste/mittlere/ schlechteste Wertigkeit ueber die Zyklus-Aktionen) - web/main.py: neuer Endpunkt /tische-datei (tische.ini-Upload, ast.literal_eval), Frontend kann jetzt eine tische.ini laden - libs/webserver.py: plattformunabhaengige start/stop/status-Steuerung der Web-Testinstanz (ersetzt run_web_helper.ps1), raeumt auch verwaiste --reload-Prozessbaeume zuverlaessig auf - Sicherheit/Modernisierung: eval() -> ast.literal_eval in Tische/Strafliste, sicherer Mini-Auswerter fuer die String-Ausdruecke in zyklus.cfg, defaultdict(set) in Plaetze - SVG: Tischnummer fett zentriert im Tischkreis statt "Tisch x" darueber - Tests: JSONConfig, Entwicklung und der Zyklus-Auswerter abgedeckt (9 Tests) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
"""
|
|
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 = {}
|
|
|
|
# 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:
|
|
# VIP: fester Tisch, keine GA-Platzierung
|
|
VipHash[ PID ] = TischNr
|
|
VIPGruppe.Person_dazu( P )
|
|
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 ]
|
|
|
|
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 ] = C.HandleGaesteliste( Beispiel )
|
|
print(PN)
|
|
print(GN)
|
|
print(VIPGruppe)
|
|
print(VipHash)
|
|
print(GruppenNamen)
|
|
|
|
else:
|
|
pass
|