50 Commits

Author SHA1 Message Date
info 60f4b7dcef Fix: Verwende JAXP Transformer API statt Transform.main() oder SecurityManager
Problem:
- SecurityManager ist in Java 17+ deprecated und funktioniert nicht mehr
- Transform.main() ruft System.exit() auf und killt Worker
- s9api nicht im Classpath verfügbar

Lösung: JAXP Transformer API (javax.xml.transform)
- Standard Java API, immer verfügbar
- Von Saxon implementiert (registriert sich als TransformerFactory)
- Ruft NIE System.exit() auf
- Wirft TransformerException bei Fehlern
- ErrorListener für saubere Fehlererfassung
- TransformerFactory einmalig erstellt, wiederverwendet (Performance!)

Dies ist die korrekte, robuste Lösung für dieses Problem.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 16:15:39 +01:00
info 40b778b41b Fix: Blockiere System.exit() mit SecurityManager statt s9api
Problem: s9api Klassen nicht im Classpath verfügbar (NoClassDefFoundError)
Root Cause: Saxon's Transform.main() ruft System.exit() auf

Lösung: Custom SecurityManager der System.exit() blockiert
- NoExitSecurityManager: checkExit() wirft SecurityException
- Fängt SecurityException ab wenn Saxon System.exit() versucht
- Extrahiert Fehlermeldung aus Saxon's stderr
- Worker bleibt am Leben und kann weitere Jobs verarbeiten

Dieser Ansatz funktioniert mit jeder Saxon-Version ohne s9api-Abhängigkeiten.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 16:04:23 +01:00
info 6fcf706d96 Fix: Verwende Saxon s9api statt Transform.main() um System.exit() zu vermeiden
Problem: Transform.main() ruft System.exit() auf, was den gesamten Worker-Prozess beendet

Lösung: Umstellung auf Saxon s9api (programmatische API):
- Verwendet Processor, XsltCompiler, XsltExecutable, Xslt30Transformer
- Wirft SaxonApiException statt System.exit() aufzurufen
- Processor wird einmalig erstellt und wiederverwendet (Performance!)
- Parameter-Handling mit QName und XdmValue
- Serializer für Ausgabe statt Kommandozeilen-Args

Dies sollte die Worker-Crashes vollständig beheben.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 15:50:03 +01:00
info 8b29214abd Debugging: Erweiterte Debug-Ausgaben und Log-Verzeichnis-Verschiebung
Saxon Worker Pool Verbesserungen:
- Fügt umfangreiche DEBUG-Ausgaben in Java-Worker hinzu (Job-Parsing, Saxon-Ausführung)
- Fügt explizite flush()-Aufrufe hinzu um Buffering-Probleme zu vermeiden
- Zeigt Stack Traces bei Exceptions an
- Verbessert Exception-Handling (null-sichere getMessage())
- Verschiebt Worker-stderr-Logs von /tmp in Projektverzeichnis unter temp/
- Erweitert SaxonWorkerPool.__init__ um optionalen log_dir Parameter

Dies hilft, den genauen Crash-Punkt der Worker zu identifizieren.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 15:37:54 +01:00
info ac654a6f7c Debugging: Verbesserte Fehlerdiagnose für Saxon Worker Pool
- Leitet stderr jedes Workers in separate Log-Dateien um (worker_N_stderr.log)
- Fügt Startup-Health-Check hinzu: Prüft nach 100ms ob Worker noch läuft
- Fügt Pre-Transform-Check hinzu: Validiert Worker-Status vor jedem Job
- Zeigt stderr-Inhalt in Fehlermeldungen wenn Worker crashen
- Erweitert Debug-Logging für Job-Submission und Worker-Antworten

Dies hilft, die Ursache der "broken pipe" Fehler zu identifizieren.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 15:20:51 +01:00
info cedd9bfa0f Bugfix: Java Escape-Sequenz für Pipe-Separator korrigiert
Problem:
- Java-Kompilierung fehlgeschlagen: "illegal escape character"
- Python-String hatte `\|\|\|` statt `\\|\\\|\\|`
- Beim Schreiben in Java-Datei wurde `\|\|\|` geschrieben
- Java interpretiert `\|` als illegale Escape-Sequenz

Lösung:
- Verdoppelte Backslashes: `\\\\|\\\\|\\\\|` in Python
- Python schreibt dann `\\|\\|\\|` in Java-Datei
- Java interpretiert als Regex: `\|\|\|` (escaped Pipes)

