feat: Integrate gdoctableapppy for table creation and header styling

Replaces plain text table generation with gdoctableapppy library
to create true Docs tables. Attempts to bold header row.
Retains manual copy for main doc header/footer.
This commit is contained in:
2025-05-27 05:16:08 +00:00
parent e6baac5723
commit ea3ee863d0

View File

@@ -7,9 +7,13 @@ from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# Importiere die neue Bibliothek
from gdoctableapppy import DocsTableApp # Hauptklasse
from gdoctableapppy import Utility # Für endOfSegmentLocation, falls benötigt
# --- Konfiguration ---
CSV_FILENAME = 'Namensliste.csv'
GOOGLE_DOC_TITLE = f"Gruppenlisten_{datetime.now().strftime('%Y-%m-%d_%H-%M')}" # Einfacherer Titel
GOOGLE_DOC_TITLE = f"Gruppenlisten_{datetime.now().strftime('%Y-%m-%d_%H-%M')}"
TARGET_FOLDER_ID = "18DNQaH9zbcBzwhckJI-4Uah-WXTXg6bg"
# Diese werden nur für den einmaligen Infoblock verwendet
@@ -38,16 +42,17 @@ def get_services_with_service_account():
except Exception as e: print(f"Fehler Drive Service: {e}")
return docs_service, drive_service
def generate_group_data_for_doc(docs_service, document_id_to_fill):
def generate_tables_with_gdoctableapp(docs_service, document_id_to_fill):
"""
Befüllt ein existierendes Dokument mit:
- Pro Gruppe: Tabellenüberschrift, Kinderliste, Anzahl Kinder, Erinnerungstext, Stand.
Befüllt ein existierendes Dokument mit echten Tabellen pro Gruppe
unter Verwendung der gdoctableapppy Bibliothek.
"""
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=';')
# ... (CSV Leselogik wie gehabt) ...
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: continue
@@ -58,68 +63,127 @@ def generate_group_data_for_doc(docs_service, document_id_to_fill):
for gk_key in kinder_nach_gruppen:
kinder_nach_gruppen[gk_key].sort(key=lambda x: (x['Nachname'].lower(), x['Vorname'].lower()))
sorted_gruppen_namen = sorted(kinder_nach_gruppen.keys())
# Das Stand-Datum wird einmalig ermittelt und für alle Gruppen verwendet
stand_zeit = datetime.now().strftime("%d.%m.%Y %H:%M Uhr")
requests = []
col_width_nachname = 25
col_width_vorname = 25
# Initialisiere DocsTableApp
# Die Bibliothek erwartet 'creds' oder 'service'. Da wir 'service' haben:
try:
dta = DocsTableApp(service=docs_service)
util = Utility() # Für endOfSegmentLocation, falls wir es für Text außerhalb brauchen
except Exception as e_dta_init:
print(f"Fehler bei der Initialisierung von DocsTableApp: {e_dta_init}")
return False
# Liste für Requests, die nicht von DocsTableApp direkt gehandhabt werden (z.B. reiner Text, PageBreaks)
additional_requests = []
for i, gruppe_original in enumerate(sorted_gruppen_namen):
kinder_liste = kinder_nach_gruppen[gruppe_original]
anzahl_kinder = len(kinder_liste) # Anzahl Kinder für die aktuelle Gruppe
anzahl_kinder = len(kinder_liste)
gruppe_display_name = gruppe_original + GRUPPENNAME_SUFFIX
group_block_lines = []
# "Tabellen"-Daten für die aktuelle Gruppe
kopf_nachname = "Nachname".ljust(col_width_nachname)
kopf_vorname = "Vorname".ljust(col_width_vorname)
group_block_lines.append(f"{kopf_nachname}\t{kopf_vorname}\tGruppe") # Überschriften
# 1. Daten für die Tabelle vorbereiten (Liste von Listen)
table_values = []
table_values.append(["Nachname", "Vorname", "Gruppe"]) # Kopfzeile
for kind in kinder_liste:
nachname_gepadded = kind['Nachname'].ljust(col_width_nachname)
vorname_gepadded = kind['Vorname'].ljust(col_width_vorname)
group_block_lines.append(f"{nachname_gepadded}\t{vorname_gepadded}\t{gruppe_display_name}")
group_block_lines.append("") # Leerzeile nach der Kinderliste
# --- HIER DIE GEWÜNSCHTEN INFOS WIEDER HINZUFÜGEN ---
group_block_lines.append(f"{anzahl_kinder} angemeldete Kinder")
group_block_lines.append("") # Leerzeile
group_block_lines.append("Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden")
group_block_lines.append("Kinder an die Anmeldung erinnern.")
group_block_lines.append("") # Leerzeile
group_block_lines.append(f"Stand {stand_zeit}") # Verwendet das einmalig ermittelte Stand-Datum
group_block_lines.append("") # Weitere Leerzeile für Abstand
# --- ENDE HINZUGEFÜGTE INFOS ---
full_group_block_text = "\n".join(group_block_lines) + "\n"
table_values.append([kind['Nachname'], kind['Vorname'], gruppe_display_name])
if i == 0: # Erster Block nach der einmaligen Info (die in main eingefügt wurde)
requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': full_group_block_text}})
else:
requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': full_group_block_text}})
if i < len(sorted_gruppen_namen) - 1:
requests.append({'insertPageBreak': {'endOfSegmentLocation': {}}})
# 2. Tabelle mit gdoctableapppy einfügen
# Wir müssen den Einfügeindex bestimmen.
# Für die erste Tabelle (i==0) nach der initialen Info (die in main eingefügt wurde),
# verwenden wir endOfSegmentLocation. Für weitere Tabellen auch.
current_insert_location = util.get_end_of_segment_location()
# ... (Rest der Funktion: Batch-Update ausführen, bleibt gleich) ...
if requests:
if not docs_service: print("FEHLER: Docs Service nicht da für Befüllung."); return False
try:
print(f"Sende Batch Update (Gruppenlisten, Anzahl, Stand) für Doc ID '{document_id_to_fill}'...")
docs_service.documents().batchUpdate(documentId=document_id_to_fill, body={'requests': requests}).execute()
print("Dokument erfolgreich mit Gruppenlisten, Anzahl und Stand befüllt.")
return True
print(f"Versuche Tabelle für Gruppe '{gruppe_original}' mit gdoctableapppy einzufügen...")
# Definiere Stil für die Kopfzeile (fett)
# Die Syntax hier ist eine Annahme basierend auf typischen Bibliotheksmustern.
# Ggf. muss dies an die genaue Implementierung von gdoctableapppy angepasst werden.
# Die Bibliothek könnte eine Methode wie `set_range_style` oder `format_cells` haben.
# Tanaikes Bibliotheken sind oft sehr flexibel.
# `add_table` sendet das BatchUpdate direkt oder gibt Requests zurück.
# Laut Doku: "This method uses Docs API. So when this method is used, the table is directly created to Google Document."
# Das bedeutet, wir müssen die Requests nicht sammeln und selbst senden für die Tabelle.
# Index für add_table: Wenn None, wird es ans Ende angehängt.
# Da wir die einmalige Info in main eingefügt haben, sollte None hier passen.
# Und für Folgetabellen wird es auch ans Ende (nach dem PageBreak) angehängt.
# Einfügen der Tabelle
res_table = dta.add_table(document_id_to_fill, values=table_values, index=None) # index=None hängt ans Ende an
# Nun versuchen, die Kopfzeile fett zu machen.
# Wir brauchen eine Referenz auf die Tabelle oder ihre Position, um die Kopfzeile zu formatieren.
# `add_table` gibt laut Doku die `startIndex` der Tabelle zurück.
# { "table_object": created_table_object, "startIndex": table_start_index, "endIndex": table_end_index }
# Die Formatierung ist komplexer, da sie Zellbereiche benötigt.
# Die Bibliothek hat `update_text_style_to_range`
# update_text_style_to_range(document_id, text_style, fields, text_range=None, table_range=None)
# table_range = {"table_start_index": tableStartIndex, "row_index": 0, "column_index": 0, "row_span": 1, "column_span": 3} (Beispiel)
if res_table and "startIndex" in res_table:
table_start_idx = res_table["startIndex"]
header_range = {
"table_start_index": table_start_idx,
"row_index": 0, # Erste Zeile (Kopfzeile)
"column_index": 0, # Erste Spalte
"row_span": 1, # Nur eine Zeile
"column_span": 3 # Über alle 3 Spalten der Kopfzeile
}
text_style_bold = {"bold": True}
fields_bold = "bold"
dta.update_text_style_to_range(document_id_to_fill, text_style_bold, fields_bold, table_range=header_range)
print(f" Kopfzeile für Tabelle '{gruppe_original}' versucht fett zu formatieren.")
else:
print(f" Konnte startIndex für Tabelle '{gruppe_original}' nicht erhalten, Kopfzeile nicht formatiert.")
# 3. Text unter der Tabelle einfügen (Anzahl Kinder, etc.)
# Dies muss als separater Textblock NACH der Tabelle eingefügt werden.
# Wir verwenden dafür `additional_requests` und `endOfSegmentLocation`.
footer_lines_for_group = []
footer_lines_for_group.append("") # Leerzeile nach der Tabelle
footer_lines_for_group.append(f"{anzahl_kinder} angemeldete Kinder")
footer_lines_for_group.append("")
footer_lines_for_group.append("Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden")
footer_lines_for_group.append("Kinder an die Anmeldung erinnern.")
footer_lines_for_group.append("")
footer_lines_for_group.append(f"Stand {stand_zeit}")
footer_lines_for_group.append("")
full_footer_text_for_group = "\n".join(footer_lines_for_group) + "\n"
additional_requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': full_footer_text_for_group}})
except Exception as e_table:
print(f"Fehler beim Erstellen/Formatieren der Tabelle für Gruppe '{gruppe_original}' mit gdoctableapppy: {e_table}")
# Fallback: Füge die Daten als einfachen Text ein, wenn die Bibliothek fehlschlägt
fallback_text_lines = ["FEHLER BEI TABELLENERSTELLUNG FÜR DIESE GRUPPE"]
kopf_nachname = "Nachname".ljust(25); kopf_vorname = "Vorname".ljust(25)
fallback_text_lines.append(f"{kopf_nachname}\t{kopf_vorname}\tGruppe")
for r in table_values[1:]: fallback_text_lines.append(f"{r[0].ljust(25)}\t{r[1].ljust(25)}\t{r[2]}")
additional_requests.append({'insertText': {'endOfSegmentLocation': {}, 'text': "\n".join(fallback_text_lines) + "\n\n"}})
# 4. Seitenumbruch (wenn nicht die letzte Gruppe)
if i < len(sorted_gruppen_namen) - 1:
additional_requests.append({'insertPageBreak': {'endOfSegmentLocation': {}}})
# Führe die zusätzlichen Requests aus (für Text unter Tabellen und Seitenumbrüche)
if additional_requests:
try:
print(f"Sende zusätzliche Requests (Footer-Texte, PageBreaks) für Doc ID '{document_id_to_fill}'...")
docs_service.documents().batchUpdate(documentId=document_id_to_fill, body={'requests': additional_requests}).execute()
print("Zusätzliche Requests erfolgreich ausgeführt.")
except HttpError as err:
print(f"Fehler beim Befüllen (Gruppenlisten, Anzahl, Stand) Doc ID '{document_id_to_fill}': {err}")
print(f"Fehler beim Ausführen der zusätzlichen Requests für Doc ID '{document_id_to_fill}': {err}")
# ... (Fehlerdetails) ...
return False
return False # Befüllen war nicht vollständig erfolgreich
return True
# --- Main execution block ---
# (Der Main Block bleibt exakt so wie in der letzten Version, die die einmalige Info eingefügt hat)
if __name__ == "__main__":
print(f"Info: Ordner-ID für Dokument: '{TARGET_FOLDER_ID}'")
docs_api_service, drive_api_service = get_services_with_service_account()
@@ -128,15 +192,14 @@ if __name__ == "__main__":
print("Konnte Google Docs API Service nicht initialisieren. Skript wird beendet.")
else:
document_id = None
# 1. Dokument erstellen (leer oder nur mit Titel)
# 1. Dokument erstellen
if drive_api_service and TARGET_FOLDER_ID:
file_metadata = {'name': GOOGLE_DOC_TITLE, 'mimeType': 'application/vnd.google-apps.document', 'parents': [TARGET_FOLDER_ID]}
try:
created_file = drive_api_service.files().create(body=file_metadata, fields='id').execute()
document_id = created_file.get('id')
print(f"Neues leeres Doc via Drive API in Ordner '{TARGET_FOLDER_ID}' erstellt, ID: {document_id}")
except Exception as e: print(f"Fehler Drive API Erstellung für leeres Doc: {e}\n Versuche Fallback...")
except Exception as e: print(f"Fehler Drive API Erstellung: {e}\n Versuche Fallback...")
if not document_id: # Fallback
try:
doc = docs_api_service.documents().create(body={'title': GOOGLE_DOC_TITLE}).execute()
@@ -149,30 +212,25 @@ if __name__ == "__main__":
# 2. Einmalige Info (Kita, Datum) GANZ AM ANFANG des Dokuments einfügen
initial_info_lines = [
"Info zum Kopieren für Ihre manuelle Kopfzeile:",
EINRICHTUNG_INFO, # Verwendet die Konstanten
FOTODATUM_INFO, # Verwendet die Konstanten
EINRICHTUNG_INFO, FOTODATUM_INFO,
"\n" + "="*70 + "\n"
]
initial_text = "\n".join(initial_info_lines) + "\n"
initial_requests = [{'insertText': {'location': {'index': 1}, 'text': initial_text}}]
try:
docs_api_service.documents().batchUpdate(documentId=document_id, body={'requests': initial_requests}).execute()
print("Einmalige Info am Dokumentanfang eingefügt.")
except HttpError as err:
print(f"Fehler beim Einfügen der einmaligen Info: {err}")
except HttpError as err: print(f"Fehler beim Einfügen der einmaligen Info: {err}")
# 3. Dokument mit gruppenspezifischen Daten befüllen
success_filling = generate_group_data_for_doc(docs_api_service, document_id) # Funktionsname angepasst
# 3. Dokument mit gruppenspezifischen Daten (echte Tabellen) befüllen
success_filling = generate_tables_with_gdoctableapp(docs_api_service, document_id)
if success_filling:
print(f"\n--- SKRIPT BEENDET ---")
print(f"Dokument-ID: {document_id}")
print(f"Link: https://docs.google.com/document/d/{document_id}/edit")
print("HINWEIS: Die Kinderlisten wurden als tabulatorgetrennter Text eingefügt.")
print(" Markieren Sie den Text in Google Docs und verwenden Sie 'Einfügen > Tabelle > Tabelle aus Text erstellen...'")
print(" Formatieren Sie die Überschriften 'Nachname, Vorname, Gruppe' danach manuell fett.")
print("HINWEIS: Es wurde versucht, echte Tabellen mit fetter Kopfzeile zu erstellen.")
else:
print("\n--- FEHLER BEIM BEFÜLLEN DES DOKUMENTS MIT GRUPPENDATEN ---")
print("\n--- FEHLER BEIM BEFÜLLEN DES DOKUMENTS MIT TABELLEN ---")
else:
print("\n--- FEHLGESCHLAGEN: Konnte kein Dokument erstellen ---")