Feat: XML-Knoten-Bearbeiten-Dialog implementiert (v1.6.0)

Neuer Dialog ermöglicht es, einem XML-Knoten XSL-Zuordnungen hinzuzufügen
oder zu entfernen. XmlToXslAssignDialog wiederverwendet mit edit_mode,
Vorauswahl per preselected_xsl_ids und get_selection_diff(). Beim Entfernen
werden zugehörige PDF-Dateien gelöscht; bei verbleibend leerer Zuordnung
wird das physische Löschen der XML-Datei angeboten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 21:10:34 +02:00
parent 762523d669
commit 9fad317891
7 changed files with 262 additions and 23 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<!-- Paket-Definition (ersetzt Product in v4) --> <!-- Paket-Definition (ersetzt Product in v4) -->
<Package <Package
Name="DocuMentor" Name="DocuMentor"
Version="1.5.1" Version="1.6.0"
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung" Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E" UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
Language="1031" Language="1031"
+1 -1
View File
@@ -253,5 +253,5 @@ HINWEISE
================================================================================ ================================================================================
Stand: April 2026 Stand: April 2026
Erstellt für: DocuMentor v1.5.1 Erstellt für: DocuMentor v1.6.0
================================================================================ ================================================================================
+1 -1
View File
@@ -10,7 +10,7 @@
; Build-Befehl: iscc installer.iss ; Build-Befehl: iscc installer.iss
#define MyAppName "DocuMentor" #define MyAppName "DocuMentor"
#define MyAppVersion "1.5.1" #define MyAppVersion "1.6.0"
#define MyAppPublisher "Ihr Name/Organisation" #define MyAppPublisher "Ihr Name/Organisation"
#define MyAppURL "https://github.com/yourusername/xsl-validator" #define MyAppURL "https://github.com/yourusername/xsl-validator"
#define MyAppExeName "DocuMentor.exe" #define MyAppExeName "DocuMentor.exe"
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "DocuMentor" name = "DocuMentor"
version = "1.5.1" version = "1.6.0"
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung" description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = {text = "MIT"}
+94 -9
View File
@@ -13,7 +13,14 @@ logger = logging.getLogger(__name__)
class XmlToXslAssignDialog(QDialog): class XmlToXslAssignDialog(QDialog):
"""Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten.""" """Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten."""
def __init__(self, parent=None, xml_file_path=None, project_nodes=None): def __init__(
self,
parent=None,
xml_file_path=None,
project_nodes=None,
preselected_xsl_ids: set | None = None,
edit_mode: bool = False,
):
""" """
Initialisiert den Dialog. Initialisiert den Dialog.
@@ -21,6 +28,8 @@ class XmlToXslAssignDialog(QDialog):
parent: Übergeordnetes Widget parent: Übergeordnetes Widget
xml_file_path: Pfad zur XML-Datei xml_file_path: Pfad zur XML-Datei
project_nodes: Liste der Projekt-Knoten project_nodes: Liste der Projekt-Knoten
preselected_xsl_ids: Set von XslFile-IDs (tuple), deren Checkbox initial angehakt sein soll
edit_mode: True = Bearbeiten-Modus (Zuordnungen ändern), False = Neu-Zuordnen
""" """
super().__init__(parent) super().__init__(parent)
@@ -31,9 +40,23 @@ class XmlToXslAssignDialog(QDialog):
# Parameter speichern # Parameter speichern
self.xml_file_path = Path(xml_file_path) if xml_file_path else None self.xml_file_path = Path(xml_file_path) if xml_file_path else None
self.project_nodes = project_nodes or [] self.project_nodes = project_nodes or []
self.preselected_xsl_ids: set = set(preselected_xsl_ids) if preselected_xsl_ids else set()
self.edit_mode = edit_mode
# Dictionary zum Speichern der Checkbox-Referenzen # Dictionary zum Speichern der Checkbox-Referenzen
self.xsl_checkboxes = {} # {xsl_node_id: checkbox} self.xsl_checkboxes = {} # {python_id(node): checkbox}
self.xsl_nodes = {} # {python_id(node): XslFile}
# Initial ausgewählte XslFile-IDs (tuple), um Diff beim Accept zu berechnen
self._initial_selected_ids: set = set()
# Edit-Modus: UI anpassen
if self.edit_mode:
self.setWindowTitle("XML-Zuordnungen bearbeiten")
self.ui.infoLabel.setText(
"Passen Sie die Zuordnungen der XML-Datei an. Hinzufügen per Haken, "
"Entfernen durch Abhaken (zugehörige PDFs werden gelöscht):"
)
self.ui.alle_xml.setVisible(False)
# Signale verbinden # Signale verbinden
self.ui.selectAllButton.clicked.connect(self.select_all) self.ui.selectAllButton.clicked.connect(self.select_all)
@@ -45,6 +68,10 @@ class XmlToXslAssignDialog(QDialog):
# Daten laden # Daten laden
self._load_data() self._load_data()
# Duplikat-Warnung nur im Edit-Modus (um bestehende Aufrufer nicht bei jeder XML zu stören)
if self.edit_mode:
self._warn_on_duplicate_xsl_ids()
def _setup_tree(self): def _setup_tree(self):
"""Konfiguriert das TreeWidget.""" """Konfiguriert das TreeWidget."""
# Spaltenbreiten setzen # Spaltenbreiten setzen
@@ -111,11 +138,17 @@ class XmlToXslAssignDialog(QDialog):
# Erstelle zentrierte Checkbox für XSL-Knoten # Erstelle zentrierte Checkbox für XSL-Knoten
checkbox_widget, checkbox = self._create_centered_checkbox() checkbox_widget, checkbox = self._create_centered_checkbox()
# Vorauswahl setzen, wenn ID in preselected_xsl_ids
if node.id in self.preselected_xsl_ids:
checkbox.setChecked(True)
self._initial_selected_ids.add(node.id)
# Setze das Widget in Spalte 2 # Setze das Widget in Spalte 2
self.ui.xslNodesTree.setItemWidget(item, 2, checkbox_widget) self.ui.xslNodesTree.setItemWidget(item, 2, checkbox_widget)
# Speichere Checkbox-Referenz # Speichere Checkbox- und Node-Referenzen (id() als Key, da XSL-IDs theoretisch doppelt sein können)
self.xsl_checkboxes[id(node)] = checkbox self.xsl_checkboxes[id(node)] = checkbox
self.xsl_nodes[id(node)] = node
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt") logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
@@ -141,6 +174,8 @@ class XmlToXslAssignDialog(QDialog):
# TreeWidget leeren # TreeWidget leeren
self.ui.xslNodesTree.clear() self.ui.xslNodesTree.clear()
self.xsl_checkboxes.clear() self.xsl_checkboxes.clear()
self.xsl_nodes.clear()
self._initial_selected_ids.clear()
# Sortiere Root-Nodes alphabetisch nach ID # Sortiere Root-Nodes alphabetisch nach ID
sorted_nodes = sorted(self.project_nodes, key=lambda node: node.id) sorted_nodes = sorted(self.project_nodes, key=lambda node: node.id)
@@ -295,16 +330,66 @@ class XmlToXslAssignDialog(QDialog):
""" """
return self.ui.alle_xml.isChecked() return self.ui.alle_xml.isChecked()
def get_selection_diff(self) -> tuple[list, list]:
"""
Gibt die Änderungen der Auswahl gegenüber der Vorauswahl zurück.
Nützlich im Edit-Modus, um nur die tatsächlich geänderten Zuordnungen zu verarbeiten.
Returns:
tuple[list[XslFile], list[XslFile]]: (added_nodes, removed_nodes)
- added_nodes: XslFile-Objekte, die neu angehakt wurden
- removed_nodes: XslFile-Objekte, deren Haken entfernt wurde
"""
added_nodes = []
removed_nodes = []
for py_id, checkbox in self.xsl_checkboxes.items():
node = self.xsl_nodes.get(py_id)
if node is None:
continue
was_selected = node.id in self._initial_selected_ids
is_selected = checkbox.isChecked()
if is_selected and not was_selected:
added_nodes.append(node)
elif not is_selected and was_selected:
removed_nodes.append(node)
return added_nodes, removed_nodes
def _warn_on_duplicate_xsl_ids(self):
"""
Zeigt eine Warnung, wenn im Projekt mehrere XslFile-Instanzen mit identischer ID existieren.
Die ID soll eindeutig sein - Duplikate weisen auf einen Datenfehler hin.
"""
id_to_nodes: dict = {}
for py_id, node in self.xsl_nodes.items():
id_to_nodes.setdefault(node.id, []).append(node)
duplicates = {xsl_id: nodes for xsl_id, nodes in id_to_nodes.items() if len(nodes) > 1}
if not duplicates:
return
dup_lines = [f" - ID {xsl_id}: {len(nodes)}× ({', '.join(n.bez for n in nodes)})" for xsl_id, nodes in duplicates.items()]
logger.warning(f"Doppelte XSL-IDs im Projekt gefunden:\n{chr(10).join(dup_lines)}")
QMessageBox.warning(
self,
"Doppelte XSL-IDs",
"Im Projekt existieren XSL-Knoten mit identischer ID. "
"Die IDs sollten eindeutig sein:\n\n" + "\n".join(dup_lines),
)
def accept(self): def accept(self):
"""Überschreibt accept() um Validierung durchzuführen.""" """Überschreibt accept() um Validierung durchzuführen."""
selected_nodes = self.get_selected_xsl_nodes() # Im Edit-Modus: 0 Auswahlen erlauben (bedeutet: XML überall entfernen)
if self.edit_mode:
super().accept()
return
selected_nodes = self.get_selected_xsl_nodes()
if not selected_nodes: if not selected_nodes:
QMessageBox.warning( QMessageBox.warning(self, "Warnung", "Bitte wählen Sie mindestens einen XSL-Knoten aus.")
self,
"Warnung",
"Bitte wählen Sie mindestens einen XSL-Knoten aus."
)
return return
super().accept() super().accept()
+156 -2
View File
@@ -30,6 +30,7 @@ from PySide6.QtWidgets import (
from conf import TreeNode, XslFile, XmlFile from conf import TreeNode, XslFile, XmlFile
from ui.TreeNodeEditDialog import TreeNodeEditDialog from ui.TreeNodeEditDialog import TreeNodeEditDialog
from ui.XslFileEditDialog import XslFileEditDialog from ui.XslFileEditDialog import XslFileEditDialog
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
class ItemType(Enum): class ItemType(Enum):
@@ -1397,9 +1398,162 @@ class TreeManagerMixin:
# Kontextmenü-Aktionen für XmlFile # Kontextmenü-Aktionen für XmlFile
def _edit_xml_file(self, item): def _edit_xml_file(self, item):
"""Bearbeitet eine XML-Datei.""" """
Bearbeitet die Zuordnungen einer XML-Datei zu XSL-Knoten.
Öffnet den XmlToXslAssignDialog im Edit-Modus mit Vorauswahl aller bestehenden Zuordnungen.
"""
logger.debug(f"XmlFile bearbeiten: {item.text(0)}") logger.debug(f"XmlFile bearbeiten: {item.text(0)}")
# TODO: Dialog zum Bearbeiten der XML-Datei öffnen
try:
if not hasattr(self, "project") or not self.project:
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
return
if not hasattr(self, "pdf_project") or not self.pdf_project:
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
return
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not xml_file_obj or not isinstance(xml_file_obj, XmlFile):
QMessageBox.warning(self, "Warnung", "Keine gültige XML-Datei gefunden.")
return
xml_path = xml_file_obj.xml
# Alle aktuellen XSL-IDs, an denen diese XML hängt
current_xsl_ids = self._find_xsl_ids_for_xml(xml_path)
# Hash von einer vorhandenen Instanz übernehmen (falls vorhanden)
existing_hash = self._find_existing_xml_hash(xml_path)
dialog = XmlToXslAssignDialog(
parent=self,
xml_file_path=xml_path,
project_nodes=self.pdf_project.nodes,
preselected_xsl_ids=current_xsl_ids,
edit_mode=True,
)
if dialog.exec() != XmlToXslAssignDialog.DialogCode.Accepted:
logger.debug("Bearbeiten abgebrochen")
return
added_nodes, removed_nodes = dialog.get_selection_diff()
if not added_nodes and not removed_nodes:
logger.debug("Keine Änderungen an den XML-Zuordnungen")
return
self._apply_xml_assignment_changes(xml_path, existing_hash, added_nodes, removed_nodes)
# Speichern und Baum neu laden
self._save_project_settings()
self._load_nodes_to_tree()
# Wenn keine Zuordnungen mehr bestehen: Physische Datei löschen anbieten
if not self._find_xsl_ids_for_xml(xml_path):
xml_file_path = Path(self.project.project_dir) / xml_path
if xml_file_path.exists():
reply = QMessageBox.question(
self,
"Physische Datei löschen",
f"Die XML-Datei '{xml_path.name}' ist keinem XSL-Knoten mehr zugeordnet.\n\n"
"Möchten Sie auch die physische Datei aus dem xml-Ordner löschen?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
try:
xml_file_path.unlink()
logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
except Exception as e:
QMessageBox.warning(self, "Warnung", f"Fehler beim Löschen der physischen Datei:\n{str(e)}")
logger.info(
f"XML-Zuordnungen aktualisiert: {len(added_nodes)} hinzugefügt, {len(removed_nodes)} entfernt"
)
except Exception as e:
error_msg = f"Fehler beim Bearbeiten der XML-Zuordnungen: {str(e)}"
logger.exception(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _find_xsl_ids_for_xml(self, xml_path: Path) -> set:
"""
Findet alle XslFile-IDs (tuple), deren `xmls`-Liste den angegebenen Pfad enthält.
Args:
xml_path: Relativer Pfad zur XML-Datei
Returns:
set[tuple]: Set von XslFile-IDs
"""
found_ids: set = set()
if not self.pdf_project or not self.pdf_project.nodes:
return found_ids
def _walk(nodes):
for node in nodes:
if isinstance(node, XslFile):
if any(xml.xml == xml_path for xml in node.xmls):
found_ids.add(node.id)
elif isinstance(node, TreeNode) and node.children:
_walk(node.children)
_walk(self.pdf_project.nodes)
return found_ids
def _find_existing_xml_hash(self, xml_path: Path) -> str | None:
"""
Sucht den blake2b-Hash einer bereits zugeordneten XmlFile-Instanz.
Gibt den ersten gefundenen Hash zurück (oder None, wenn keiner vorhanden).
"""
if not self.pdf_project or not self.pdf_project.nodes:
return None
def _walk(nodes):
for node in nodes:
if isinstance(node, XslFile):
for xml_file in node.xmls:
if xml_file.xml == xml_path and xml_file.hashsum:
return xml_file.hashsum
elif isinstance(node, TreeNode) and node.children:
found = _walk(node.children)
if found:
return found
return None
return _walk(self.pdf_project.nodes)
def _apply_xml_assignment_changes(
self, xml_path: Path, hashsum: str | None, added_nodes: list, removed_nodes: list
):
"""
Wendet Änderungen an den XML-Zuordnungen an:
- Entfernt XmlFile-Instanzen aus `removed_nodes` und löscht zugehörige PDFs (falls Kombination nirgends mehr existiert)
- Fügt XmlFile-Instanzen zu `added_nodes` hinzu (mit übernommenem Hash)
Args:
xml_path: Relativer Pfad zur XML-Datei
hashsum: blake2b-Hash, der für neue Instanzen übernommen wird
added_nodes: Liste der XslFile-Objekte, denen die XML neu zugeordnet wird
removed_nodes: Liste der XslFile-Objekte, aus denen die XML entfernt wird
"""
# 1. Entfernen
for xsl_node in removed_nodes:
xsl_node.xmls = [x for x in xsl_node.xmls if x.xml != xml_path]
# PDFs nur löschen, wenn die Kombination nirgends mehr existiert
if not self._is_xml_xsl_combination_used_elsewhere(xml_path, xsl_node.id, xsl_node):
deleted = self._delete_pdf_files_for_xml_xsl_combination(xml_path, xsl_node.id)
if deleted:
logger.info(f"{deleted} PDF-Datei(en) für '{xml_path.name}' (XSL: {xsl_node.bez}) gelöscht")
# 2. Hinzufügen (Hash übernehmen, Duplikate im gleichen XslFile vermeiden)
for xsl_node in added_nodes:
if any(x.xml == xml_path for x in xsl_node.xmls):
logger.debug(f"XML '{xml_path.name}' bereits an XSL '{xsl_node.bez}' zugeordnet — übersprungen")
continue
xsl_node.xmls.append(XmlFile(xml=xml_path, hashsum=hashsum))
logger.info(f"XML '{xml_path.name}' zu XSL '{xsl_node.bez}' hinzugefügt")
def _delete_xml_file(self, item): def _delete_xml_file(self, item):
""" """
Generated
+1 -1
View File
@@ -34,7 +34,7 @@ wheels = [
[[package]] [[package]]
name = "documentor" name = "documentor"
version = "1.5.1" version = "1.6.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "connectorx" }, { name = "connectorx" },