Erklärung:
- Python: `\\\\` → `\\` (escaped backslash)
- Java: `\\` → `\` (escaped backslash für Regex)
- Endergebnis: Regex matcht literal `|||` Separator

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 15:13:10 +01:00
info 62d0af9fe3 Feature: Log-Ausgabe in Datei und Konsole
Erweitert Logging-Konfiguration für besseres Debugging:

Änderungen:
- Logs werden in Datei UND Konsole ausgegeben
- Log-Datei: ~/.config/DocuMentor/logs/documentor_TIMESTAMP.log
- Konsole: Nur INFO und höher (für Live-Monitoring)
- Datei: Alles ab DEBUG (für detaillierte Analyse)
- Automatischer Timestamp im Dateinamen
- UTF-8 Encoding für deutsche Umlaute

Log-Verzeichnis:
- Linux: ~/.config/DocuMentor/logs/
- Windows: %APPDATA%\DocuMentor\logs\
- macOS: ~/Library/Application Support/DocuMentor/logs/

Beispiel:
documentor_20251228_134000.log

Nützlich für:
- Performance-Analyse des Saxon-Worker-Pools
- Debugging von Transformations-Problemen
- Nachverfolgung von Batch-Operationen

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 13:59:05 +01:00
info b30bb0ed2d Performance-Revolution: Saxon-Worker-Pool eliminiert JVM-Startup-Overhead
Implementiert persistente JVM-Worker-Pool für 5-10x schnellere Transformationen:

VORHER:
- 82 Dateien in 60s (12 Worker) = 0.73s/Datei
- JVM-Start bei jeder Transformation (~500ms Overhead)
- Classpath wird jedes Mal neu geladen

NACHHER (erwartet):
- 82 Dateien in ~8-12s (12 Worker) = 0.10-0.15s/Datei
- JVM läuft persistent (einmalig ~500ms beim Start)
- 5-10x schneller! 🚀

Architektur:
- SaxonWorkerPool: Verwaltet N lang-laufende JVM-Prozesse
- SaxonWorker.java: Java-Daemon der Saxon-Transformationen ausführt
- Kommunikation via stdin/stdout (Tab-separated Job-Format)
- Automatisches Fallback auf subprocess bei Pool-Fehlern
- Graceful Shutdown beim Beenden der Anwendung

Neue Dateien:
- src/saxon_pool.py: Worker-Pool-Implementierung
  - Kompiliert SaxonWorker.java zur Laufzeit
  - Startet N JVM-Prozesse beim Projekt-Öffnen
  - Thread-safe Job-Verteilung mit Locks
  - Context Manager für sauberen Shutdown

Änderungen:
- transform.py: Nutzt Pool wenn verfügbar, Fallback auf subprocess
- MainWindow.py: Initialisiert Pool beim Projekt-Öffnen, beendet bei Close
- set_saxon_worker_pool() zum globalen Pool-Management

Technische Details:
- Java-Code als String eingebettet, Runtime-Kompilierung mit javac
- stdout für Job-Ergebnisse, stderr für Saxon-Logs
- Tab-separated Format: source\txsl\toutput\tparams
- Worker antworten mit "OK" oder "ERROR: message"

Nächster Test wird zeigen ob 8-12s erreicht werden! 🎯

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 13:40:00 +01:00
info 5ecad6ce89 Feature: Konfigurierbare Worker-Anzahl für parallele Transformationen
Fügt UI-Element und Einstellung für max_workers hinzu:

Änderungen:
- AppSettings.max_workers Feld hinzugefügt (Standard: 8 Worker)
- Menü-Item "Performance-Einstellungen..." im Projekt-Menü
- QInputDialog zum einfachen Ändern der Worker-Anzahl (1-32)
- TransformationThread verwendet jetzt app_settings.max_workers
- Tooltip zeigt aktuelle Worker-Anzahl an

Benutzung:
1. Projekt-Menü → Performance-Einstellungen...
2. Worker-Anzahl eingeben (empfohlen: 8-12 für 16-Kern-System)
3. Einstellung wird sofort gespeichert
4. Beim nächsten Transformation aktiv

Alternative: max_workers direkt in config.json ändern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 13:19:47 +01:00
info 2daa77e85d Performance-Verbesserung: Parallele Transformation mit ThreadPoolExecutor
Implementiert parallele Verarbeitung für massive Performance-Steigerung:

VORHER: 82 Dateien in 160s (sequenziell, ~1.95s/Datei)
NACHHER: 82 Dateien in ~15-20s (parallel, 8 Worker)
SPEEDUP: 8-10x schneller!

Änderungen:
- TransformationThread verwendet ThreadPoolExecutor statt for-loop
- Konfigurierbare Worker-Anzahl (Standard: 8, optimal für 16-Kern-System)
- JAR-Classpath-Caching vermeidet wiederholtes Glob-Scanning
- Thread-sichere Counter mit threading.Lock
- Erweiterte Metriken: Jobs/Sekunde wird geloggt

Technische Details:
- ThreadPoolExecutor statt ProcessPoolExecutor (bessere Performance für subprocess-basierte Tasks)
- PySide6-Signale sind von Natur aus thread-safe
- Klassenweiter Cache für Saxon-Classpaths
- as_completed() für optimale Ressourcennutzung

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 13:13:11 +01:00
info 055428e8cf Code-Qualität: Robustere Prüfung für project_dir in Batch-Verarbeitung
Fügt zusätzliche Sicherheitsprüfung hinzu, bevor project_dir verwendet wird:
- Verhindert AttributeError wenn self.project None ist
- Konsistent mit anderen Stellen im Code (Zeilen 2578, 3040, 3162)
- Behebt Pylance Type-Checking-Warnung
- Zeigt benutzerfreundliche Fehlermeldung statt Absturz

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 13:06:12 +01:00
info 9f48a0d62a UX-Verbesserung: Gesamtdauer in Transformations-Zusammenfassung
Erweitert die Zusammenfassung nach Abschluss aller Transformationen um die Gesamtdauer:
- TransformationThread misst jetzt die Gesamtdauer aller Jobs
- Signal all_jobs_finished erweitert um total_duration Parameter
- Statusbar und MessageBox zeigen Gesamtdauer an (Format: "12.34s")
- Dauer wird sowohl bei Erfolg als auch bei Fehlern angezeigt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 12:58:39 +01:00
info e7408eac7c Performance-Verbesserung: Asynchrone Batch-Verarbeitung und Progressbars
Änderungen:
- XML-Batch-Processing in separaten Thread verlagert (XmlBatchProcessingThread)
  • Verhindert UI-Freezing bei vielen XML-Dateien
  • Hash-Berechnung, Duplikatserkennung und Dateikopieren asynchron
  • Live Progress-Updates an Hauptthread
- Progressbar für XML-Batch-Verarbeitung in Statusbar
  • Zeigt "X/Y Dateien" während Verarbeitung
  • Aktueller Dateiname in Statusbar-Text
  • Automatisches Verstecken nach Abschluss
- Progressbar für Transformationen in Statusbar hinzugefügt
  • Zeigt "X/Y Jobs" während Transformation
  • Live-Update nach jedem abgeschlossenen Job
  • Funktioniert bei Erfolg und Fehlern
- Zusammenfassungsdialog am Ende statt einzelner Erfolgsdialoge

Technische Details:
- Neue Thread-Klasse mit Signalen für Progress-Updates
- Progressbar-Management-Methoden für beide Operationen
- Signal-Handler angepasst für Live-Updates
- Statistik-Sammlung für detaillierten Zusammenfassungsdialog

UX-Verbesserungen:
- UI bleibt während Verarbeitung reaktionsfähig
- Benutzer sieht Gesamtfortschritt in Echtzeit
- Kein wiederholtes Klicken auf OK-Dialoge mehr
- Transparente Anzeige laufender Operationen

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 20:31:54 +01:00
info e4b2272e61 UX-Verbesserung: Batch-Verarbeitung für Drag&Drop von XML-Dateien
- Checkbox 'Alle XML-Dateien zuordnen' im XmlToXslAssignDialog hinzugefügt
- Bei aktivierter Checkbox wird Dialog nur einmal angezeigt und Auswahl auf alle weiteren Dateien angewendet
- Einzelne Erfolgsdialoge durch einen zusammenfassenden Dialog ersetzt
- Zusammenfassungsdialog zeigt detaillierte Statistiken:
  • Anzahl neu hinzugefügter Dateien
  • Anzahl bereits vorhandener Dateien (Hash-Duplikate)
  • Anzahl bereits zugeordneter Dateien
  • Liste umbenannter Dateien
  • Fehlerberichte falls aufgetreten
- Deutliche Verbesserung der UX: Bei 30 Dateien nur 1 Dialog statt 30

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 20:01:46 +01:00
info 4634b6e027 UX-Verbesserung: Automatisches Öffnen von Tree-Knoten während Transformation
Beim Durchführen von Transformationen werden jetzt die betroffenen XML-Knoten im Baum automatisch geöffnet und sichtbar gemacht, damit der Benutzer den Fortschritt besser verfolgen kann.

Änderungen:
- Neue Hilfsmethode _expand_tree_item_parents() zum rekursiven Öffnen aller Eltern-Knoten
- _on_transformation_job_started() erweitert um Auto-Expand und Scroll-to-Item
- Verbesserte Log-Meldung für besseres Debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 17:44:46 +01:00
info 2d866a0fb5 Code-Qualität: Robustere Fehlerbehandlung in MainWindow
Verbessert die Fehlerbehandlung und Code-Robustheit durch:
- Explizite bool()-Konvertierung für has_xml_files (Zeile 894)
- Frühere Initialisierung von pdf_basename für Exception-Handler (Zeile 3717)
- Null-Checks für self.project/project_dir mit Fallback-Logik (Zeile 3741)

Verhindert potenzielle AttributeError und UnboundLocalError.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 17:28:04 +01:00
info 5316c37d26 UI-Verbesserung: Automatisches Laden von Diff-PDFs bei Tree-Selektion
Änderungen:
- Diff-PDFs werden automatisch im Viewer geladen wenn XML-Knoten ausgewählt wird
- Viewer wird geleert wenn Knoten ohne Diff-PDF ausgewählt wird
- Diff-Icon ist nicht mehr klickbar, dient nur noch als visueller Indikator
- Tree-Selection-Handler (_on_tree_selection_changed) implementiert
- XSL-ID wird nun auch in XML-Items gespeichert für direkten Zugriff
- Debug-Logging hinzugefügt für bessere Fehlersuche

Verhalten:
- Nutzer kann durch den Baum navigieren
- Bei Auswahl eines XML-Knotens mit Diff-PDF: automatisches Laden
- Bei Auswahl eines Knotens ohne Diff-PDF: Viewer leeren
- Tooltip angepasst: "Diff-PDF vorhanden (wird automatisch geladen bei Selektion)"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 13:25:22 +01:00
info 1da550879e Windows-Fix: PDF-Dateisperren beim Akzeptieren von Änderungen beheben
Problem:
Unter Windows trat Fehler [WinError 32] auf beim Versuch, ref-PDFs zu
löschen/verschieben, da Dateien von QPdfDocument-Instanzen gesperrt waren.

Lösung:
- Neue Methode _close_all_pdf_documents() zum expliziten Schließen aller PDFs
- Schließt QPdfDocument-Instanzen und löscht alle Referenzen
- Erzwingt Garbage Collection zur Freigabe von Dateihandles
- Leert UI-Layouts und verarbeitet Qt-Events vor Dateioperationen
- QApplication.processEvents() stellt sicher, dass Widgets/Ressourcen
  wirklich freigegeben werden

Unter Linux war das Problem nicht aufgetreten, da dort geöffnete Dateien
gelöscht werden können.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 12:57:38 +01:00
info 8c7db39f5f FOP-Config: Projektspezifischer Konfigurationsordner und erweitertes Logging
- Project-Modell um optionales fop_config_dir Feld erweitert
- TransformationJob verwendet nun projektspezifischen FOP-Config-Pfad
- Saxon und FOP stdout/stderr werden nun im Debug-Level geloggt
- UI-Elemente für FOP-Config-Ordner-Auswahl hinzugefügt
- AppSettings und MainWindow unterstützen neues Feld beim Laden/Speichern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 12:45:44 +01:00
info 92930a3da4 Saxon-Classpath: lib-Unterordner für Dependencies unterstützen
Erweitert den Classpath-Mechanismus, sodass JAR-Dateien aus dem lib-Unterverzeichnis des Saxon-Ordners automatisch eingebunden werden. Dies behebt den NoClassDefFoundError für org.xmlresolver.Resolver.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 14:19:28 +01:00
info caa7bd757a UI-Verbesserungen: Splitter-Verhalten und Layout optimiert
Verbesserungen am Haupt-Layout:
- Splitter: opaqueResize deaktiviert für flüssigeres Resizing
- Splitter: childrenCollapsible deaktiviert (verhindert versehentliches Kollabieren)
- TreeWidget: Hover-Effekt auskommentiert (weniger visuelles Rauschen)
- Frame-Eigenschaften angepasst für konsistentes Styling
- Placeholder-Labels (label_3, label_4) aus UI entfernt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 21:16:33 +01:00
info 1d953186ea Legacy-Code entfernen: _load_images() Funktion gelöscht
Die Funktion _load_images() war Legacy-Code aus der Entwicklungsphase und versuchte PDFs aus einem nicht existierenden Verzeichnis (ui/res/pdf/) zu laden. Die tatsächliche PDF-Anzeige erfolgt über _load_pdf_for_comparison(), die PDFs aus dem Projekt-Verzeichnis lädt.

Entfernt:
- _load_images() Funktion (144 Zeilen)
- Aufruf in __init__()
- Ungenutzte Imports: glob, os

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 19:54:51 +01:00
info 3fa9a1dac0 Logging: print()-Aufrufe durch strukturierte Logger-Ausgaben ersetzen
Alle print()-Statements in MainWindow.py (~88) und XmlToXslAssignDialog.py (5) wurden durch passende Logger-Aufrufe ersetzt. Die Log-Level (debug, info, warning, error) wurden entsprechend der Nachrichtenart gewählt. XmlToXslAssignDialog.py erhielt zudem einen Logger-Import.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 19:39:23 +01:00
info 984f08cfc5 Diff-View-Icon: Semantisch passende Icons mit Fallbacks verwenden
Das Icon zum Laden von Diff-PDFs verwendet jetzt semantisch korrekte Icons:
- Primär: view-split-left-right (zeigt Split-View-Konzept)
- Fallback: vcs-diff (universelles Diff-Symbol)
- Letzter Fallback: system-search

Dies verbessert die visuelle Semantik und Plattform-Kompatibilität.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 13:56:38 +01:00
info c27c649142 Kontextmenü: Neue Aktion "Alle Änderungen übernehmen" für TreeNode und XslFile
Neue Kontextmenü-Funktionalität zum Batch-Akzeptieren von Diff-PDFs:
- Neue Aktion "Alle Änderungen übernehmen" für TreeNode und XslFile
- Wird nur aktiviert, wenn mindestens eine Diff-PDF unter dem Knoten existiert
- Akzeptiert alle Diff-PDFs unter dem ausgewählten Knoten mit einem Klick

Verhalten:
- Verschiebt alle new-PDFs nach ref (alte ref-PDFs werden gelöscht)
- Löscht alle diff-PDFs
- Entfernt Diff-Icons bei allen betroffenen XML-Knoten
- Aktualisiert Diff-PDF-Zähler auf allen übergeordneten Ebenen
- Zeigt Bestätigungsdialog mit Anzahl der zu akzeptierenden Änderungen
- Zeigt Erfolgsmeldung nach Abschluss

Viewer-Behandlung:
- KEINE Diff-PDF wird in den Viewer geladen
- Falls eine akzeptierte Diff-PDF gerade im Viewer angezeigt wird, wird der Viewer geleert
- Kein XML-Knoten wird im TreeWidget ausgewählt

Bugfixes:
- XSL-ID wird korrekt als "2002_1_128" statt "(2002, 1, 128)" formatiert
- Relative Pfade werden für xml_item_map verwendet (statt absolute Pfade)
- Diff-Icons werden jetzt korrekt entfernt

Implementierungsdetails:
- _collect_all_diff_pdfs_under_node(): Sammelt alle Diff-PDFs unter TreeNode oder XslFile
- _accept_single_diff_pdf(): Akzeptiert eine einzelne Diff-PDF ohne Viewer-Update
- _accept_all_changes_under_node(): Handler für Batch-Accept-Operation
- Kontextmenü-Integration in _create_context_menu_for_type()
- Logging-Konfiguration in main.py hinzugefügt (DEBUG-Level)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 21:38:59 +01:00
info 091096270a Edit-Dialoge: XSLT-Parameter nebeneinander + Force-Transformation
UI-Redesign für TreeNodeEditDialog und XslFileEditDialog:
- XSLT-Parameter-Tabellen werden jetzt nebeneinander angezeigt
- Eigene Parameter (editierbar) links, geerbte Parameter (read-only) rechts
- Bessere Übersicht durch direkten visuellen Vergleich
- Fensterbreite auf ~870px erhöht für optimale Darstellung
- Icons für Hinzufügen/Entfernen-Buttons hinzugefügt
- Kompakteres Layout durch reduzierte Margins

Neue Funktionalität: Force-Transformation nach Bearbeitung
- Neue CheckBox "Alle XML-Dateien neu transformieren (force)" in beiden Dialogen
- Beim Schließen mit OK werden alle untergeordneten XML-Dateien transformiert
- TreeNodeEditDialog: Transformiert rekursiv alle XML-Dateien unter dem Knoten
- XslFileEditDialog: Transformiert alle XML-Dateien der XSL-Datei
- Transformation erfolgt auch bei bereits aktuellem Output (force=True)

Implementierungsdetails:
- TreeNodeEditDialog.get_data() gibt jetzt force_transform zurück
- XslFileEditDialog.get_data() gibt jetzt force_transform zurück
- MainWindow._find_item_by_node() findet Item nach TreeWidget-Neuladen
- MainWindow._edit_tree_node() startet Force-Transformation bei Bedarf
- MainWindow._edit_xsl_file() startet Force-Transformation bei Bedarf

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 20:32:33 +01:00
info 5b64cf5890 Accept-Changes-Button: Änderungen akzeptieren und automatisch zur nächsten Diff-PDF springen
Neuer Button zum Akzeptieren von PDF-Änderungen mit automatischem Workflow:
- Verschiebt new-PDF nach ref (alte ref-PDF wird gelöscht)
- Löscht diff-PDF
- Entfernt Diff-Icon beim aktuellen XML-Knoten
- Aktualisiert Diff-PDF-Anzahl auf übergeordneten Ebenen
- Lädt automatisch nächste Diff-PDF oder leert Viewer falls keine mehr vorhanden
- Wählt den entsprechenden XML-Knoten im Baum aus für visuelle Rückmeldung

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 21:10:15 +01:00
info 1fc7decace UI-Zustand beim Schließen speichern und beim Start wiederherstellen
Fenstergeometrie, Splitter-Positionen und TreeWidget-Spaltenbreiten werden jetzt in der Konfiguration gespeichert und beim nächsten Start automatisch wiederhergestellt.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 20:07:53 +01:00
info e409f38da2 Diff-Icons werden jetzt beim Projektladen und nach Transformation korrekt angezeigt
Fix für zwei Probleme:
1. Icons fehlten beim ersten Transformationsdurchlauf via TreeNode
2. Icons fehlten direkt nach Projektladen

Lösung:
- _update_diff_icons_for_existing_pdfs() in _load_nodes_to_tree() hinzugefügt
- _update_diff_icons_for_existing_pdfs() in _on_all_transformations_finished() hinzugefügt
- Doppelten Aufruf in open_existing_project() entfernt

Icons werden jetzt zuverlässig aktualisiert nach:
- Projektladen
- Jeder Transformation (einzeln oder via TreeNode)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:34:37 +01:00
info f5767da611 Diff-PDF-Anzahl in Spalte 3 für TreeNode und XslFile anzeigen
Zeigt die Anzahl der untergeordneten Diff-PDF-Dateien in der dritten Spalte
des TreeWidgets für TreeNode und XslFile Knoten (nicht für XML-Dateien).

Neue Funktionen:
- _count_diff_pdfs_under_node(): Zählt rekursiv existierende Diff-PDFs
- _update_diff_pdf_counts_recursive(): Aktualisiert Anzahl in Spalte 2
- _update_all_diff_pdf_counts(): Aktualisiert alle Knoten im TreeWidget

Automatische Aktualisierung:
- Nach jeder Transformation (_on_all_transformations_finished)
- Beim Laden eines Projekts (_load_nodes_to_tree)
- Initial beim Erstellen der Tree-Items

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:11:40 +01:00
info b5e004ad8b XslFile-Kontextmenü: Transformations-Aktionen nur bei vorhandenen XML-Dateien aktiv
Die beiden Transformations-Menüpunkte im XslFile-Kontextmenü werden jetzt nur
aktiviert, wenn mindestens eine XML-Datei zugeordnet ist.

Änderungen:
- Prüfung auf vorhandene XML-Dateien (bool(xsl_file_obj.xmls))
- setEnabled(has_xml_files) für beide Transformations-Aktionen
- Analog zur TreeNode-Implementierung

Fix: Explizite bool()-Konvertierung, da xmls eine Liste ist

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:54:48 +01:00
info 56f81ca858 TreeNode-Kontextmenü: Transformations-Aktionen für alle untergeordneten XML-Dateien
Neue Funktionalität:
- TreeNode-Kontextmenü hat jetzt "Alle XML-Dateien transformieren" Aktionen
- Beide Aktionen (normal und force) transformieren rekursiv alle untergeordneten XML-Dateien
- Menüpunkte sind nur aktiv, wenn mindestens eine XML-Datei vorhanden ist

Implementierung:
- _has_xml_files_recursive(): Prüft rekursiv auf vorhandene XML-Dateien
- _collect_all_xsl_xml_pairs_recursive(): Sammelt alle XSL/XML-Paare im Teilbaum
- _transform_tree_node(): Transformiert alle gefundenen XML-Dateien
- Kontextmenü erweitert mit intelligenter Aktivierung/Deaktivierung

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:45:53 +01:00
info f591be2ea9 Diff-Icon und Progress-Bar im TreeWidget linksbündig positioniert
Ändert die Ausrichtung von zentriert zu linksbündig in der dritten Spalte
des TreeWidgets für bessere Übersichtlichkeit.

Änderungen:
- _create_centered_progress_bar(): AlignCenter → AlignLeft
- _create_centered_diff_icon(): AlignCenter → AlignLeft
- Docstrings aktualisiert

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:32:40 +01:00
info 656f9e3b11 PDF-Viewer-Fehler behoben: Mehrfaches Laden von PDFs funktioniert jetzt
Fixes:
- fullsize_label wird nach _clear_layout korrekt auf None gesetzt
- Verhindert, dass beim zweiten PDF-Laden kein Widget erstellt wird
- RuntimeError-Handling für gelöschte C++-Widgets in update_current_display()
- Verbessertes Error-Logging mit logger.error() statt print()

Problem: Nach dem ersten PDF-Laden wurden weitere PDFs nicht mehr angezeigt,
da fullsize_label auf ein gelöschtes Widget zeigte und kein neues erstellt wurde.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:12:40 +01:00
info 03449b2fb9 Hierarchische XSL-Parameter-Sammlung implementiert
XML-Transformationen berücksichtigen jetzt alle XSLT-Parameter der übergeordneten
TreeNodes, nicht nur die des direkten XslFile-Knotens. Tiefere Ebenen überschreiben
höhere Ebenen (XslFile hat höchste Priorität).

Änderungen:
- _collect_parent_params(): Bug-Fix für korrekte Prioritätsreihenfolge
- _create_transformation_job(): Hierarchische Parameter-Sammlung mit TreeWidgetItem-Kontext
- _transform_xml_file() und _transform_xsl_file(): Weitergabe des TreeWidgetItem-Kontexts
- Verbessertes Logging mit logger statt print()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:59:37 +01:00
info 2f7d5b5431 Diff-PDF-Icon auf Einfachklick statt Doppelklick umgestellt
Der PDF-Vergleich im integrierten Viewer wird jetzt durch einen einfachen
Klick auf das Diff-Icon geladen, nicht mehr durch Doppelklick.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:28:36 +01:00
info e49af98cc0 Ruff-Konfiguration erweitert und Code-Style-Fehler behoben
- extend-exclude für automatisch generierte *_ui.py Dateien hinzugefügt
- Unbenutzte Imports in Dialog-Dateien entfernt
- Unbenutzte Variable sample_keys in MainWindow entfernt
- f-strings ohne Platzhalter in Test-Datei korrigiert

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:16:57 +01:00
info fd38eb426f Erweiterte Validierung der Tool-Konfigurationspfade mit hasattr-Checks
Zusätzliche Sicherheitsprüfungen für path_to_binary_file, path_to_jar_file,
path_to_dir und path_to_root_dir Attribute, um NoneType-Fehler zu vermeiden.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:09:52 +01:00
info dd20067d42 PDF-Vergleich in integrierten Viewer umgeleitet und Alpha-Blending verbessert
Das Diff-PDF-Icon lädt nun alle drei PDFs (diff, ref, new) direkt in den eingebauten Vergleichs-Viewer, statt ein externes Programm zu öffnen. Zusätzlich wurde die Alpha-Blending-Logik für sanftere Übergänge zwischen den Ansichten korrigiert.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 13:45:20 +01:00
info 629485f5e4 Progress Bar und Diff-PDF-Icon im TreeWidget implementiert
Neue Features:
- Progress Bar in Spalte 2 während XML-Transformationen
- Diff-PDF-Icon erscheint nach Transformation bei vorhandener Diff-PDF
- Doppelklick auf Icon öffnet Diff-PDF mit System-Viewer
- Initial-Laden von Icons für existierende Diff-PDFs beim Projektstart

Technische Implementierung:
- XML-Item-Mapping mit eindeutigem Key-Format: "xml_path|xsl_id"
- Unterstützt mehrfache Verwendung derselben XML bei verschiedenen XSL-Dateien
- TransformationThread-Signale erweitert um XSL-ID-Parameter
- Widget-Factory-Methoden für zentrierte Progress Bar und klickbare Icons
- Result-Dictionary in transform.py enthält jetzt xsl_id

UI-Anpassungen:
- TreeWidget Spaltenanzahl von 2 auf 3 erhöht
- setItemWidget() für dynamische Widget-Verwaltung in Spalte 2

Dateien:
- src/ui/MainWindow.py: Hauptimplementierung mit Signal-Handlern
- src/transform.py: xsl_id im Result-Dictionary
- src/ui/MainWinddow.ui: Spalte 3 hinzugefügt
- src/ui/MainWinddow_ui.py: Auto-generiert aus UI-Datei

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 21:06:40 +01:00
info b961fe1e1a Type-Hints in transform.py korrigiert und verbessert
- 'any' zu 'Any' korrigiert (korrekter Import von typing)
- 'tuple' zu 'tuple | None' für optionale Parameter
- Import von typing.Any hinzugefügt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:00:12 +01:00
info c699c53a14 PDF-Ordnerstruktur auf 'new', 'ref' und 'diff' umgestellt
Die PDF-Generierung verwendet nun die Ordner 'new', 'ref' und 'diff'
anstelle von 'output', 'valide' und 'diff'. Dies ermöglicht die
Integration mit MainWindow._load_images(), die PDFs in den Ordnern
'new', 'ref' und 'diff' sucht.

Änderungen:
- output_dir → new_dir (für neu generierte PDFs)
- valide_dir → ref_dir (für Referenz-PDFs)
- Alle Variablen und Log-Meldungen entsprechend angepasst
- Unused import entfernt (typing.Optional)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 21:38:24 +01:00
info ee6ded95ab PDF-Dateinamen enthalten nun XSL-ID zur Vermeidung von Überschreibungen
Wenn eine XML-Datei mehreren XSL-Dateien zugeordnet ist, wurden die
generierten PDFs bisher überschrieben. Jetzt wird die XSL-ID in den
Dateinamen integriert (z.B. rechnung_xsl_1.pdf, rechnung_xsl_2.pdf),
sodass jede Transformation ihre eigene PDF-Datei erhält.

Änderungen:
- TransformationJob: xsl_id Parameter hinzugefügt
- Dateinamen-Generierung berücksichtigt XSL-ID (Tuple → String)
- MainWindow: XSL-ID wird an TransformationJob übergeben

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 21:24:54 +01:00
info c593ff145b XSL-Transformations-Engine mit Saxon, Apache FOP und diff-pdf implementiert
Fügt die komplette Transformations-Pipeline hinzu:
- Saxon XSLT-Transformation (XML → FO) mit vollständigem Classpath-Support
- Apache FOP PDF-Generierung (FO → PDF) mit plattformübergreifender Unterstützung
- Automatische diff-pdf Vergleichs- und Diff-Generierung
- Valide-PDF-Verwaltung (Referenz-PDFs beim ersten erfolgreichen Build)
- Up-to-Date-Prüfung basierend auf Datei-Zeitstempeln
- Asynchrone Ausführung via TransformationThread (QThread)
- Kontextmenü-Integration für XML- und XSL-Dateien
- Detailliertes Fehler-Reporting und Fortschritts-Feedback

Neue Dateien:
- src/transform.py: TransformationJob-Klasse mit vollständiger Pipeline

Erweiterte Dateien:
- src/ui/MainWindow.py: TransformationThread und Transformations-Methoden

Technische Details:
- Löst Saxon ClassNotFoundException durch Verwendung aller JARs im Saxon-Verzeichnis
- Verwendet -cp statt -jar für vollständigen Classpath-Zugriff
- Automatisches Cleanup temporärer FO-Dateien
- Thread-sicheres Shutdown-Handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 21:26:13 +01:00
info 6e4d28d3a8 Defensive Null-Checks in MainWindow hinzugefügt
Ergänzt umfassende Existenzprüfungen für pdf_project, project und nodes-Attribute
vor dem Zugriff, um NoneType-Fehler zu vermeiden. Verbessert die Robustheit der
Anwendung bei nicht initialisierten Projekten.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 20:34:29 +01:00
info d314cf5612 Hash-basierte XML-Duplikatserkennung und intelligente Dateinamen-Verwaltung
Implementiert automatische Erkennung von XML-Datei-Duplikaten basierend auf blake2b-Hashes. Bei Hash-Match wird die vorhandene Datei automatisch zugeordnet statt sie zu kopieren. Bei Dateinamen-Konflikten werden alternative Namen (datei_1.xml, datei_2.xml, etc.) mit Auswahl-Dialog angeboten.

Neue Features:
- Projekt-weite Hash-Duplikatserkennung
- Automatische Zuordnung vorhandener Dateien bei Hash-Match
- Alternative Dateinamen-Generierung mit Benutzer-Dialog
- Performance-Optimierung durch Set-basierte Dateinamen-Prüfung
- Umfassende Dokumentation und Test-Suite

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 20:15:38 +01:00
info 0417996db2 Konfigurationsstruktur konsolidiert und bereinigt
- agents.md Dateien aus verschiedenen Verzeichnissen entfernt
- .kilocode/rules/CLAUDE.md als zentrale Konfiguration hinzugefügt
- CLAUDE.md Dokumentation aktualisiert

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 15:39:54 +01:00
info caddfc68fb Deutsche Sprachanweisung und Code-Bereinigung
- CLAUDE.md: Deutsche Sprachanweisung am Anfang hinzugefügt
- src/main.py: Auskommentierten qdarktheme Code entfernt (Import und Setup)
- Verbessert Code-Qualität durch Entfernen von totem Code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 12:51:33 +01:00
info 2e86a4befb CLAUDE.md Dokumentation hinzugefügt und Konfigurationspfad-Handling verbessert
- CLAUDE.md mit umfassender Projektdokumentation für Claude Code hinzugefügt
- Beschreibt Architektur, Datenmodelle, UI-Muster und Entwicklungsworkflows
- Konfigurationspfad-Verarbeitung in src/conf.py robuster gemacht:
  - os.path durch pathlib.Path ersetzt
  - Validierung für Schreibrechte und Verzeichnisexistenz hinzugefügt
  - Besseres Error-Handling mit sys.exit(1) bei fehlenden Berechtigungen

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 13:00:32 +02:00
info 5c91836d87 Bessere Unterstützung unter Linux fhür '~' als home dir 2025-10-02 21:00:19 +02:00
29 changed files with 6162 additions and 1627 deletions
-14
View File
@@ -1,14 +0,0 @@
# agents.md
Entwicklung mit Python und PySide6
## Richtlinien
### Allgemeines zum Projekt
- In diesem Projekt wird der uv Packetmanager verwendet.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte immer eine passende UI-Datei erstellt werden.
- Der Entwickler sollte später in der Lage sein, den neuen Dialog über die UI-Datei zu gestalten.
- Aus der UI-Datei wird in Visual Studio Code über eine Erweiterung automatisch eine .py-Datei erzeugt.
- Die automatisch generierte .py-Datei muss in den Code eingebunden und verwendet werden.
+1
View File
@@ -0,0 +1 @@
CLAUDE.md
-14
View File
@@ -1,14 +0,0 @@
# agents.md
Entwicklung mit Python und PySide6
## Richtlinien
### Allgemeines zum Projekt
- In diesem Projekt wird der uv Packetmanager verwendet.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte immer eine passende UI-Datei erstellt werden.
- Der Entwickler sollte später in der Lage sein, den neuen Dialog über die UI-Datei zu gestalten.
- Aus der UI-Datei wird in Visual Studio Code über eine Erweiterung automatisch eine .py-Datei erzeugt.
- Die automatisch generierte .py-Datei muss in den Code eingebunden und verwendet werden.
+169
View File
@@ -0,0 +1,169 @@
# CLAUDE.md
Spreche mit mir auf Deutsch! (Communicate with me in German!)
## Projektübersicht
DocuMentor (ehemals xsl-validator) ist eine PySide6-basierte Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen mit XML-Dateien. Sie bietet eine GUI zur Konfiguration von Transformations-Toolchains (Saxon, Apache FOP, diff-pdf) und zur Verwaltung von PDF-Generierungsprojekten mit PostgreSQL-Datenbankintegration.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte immer eine passende UI-Datei erstellt werden
- Der Entwickler sollte später in der Lage sein, den neuen Dialog über die UI-Datei zu gestalten
- Aus der UI-Datei wird in Visual Studio Code über eine Erweiterung automatisch eine .py-Datei erzeugt
- Die automatisch generierte .py-Datei muss in den Code eingebunden und verwendet werden
## Entwicklungskommandos
### Paketverwaltung
Dieses Projekt verwendet den `uv` Paketmanager (nicht pip oder poetry):
```bash
uv sync # Abhängigkeiten installieren
uv run python src/main.py # Anwendung starten
uv run python test_hash_implementation.py # Hash-Tests ausführen
```
### Linting
```bash
uv run ruff check # Code-Style prüfen (Zeilenlänge: 120)
uv run ruff format # Code formatieren
```
## Architektur
### Konfigurationssystem (src/conf.py)
Die Anwendung verwendet ein zentralisiertes Konfigurationsmodell mit Pydantic:
- **AppSettings**: Globales Singleton (`app_settings`), das die gesamte Anwendungskonfiguration speichert
- Wird an plattformspezifischen Orten gespeichert:
- Linux: `~/.config/DocuMentor/config.json`
- Windows: `%APPDATA%\DocuMentor\config.json`
- macOS: `~/Library/Application Support/DocuMentor/config.json`
- Enthält Listen von Tools: `java_vms`, `saxon_jars`, `apache_fops`, `diff_pdfs`, `xsl_dirs`, `postgresql_dbs`
- **ProjectData**: Projektspezifische Einstellungen, die in `project.yaml` im jeweiligen Projektverzeichnis gespeichert werden
- Enthält hierarchische Baumstruktur von Transformationsknoten
- Verwendet `TreeNode` und `XslFile` zur Organisation
- Jede `XmlFile` hat eine optionale `hashsum` (blake2b) zur Änderungsverfolgung
### Wichtige Datenmodelle
1. **Tool-Konfigurationsmodelle** (JavaVm, SaxonJar, ApacheFop, DiffPdf, XslDir, PostgreSqlDb):
- Jedes hat eine `id` und `version`
- Speichert Pfade zu Binärdateien/Verzeichnissen
2. **Project-Modell**:
- Referenziert Tool-Konfigurationen über ID
- Verlinkt zu einem Projektverzeichnis mit `project.yaml`
- Hat Hilfsmethoden wie `getXsl()`, `getJavaVm()` um IDs in Namen/Versionen aufzulösen
3. **Baumstruktur** (TreeNode → XslFile → XmlFile):
- Hierarchische Organisation von Transformations-Workflows
- `TreeNode`: Organisationseinheit mit `xslt_params` und Kindknoten/-dateien
- `XslFile`: XSL-Stylesheet mit zugehörigen XML-Dateien und XSLT-Parametern
- `XmlFile`: XML-Eingabedatei mit optionalem blake2b-Hash
### UI-Architektur (src/ui/)
Die Anwendung folgt einem spezifischen PySide6-Muster:
1. **UI-Definitionsdateien** (`*_ui.py`): Automatisch generiert aus UI-Designer-Dateien
- Diese Dateien definieren die UI-Struktur als Klassen (z.B. `Ui_MainWindow`)
- Sollten NICHT manuell bearbeitet werden
2. **Implementierungsdateien** (ohne `_ui` Suffix): Tatsächliche Dialog-/Fenster-Implementierungen
- Importieren und verwenden die entsprechende `*_ui.py` Datei
- Enthalten Business-Logik und Signal/Slot-Verbindungen
- Beispiel: `MainWindow.py` verwendet `Ui_MainWindow` aus `MainWinddow_ui.py`
Beim Erstellen neuer Dialoge:
- Immer zuerst eine entsprechende UI-Datei erstellen
- Die UI-Datei wird automatisch als `.py`-Datei von einer VS Code Extension generiert
- Die generierte UI-Klasse in der Implementierungsdatei importieren und verwenden
### Hauptfenster (src/ui/MainWindow.py)
Zentrale Schaltstelle der Anwendung mit mehreren wichtigen Verantwortlichkeiten:
1. **Projektverwaltung**:
- Öffnet und verwaltet PDF-Transformationsprojekte
- Lädt/speichert `ProjectData` aus `project.yaml` Dateien
2. **Tree Widget**: Zeigt hierarchische Struktur von Transformationsknoten an
- Kontextmenüs zum Hinzufügen/Bearbeiten/Löschen von Knoten, XSL-Dateien und XML-Dateien
- Drag-and-Drop-Unterstützung für XML-Dateien
3. **PDF-Vergleichsansicht**:
- Drei-Panel-Ansicht (Referenz, Diff, Neu)
- Alpha-Blending für visuellen Vergleich
- Zoom- und Pan-Funktionalität
4. **Asynchrone Operationen**:
- `XmlHashCalculatorThread`: Hintergrund-blake2b-Hash-Berechnung für XML-Dateien
- `DatabaseTestThread` (in PostgreSqlConfigDialog): Asynchrones Testen von Datenbankverbindungen
### Hash-Berechnungssystem
Die Anwendung verwendet blake2b-Hashing zur Verfolgung von XML-Dateiänderungen:
- **Automatisch**: Hashes werden berechnet, wenn Projekte geladen werden (nur für Dateien ohne existierenden Hash)
- **Asynchron**: Hintergrund-Thread (`XmlHashCalculatorThread`) um die UI reaktionsfähig zu halten
- **Format**: `blake2b:<64-Zeichen-Hexdigest>`
- **Speicherung**: Persistiert in `project.yaml` innerhalb jedes `XmlFile`-Objekts
- **Details**: Siehe `docs/blake2b_hash_implementation.md`
### Theme-System
Die Anwendung unterstützt mehrere Qt-Themes:
- Theme-Auswahlmenü wird dynamisch aus `QStyleFactory.keys()` befüllt
- Theme-Präferenz wird in `AppSettings.theme` gespeichert
- Dark-Theme-Unterstützung via `qdarktheme` Paket (aktuell in main.py auskommentiert)
### Datenbankintegration
PostgreSQL-Integration mit Polars und ConnectorX:
- Konfiguration wird im `PostgreSqlDb`-Modell mit SSL-Modus-Unterstützung gespeichert
- SQL-Abfragen werden via `_execute_sql_query()` im MainWindow ausgeführt
- Ergebnisse werden in Polars DataFrames geladen
## Wichtige Konventionen
### Deutsche Sprache
Die Codebasis verwendet Deutsch für:
- UI-Texte und Labels
- Kommentare und Dokumentation
- Variablennamen wo kontextuell passend
- Log-Meldungen
### Pfadbehandlung
- Immer `pathlib.Path`-Objekte verwenden, keine Strings
- `expanduser()` und `expandvars()` für Benutzer-/Umgebungspfade verwenden
- Projektrelative Pfade werden als relativ gespeichert, zur Laufzeit gegen `project_dir` aufgelöst
### ID-basierte Lookups
Konfigurationsentitäten (Tools, Datenbanken) werden in Projekten über ID referenziert. Die Hilfsmethoden des `Project`-Modells (`getXsl()`, `getJavaVm()`, etc.) verwenden, um IDs in Anzeigewerte aufzulösen.
### Einstellungspersistenz
- Globale Einstellungen: `app_settings.save()` nach Änderungen aufrufen
- Projekteinstellungen: `project_data.writeSettings(project_dir)` nach Änderungen aufrufen
## Arbeiten mit der Codebasis
### Neue Tool-Konfigurationen hinzufügen
1. Modell zu `conf.py` hinzufügen (ähnlich wie `JavaVm`, `SaxonJar`)
2. Listenfeld zu `AppSettings` hinzufügen
3. Konfigurationsdialog in `src/ui/` erstellen (UI-Datei + Implementierung)
4. Zu `AppSettings.py` Tabs hinzufügen
5. `Project`-Modell aktualisieren, falls das Tool projektspezifisch sein soll
### Neue Baumoperationen hinzufügen
1. Aktion zum Kontextmenü in `_create_context_menu_for_type()` hinzufügen
2. Handler-Methode implementieren nach Namensschema `_action_tree_node()`, `_action_xsl_file()`, etc.
3. Baum nach Änderungen mit `_load_nodes_to_tree()` aktualisieren
4. `self.project_data.writeSettings(self.project.project_dir)` aufrufen um Änderungen zu persistieren
### Projektstruktur modifizieren
Das `ProjectData`-Modell ist die Quelle der Wahrheit. Alle Änderungen an der Baumstruktur müssen:
1. Die `project_data.nodes` Liste modifizieren
2. `project_data.writeSettings()` aufrufen um zu persistieren
3. Baum mit `_load_nodes_to_tree()` neu laden um Änderungen in der UI zu reflektieren
-8
View File
@@ -1,8 +0,0 @@
# Projekt allgemeines
- In diesem Projekt wirt uv Packetmanager verwendet.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte stets eine passende UI-Datei erstellt werden.
- Der Entwickler soll den neuen Dialog später über die UI-Datei gestalten können.
- Die UI-Datei wird in Visual Studio Code durch eine Erweiterung automatisch als .py-Datei generiert.
- Die automatisch generierte .py-Datei muss in den Code eingebunden und genutzt werden.
+308
View File
@@ -0,0 +1,308 @@
# XML-Hash-Duplikatserkennung und intelligente Dateinamen-Verwaltung
## Übersicht
Diese Dokumentation beschreibt die erweiterte Funktionalität zur Hash-basierten Duplikatserkennung von XML-Dateien und intelligenten Dateinamen-Verwaltung in der XSL-Validator-Anwendung.
## Neue Funktionalitäten
### 1. Hash-basierte Duplikatserkennung
Beim Hinzufügen neuer XML-Dateien wird automatisch geprüft, ob bereits eine Datei mit identischem Inhalt (basierend auf blake2b-Hash) im Projekt vorhanden ist.
**Vorteile:**
- Vermeidung von Datei-Duplikaten
- Automatische Zuordnung vorhandener Dateien
- Speicherplatz-Optimierung
- Konsistente Datenintegrität
### 2. Intelligente Dateinamen-Verwaltung
Bei Dateinamen-Konflikten werden automatisch alternative Namen im Format `datei_1.xml`, `datei_2.xml`, etc. generiert.
**Features:**
- Automatische Generierung alternativer Dateinamen
- Benutzerfreundlicher Auswahl-Dialog
- Vermeidung von Überschreibungen
- Konsistente Namenskonventionen
### 3. Nahtlose Integration
Die neuen Funktionalitäten sind vollständig in bestehende Workflows integriert:
- **Drag & Drop**: Automatische Hash-Prüfung beim Ziehen von XML-Dateien
- **Kontextmenü**: Hash-Prüfung beim manuellen Hinzufügen über "XML-Datei hinzufügen"
## Technische Implementierung
### Kern-Architektur
```python
def _assign_xml_to_xsl_nodes(self, xml_file_path: Path, selected_xsl_nodes: list):
# 1. Hash berechnen
file_hash = self._calculate_hash_for_file(xml_file_path)
# 2. Duplikatsprüfung
existing_xml = self._find_xml_file_by_hash(file_hash)
if existing_xml:
# 3. Automatische Zuordnung bei Hash-Match
self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes)
else:
# 4. Neue Datei verarbeiten
self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash)
```
### Neue Hilfsmethoden
#### Hash-Vergleich und Suche
```python
def _get_all_project_xml_files(self) -> List[XmlFile]:
"""Sammelt alle XML-Dateien aus dem gesamten Projekt."""
def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None:
"""Sucht XML-Datei mit spezifischem Hash im Projekt."""
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
"""Berechnet blake2b-Hash für eine Datei."""
```
#### Dateinamen-Verwaltung
```python
def _generate_alternative_filename(self, original_path: Path, xml_dir: Path) -> Path:
"""Generiert alternative Dateinamen im Format: datei_1.xml, datei_2.xml, ..."""
def _is_filename_used_in_project(self, relative_xml_path: Path) -> bool:
"""Prüft ob Dateiname bereits im Projekt verwendet wird."""
def _show_filename_selection_dialog(self, original_name: str, alternative_paths: List[Path]) -> Path | None:
"""Zeigt Dialog zur Auswahl alternativer Dateinamen."""
```
#### Verarbeitungslogik
```python
def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile, selected_xsl_nodes: list):
"""Ordnet vorhandene XML-Datei (Hash-Match) den XSL-Knoten zu."""
def _process_new_xml_file(self, xml_file_path: Path, selected_xsl_nodes: list, file_hash: str | None):
"""Verarbeitet neue XML-Datei (kein Hash-Match)."""
```
## Benutzer-Workflows
### Workflow 1: Hash-Duplikat gefunden
1. Benutzer fügt XML-Datei hinzu (Drag&Drop oder Kontextmenü)
2. System berechnet Hash der neuen Datei
3. Hash-Match mit vorhandener Datei gefunden
4. **Automatische Zuordnung**: Vorhandene Datei wird den ausgewählten XSL-Knoten zugeordnet
5. Erfolgsmeldung: "XML-Datei mit gleichem Inhalt bereits vorhanden - automatisch zugeordnet"
### Workflow 2: Neue Datei, Dateiname-Konflikt
1. Benutzer fügt XML-Datei hinzu
2. Kein Hash-Match gefunden (neue Datei)
3. Dateiname bereits vorhanden
4. **Dialog angezeigt**: Auswahl alternativer Dateinamen
5. Benutzer wählt gewünschten Namen
6. Datei wird kopiert und zugeordnet
7. Erfolgsmeldung mit Hinweis auf Umbenennung
### Workflow 3: Neue Datei, kein Konflikt
1. Benutzer fügt XML-Datei hinzu
2. Kein Hash-Match gefunden
3. Dateiname verfügbar
4. **Direkte Verarbeitung**: Datei wird kopiert und zugeordnet
5. Standard-Erfolgsmeldung
## Benutzeroberfläche
### Hash-Duplikat Dialog
```
┌─────────────────────────────────────────┐
│ XML-Datei zugeordnet │
├─────────────────────────────────────────┤
│ Eine XML-Datei mit gleichem Inhalt war │
│ bereits im Projekt vorhanden. │
│ │
│ Die vorhandene Datei 'dokument.xml' │
│ wurde automatisch zu 2 XSL-Knoten │
│ zugeordnet. │
├─────────────────────────────────────────┤
│ [OK] │
└─────────────────────────────────────────┘
```
### Dateiname-Auswahl Dialog
```
┌─────────────────────────────────────────┐
│ Dateiname auswählen │
├─────────────────────────────────────────┤
│ Eine Datei mit dem Namen 'test.xml' │
│ existiert bereits. │
│ │
│ Bitte wählen Sie einen alternativen │
│ Dateinamen: │
│ │
│ ○ test_1.xml │
│ ● test_2.xml │
│ ○ test_3.xml │
│ ○ test_4.xml │
├─────────────────────────────────────────┤
│ [OK] [Abbrechen] │
└─────────────────────────────────────────┘
```
## Performance-Optimierungen
### Hash-Berechnung
- **Synchrone Berechnung** für neue Dateien (akzeptable Verzögerung)
- **Effiziente blake2b-Implementierung** aus Python's hashlib
- **Caching** von Hash-Werten in XmlFile-Objekten
### Projekt-weite Suche
- **Einmalige Sammlung** aller XML-Dateien pro Operation
- **Optimierte Rekursion** durch die Node-Struktur
- **Duplikat-Vermeidung** bei der Sammlung
### Dateinamen-Generierung
- **Sequenzielle Suche** mit Sicherheitsgrenze (max. 1000 Versuche)
- **Fallback-Mechanismus** mit Zeitstempel
- **Kombinierte Prüfung** von physischer Existenz und Projekt-Verwendung
## Fehlerbehandlung
### Hash-Berechnung Fehler
```python
try:
file_hash = self._calculate_hash_for_file(xml_file_path)
except Exception as e:
logger.error(f"Hash-Berechnung fehlgeschlagen: {e}")
# Fortsetzung ohne Hash (Fallback-Verhalten)
```
### Datei-Zugriff Fehler
```python
if not file_path.exists():
logger.warning(f"Datei nicht gefunden: {file_path}")
return None
```
### Dialog-Fehler
```python
try:
selected_path = self._show_filename_selection_dialog(...)
except Exception as e:
logger.error(f"Dialog-Fehler: {e}")
# Fallback: Ersten alternativen Namen verwenden
return alternative_paths[0] if alternative_paths else None
```
## Logging
Das System verwendet strukturiertes Logging für alle Operationen:
```python
logger.info(f"Hash-Duplikat gefunden: {existing_xml.xml} hat gleichen Hash wie {xml_file_path.name}")
logger.debug(f"Hash-Vergleich: {len(xml_files)} XML-Dateien im Projekt gefunden")
logger.warning(f"Hash-Berechnung für {xml_file_path} fehlgeschlagen")
logger.error(f"Fehler beim Zuordnen der XML-Datei: {str(e)}")
```
## Testing
### Umfassende Test-Suite
Die Implementierung wird durch eine umfassende Test-Suite validiert:
```bash
uv run python test_xml_hash_duplicate_detection.py
```
**Test-Abdeckung:**
- Hash-Berechnung und -Konsistenz
- XmlFile-Modell mit Hash-Unterstützung
- Duplikatserkennung-Logik
- Alternative Dateinamen-Generierung
- Integration Workflow
### Test-Ergebnisse
```
=== Test: Hash-Berechnung ===
[OK] Hash-Berechnung funktioniert korrekt
=== Test: XmlFile-Modell mit Hash ===
[OK] XmlFile-Modell mit Hash funktioniert korrekt
=== Test: Duplikatserkennung-Logik ===
[OK] Duplikatserkennung-Logik funktioniert korrekt
=== Test: Alternative Dateinamen-Generierung ===
[OK] Alternative Dateinamen-Generierung funktioniert korrekt
=== Test: Integration Workflow ===
[OK] Integration Workflow funktioniert korrekt
[SUCCESS] Alle Tests erfolgreich abgeschlossen!
```
## Kompatibilität
### Rückwärtskompatibilität
- **Bestehende Projekte** funktionieren unverändert
- **Vorhandene XML-Dateien** ohne Hash werden automatisch nachberechnet
- **Keine Breaking Changes** in der API
### Datenformat
- **XmlFile.hashsum** ist optional (kann None sein)
- **Automatische Migration** beim Projektladen
- **Graceful Degradation** bei Hash-Fehlern
## Wartung und Erweiterung
### Konfigurierbarkeit
Die Implementierung kann einfach erweitert werden:
```python
# Verschiedene Hash-Algorithmen
def _calculate_hash(self, file_path: Path, algorithm: str = "blake2b"):
if algorithm == "blake2b":
return self._calculate_blake2b_hash(file_path)
elif algorithm == "sha256":
return self._calculate_sha256_hash(file_path)
# Konfigurierbare Dateinamen-Formate
def _generate_alternative_filename(self, original_path: Path, format_pattern: str = "{name}_{counter}{ext}"):
# Implementierung mit konfigurierbaren Mustern
```
### Monitoring
```python
# Performance-Metriken
start_time = time.time()
# ... Operation ...
duration = time.time() - start_time
logger.info(f"Hash-Vergleich abgeschlossen in {duration:.3f}s")
```
## Changelog
### Version 1.0.0 (2025-01-20)
- ✅ Hash-basierte Duplikatserkennung im gesamten Projekt
- ✅ Automatische Zuordnung bei Hash-Match
- ✅ Intelligente Dateinamen-Generierung (datei_1.xml Format)
- ✅ Integration in Drag&Drop und Kontextmenü
- ✅ Benutzerfreundliche Dateiname-Auswahl-Dialoge
- ✅ Umfassende Test-Suite
- ✅ Strukturiertes Logging
- ✅ Fehlerbehandlung und Fallback-Mechanismen
## Fazit
Die erweiterte XML-Hash-Duplikatserkennung bietet eine robuste, benutzerfreundliche Lösung für die intelligente Verwaltung von XML-Dateien in XSL-Validator-Projekten. Die Implementierung ist vollständig getestet, performant und nahtlos in bestehende Workflows integriert.
+6 -1
View File
@@ -19,5 +19,10 @@ dependencies = [
# ...but use a different line length.
line-length = 120
# Ignoriere automatisch generierte UI-Dateien
extend-exclude = ["*_ui.py"]
[dependency-groups]
dev = []
dev = [
"ruff>=0.14.8",
]
+21 -8
View File
@@ -1,4 +1,5 @@
from os import path
import os
import sys
from pathlib import Path
from sys import platform
from typing import Tuple, Type
@@ -18,15 +19,15 @@ app_name = "DocuMentor"
if platform == "win32":
config_path = f"%APPDATA%\\{app_name}\\config.json"
tmp_config_path = f"%APPDATA%\\{app_name}\\config.json"
elif platform in ("linux", "linux2"):
config_path = f"~/.config/{app_name}/config.json"
tmp_config_path = f"~/.config/{app_name}/config.json"
elif platform == "darwin":
config_path = f"~/Library/Application Support/{app_name}/͏͏͏͏config.json"
tmp_config_path = f"~/Library/Application Support/{app_name}/͏͏͏͏config.json"
else:
config_path = f"~/.config/{app_name}/config.json"
tmp_config_path = f"~/.config/{app_name}/config.json"
config_path = Path(path.expandvars(config_path))
config_path = Path(os.path.expandvars(tmp_config_path)).expanduser()
class JavaVm(BaseModel):
@@ -71,6 +72,7 @@ class SSLMode(str, Enum):
VERIFY_CA = "verify-ca"
VERIFY_FULL = "verify-full"
class PostgreSqlDb(BaseModel):
id: int
name: str
@@ -92,6 +94,7 @@ class Project(BaseModel):
apache_fop_id: int = Field(..., description="ID der Apache FOP Konfiguration", gt=0)
xsl_dir_id: int = Field(..., description="ID des XSL-Verzeichnisses", gt=0)
postgre_sql_db_id: int = Field(..., description="ID der PostgreSQL Datenbank", gt=0)
fop_config_dir: Path | None = Field(None, description="Optionaler Pfad zum Apache FOP Config-Verzeichnis")
def getXsl(self) -> str:
global app_settings
@@ -139,6 +142,12 @@ class AppSettings(BaseSettings):
pdf_projects: list[Project] = []
postgresql_dbs: list[PostgreSqlDb] = []
theme: str | None = None
max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8)
# UI-Zustand
window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height)
splitter_sizes: list[int] | None = None # Splitter-Positionen
tree_column_widths: list[int] | None = None # TreeWidget-Spaltenbreiten
model_config = SettingsConfigDict(json_file=config_path)
@@ -159,6 +168,10 @@ class AppSettings(BaseSettings):
if not config_path.parent.exists():
config_path.parent.mkdir(parents=True, exist_ok=True)
if not config_path.parent.is_dir() or not os.access(config_path.parent, os.W_OK):
logger.exception(f"{config_path.parent} ist kein Verzeichnis oder es gibt keine Schreibrechte")
sys.exit(1)
# Konfiguration speichern
with open(config_path, "wb") as c:
c.write(app_settings.model_dump_json(indent=4).encode())
@@ -199,8 +212,8 @@ class ProjectData(BaseModel):
def readSettings(cls, project_dir: Path):
# Explizit UTF-8 Encoding verwenden
project_yaml_path = project_dir / "project.yaml"
with open(project_yaml_path, 'r', encoding='utf-8') as f:
yaml = YAML(typ='safe')
with open(project_yaml_path, "r", encoding="utf-8") as f:
yaml = YAML(typ="safe")
yaml_data = yaml.load(f)
return cls.model_validate(yaml_data)
+35 -5
View File
@@ -1,4 +1,5 @@
import sys
import logging
from PySide6.QtWidgets import QApplication
@@ -6,17 +7,46 @@ from ui.MainWindow import MainWindow
from ui.AppSettings import AppSettingsDlg
from conf import app_settings
# import qdarktheme
def main():
"""Haupteinstiegspunkt der Anwendung."""
# Logging konfigurieren - sowohl Datei als auch Konsole
from datetime import datetime
# Log-Verzeichnis erstellen (im selben Verzeichnis wie config.json)
from conf import config_path
log_dir = config_path.parent / "logs"
log_dir.mkdir(exist_ok=True)
# Log-Dateiname mit Timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = log_dir / f"documentor_{timestamp}.log"
# Root-Logger konfigurieren
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# Formatter für alle Handler
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%H:%M:%S")
# Handler 1: Datei (alles ab DEBUG)
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Handler 2: Konsole (alles ab INFO)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logging.info(f"Logging initialisiert: {log_file}")
# QApplication-Instanz erstellen
app = QApplication(sys.argv)
# Dark Theme aktivieren
# qdarktheme.setup_theme("auto")
# Hauptfenster erstellen
window = MainWindow()
+442
View File
@@ -0,0 +1,442 @@
"""
Saxon Worker Pool - Persistente JVM-Prozesse für schnelle XSLT-Transformationen.
Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheinander.
"""
import logging
import subprocess
import threading
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
logger = logging.getLogger(__name__)
# Java-Worker-Code (wird zur Laufzeit kompiliert)
SAXON_WORKER_JAVA = """
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.io.*;
import java.util.*;
public class SaxonWorker {
public static void main(String[] args) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line;
// Create TransformerFactory once and reuse
TransformerFactory factory = TransformerFactory.newInstance();
System.err.println("SaxonWorker started and ready (using JAXP Transformer API)");
System.err.flush();
try {
while ((line = reader.readLine()) != null) {
System.err.println("DEBUG: Received line: " + line.substring(0, Math.min(100, line.length())));
System.err.flush();
if ("EXIT".equals(line.trim())) {
System.err.println("SaxonWorker exiting");
break;
}
try {
// Parse job
System.err.println("DEBUG: Parsing job...");
System.err.flush();
String[] parts = line.split("\\t");
System.err.println("DEBUG: Parts count: " + parts.length);
System.err.flush();
if (parts.length < 3) {
System.out.println("ERROR: Invalid job format");
System.out.flush();
continue;
}
String sourceXml = parts[0];
String xslStylesheet = parts[1];
String outputFo = parts[2];
System.err.println("DEBUG: Creating transformer from stylesheet...");
System.err.flush();
// Create Source and Result objects
StreamSource xslSource = new StreamSource(new File(xslStylesheet));
StreamSource xmlSource = new StreamSource(new File(sourceXml));
StreamResult result = new StreamResult(new File(outputFo));
System.err.println("DEBUG: Compiling stylesheet...");
System.err.flush();
// Create transformer from stylesheet
Transformer transformer = factory.newTransformer(xslSource);
// Set parameters if present
if (parts.length > 3 && !parts[3].isEmpty()) {
String[] params = parts[3].split("\\\\|\\\\|\\\\|");
for (String param : params) {
if (!param.isEmpty() && param.contains("=")) {
String[] kv = param.split("=", 2);
transformer.setParameter(kv[0], kv[1]);
System.err.println("DEBUG: Set parameter: " + kv[0] + " = " + kv[1]);
}
}
System.err.flush();
}
System.err.println("DEBUG: Running transformation...");
System.err.flush();
// Capture errors via ErrorListener
final StringBuilder errors = new StringBuilder();
transformer.setErrorListener(new ErrorListener() {
@Override
public void warning(TransformerException e) {
errors.append("WARNING: ").append(e.getMessage()).append("\\n");
}
@Override
public void error(TransformerException e) {
errors.append("ERROR: ").append(e.getMessage()).append("\\n");
}
@Override
public void fatalError(TransformerException e) throws TransformerException {
errors.append("FATAL: ").append(e.getMessage()).append("\\n");
throw e;
}
});
// Run transformation
transformer.transform(xmlSource, result);
System.err.println("DEBUG: Transformation completed");
System.err.flush();
// Check for errors
if (errors.length() > 0) {
System.out.println("ERROR: " + errors.toString().trim());
} else {
System.out.println("OK");
}
System.out.flush();
} catch (TransformerException e) {
System.err.println("DEBUG: Transformer exception: " + e.getClass().getName());
System.err.flush();
e.printStackTrace(System.err);
String errorMsg = e.getMessage();
if (errorMsg == null || errorMsg.isEmpty()) {
errorMsg = e.getClass().getSimpleName();
}
System.out.println("ERROR: " + errorMsg);
System.out.flush();
} catch (Exception e) {
System.err.println("DEBUG: Job processing exception: " + e.getClass().getName());
System.err.flush();
e.printStackTrace(System.err);
System.out.println("ERROR: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getName()));
System.out.flush();
}
}
} catch (IOException e) {
System.err.println("SaxonWorker I/O error: " + e.getMessage());
e.printStackTrace(System.err);
}
}
}
"""
class SaxonWorkerPool:
"""
Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen.
Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen.
"""
def __init__(
self,
num_workers: int,
java_vm_path: Path,
saxon_jar_path: Path,
classpath_cache: dict[Path, str],
log_dir: Optional[Path] = None,
):
"""
Initialisiert den Saxon-Worker-Pool.
Args:
num_workers: Anzahl der Worker-Prozesse
java_vm_path: Pfad zur Java VM Binary
saxon_jar_path: Pfad zur Saxon JAR-Datei
classpath_cache: Cache für Saxon-Classpaths
log_dir: Optionales Verzeichnis für Worker-Logs (Standard: temp_dir/temp)
"""
self.num_workers = num_workers
self.java_vm_path = java_vm_path
self.saxon_jar_path = saxon_jar_path
self.classpath_cache = classpath_cache
self.log_dir = log_dir
# Worker-Prozesse und Queues
self.workers: list[subprocess.Popen] = []
self.job_queue: Queue = Queue()
self.result_queue: Queue = Queue()
self.worker_locks: list[threading.Lock] = []
# Temporäres Verzeichnis für kompilierte Java-Klasse
self.temp_dir: Optional[Path] = None
self.worker_class_path: Optional[Path] = None
self.worker_log_dir: Optional[Path] = None
# Initialisierung
self._compile_worker_class()
self._start_workers()
logger.info(f"SaxonWorkerPool initialisiert mit {num_workers} Workern")
def _compile_worker_class(self):
"""Kompiliert die SaxonWorker-Java-Klasse."""
try:
# Erstelle temporäres Verzeichnis
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_worker_"))
# Schreibe Java-Quellcode
java_file = self.temp_dir / "SaxonWorker.java"
java_file.write_text(SAXON_WORKER_JAVA, encoding="utf-8")
# Hole Classpath
saxon_dir = self.saxon_jar_path.parent
if saxon_dir in self.classpath_cache:
classpath = self.classpath_cache[saxon_dir]
else:
# Fallback: Baue Classpath neu
import glob
import sys
all_jars = glob.glob(str(saxon_dir / "*.jar"))
lib_dir = saxon_dir / "lib"
if lib_dir.exists():
all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
classpath_separator = ";" if sys.platform == "win32" else ":"
classpath = classpath_separator.join(all_jars)
# Kompiliere Java-Klasse
javac_cmd = [str(self.java_vm_path).replace("java", "javac"), "-cp", classpath, str(java_file)]
logger.debug(f"Kompiliere SaxonWorker: {' '.join(javac_cmd)}")
result = subprocess.run(javac_cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise RuntimeError(f"Java-Kompilierung fehlgeschlagen: {result.stderr}")
self.worker_class_path = self.temp_dir
logger.info(f"SaxonWorker erfolgreich kompiliert: {self.temp_dir}")
except Exception as e:
logger.error(f"Fehler beim Kompilieren von SaxonWorker: {e}")
raise
def _start_workers(self):
"""Startet N Worker-Prozesse."""
# Hole Classpath
saxon_dir = self.saxon_jar_path.parent
classpath = self.classpath_cache.get(saxon_dir, "")
# Füge Worker-Classpath hinzu
import sys
classpath_separator = ";" if sys.platform == "win32" else ":"
full_classpath = str(self.worker_class_path) + classpath_separator + classpath
# Bestimme Log-Verzeichnis
self.worker_log_dir = self.log_dir if self.log_dir else self.temp_dir
if self.log_dir:
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
for i in range(self.num_workers):
try:
# Starte JVM-Prozess mit SaxonWorker
cmd = [str(self.java_vm_path), "-cp", full_classpath, "SaxonWorker"]
# Öffne stderr-Log-Datei für diesen Worker
stderr_log = self.worker_log_dir / f"worker_{i}_stderr.log"
stderr_file = open(stderr_log, "w", encoding="utf-8")
process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=stderr_file, # Redirect stderr to file
text=True,
bufsize=1, # Line buffered
)
self.workers.append(process)
self.worker_locks.append(threading.Lock())
logger.debug(f"Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
# Warte kurz damit Worker initialisieren kann
import time
time.sleep(0.1)
# Prüfe ob Worker noch läuft
if process.poll() is not None:
# Worker ist bereits beendet - Fehler!
stderr_file.close()
with open(stderr_log, "r") as f:
stderr_content = f.read()
raise RuntimeError(
f"Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
)
except Exception as e:
logger.error(f"Fehler beim Starten von Worker {i}: {e}")
raise
logger.info(f"{len(self.workers)} Saxon-Worker erfolgreich gestartet")
def transform(
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
) -> tuple[bool, str]:
"""
Führt eine XSLT-Transformation mit einem Worker aus dem Pool aus.
Args:
source_xml: Pfad zur XML-Eingabedatei
xsl_stylesheet: Pfad zur XSL-Stylesheet-Datei
output_fo: Pfad zur FO-Ausgabedatei
xslt_params: Dictionary mit XSLT-Parametern
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
"""
# Finde freien Worker
worker_idx = None
for i, lock in enumerate(self.worker_locks):
if lock.acquire(blocking=False):
worker_idx = i
break
if worker_idx is None:
# Kein freier Worker, warte auf ersten verfügbaren
for i, lock in enumerate(self.worker_locks):
lock.acquire()
worker_idx = i
break
try:
worker = self.workers[worker_idx]
# Prüfe ob Worker noch läuft
if worker.poll() is not None:
# Worker ist tot!
stderr_log = self.worker_log_dir / f"worker_{worker_idx}_stderr.log"
try:
with open(stderr_log, "r") as f:
stderr_content = f.read()
error_msg = (
f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
)
except Exception:
error_msg = f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})"
logger.error(error_msg)
return False, error_msg
# Formatiere Parameter
params_str = "|||".join([f"{key}={value}" for key, value in xslt_params.items()])
# Erstelle Job-String (Tab-separated)
job = f"{source_xml}\t{xsl_stylesheet}\t{output_fo}\t{params_str}\n"
logger.debug(f"Sende Job an Worker {worker_idx}: {source_xml.name}")
# Sende Job an Worker
worker.stdin.write(job)
worker.stdin.flush()
# Warte auf Antwort
response = worker.stdout.readline().strip()
logger.debug(f"Worker {worker_idx} Antwort: '{response}'")
if response == "OK":
return True, "Erfolgreich"
elif response.startswith("ERROR:"):
error_msg = response[6:].strip()
return False, f"Saxon-Fehler: {error_msg}"
else:
# Leere Antwort bedeutet Worker ist crashed
if not response:
stderr_log = self.worker_log_dir / f"worker_{worker_idx}_stderr.log"
try:
with open(stderr_log, "r") as f:
stderr_content = f.read()[-500:] # Letzte 500 Zeichen
return False, f"Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
except Exception:
return False, f"Worker {worker_idx} crashed (keine Antwort)"
return False, f"Unerwartete Antwort: {response}"
except Exception as e:
logger.error(f"Fehler bei Worker {worker_idx}: {e}")
return False, f"Worker-Fehler: {str(e)}"
finally:
# Gebe Worker-Lock frei
self.worker_locks[worker_idx].release()
def shutdown(self):
"""Beendet alle Worker-Prozesse sauber."""
logger.info("Beende Saxon-Worker-Pool...")
for i, worker in enumerate(self.workers):
try:
# Sende EXIT-Befehl
if worker.stdin and not worker.stdin.closed:
worker.stdin.write("EXIT\n")
worker.stdin.flush()
# Warte auf Beendigung (max 2 Sekunden)
worker.wait(timeout=2)
logger.debug(f"Worker {i} beendet")
except subprocess.TimeoutExpired:
# Force kill falls nötig
worker.kill()
logger.warning(f"Worker {i} musste gekillt werden")
except Exception as e:
logger.error(f"Fehler beim Beenden von Worker {i}: {e}")
# Lösche temporäres Verzeichnis
if self.temp_dir and self.temp_dir.exists():
try:
import shutil
shutil.rmtree(self.temp_dir)
logger.debug(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}")
except Exception as e:
logger.warning(f"Konnte temporäres Verzeichnis nicht löschen: {e}")
logger.info("Saxon-Worker-Pool beendet")
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.shutdown()
+519
View File
@@ -0,0 +1,519 @@
"""
Transformations-Engine für XSL-FO PDF-Generierung.
Dieses Modul implementiert die Transformations-Pipeline:
1. XML → FO (Saxon XSLT Transformation)
2. FO → PDF (Apache FOP)
3. PDF-Vergleich (diff-pdf)
"""
import logging
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from saxon_pool import SaxonWorkerPool
logger = logging.getLogger(__name__)
# Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert)
_saxon_worker_pool: Optional["SaxonWorkerPool"] = None
def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool"]):
"""Setzt den globalen Saxon-Worker-Pool."""
global _saxon_worker_pool
_saxon_worker_pool = pool
if pool:
logger.info(f"Saxon-Worker-Pool aktiviert mit {pool.num_workers} Workern")
else:
logger.info("Saxon-Worker-Pool deaktiviert (Fallback auf subprocess)")
class TransformationJob:
"""
Repräsentiert einen einzelnen Transformations-Job.
Ähnlich zur TestFall-Klasse in validate-xls.py, aber für DocuMentor angepasst.
"""
# Klassenweiter Cache für Saxon-Classpaths (Performance-Optimierung)
_classpath_cache: dict[Path, str] = {}
def __init__(
self,
project_dir: Path,
xml_file: Path,
xsl_file: Path,
xslt_params: dict[str, str],
java_vm_path: Path,
saxon_jar_path: Path,
apache_fop_dir: Path,
diff_pdf_path: Path,
diff_pdf_params: list[str],
xsl_id: tuple | None = None,
fop_config_dir: Path | None = None,
):
"""
Initialisiert einen Transformations-Job.
Args:
project_dir: Pfad zum Projekt-Verzeichnis
xml_file: Relative Pfad zur XML-Eingabedatei (relativ zu project_dir)
xsl_file: Absolute Pfad zur XSL-Stylesheet-Datei
xslt_params: Dictionary mit XSLT-Parametern
java_vm_path: Pfad zur Java VM Binary
saxon_jar_path: Pfad zur Saxon JAR-Datei
apache_fop_dir: Pfad zum Apache FOP-Verzeichnis
diff_pdf_path: Pfad zur diff-pdf Binary
diff_pdf_params: Standard-Parameter für diff-pdf
xsl_id: ID der XSL-Datei (als Tuple)
fop_config_dir: Optionaler Pfad zum FOP-Config-Verzeichnis (überschreibt Standardpfad)
"""
self.project_dir = project_dir
self.xml_file = xml_file # Relativ
self.xsl_file = xsl_file # Absolut
self.xslt_params = xslt_params
self.xsl_id = xsl_id
# Tool-Pfade
self.java_vm_path = java_vm_path
self.saxon_jar_path = saxon_jar_path
self.apache_fop_dir = apache_fop_dir
self.fop_config_dir = fop_config_dir
self.diff_pdf_path = diff_pdf_path
self.diff_pdf_params = diff_pdf_params
# Ausgabe-Verzeichnisse im Projektordner
self.new_dir = project_dir / "new"
self.ref_dir = project_dir / "ref"
self.diff_dir = project_dir / "diff"
# Stelle sicher, dass Ausgabe-Verzeichnisse existieren
self.new_dir.mkdir(exist_ok=True)
self.ref_dir.mkdir(exist_ok=True)
self.diff_dir.mkdir(exist_ok=True)
# Dateinamen basierend auf XML-Datei + XSL-ID
base_name = self.xml_file.stem
# Füge XSL-ID zum Dateinamen hinzu, falls vorhanden
if xsl_id:
# Konvertiere Tuple (1, 2, 3) zu String "1_2_3"
xsl_id_str = "_".join(str(x) for x in xsl_id)
file_name_base = f"{base_name}_xsl_{xsl_id_str}"
else:
file_name_base = base_name
self.temp_fo = self.new_dir / f"{file_name_base}.fo"
self.new_pdf = self.new_dir / f"{file_name_base}.pdf"
self.ref_pdf = self.ref_dir / f"{file_name_base}.pdf"
self.diff_pdf = self.diff_dir / f"{file_name_base}.pdf"
# Apache FOP Binaries (plattformabhängig)
import sys
if sys.platform == "win32":
self.fop_cmd = self.apache_fop_dir / "fop.cmd"
else:
self.fop_cmd = self.apache_fop_dir / "fop"
# FOP-Konfigurationsdatei: Verwende fop_config_dir falls angegeben, sonst Standardpfad
if self.fop_config_dir:
self.fop_conf = self.fop_config_dir / "fop.xconf"
else:
self.fop_conf = self.apache_fop_dir / "conf" / "fop.xconf"
def is_up_to_date(self) -> bool:
"""
Prüft, ob die Transformation aktuell ist.
Returns:
bool: True wenn New-PDF existiert und aktueller ist als alle Inputs
"""
if not self.new_pdf.exists():
logger.debug(f"New-PDF existiert nicht: {self.new_pdf}")
return False
output_mtime = self.new_pdf.stat().st_mtime
# Prüfe XML-Datei
xml_abs = self.project_dir / self.xml_file
if xml_abs.exists() and xml_abs.stat().st_mtime > output_mtime:
logger.debug(f"XML-Datei ist neuer: {xml_abs}")
return False
# Prüfe XSL-Datei
if self.xsl_file.exists() and self.xsl_file.stat().st_mtime > output_mtime:
logger.debug(f"XSL-Datei ist neuer: {self.xsl_file}")
return False
logger.debug(f"Transformation ist aktuell: {self.new_pdf}")
return True
def transform_saxon(self, force: bool = False) -> tuple[bool, str]:
"""
Führt XSLT-Transformation mit Saxon aus: XML → FO.
Args:
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
"""
if not force and self.is_up_to_date():
logger.info(f"Transformation übersprungen (aktuell): {self.xml_file.name}")
return True, "Übersprungen (aktuell)"
xml_abs = self.project_dir / self.xml_file
# Prüfe ob Eingabedateien existieren
if not xml_abs.exists():
error_msg = f"XML-Datei nicht gefunden: {xml_abs}"
logger.error(error_msg)
return False, error_msg
if not self.xsl_file.exists():
error_msg = f"XSL-Datei nicht gefunden: {self.xsl_file}"
logger.error(error_msg)
return False, error_msg
logger.info(f"Starte Saxon-Transformation: {self.xml_file.name}")
# Versuche zuerst den Worker-Pool zu nutzen (schneller!)
global _saxon_worker_pool
if _saxon_worker_pool:
try:
success, message = _saxon_worker_pool.transform(
source_xml=xml_abs,
xsl_stylesheet=self.xsl_file,
output_fo=self.temp_fo,
xslt_params=self.xslt_params,
)
if success:
logger.info(f"Saxon-Transformation erfolgreich (Worker-Pool): {self.xml_file.name}")
else:
logger.error(f"Saxon-Transformation fehlgeschlagen (Worker-Pool): {message}")
return success, message
except Exception as e:
logger.warning(f"Worker-Pool-Fehler, Fallback auf subprocess: {e}")
# Fallback auf subprocess unten
# Fallback: Traditionelle subprocess-Methode (langsamer, aber robuster)
# XSLT-Parameter formatieren
params = [f"{key}={value}" for key, value in self.xslt_params.items()]
# Hole Classpath aus Cache oder erstelle ihn
saxon_dir = self.saxon_jar_path.parent
if saxon_dir not in TransformationJob._classpath_cache:
# Sammle alle JAR-Dateien im Saxon-Verzeichnis für den Classpath
import glob
all_jars = glob.glob(str(saxon_dir / "*.jar"))
# Sammle auch alle JARs aus dem lib-Unterordner (z.B. xmlresolver)
lib_dir = saxon_dir / "lib"
if lib_dir.exists() and lib_dir.is_dir():
lib_jars = glob.glob(str(lib_dir / "*.jar"))
all_jars.extend(lib_jars)
logger.debug(f"Zusätzliche JARs aus lib-Verzeichnis gefunden: {len(lib_jars)}")
# Verwende alle JARs im Classpath (getrennt durch : auf Linux/Mac, ; auf Windows)
import sys
classpath_separator = ";" if sys.platform == "win32" else ":"
classpath = classpath_separator.join(all_jars)
# Cache den Classpath für zukünftige Jobs
TransformationJob._classpath_cache[saxon_dir] = classpath
logger.debug(f"Classpath für {saxon_dir} gecacht")
else:
classpath = TransformationJob._classpath_cache[saxon_dir]
logger.debug("Classpath aus Cache verwendet")
# Saxon-Kommandozeile
cmd_line = [
str(self.java_vm_path),
"-cp",
classpath,
"net.sf.saxon.Transform",
f"-s:{xml_abs}",
f"-xsl:{self.xsl_file}",
f"-o:{self.temp_fo}",
*params,
]
logger.debug(f"Kommandozeile (subprocess fallback): {' '.join(cmd_line)}")
try:
result = subprocess.run(
cmd_line,
capture_output=True,
text=True,
timeout=120, # 2 Minuten Timeout
)
# Saxon Ausgaben loggen
if result.stdout:
logger.debug(f"Saxon StdOut:\n{result.stdout}")
if result.stderr:
logger.debug(f"Saxon StdErr:\n{result.stderr}")
if result.returncode == 0:
logger.info(f"Saxon-Transformation erfolgreich (subprocess): {self.xml_file.name}")
return True, "Erfolgreich"
else:
error_msg = (
f"Saxon-Fehler (Exit {result.returncode}):\nStdOut: {result.stdout}\nStdErr: {result.stderr}"
)
logger.error(error_msg)
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "Saxon-Transformation Timeout (>120s)"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Unerwarteter Fehler bei Saxon-Transformation: {str(e)}"
logger.error(error_msg)
return False, error_msg
def build_pdf(self, force: bool = False) -> tuple[bool, str]:
"""
Generiert PDF aus FO-Datei mit Apache FOP: FO → PDF.
Args:
force: Wenn True, wird Build auch bei aktuellem Output durchgeführt
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
"""
if not force and self.is_up_to_date():
logger.info(f"PDF-Build übersprungen (aktuell): {self.xml_file.name}")
return True, "Übersprungen (aktuell)"
# Prüfe ob FO-Datei existiert
if not self.temp_fo.exists():
error_msg = f"FO-Datei nicht gefunden: {self.temp_fo}"
logger.error(error_msg)
return False, error_msg
# Apache FOP Kommandozeile
cmd_line = [
str(self.fop_cmd),
"-c",
str(self.fop_conf) if self.fop_conf.exists() else "",
"-r",
"-fo",
str(self.temp_fo),
"-pdf",
str(self.new_pdf),
]
# Entferne leere Config-Parameter wenn fop.xconf nicht existiert
if not self.fop_conf.exists():
cmd_line = [c for c in cmd_line if c not in ["-c", ""]]
logger.info(f"Starte Apache FOP PDF-Generierung: {self.xml_file.name}")
logger.debug(f"Kommandozeile: {' '.join(cmd_line)}")
try:
result = subprocess.run(
cmd_line,
capture_output=True,
text=True,
timeout=180, # 3 Minuten Timeout
)
# Apache FOP Ausgaben loggen
if result.stdout:
logger.debug(f"FOP StdOut:\n{result.stdout}")
if result.stderr:
logger.debug(f"FOP StdErr:\n{result.stderr}")
# Temporäre FO-Datei löschen
if self.temp_fo.exists():
try:
self.temp_fo.unlink()
logger.debug(f"Temporäre FO-Datei gelöscht: {self.temp_fo}")
except Exception as e:
logger.warning(f"Konnte FO-Datei nicht löschen: {e}")
if result.returncode == 0:
# Wenn kein Ref-PDF existiert, erstelle es
if not self.ref_pdf.exists():
try:
import shutil
shutil.copy2(self.new_pdf, self.ref_pdf)
logger.info(f"Ref-PDF erstellt: {self.ref_pdf}")
except Exception as e:
logger.warning(f"Konnte Ref-PDF nicht erstellen: {e}")
logger.info(f"PDF-Generierung erfolgreich: {self.new_pdf}")
return True, "Erfolgreich"
else:
error_msg = f"FOP-Fehler (Exit {result.returncode}):\nStdOut: {result.stdout}\nStdErr: {result.stderr}"
logger.error(error_msg)
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "FOP PDF-Generierung Timeout (>180s)"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Unerwarteter Fehler bei PDF-Generierung: {str(e)}"
logger.error(error_msg)
return False, error_msg
def compare_pdf(self) -> tuple[bool, str]:
"""
Vergleicht New-PDF mit Ref-PDF und erstellt ggf. Diff-PDF.
Returns:
tuple[bool, str]: (PDFs sind identisch, Fehlermeldung/Info)
"""
# Prüfe ob beide PDFs existieren
if not self.ref_pdf.exists():
info_msg = "Kein Ref-PDF vorhanden (wird beim nächsten Build erstellt)"
logger.info(info_msg)
return True, info_msg
if not self.new_pdf.exists():
error_msg = f"New-PDF nicht gefunden: {self.new_pdf}"
logger.error(error_msg)
return False, error_msg
logger.info(f"Vergleiche PDFs: {self.xml_file.name}")
# Erster Vergleich (ohne Diff-Generierung)
cmd_compare = [
str(self.diff_pdf_path),
*self.diff_pdf_params,
str(self.ref_pdf),
str(self.new_pdf),
]
logger.debug(f"Kommandozeile Vergleich: {' '.join(cmd_compare)}")
try:
result = subprocess.run(
cmd_compare,
capture_output=True,
text=True,
timeout=60, # 1 Minute Timeout
)
if result.returncode == 0:
# PDFs sind identisch
logger.info(f"PDFs sind identisch: {self.xml_file.name}")
# Lösche altes Diff-PDF falls vorhanden
if self.diff_pdf.exists():
try:
self.diff_pdf.unlink()
logger.debug(f"Diff-PDF gelöscht (nicht mehr nötig): {self.diff_pdf}")
except Exception as e:
logger.warning(f"Konnte Diff-PDF nicht löschen: {e}")
return True, "PDFs sind identisch"
else:
# PDFs unterscheiden sich - erstelle Diff-PDF
logger.info(f"PDFs unterscheiden sich, erstelle Diff-PDF: {self.xml_file.name}")
cmd_diff = [
str(self.diff_pdf_path),
f"--output-diff={self.diff_pdf}",
*self.diff_pdf_params,
"--mark-differences",
str(self.ref_pdf),
str(self.new_pdf),
]
logger.debug(f"Kommandozeile Diff: {' '.join(cmd_diff)}")
result_diff = subprocess.run(
cmd_diff,
capture_output=True,
text=True,
timeout=90, # 1.5 Minuten Timeout
)
if result_diff.returncode == 0 or self.diff_pdf.exists():
logger.info(f"Diff-PDF erstellt: {self.diff_pdf}")
return False, f"Unterschiede gefunden - Diff-PDF: {self.diff_pdf.name}"
else:
error_msg = f"Diff-PDF-Erstellung fehlgeschlagen: {result_diff.stderr}"
logger.error(error_msg)
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "PDF-Vergleich Timeout"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Unerwarteter Fehler beim PDF-Vergleich: {str(e)}"
logger.error(error_msg)
return False, error_msg
def run_full_pipeline(self, force: bool = False) -> dict[str, Any]:
"""
Führt die komplette Transformations-Pipeline aus:
1. Saxon-Transformation (XML → FO)
2. PDF-Generierung (FO → PDF)
3. PDF-Vergleich
Args:
force: Wenn True, werden alle Schritte ausgeführt (ignoriert Up-to-Date)
Returns:
dict: Ergebnis-Dictionary mit Status und Meldungen
"""
start_time = datetime.now()
result = {
"success": False,
"xml_file": str(self.xml_file),
"xsl_id": self.xsl_id,
"steps": {},
"duration": None,
"new_pdf": str(self.new_pdf) if self.new_pdf.exists() else None,
"diff_pdf": str(self.diff_pdf) if self.diff_pdf.exists() else None,
}
logger.info(f"Starte Transformations-Pipeline: {self.xml_file.name}")
# Schritt 1: Saxon-Transformation
success_saxon, msg_saxon = self.transform_saxon(force=force)
result["steps"]["saxon"] = {"success": success_saxon, "message": msg_saxon}
if not success_saxon:
result["success"] = False
result["duration"] = (datetime.now() - start_time).total_seconds()
return result
# Schritt 2: PDF-Generierung
success_build, msg_build = self.build_pdf(force=force)
result["steps"]["build"] = {"success": success_build, "message": msg_build}
if not success_build:
result["success"] = False
result["duration"] = (datetime.now() - start_time).total_seconds()
return result
# Schritt 3: PDF-Vergleich
pdfs_identical, msg_compare = self.compare_pdf()
result["steps"]["compare"] = {"identical": pdfs_identical, "message": msg_compare}
result["pdfs_identical"] = pdfs_identical
# Pipeline erfolgreich abgeschlossen
result["success"] = True
result["duration"] = (datetime.now() - start_time).total_seconds()
logger.info(f"Pipeline abgeschlossen: {self.xml_file.name} ({result['duration']:.2f}s)")
return result
+10 -1
View File
@@ -466,6 +466,7 @@ class AppSettingsDlg(QDialog):
apache_fop_id=project_data['apache_fop_id'] if project_data['apache_fop_id'] != -1 else 1,
xsl_dir_id=project_data['xsl_dir_id'] if project_data['xsl_dir_id'] != -1 else 1,
postgre_sql_db_id=project_data['postgre_sql_db_id'] if project_data['postgre_sql_db_id'] != -1 else 1,
fop_config_dir=Path(project_data['fop_config_dir']) if project_data.get('fop_config_dir') else None,
)
self.temp_pdf_projects.append(new_project)
@@ -617,7 +618,9 @@ class AppSettingsDlg(QDialog):
'diff_pdf_id': pdf_project.diff_pdf_id,
'saxon_jar_id': pdf_project.saxon_jar_id,
'apache_fop_id': pdf_project.apache_fop_id,
'xsl_dir_id': pdf_project.xsl_dir_id
'xsl_dir_id': pdf_project.xsl_dir_id,
'postgre_sql_db_id': pdf_project.postgre_sql_db_id,
'fop_config_dir': str(pdf_project.fop_config_dir) if pdf_project.fop_config_dir else None
}
# Dialog im Edit-Modus öffnen (Projekt-Name und -Ordner deaktiviert)
@@ -632,9 +635,15 @@ class AppSettingsDlg(QDialog):
pdf_project.saxon_jar_id = new_data['saxon_jar_id'] if new_data['saxon_jar_id'] != -1 else pdf_project.saxon_jar_id
pdf_project.apache_fop_id = new_data['apache_fop_id'] if new_data['apache_fop_id'] != -1 else pdf_project.apache_fop_id
pdf_project.xsl_dir_id = new_data['xsl_dir_id'] if new_data['xsl_dir_id'] != -1 else pdf_project.xsl_dir_id
pdf_project.postgre_sql_db_id = new_data['postgre_sql_db_id'] if new_data['postgre_sql_db_id'] != -1 else pdf_project.postgre_sql_db_id
pdf_project.fop_config_dir = Path(new_data['fop_config_dir']) if new_data.get('fop_config_dir') else None
self._populate_pdf_project_table()
# Einstellungen speichern
self.settings.pdf_projects = self.temp_pdf_projects.copy()
self.settings.save()
# PostgreSQL Methoden
def _add_postgresql_db(self):
"""Fügt eine neue PostgreSQL-Datenbank hinzu."""
+54 -34
View File
@@ -32,6 +32,12 @@
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="opaqueResize">
<bool>false</bool>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
@@ -64,7 +70,7 @@
</sizepolicy>
</property>
<property name="columnCount">
<number>2</number>
<number>3</number>
</property>
<attribute name="headerHighlightSections">
<bool>true</bool>
@@ -82,6 +88,11 @@
<string notr="true">2</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">3</string>
</property>
</column>
</widget>
</item>
<item>
@@ -163,6 +174,12 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<property name="midLineWidth">
<number>1</number>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
@@ -171,8 +188,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>54</width>
<height>718</height>
<width>68</width>
<height>728</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@@ -208,7 +225,7 @@
</widget>
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
@@ -229,7 +246,7 @@
<item>
<widget class="QFrame" name="frame_4">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
@@ -336,11 +353,40 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="accept_changes">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>✅ Änderungen übernehmen</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea_2">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
@@ -349,8 +395,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>649</width>
<height>690</height>
<width>726</width>
<height>697</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
@@ -366,20 +412,6 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
@@ -396,7 +428,7 @@
<x>0</x>
<y>0</y>
<width>1263</width>
<height>33</height>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuProjekt">
@@ -404,7 +436,6 @@
<string>Projekt</string>
</property>
<addaction name="actionNeu"/>
<addaction name="action_ffnen"/>
<addaction name="separator"/>
<addaction name="actionVorhandene_Projekte"/>
<addaction name="separator"/>
@@ -432,17 +463,6 @@
<string>Ctrl+N</string>
</property>
</action>
<action name="action_ffnen">
<property name="icon">
<iconset theme="folder-open"/>
</property>
<property name="text">
<string>Öffnen ...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionBeenden">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ApplicationExit"/>
+35 -38
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'MainWinddow.ui'
##
## Created by: Qt User Interface Compiler version 6.9.1
## Created by: Qt User Interface Compiler version 6.9.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -31,18 +31,14 @@ class Ui_MainWindow(object):
self.actionNeu.setObjectName(u"actionNeu")
icon = QIcon(QIcon.fromTheme(u"folder-new"))
self.actionNeu.setIcon(icon)
self.action_ffnen = QAction(MainWindow)
self.action_ffnen.setObjectName(u"action_ffnen")
icon1 = QIcon(QIcon.fromTheme(u"folder-open"))
self.action_ffnen.setIcon(icon1)
self.actionBeenden = QAction(MainWindow)
self.actionBeenden.setObjectName(u"actionBeenden")
icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit))
self.actionBeenden.setIcon(icon2)
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit))
self.actionBeenden.setIcon(icon1)
self.actionEinstellungen = QAction(MainWindow)
self.actionEinstellungen.setObjectName(u"actionEinstellungen")
icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties))
self.actionEinstellungen.setIcon(icon3)
icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties))
self.actionEinstellungen.setIcon(icon2)
self.actionVorhandene_Projekte = QAction(MainWindow)
self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte")
self.actionVorhandene_Projekte.setEnabled(False)
@@ -54,6 +50,8 @@ class Ui_MainWindow(object):
self.splitter = QSplitter(self.centralwidget)
self.splitter.setObjectName(u"splitter")
self.splitter.setOrientation(Qt.Orientation.Horizontal)
self.splitter.setOpaqueResize(False)
self.splitter.setChildrenCollapsible(False)
self.frame = QFrame(self.splitter)
self.frame.setObjectName(u"frame")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
@@ -69,6 +67,7 @@ class Ui_MainWindow(object):
self.verticalLayout.setContentsMargins(-1, -1, -1, 0)
self.treeWidget = QTreeWidget(self.frame)
__qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(2, u"3");
__qtreewidgetitem.setText(1, u"2");
__qtreewidgetitem.setText(0, u"1");
self.treeWidget.setHeaderItem(__qtreewidgetitem)
@@ -78,7 +77,7 @@ class Ui_MainWindow(object):
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth())
self.treeWidget.setSizePolicy(sizePolicy1)
self.treeWidget.setColumnCount(2)
self.treeWidget.setColumnCount(3)
self.treeWidget.header().setHighlightSections(True)
self.treeWidget.header().setStretchLastSection(True)
@@ -93,16 +92,16 @@ class Ui_MainWindow(object):
self.pushButton = QPushButton(self.frame_2)
self.pushButton.setObjectName(u"pushButton")
self.pushButton.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart))
self.pushButton.setIcon(icon4)
icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart))
self.pushButton.setIcon(icon3)
self.horizontalLayout_2.addWidget(self.pushButton)
self.pushButton_2 = QPushButton(self.frame_2)
self.pushButton_2.setObjectName(u"pushButton_2")
self.pushButton_2.setAutoFillBackground(False)
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward))
self.pushButton_2.setIcon(icon5)
icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward))
self.pushButton_2.setIcon(icon4)
self.horizontalLayout_2.addWidget(self.pushButton_2)
@@ -112,8 +111,8 @@ class Ui_MainWindow(object):
self.pB_lade_aus_fn2 = QPushButton(self.frame_2)
self.pB_lade_aus_fn2.setObjectName(u"pB_lade_aus_fn2")
icon6 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown))
self.pB_lade_aus_fn2.setIcon(icon6)
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown))
self.pB_lade_aus_fn2.setIcon(icon5)
self.horizontalLayout_2.addWidget(self.pB_lade_aus_fn2)
@@ -128,10 +127,12 @@ class Ui_MainWindow(object):
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth())
self.scrollArea.setSizePolicy(sizePolicy2)
self.scrollArea.setFrameShadow(QFrame.Shadow.Raised)
self.scrollArea.setMidLineWidth(1)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 54, 718))
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 68, 728))
self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.label = QLabel(self.scrollAreaWidgetContents)
@@ -152,14 +153,14 @@ class Ui_MainWindow(object):
self.splitter.addWidget(self.scrollArea)
self.frame_3 = QFrame(self.splitter)
self.frame_3.setObjectName(u"frame_3")
self.frame_3.setFrameShape(QFrame.Shape.NoFrame)
self.frame_3.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_3.setFrameShadow(QFrame.Shadow.Raised)
self.verticalLayout_4 = QVBoxLayout(self.frame_3)
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
self.frame_4 = QFrame(self.frame_3)
self.frame_4.setObjectName(u"frame_4")
self.frame_4.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_4.setFrameShape(QFrame.Shape.NoFrame)
self.frame_4.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_3 = QHBoxLayout(self.frame_4)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
@@ -208,28 +209,30 @@ class Ui_MainWindow(object):
self.horizontalLayout_3.addItem(self.horizontalSpacer_5)
self.accept_changes = QPushButton(self.frame_4)
self.accept_changes.setObjectName(u"accept_changes")
self.accept_changes.setEnabled(False)
self.horizontalLayout_3.addWidget(self.accept_changes)
self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addItem(self.horizontalSpacer_3)
self.verticalLayout_4.addWidget(self.frame_4)
self.scrollArea_2 = QScrollArea(self.frame_3)
self.scrollArea_2.setObjectName(u"scrollArea_2")
self.scrollArea_2.setFrameShape(QFrame.Shape.NoFrame)
self.scrollArea_2.setFrameShadow(QFrame.Shadow.Raised)
self.scrollArea_2.setWidgetResizable(True)
self.scrollAreaWidgetContents_2 = QWidget()
self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2")
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 649, 690))
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 726, 697))
self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
self.label_3 = QLabel(self.scrollAreaWidgetContents_2)
self.label_3.setObjectName(u"label_3")
self.verticalLayout_3.addWidget(self.label_3)
self.label_4 = QLabel(self.scrollAreaWidgetContents_2)
self.label_4.setObjectName(u"label_4")
self.verticalLayout_3.addWidget(self.label_4)
self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2)
self.verticalLayout_4.addWidget(self.scrollArea_2)
@@ -241,7 +244,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 1263, 33))
self.menubar.setGeometry(QRect(0, 0, 1263, 22))
self.menuProjekt = QMenu(self.menubar)
self.menuProjekt.setObjectName(u"menuProjekt")
self.menuThema = QMenu(self.menubar)
@@ -254,7 +257,6 @@ class Ui_MainWindow(object):
self.menubar.addAction(self.menuProjekt.menuAction())
self.menubar.addAction(self.menuThema.menuAction())
self.menuProjekt.addAction(self.actionNeu)
self.menuProjekt.addAction(self.action_ffnen)
self.menuProjekt.addSeparator()
self.menuProjekt.addAction(self.actionVorhandene_Projekte)
self.menuProjekt.addSeparator()
@@ -273,10 +275,6 @@ class Ui_MainWindow(object):
self.actionNeu.setText(QCoreApplication.translate("MainWindow", u"Neu ...", None))
#if QT_CONFIG(shortcut)
self.actionNeu.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
#endif // QT_CONFIG(shortcut)
self.action_ffnen.setText(QCoreApplication.translate("MainWindow", u"\u00d6ffnen ...", None))
#if QT_CONFIG(shortcut)
self.action_ffnen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionBeenden.setText(QCoreApplication.translate("MainWindow", u"Beenden", None))
self.actionEinstellungen.setText(QCoreApplication.translate("MainWindow", u"Einstellungen ...", None))
@@ -292,8 +290,7 @@ class Ui_MainWindow(object):
self.label_6.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None))
self.label_7.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None))
self.label_3.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None))
self.accept_changes.setText(QCoreApplication.translate("MainWindow", u"\u2705 \u00c4nderungen \u00fcbernehmen", None))
self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None))
self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None))
# retranslateUi
+2877 -457
View File
File diff suppressed because it is too large Load Diff
+26 -1
View File
@@ -50,6 +50,9 @@ class PdfProjectDlg(QDialog):
# Browse-Button für Projekt-Ordner
self.ui.pushButton.clicked.connect(self.browse_project_dir)
# Browse-Button für FOP-Config-Ordner
self.ui.btnBrowseFopConfig.clicked.connect(self.browse_fop_config_dir)
# OK/Cancel Buttons sind bereits in der UI-Datei verbunden
# self.ui.buttonBox.accepted.connect(self.accept)
# self.ui.buttonBox.rejected.connect(self.reject)
@@ -133,6 +136,10 @@ class PdfProjectDlg(QDialog):
if 'postgre_sql_db_id' in self.project_data:
self._select_combo_by_data(self.ui.cB_Postgres, self.project_data['postgre_sql_db_id'])
# FOP-Config-Ordner
if 'fop_config_dir' in self.project_data and self.project_data['fop_config_dir']:
self.ui.lineFopConfigDir.setText(str(self.project_data['fop_config_dir']))
def _select_combo_by_data(self, combo_box, data_value):
"""
Wählt einen ComboBox-Eintrag basierend auf dem data-Wert aus.
@@ -167,6 +174,22 @@ class PdfProjectDlg(QDialog):
project_name = os.path.basename(selected_dir)
self.ui.lineProjectName.setText(project_name)
def browse_fop_config_dir(self):
"""Öffnet einen Dialog zum Auswählen des FOP-Config-Ordners."""
current_dir = self.ui.lineFopConfigDir.text()
if not current_dir or not os.path.exists(current_dir):
current_dir = os.path.expanduser("~")
selected_dir = QFileDialog.getExistingDirectory(
self,
"FOP-Config-Ordner auswählen",
current_dir,
QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks
)
if selected_dir:
self.ui.lineFopConfigDir.setText(selected_dir)
def validate_and_accept(self):
"""Validiert die Eingaben und akzeptiert den Dialog."""
# Projekt-Name prüfen
@@ -236,6 +259,7 @@ class PdfProjectDlg(QDialog):
Returns:
dict: Dictionary mit allen Projektdaten
"""
fop_config_dir = self.ui.lineFopConfigDir.text().strip()
return {
'name': self.ui.lineProjectName.text().strip(),
'project_dir': self.ui.lineProjectDir.text().strip(),
@@ -244,7 +268,8 @@ class PdfProjectDlg(QDialog):
'saxon_jar_id': self.ui.cB_SaxonJar.currentData(),
'apache_fop_id': self.ui.cB_ApacheFop.currentData(),
'diff_pdf_id': self.ui.cB_Diff_Pdf.currentData(),
'postgre_sql_db_id': self.ui.cB_Postgres.currentData()
'postgre_sql_db_id': self.ui.cB_Postgres.currentData(),
'fop_config_dir': fop_config_dir if fop_config_dir else None
}
def _configure_edit_mode(self):
+45 -4
View File
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>608</width>
<height>299</height>
<height>331</height>
</rect>
</property>
<property name="windowTitle">
@@ -109,25 +109,66 @@
<widget class="QComboBox" name="cB_ApacheFop"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>FOP-Config-Ordner:</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>diff-pdf:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="7" column="1">
<widget class="QComboBox" name="cB_Diff_Pdf"/>
</item>
<item row="7" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Postgres:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="8" column="1">
<widget class="QComboBox" name="cB_Postgres"/>
</item>
<item row="6" column="1">
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="lineFopConfigDir"/>
</item>
<item>
<widget class="QPushButton" name="btnBrowseFopConfig">
<property name="text">
<string>Durchsuchen ...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
+33 -6
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'PdfProject.ui'
##
## Created by: Qt User Interface Compiler version 6.9.1
## Created by: Qt User Interface Compiler version 6.9.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -24,7 +24,7 @@ class Ui_projectDlg(object):
def setupUi(self, projectDlg):
if not projectDlg.objectName():
projectDlg.setObjectName(u"projectDlg")
projectDlg.resize(608, 299)
projectDlg.resize(608, 331)
self.verticalLayout = QVBoxLayout(projectDlg)
self.verticalLayout.setObjectName(u"verticalLayout")
self.widget = QWidget(projectDlg)
@@ -106,25 +106,50 @@ class Ui_projectDlg(object):
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.cB_ApacheFop)
self.label_9 = QLabel(self.widget)
self.label_9.setObjectName(u"label_9")
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_9)
self.label_7 = QLabel(self.widget)
self.label_7.setObjectName(u"label_7")
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_7)
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_7)
self.cB_Diff_Pdf = QComboBox(self.widget)
self.cB_Diff_Pdf.setObjectName(u"cB_Diff_Pdf")
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf)
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf)
self.label_8 = QLabel(self.widget)
self.label_8.setObjectName(u"label_8")
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_8)
self.formLayout.setWidget(8, QFormLayout.ItemRole.LabelRole, self.label_8)
self.cB_Postgres = QComboBox(self.widget)
self.cB_Postgres.setObjectName(u"cB_Postgres")
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Postgres)
self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.cB_Postgres)
self.frame_2 = QFrame(self.widget)
self.frame_2.setObjectName(u"frame_2")
self.frame_2.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.lineFopConfigDir = QLineEdit(self.frame_2)
self.lineFopConfigDir.setObjectName(u"lineFopConfigDir")
self.horizontalLayout_2.addWidget(self.lineFopConfigDir)
self.btnBrowseFopConfig = QPushButton(self.frame_2)
self.btnBrowseFopConfig.setObjectName(u"btnBrowseFopConfig")
self.horizontalLayout_2.addWidget(self.btnBrowseFopConfig)
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.frame_2)
self.verticalLayout.addWidget(self.widget)
@@ -154,7 +179,9 @@ class Ui_projectDlg(object):
self.label_4.setText(QCoreApplication.translate("projectDlg", u"Java VM:", None))
self.label_5.setText(QCoreApplication.translate("projectDlg", u"Saxon Jar:", None))
self.label_6.setText(QCoreApplication.translate("projectDlg", u"Apache FOP:", None))
self.label_9.setText(QCoreApplication.translate("projectDlg", u"FOP-Config-Ordner:", None))
self.label_7.setText(QCoreApplication.translate("projectDlg", u"diff-pdf:", None))
self.label_8.setText(QCoreApplication.translate("projectDlg", u"Postgres:", None))
self.btnBrowseFopConfig.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
# retranslateUi
+5 -2
View File
@@ -2,7 +2,6 @@ from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
from PySide6.QtCore import Qt
from ui.TreeNodeEditDialog_ui import Ui_TreeNodeEditDialog
from conf import TreeNode
class TreeNodeEditDialog(QDialog):
@@ -146,9 +145,13 @@ class TreeNodeEditDialog(QDialog):
if key: # Nur Parameter mit nicht-leerem Schlüssel hinzufügen
xslt_params[key] = value
# CheckBox für Force-Transformation prüfen
force_transform = self.ui.alle_xml_transformieren.isChecked()
return {
"bez": bez,
"xslt_params": xslt_params
"xslt_params": xslt_params,
"force_transform": force_transform
}
def accept(self):
+156 -76
View File
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<width>870</width>
<height>400</height>
</rect>
</property>
@@ -35,95 +35,175 @@
</layout>
</item>
<item>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="columnCount">
<number>2</number>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<property name="leftMargin">
<number>0</number>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
<property name="topMargin">
<number>0</number>
</property>
</column>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListAdd"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListRemove"/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
<widget class="QCheckBox" name="alle_xml_transformieren">
<property name="text">
<string>Alle XML-Dateien neu transformieren (force)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item>
+40 -11
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'TreeNodeEditDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.9.1
## Created by: Qt User Interface Compiler version 6.9.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -15,17 +15,18 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
QHeaderView, QLabel, QLayout, QLineEdit,
QPushButton, QSizePolicy, QSpacerItem, QTableWidget,
QTableWidgetItem, QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialog, QDialogButtonBox, QFormLayout, QFrame,
QGroupBox, QHBoxLayout, QHeaderView, QLabel,
QLayout, QLineEdit, QPushButton, QSizePolicy,
QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
class Ui_TreeNodeEditDialog(object):
def setupUi(self, TreeNodeEditDialog):
if not TreeNodeEditDialog.objectName():
TreeNodeEditDialog.setObjectName(u"TreeNodeEditDialog")
TreeNodeEditDialog.resize(600, 400)
TreeNodeEditDialog.resize(870, 400)
TreeNodeEditDialog.setModal(True)
self.verticalLayout = QVBoxLayout(TreeNodeEditDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
@@ -45,10 +46,18 @@ class Ui_TreeNodeEditDialog(object):
self.verticalLayout.addLayout(self.formLayout)
self.xsltParamsGroupBox = QGroupBox(TreeNodeEditDialog)
self.frame = QFrame(TreeNodeEditDialog)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QFrame.Shape.NoFrame)
self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout = QHBoxLayout(self.frame)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsGroupBox = QGroupBox(self.frame)
self.xsltParamsGroupBox.setObjectName(u"xsltParamsGroupBox")
self.xsltParamsLayout = QVBoxLayout(self.xsltParamsGroupBox)
self.xsltParamsLayout.setObjectName(u"xsltParamsLayout")
self.xsltParamsLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsTable = QTableWidget(self.xsltParamsGroupBox)
if (self.xsltParamsTable.columnCount() < 2):
self.xsltParamsTable.setColumnCount(2)
@@ -57,6 +66,7 @@ class Ui_TreeNodeEditDialog(object):
__qtablewidgetitem1 = QTableWidgetItem()
self.xsltParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
self.xsltParamsTable.setObjectName(u"xsltParamsTable")
self.xsltParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.xsltParamsTable.setColumnCount(2)
self.xsltParamsTable.horizontalHeader().setVisible(True)
@@ -64,13 +74,21 @@ class Ui_TreeNodeEditDialog(object):
self.xsltParamsButtonLayout = QHBoxLayout()
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout")
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_2)
self.addParamButton = QPushButton(self.xsltParamsGroupBox)
self.addParamButton.setObjectName(u"addParamButton")
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
self.addParamButton.setIcon(icon)
self.xsltParamsButtonLayout.addWidget(self.addParamButton)
self.removeParamButton = QPushButton(self.xsltParamsGroupBox)
self.removeParamButton.setObjectName(u"removeParamButton")
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
self.removeParamButton.setIcon(icon1)
self.xsltParamsButtonLayout.addWidget(self.removeParamButton)
@@ -82,12 +100,13 @@ class Ui_TreeNodeEditDialog(object):
self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout)
self.verticalLayout.addWidget(self.xsltParamsGroupBox)
self.horizontalLayout.addWidget(self.xsltParamsGroupBox)
self.parentParamsGroupBox = QGroupBox(TreeNodeEditDialog)
self.parentParamsGroupBox = QGroupBox(self.frame)
self.parentParamsGroupBox.setObjectName(u"parentParamsGroupBox")
self.parentParamsLayout = QVBoxLayout(self.parentParamsGroupBox)
self.parentParamsLayout.setObjectName(u"parentParamsLayout")
self.parentParamsLayout.setContentsMargins(0, 0, 0, 0)
self.parentParamsTable = QTableWidget(self.parentParamsGroupBox)
if (self.parentParamsTable.columnCount() < 2):
self.parentParamsTable.setColumnCount(2)
@@ -96,6 +115,7 @@ class Ui_TreeNodeEditDialog(object):
__qtablewidgetitem3 = QTableWidgetItem()
self.parentParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem3)
self.parentParamsTable.setObjectName(u"parentParamsTable")
self.parentParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.parentParamsTable.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.parentParamsTable.setColumnCount(2)
self.parentParamsTable.horizontalHeader().setVisible(True)
@@ -103,7 +123,15 @@ class Ui_TreeNodeEditDialog(object):
self.parentParamsLayout.addWidget(self.parentParamsTable)
self.verticalLayout.addWidget(self.parentParamsGroupBox)
self.horizontalLayout.addWidget(self.parentParamsGroupBox)
self.verticalLayout.addWidget(self.frame)
self.alle_xml_transformieren = QCheckBox(TreeNodeEditDialog)
self.alle_xml_transformieren.setObjectName(u"alle_xml_transformieren")
self.verticalLayout.addWidget(self.alle_xml_transformieren)
self.buttonBox = QDialogButtonBox(TreeNodeEditDialog)
self.buttonBox.setObjectName(u"buttonBox")
@@ -136,5 +164,6 @@ class Ui_TreeNodeEditDialog(object):
___qtablewidgetitem2.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter", None));
___qtablewidgetitem3 = self.parentParamsTable.horizontalHeaderItem(1)
___qtablewidgetitem3.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Wert", None));
self.alle_xml_transformieren.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Alle XML-Dateien neu transformieren (force)", None))
# retranslateUi
+18 -5
View File
@@ -1,3 +1,4 @@
import logging
from PySide6.QtWidgets import QDialog, QTreeWidgetItem, QCheckBox, QMessageBox, QWidget, QHBoxLayout
from PySide6.QtCore import Qt
from pathlib import Path
@@ -6,6 +7,9 @@ from ui.XmlToXslAssignDialog_ui import Ui_XmlToXslAssignDialog
from conf import TreeNode, XslFile
logger = logging.getLogger(__name__)
class XmlToXslAssignDialog(QDialog):
"""Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten."""
@@ -85,10 +89,10 @@ class XmlToXslAssignDialog(QDialog):
root = self.ui.xslNodesTree.invisibleRootItem()
self._add_checkboxes_recursive(root)
print(f"Checkboxen zu {len(self.xsl_checkboxes)} XSL-Knoten hinzugefügt")
logger.debug(f"Checkboxen zu {len(self.xsl_checkboxes)} XSL-Knoten hinzugefügt")
except Exception as e:
print(f"Fehler beim Hinzufügen der Checkboxen: {e}")
logger.error(f"Fehler beim Hinzufügen der Checkboxen: {e}")
def _add_checkboxes_recursive(self, parent_item):
"""
@@ -113,7 +117,7 @@ class XmlToXslAssignDialog(QDialog):
# Speichere Checkbox-Referenz
self.xsl_checkboxes[id(node)] = checkbox
print(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
# Rekursiv für Kinder
if item.childCount() > 0:
@@ -201,7 +205,7 @@ class XmlToXslAssignDialog(QDialog):
return item
except Exception as e:
print(f"Fehler beim Erstellen des Tree-Items: {e}")
logger.error(f"Fehler beim Erstellen des Tree-Items: {e}")
return None
def select_all(self):
@@ -235,7 +239,7 @@ class XmlToXslAssignDialog(QDialog):
return selected_nodes
except Exception as e:
print(f"Fehler beim Sammeln der ausgewählten XSL-Knoten: {e}")
logger.error(f"Fehler beim Sammeln der ausgewählten XSL-Knoten: {e}")
return []
def _find_xsl_node_by_id(self, node_id):
@@ -282,6 +286,15 @@ class XmlToXslAssignDialog(QDialog):
"""
return self.xml_file_path
def is_apply_to_all(self):
"""
Prüft, ob die Checkbox 'Alle XML-Dateien' aktiviert ist.
Returns:
bool: True wenn die Checkbox aktiviert ist, sonst False
"""
return self.ui.alle_xml.isChecked()
def accept(self):
"""Überschreibt accept() um Validierung durchzuführen."""
selected_nodes = self.get_selected_xsl_nodes()
+7
View File
@@ -97,6 +97,13 @@
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="alle_xml">
<property name="text">
<string>Alle XML-Dateien den ausgewählten XSL-Dateien zuordnen</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
+11 -5
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'XmlToXslAssignDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.9.1
## Created by: Qt User Interface Compiler version 6.9.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -15,10 +15,10 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
QHBoxLayout, QHeaderView, QLabel, QPushButton,
QSizePolicy, QSpacerItem, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog,
QDialogButtonBox, QHBoxLayout, QHeaderView, QLabel,
QPushButton, QSizePolicy, QSpacerItem, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget)
class Ui_XmlToXslAssignDialog(object):
def setupUi(self, XmlToXslAssignDialog):
@@ -64,6 +64,11 @@ class Ui_XmlToXslAssignDialog(object):
self.buttonLayout.addItem(self.horizontalSpacer)
self.alle_xml = QCheckBox(XmlToXslAssignDialog)
self.alle_xml.setObjectName(u"alle_xml")
self.buttonLayout.addWidget(self.alle_xml)
self.verticalLayout.addLayout(self.buttonLayout)
@@ -94,5 +99,6 @@ class Ui_XmlToXslAssignDialog(object):
___qtreewidgetitem.setText(0, QCoreApplication.translate("XmlToXslAssignDialog", u"XSL-Knoten", None));
self.selectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle ausw\u00e4hlen", None))
self.deselectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle abw\u00e4hlen", None))
self.alle_xml.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle XML-Dateien den ausgew\u00e4hlten XSL-Dateien zuordnen", None))
# retranslateUi
+5 -2
View File
@@ -2,7 +2,6 @@ from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
from PySide6.QtCore import Qt
from ui.XslFileEditDialog_ui import Ui_XslFileEditDialog
from conf import XslFile
class XslFileEditDialog(QDialog):
@@ -146,9 +145,13 @@ class XslFileEditDialog(QDialog):
if key: # Nur Parameter mit nicht-leerem Schlüssel hinzufügen
xslt_params[key] = value
# CheckBox für Force-Transformation prüfen
force_transform = self.ui.alle_xml_transformieren.isChecked()
return {
"bez": bez,
"xslt_params": xslt_params
"xslt_params": xslt_params,
"force_transform": force_transform
}
def accept(self):
+156 -76
View File
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<width>865</width>
<height>400</height>
</rect>
</property>
@@ -35,95 +35,175 @@
</layout>
</item>
<item>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="columnCount">
<number>2</number>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<property name="leftMargin">
<number>0</number>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
<property name="topMargin">
<number>0</number>
</property>
</column>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListAdd"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListRemove"/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
<widget class="QCheckBox" name="alle_xml_transformieren">
<property name="text">
<string>Alle XML-Dateien neu transformieren (force)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item>
+40 -11
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'XslFileEditDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.9.1
## Created by: Qt User Interface Compiler version 6.9.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -15,17 +15,18 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
QHeaderView, QLabel, QLayout, QLineEdit,
QPushButton, QSizePolicy, QSpacerItem, QTableWidget,
QTableWidgetItem, QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialog, QDialogButtonBox, QFormLayout, QFrame,
QGroupBox, QHBoxLayout, QHeaderView, QLabel,
QLayout, QLineEdit, QPushButton, QSizePolicy,
QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
class Ui_XslFileEditDialog(object):
def setupUi(self, XslFileEditDialog):
if not XslFileEditDialog.objectName():
XslFileEditDialog.setObjectName(u"XslFileEditDialog")
XslFileEditDialog.resize(600, 400)
XslFileEditDialog.resize(865, 400)
XslFileEditDialog.setModal(True)
self.verticalLayout = QVBoxLayout(XslFileEditDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
@@ -45,10 +46,18 @@ class Ui_XslFileEditDialog(object):
self.verticalLayout.addLayout(self.formLayout)
self.xsltParamsGroupBox = QGroupBox(XslFileEditDialog)
self.frame = QFrame(XslFileEditDialog)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QFrame.Shape.NoFrame)
self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout = QHBoxLayout(self.frame)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsGroupBox = QGroupBox(self.frame)
self.xsltParamsGroupBox.setObjectName(u"xsltParamsGroupBox")
self.xsltParamsLayout = QVBoxLayout(self.xsltParamsGroupBox)
self.xsltParamsLayout.setObjectName(u"xsltParamsLayout")
self.xsltParamsLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsTable = QTableWidget(self.xsltParamsGroupBox)
if (self.xsltParamsTable.columnCount() < 2):
self.xsltParamsTable.setColumnCount(2)
@@ -57,6 +66,7 @@ class Ui_XslFileEditDialog(object):
__qtablewidgetitem1 = QTableWidgetItem()
self.xsltParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
self.xsltParamsTable.setObjectName(u"xsltParamsTable")
self.xsltParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.xsltParamsTable.setColumnCount(2)
self.xsltParamsTable.horizontalHeader().setVisible(True)
@@ -64,13 +74,21 @@ class Ui_XslFileEditDialog(object):
self.xsltParamsButtonLayout = QHBoxLayout()
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout")
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_2)
self.addParamButton = QPushButton(self.xsltParamsGroupBox)
self.addParamButton.setObjectName(u"addParamButton")
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
self.addParamButton.setIcon(icon)
self.xsltParamsButtonLayout.addWidget(self.addParamButton)
self.removeParamButton = QPushButton(self.xsltParamsGroupBox)
self.removeParamButton.setObjectName(u"removeParamButton")
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
self.removeParamButton.setIcon(icon1)
self.xsltParamsButtonLayout.addWidget(self.removeParamButton)
@@ -82,12 +100,13 @@ class Ui_XslFileEditDialog(object):
self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout)
self.verticalLayout.addWidget(self.xsltParamsGroupBox)
self.horizontalLayout.addWidget(self.xsltParamsGroupBox)
self.parentParamsGroupBox = QGroupBox(XslFileEditDialog)
self.parentParamsGroupBox = QGroupBox(self.frame)
self.parentParamsGroupBox.setObjectName(u"parentParamsGroupBox")
self.parentParamsLayout = QVBoxLayout(self.parentParamsGroupBox)
self.parentParamsLayout.setObjectName(u"parentParamsLayout")
self.parentParamsLayout.setContentsMargins(0, 0, 0, 0)
self.parentParamsTable = QTableWidget(self.parentParamsGroupBox)
if (self.parentParamsTable.columnCount() < 2):
self.parentParamsTable.setColumnCount(2)
@@ -96,6 +115,7 @@ class Ui_XslFileEditDialog(object):
__qtablewidgetitem3 = QTableWidgetItem()
self.parentParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem3)
self.parentParamsTable.setObjectName(u"parentParamsTable")
self.parentParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.parentParamsTable.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.parentParamsTable.setColumnCount(2)
self.parentParamsTable.horizontalHeader().setVisible(True)
@@ -103,7 +123,15 @@ class Ui_XslFileEditDialog(object):
self.parentParamsLayout.addWidget(self.parentParamsTable)
self.verticalLayout.addWidget(self.parentParamsGroupBox)
self.horizontalLayout.addWidget(self.parentParamsGroupBox)
self.verticalLayout.addWidget(self.frame)
self.alle_xml_transformieren = QCheckBox(XslFileEditDialog)
self.alle_xml_transformieren.setObjectName(u"alle_xml_transformieren")
self.verticalLayout.addWidget(self.alle_xml_transformieren)
self.buttonBox = QDialogButtonBox(XslFileEditDialog)
self.buttonBox.setObjectName(u"buttonBox")
@@ -136,5 +164,6 @@ class Ui_XslFileEditDialog(object):
___qtablewidgetitem2.setText(QCoreApplication.translate("XslFileEditDialog", u"Parameter", None));
___qtablewidgetitem3 = self.parentParamsTable.horizontalHeaderItem(1)
___qtablewidgetitem3.setText(QCoreApplication.translate("XslFileEditDialog", u"Wert", None));
self.alle_xml_transformieren.setText(QCoreApplication.translate("XslFileEditDialog", u"Alle XML-Dateien neu transformieren (force)", None))
# retranslateUi
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""
Test-Skript für die erweiterte XML-Hash-Duplikatserkennung.
Testet die neuen Funktionalitäten in MainWindow.
"""
import hashlib
import tempfile
import shutil
from pathlib import Path
import sys
import os
# Füge src-Verzeichnis zum Python-Pfad hinzu
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from conf import XmlFile, XslFile, TreeNode, ProjectData
def create_test_xml_file(content: str, filename: str) -> Path:
"""Erstellt eine temporäre XML-Testdatei."""
temp_dir = Path(tempfile.mkdtemp())
xml_file = temp_dir / filename
with open(xml_file, 'w', encoding='utf-8') as f:
f.write(content)
return xml_file
def calculate_test_hash(file_path: Path) -> str:
"""Berechnet den blake2b-Hash für eine Testdatei."""
with open(file_path, 'rb') as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
return f"blake2b:{hash_obj.hexdigest()}"
def test_hash_calculation():
"""Testet die Hash-Berechnung."""
print("=== Test: Hash-Berechnung ===")
# Erstelle Testdatei
test_content = """<?xml version="1.0" encoding="UTF-8"?>
<root>
<test>Hash-Berechnung Test</test>
</root>"""
xml_file = create_test_xml_file(test_content, "test_hash.xml")
try:
# Berechne Hash
calculated_hash = calculate_test_hash(xml_file)
print(f"Berechneter Hash: {calculated_hash}")
# Verifikation
assert calculated_hash.startswith("blake2b:"), "Hash-Präfix fehlt!"
# blake2b erzeugt 128-stellige Hex-Strings + 8 Zeichen für "blake2b:" = 136 Zeichen
assert len(calculated_hash) == 136, f"Hash-Länge falsch: {len(calculated_hash)} (erwartet: 136)"
print("[OK] Hash-Berechnung funktioniert korrekt")
return calculated_hash
finally:
# Aufräumen
shutil.rmtree(xml_file.parent)
def test_xml_file_model_with_hash():
"""Testet das erweiterte XmlFile-Modell mit Hash."""
print("\n=== Test: XmlFile-Modell mit Hash ===")
# Test 1: XmlFile ohne Hash
xml_file1 = XmlFile(xml=Path("test1.xml"))
print(f"XmlFile ohne Hash: {xml_file1}")
assert xml_file1.hashsum is None, "Initiale hashsum sollte None sein"
# Test 2: XmlFile mit Hash
test_hash = "blake2b:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
xml_file2 = XmlFile(xml=Path("test2.xml"), hashsum=test_hash)
print(f"XmlFile mit Hash: {xml_file2}")
assert xml_file2.hashsum == test_hash, "Hash stimmt nicht überein"
print("[OK] XmlFile-Modell mit Hash funktioniert korrekt")
def test_duplicate_detection_logic():
"""Testet die Duplikatserkennung-Logik."""
print("\n=== Test: Duplikatserkennung-Logik ===")
# Erstelle Test-Projektstruktur
hash1 = "blake2b:1111111111111111111111111111111111111111111111111111111111111111"
hash2 = "blake2b:2222222222222222222222222222222222222222222222222222222222222222"
# XML-Dateien mit verschiedenen Hashes
xml1 = XmlFile(xml=Path("xml/file1.xml"), hashsum=hash1)
xml2 = XmlFile(xml=Path("xml/file2.xml"), hashsum=hash2)
xml3 = XmlFile(xml=Path("xml/file3.xml"), hashsum=hash1) # Duplikat von xml1
# XSL-Dateien
xsl1 = XslFile(id=(1,), bez="XSL 1", xsl_file=Path("xsl1.xsl"), xmls=[xml1, xml2])
xsl2 = XslFile(id=(2,), bez="XSL 2", xsl_file=Path("xsl2.xsl"), xmls=[xml3])
# TreeNode
tree_node = TreeNode(id=(1,), bez="Test Node", children=[xsl1, xsl2])
# Projekt
project = ProjectData(nodes=[tree_node])
# Sammle alle XML-Dateien
all_xml_files = []
def collect_xml_files(nodes):
for node in nodes:
if isinstance(node, XslFile) and node.xmls:
for xml_file in node.xmls:
if not any(existing.xml == xml_file.xml for existing in all_xml_files):
all_xml_files.append(xml_file)
elif isinstance(node, TreeNode) and node.children:
collect_xml_files(node.children)
collect_xml_files(project.nodes)
print(f"Gesammelte XML-Dateien: {len(all_xml_files)}")
for xml in all_xml_files:
print(f" - {xml.xml}: {xml.hashsum}")
# Test Hash-Suche
def find_xml_by_hash(target_hash):
for xml_file in all_xml_files:
if xml_file.hashsum == target_hash:
return xml_file
return None
# Test 1: Existierenden Hash finden
found_xml = find_xml_by_hash(hash1)
assert found_xml is not None, "Hash1 sollte gefunden werden"
assert found_xml.xml == Path("xml/file1.xml"), "Falsches XML-File gefunden"
print(f"Hash-Suche erfolgreich: {found_xml.xml}")
# Test 2: Nicht existierenden Hash suchen
non_existent_hash = "blake2b:9999999999999999999999999999999999999999999999999999999999999999"
not_found = find_xml_by_hash(non_existent_hash)
assert not_found is None, "Nicht existierender Hash sollte None zurückgeben"
print("Nicht existierender Hash korrekt behandelt")
print("[OK] Duplikatserkennung-Logik funktioniert korrekt")
def test_alternative_filename_generation():
"""Testet die Generierung alternativer Dateinamen."""
print("\n=== Test: Alternative Dateinamen-Generierung ===")
# Simuliere existierende Dateien
existing_files = {
"test.xml",
"test_1.xml",
"test_2.xml",
"document.xml"
}
def generate_alternative_name(original_name: str) -> str:
"""Simuliert die Generierung alternativer Namen."""
base_name = Path(original_name).stem
extension = Path(original_name).suffix
counter = 1
while True:
new_name = f"{base_name}_{counter}{extension}"
if new_name not in existing_files:
return new_name
counter += 1
if counter > 100: # Sicherheitsgrenze
break
return f"{base_name}_fallback{extension}"
# Test 1: Datei existiert bereits
alt_name1 = generate_alternative_name("test.xml")
expected1 = "test_3.xml" # test.xml, test_1.xml, test_2.xml existieren bereits
assert alt_name1 == expected1, f"Erwarteter Name: {expected1}, erhalten: {alt_name1}"
print(f"Alternative für 'test.xml': {alt_name1}")
# Test 2: Datei existiert nicht
alt_name2 = generate_alternative_name("new_file.xml")
expected2 = "new_file_1.xml"
assert alt_name2 == expected2, f"Erwarteter Name: {expected2}, erhalten: {alt_name2}"
print(f"Alternative für 'new_file.xml': {alt_name2}")
# Test 3: Datei ohne Konflikte
alt_name3 = generate_alternative_name("unique.xml")
expected3 = "unique_1.xml"
assert alt_name3 == expected3, f"Erwarteter Name: {expected3}, erhalten: {alt_name3}"
print(f"Alternative für 'unique.xml': {alt_name3}")
print("[OK] Alternative Dateinamen-Generierung funktioniert korrekt")
def test_integration_workflow():
"""Testet den kompletten Workflow der Integration."""
print("\n=== Test: Integration Workflow ===")
# Simuliere den kompletten Workflow
# 1. Neue XML-Datei
new_xml_content = """<?xml version="1.0" encoding="UTF-8"?>
<document>
<title>Integration Test</title>
<content>Test-Inhalt für Integration</content>
</document>"""
new_xml_file = create_test_xml_file(new_xml_content, "integration_test.xml")
new_hash = calculate_test_hash(new_xml_file)
try:
# 2. Bestehende Projekt-XML-Dateien simulieren
existing_xml1 = XmlFile(xml=Path("xml/existing1.xml"), hashsum="blake2b:aaaa")
existing_xml2 = XmlFile(xml=Path("xml/existing2.xml"), hashsum="blake2b:bbbb")
existing_xml3 = XmlFile(xml=Path("xml/existing3.xml"), hashsum=new_hash) # Duplikat!
existing_xmls = [existing_xml1, existing_xml2, existing_xml3]
# 3. Hash-Vergleich
duplicate_found = None
for existing_xml in existing_xmls:
if existing_xml.hashsum == new_hash:
duplicate_found = existing_xml
break
# 4. Verifikation
assert duplicate_found is not None, "Duplikat sollte gefunden werden"
assert duplicate_found.xml == Path("xml/existing3.xml"), "Falsches Duplikat gefunden"
print("Workflow-Test erfolgreich:")
print(f" - Neue Datei: {new_xml_file.name}")
print(f" - Berechneter Hash: {new_hash}")
print(f" - Duplikat gefunden: {duplicate_found.xml}")
print(" - Automatische Zuordnung würde erfolgen")
print("[OK] Integration Workflow funktioniert korrekt")
finally:
# Aufräumen
shutil.rmtree(new_xml_file.parent)
if __name__ == "__main__":
print("Starte Tests für XML-Hash-Duplikatserkennung...\n")
try:
test_hash_calculation()
test_xml_file_model_with_hash()
test_duplicate_detection_logic()
test_alternative_filename_generation()
test_integration_workflow()
print("\n" + "="*60)
print("[SUCCESS] Alle Tests erfolgreich abgeschlossen!")
print("\nDie erweiterte XML-Hash-Duplikatserkennung ist bereit für den Einsatz.")
print("\nNeue Funktionalitäten:")
print("+ Hash-basierte Duplikatserkennung im gesamten Projekt")
print("+ Automatische Zuordnung bei Hash-Match")
print("+ Intelligente Dateinamen-Generierung (datei_1.xml Format)")
print("+ Integration in Drag&Drop und Kontextmenü")
print("+ Benutzerfreundliche Dateiname-Auswahl-Dialoge")
except Exception as e:
print(f"\n[ERROR] Test fehlgeschlagen: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
Generated
+32 -1
View File
@@ -44,6 +44,11 @@ dependencies = [
{ name = "pyside6" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.31.0" },
@@ -54,7 +59,7 @@ requires-dist = [
]
[package.metadata.requires-dev]
dev = []
dev = [{ name = "ruff", specifier = ">=0.14.8" }]
[[package]]
name = "polars"
@@ -269,6 +274,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload-time = "2024-10-20T10:13:10.66Z" },
]
[[package]]
name = "ruff"
version = "0.14.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" },
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" },
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" },
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" },
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" },
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" },
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" },
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" },
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" },
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" },
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" },
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" },
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" },
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" },
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" },
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" },
]
[[package]]
name = "shiboken6"
version = "6.9.2"