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:
+1
-1
@@ -4,7 +4,7 @@
|
||||
<!-- Paket-Definition (ersetzt Product in v4) -->
|
||||
<Package
|
||||
Name="DocuMentor"
|
||||
Version="1.5.1"
|
||||
Version="1.6.0"
|
||||
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
|
||||
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
|
||||
Language="1031"
|
||||
|
||||
@@ -253,5 +253,5 @@ HINWEISE
|
||||
|
||||
================================================================================
|
||||
Stand: April 2026
|
||||
Erstellt für: DocuMentor v1.5.1
|
||||
Erstellt für: DocuMentor v1.6.0
|
||||
================================================================================
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
; Build-Befehl: iscc installer.iss
|
||||
|
||||
#define MyAppName "DocuMentor"
|
||||
#define MyAppVersion "1.5.1"
|
||||
#define MyAppVersion "1.6.0"
|
||||
#define MyAppPublisher "Ihr Name/Organisation"
|
||||
#define MyAppURL "https://github.com/yourusername/xsl-validator"
|
||||
#define MyAppExeName "DocuMentor.exe"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "DocuMentor"
|
||||
version = "1.5.1"
|
||||
version = "1.6.0"
|
||||
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
@@ -13,7 +13,14 @@ logger = logging.getLogger(__name__)
|
||||
class XmlToXslAssignDialog(QDialog):
|
||||
"""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.
|
||||
|
||||
@@ -21,6 +28,8 @@ class XmlToXslAssignDialog(QDialog):
|
||||
parent: Übergeordnetes Widget
|
||||
xml_file_path: Pfad zur XML-Datei
|
||||
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)
|
||||
|
||||
@@ -31,9 +40,23 @@ class XmlToXslAssignDialog(QDialog):
|
||||
# Parameter speichern
|
||||
self.xml_file_path = Path(xml_file_path) if xml_file_path else None
|
||||
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
|
||||
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
|
||||
self.ui.selectAllButton.clicked.connect(self.select_all)
|
||||
@@ -45,6 +68,10 @@ class XmlToXslAssignDialog(QDialog):
|
||||
# Daten laden
|
||||
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):
|
||||
"""Konfiguriert das TreeWidget."""
|
||||
# Spaltenbreiten setzen
|
||||
@@ -111,11 +138,17 @@ class XmlToXslAssignDialog(QDialog):
|
||||
# Erstelle zentrierte Checkbox für XSL-Knoten
|
||||
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
|
||||
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_nodes[id(node)] = node
|
||||
|
||||
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
|
||||
|
||||
@@ -141,6 +174,8 @@ class XmlToXslAssignDialog(QDialog):
|
||||
# TreeWidget leeren
|
||||
self.ui.xslNodesTree.clear()
|
||||
self.xsl_checkboxes.clear()
|
||||
self.xsl_nodes.clear()
|
||||
self._initial_selected_ids.clear()
|
||||
|
||||
# Sortiere Root-Nodes alphabetisch nach 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()
|
||||
|
||||
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):
|
||||
"""Ü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:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Warnung",
|
||||
"Bitte wählen Sie mindestens einen XSL-Knoten aus."
|
||||
)
|
||||
QMessageBox.warning(self, "Warnung", "Bitte wählen Sie mindestens einen XSL-Knoten aus.")
|
||||
return
|
||||
|
||||
super().accept()
|
||||
|
||||
@@ -30,6 +30,7 @@ from PySide6.QtWidgets import (
|
||||
from conf import TreeNode, XslFile, XmlFile
|
||||
from ui.TreeNodeEditDialog import TreeNodeEditDialog
|
||||
from ui.XslFileEditDialog import XslFileEditDialog
|
||||
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
@@ -1397,9 +1398,162 @@ class TreeManagerMixin:
|
||||
|
||||
# Kontextmenü-Aktionen für XmlFile
|
||||
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)}")
|
||||
# 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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user