From ea3ee863d046fab0ef8eea75bd7e3c88cf69cd1b Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 27 May 2025 05:16:08 +0000 Subject: [PATCH] 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. --- list_generator.py | 186 ++++++++++++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 64 deletions(-) diff --git a/list_generator.py b/list_generator.py index 7ae88a80..d80653a1 100644 --- a/list_generator.py +++ b/list_generator.py @@ -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 ---") \ No newline at end of file