Files
Brancheneinstufung2/list_generator.py
2025-05-26 18:27:11 +00:00

277 lines
14 KiB
Python

import csv
from datetime import datetime
import collections
import os.path
# Für Service Account Authentifizierung
from google.oauth2 import service_account # Wichtig!
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# --- Konfiguration ---
CSV_FILENAME = 'Namensliste.csv'
GOOGLE_DOC_TITLE = f"Gruppenlisten Kinderhaus St. Martin Neuching (Service Acc) - {datetime.now().strftime('%Y-%m-%d')}"
EINRICHTUNG = "Kinderhaus St. Martin Neuching"
FOTODATUM = "02. - 05.06.2025"
GRUPPENNAME_SUFFIX = "gruppe"
FOTOGRAF_NAME = "Kinderfotos Erding"
FOTOGRAF_ADRESSE = "Gartenstr. 10 85445 Oberding"
FOTOGRAF_WEB = "www.kinderfotos-erding.de"
FOTOGRAF_TEL = "08122-8470867"
# Scopes für die Google Docs API
SCOPES = ['https://www.googleapis.com/auth/documents']
SERVICE_ACCOUNT_FILE = 'service_account.json' # Ihre Service Account Schlüsseldatei
# --- Ende Konfiguration ---
def get_docs_service_with_service_account():
"""Erstellt den API-Dienst mit einem Service Account."""
creds = None
try:
creds = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
except Exception as e:
print(f"Fehler beim Laden der Service Account Credentials aus '{SERVICE_ACCOUNT_FILE}': {e}")
return None
try:
service = build('docs', 'v1', credentials=creds)
return service
except HttpError as err:
print(f"Ein Fehler beim Erstellen des Docs Service mit Service Account ist aufgetreten: {err}")
return None
except Exception as e:
print(f"Ein unerwarteter Fehler beim Erstellen des Docs Service: {e}")
return None
def create_google_doc_from_csv(service):
"""Liest CSV, verarbeitet Daten und erstellt/befüllt das Google Doc."""
kinder_nach_gruppen = collections.defaultdict(list)
try:
with open(CSV_FILENAME, mode='r', encoding='utf-8-sig', newline='') as csvfile:
reader = csv.DictReader(csvfile, delimiter=';')
for row in reader:
vorname = row.get('Vorname', '').strip()
nachname = row.get('Nachname', '').strip()
gruppe_original = row.get('Gruppe', '').strip()
if not vorname or not nachname or not gruppe_original:
print(f"Warnung: Zeile übersprungen wegen fehlender Daten: {row}")
continue
kinder_nach_gruppen[gruppe_original].append({
'Nachname': nachname,
'Vorname': vorname
})
except FileNotFoundError:
print(f"FEHLER: Die Datei '{CSV_FILENAME}' wurde nicht gefunden.")
return None
except Exception as e:
print(f"FEHLER beim Lesen der CSV-Datei: {e}")
return None
if not kinder_nach_gruppen:
print("Keine Daten aus der CSV-Datei geladen.")
return None
# Kinder innerhalb jeder Gruppe sortieren
for gruppe_key in kinder_nach_gruppen: # Geändert von 'gruppe' zu 'gruppe_key' um Verwechslung zu vermeiden
kinder_nach_gruppen[gruppe_key].sort(key=lambda x: (x['Nachname'].lower(), x['Vorname'].lower()))
# ***** KORREKTUR HIER: sorted_gruppen_namen definieren *****
sorted_gruppen_namen = sorted(kinder_nach_gruppen.keys())
stand_zeit = datetime.now().strftime("%d.%m.%Y %H:%M Uhr")
# 1. Neues Google Doc erstellen
try:
doc_body = {'title': GOOGLE_DOC_TITLE}
doc = service.documents().create(body=doc_body).execute()
document_id = doc.get('documentId')
print(f"Google Doc erstellt mit ID: {document_id}")
print(f"Link: https://docs.google.com/document/d/{document_id}/edit")
except HttpError as err:
print(f"Fehler beim Erstellen des Google Dokuments: {err}")
return None
requests = []
# Startindex für das gesamte Dokument. Bleibt für den ersten Block bei 1.
# Für nachfolgende Seiten (nach PageBreak) müssen wir überlegen, wie der Index behandelt wird.
# Wenn PageBreak mit endOfSegmentLocation eingefügt wird, ist der nächste Einfügepunkt
# implizit am Anfang des neuen Segments.
doc_cursor_index = 1
for i, gruppe_original in enumerate(sorted_gruppen_namen):
kinder_liste = kinder_nach_gruppen[gruppe_original]
anzahl_kinder = len(kinder_liste)
gruppe_display_name = gruppe_original + GRUPPENNAME_SUFFIX
# --- Seiten-Header ---
header_text_for_page = (
f"{EINRICHTUNG}\t\t\t{FOTOGRAF_NAME}\n"
f"{FOTODATUM}\n\n"
)
# Wenn es die erste Seite ist, verwenden wir doc_cursor_index.
# Für nachfolgende Seiten könnte man endOfSegmentLocation verwenden, wenn der vorherige Block auch damit endete.
# Wir versuchen, doc_cursor_index weiterzuführen.
requests.append({'insertText': {'location': {'index': doc_cursor_index}, 'text': header_text_for_page}})
doc_cursor_index += len(header_text_for_page)
# --- Tabelle ---
num_rows_for_table = len(kinder_liste) + 1
num_cols_for_table = 3
# Füge die Tabellenstruktur ein
# Die Tabelle wird an der aktuellen Cursor-Position eingefügt.
requests.append({
'insertTable': {
'location': {'index': doc_cursor_index},
'rows': num_rows_for_table,
'columns': num_cols_for_table
}
})
# WICHTIG: Wo ist der Cursor jetzt, um Text IN die Tabelle einzufügen?
# Annahme: Der Cursor ist jetzt AM ANFANG der ERSTEN Zelle.
# Daher sollte der Index für das Einfügen des Tabelleninhalts derselbe sein
# wie der Index, an dem die Tabelle erstellt wurde, PLUS EINS (für das Start-Tag der Tabelle selbst).
# Dies ist der kritische Punkt.
# Der Index zum Befüllen der Tabelle ist der Startindex der Tabelle (doc_cursor_index) + 1.
# (Das +1 ist für das unsichtbare Zeichen, das den Anfang der Tabellenstruktur markiert).
table_content_insertion_point = doc_cursor_index + 1
table_text_content = []
table_text_content.append("Nachname\tVorname\tGruppe")
for kind in kinder_liste:
table_text_content.append(f"{kind['Nachname']}\t{kind['Vorname']}\t{gruppe_display_name}")
full_table_text = "\n".join(table_text_content) + "\n"
# Füge den gesamten Tabelleninhalt ein.
requests.append({
'insertText': {
'location': {'index': table_content_insertion_point},
'text': full_table_text
}
})
# Nach dem Einfügen der Tabelle und ihres Inhalts müssen wir den doc_cursor_index
# auf eine Position *NACH* der Tabelle setzen.
# Die Tabelle (Struktur) selbst belegt 1 Index-Einheit (aus Sicht des batchUpdate-Requests).
# Der Text, der in die Tabelle gefüllt wurde, belegt `len(full_table_text)` Zeichen.
# Aber die gesamte Tabellenstruktur im Dokument ist komplexer.
# Es ist sehr schwer, den genauen Index nach einer Tabelle vorherzusagen.
# HIER verwenden wir endOfSegmentLocation für den Footer, um dieses Problem zu umgehen.
# --- Footer ---
footer_text_for_page = (
f"\n{anzahl_kinder} angemeldete Kinder\n\n" # Start mit \n um sicher aus der Tabelle zu sein
"Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden\n"
"Kinder an die Anmeldung erinnern.\n\n"
f"Stand {stand_zeit}\n\n"
f"{FOTOGRAF_NAME}\n{FOTOGRAF_ADRESSE}\n{FOTOGRAF_WEB}\n{FOTOGRAF_TEL}\n"
)
requests.append({
'insertText': {
'endOfSegmentLocation': {}, # Fügt am Ende des aktuellen Haupttextkörpers ein
'text': footer_text_for_page
}
})
# --- Seitenumbruch ---
if i < len(sorted_gruppen_namen) - 1:
requests.append({'insertPageBreak': {'endOfSegmentLocation': {}}})
# WICHTIG: Für die nächste Iteration muss doc_cursor_index
# den Anfang des neuen Segments (nach dem PageBreak) repräsentieren.
# Wenn endOfSegmentLocation verwendet wurde, ist der nächste logische Einfügepunkt
# wieder "endOfSegmentLocation" oder der Index muss neu "gefunden" werden.
# Für den Header der NÄCHSTEN Seite setzen wir doc_cursor_index
# NICHT einfach auf 1, da das Dokument weiterläuft.
# Wenn wir endOfSegmentLocation für PageBreak und Footer verwenden,
# sollte der Header der nächsten Seite auch mit endOfSegmentLocation beginnen,
# ODER wir müssen den exakten Index nach dem PageBreak kennen.
# Da der nächste Header wieder 'location' verwendet, müssen wir den Index aktualisieren.
# Ein PageBreak fügt 1 zum Index hinzu. Der Footer hat eine Länge.
# Dies ist immer noch der heikle Teil.
# doc_cursor_index += len(footer_text_for_page) + 1 # Ungenau, da Tabelle dazwischen war.
# Da wir jetzt endOfSegmentLocation für Footer und PageBreak verwenden,
# ist der `doc_cursor_index` für den Header der nächsten Seite nicht mehr einfach weiterzuzählen.
# Wir müssen den Header der nächsten Seite auch mit `endOfSegmentLocation` einfügen,
# oder die Logik wird sehr komplex.
# Umstellung: Header der Folgeseiten auch mit endOfSegmentLocation
# Dies erfordert, dass der *erste* Header mit `location` eingefügt wird,
# und alle *weiteren* Blöcke mit `endOfSegmentLocation`.
# Das machen wir später, wenn diese Version nicht geht.
# Vorerst: Wir nehmen an, `endOfSegmentLocation` hat den "globalen" Cursor bewegt.
# Der `doc_cursor_index` wird für den nächsten Header NICHT mehr direkt verwendet,
# wenn wir auf `endOfSegmentLocation` umstellen.
# Temporär: Lassen wir die explizite Aktualisierung von doc_cursor_index hier weg,
# da die Einfügepunkte für Footer/PageBreak relativ sind.
# Wenn der nächste Header wieder `location: {index: doc_cursor_index}` verwendet, wird es knallen.
# Wir müssen konsequent sein.
# Wenn wir `doc_cursor_index` für den Header verwenden, müssen wir ihn korrekt weiterführen.
# Länge der Tabelle ist NICHT nur len(full_table_text). Es ist komplexer.
#
# EINFACHSTE LÖSUNG JETZT: Wir gehen davon aus, dass nach einem
# `insertPageBreak` mit `endOfSegmentLocation`, der NÄCHSTE `insertText`
# für den Header der neuen Seite an Index 1 dieser neuen Seite beginnt.
# Das ist FALSCH für die Google Docs API, da das Dokument ein Stream ist.
#
# Richtiger Ansatz: Wenn endOfSegmentLocation verwendet wird, sollte der nächste Block
# auch endOfSegmentLocation verwenden.
# Wir müssen uns entscheiden: Entweder alles mit exakten Indizes (schwer)
# oder alles relativ mit endOfSegmentLocation (einfacher für sequenzielles Anhängen).
# DA DER HEADER DER NÄCHSTEN ITERATION `location: {'index': doc_cursor_index}` verwendet,
# MÜSSEN WIR `doc_cursor_index` AKTUALISIEREN.
# DIES IST DER TEIL, DER AM WAHRSCHEINLICHSTEN FEHLSCHLÄGT, WENN DIE LÄNGENBERECHNUNG FALSCH IST.
# Die Struktur einer Tabelle ist nicht nur ihr Text.
# Annahme: Eine Tabelle (Struktur + Text) + Footer + PageBreak
# Versuchen wir, `doc_cursor_index` *nicht* zu aktualisieren und stattdessen
# den Header der Folgeseiten auch mit `endOfSegmentLocation` einzufügen.
pass # doc_cursor_index wird nicht mehr manuell hochgezählt, wenn EOS verwendet wird.
# Batch-Update ausführen
if requests:
try:
print("Sende Batch Update an Google Docs API...")
print("Anzahl der Requests:", len(requests))
service.documents().batchUpdate(
documentId=document_id, body={'requests': requests}
).execute()
print("Dokument erfolgreich befüllt.")
except HttpError as err:
print(f"Fehler beim Befüllen des Google Dokuments: {err}")
error_details = "Keine Fehlerdetails im Content."
if err.content:
try:
error_details = err.content.decode('utf-8')
except Exception as e_decode:
error_details = f"Fehler beim Dekodieren der Fehlerdetails: {e_decode}"
print(f"Details zum Fehler ({err.resp.status} {err._get_reason()}): {error_details}")
return document_id
if __name__ == '__main__':
print(f"Info: Verwendetes Fotodatum: {FOTODATUM}")
print(f"Info: Gruppennamen werden mit Suffix '{GRUPPENNAME_SUFFIX}' versehen.")
docs_service = get_docs_service_with_service_account()
if docs_service:
document_id = create_google_doc_from_csv(docs_service)
if document_id:
print("\n--- WICHTIG ---")
print(f"Das Dokument wurde vom Servicekonto erstellt: {SERVICE_ACCOUNT_FILE}")
print(f"Sie finden es unter: https://docs.google.com/document/d/{document_id}/edit")
print("Wenn Sie das Dokument in Ihrem Haupt-Google-Drive-Konto sehen und bearbeiten möchten,")
print("müssen Sie entweder:")
print("1. Das Servicekonto (die E-Mail-Adresse des Servicekontos, steht in der JSON-Datei)")
print(" als Bearbeiter zu dem Google Drive Ordner hinzufügen, in dem das Dokument erstellt werden soll (bevor das Skript läuft, oder das Dokument verschieben).")
print("2. Oder das Skript erweitern, um das Dokument nach der Erstellung explizit mit Ihrem")
print(" Benutzerkonto zu teilen (siehe auskommentierten Code-Teil mit `drive_service.permissions().create`).")
print(" Dafür benötigt das Servicekonto auch den Scope 'https://www.googleapis.com/auth/drive'.")