Feat: Veraltete XSL-Einträge nach DB-Import erkennen und entfernen (v1.2.7)
Beim Import aus der PostgreSQL-Datenbank werden nun XSL-Einträge erkannt, die nicht mehr in der DB vorhanden sind. Ein Dialog zeigt diese gruppiert in einer Baumansicht an und bietet die Option, sie samt nicht mehr verwendeter XML-/PDF-Dateien aus dem Projekt zu entfernen. Leere TreeNodes werden automatisch bereinigt. Zusätzlich: SQL-Filter `r3.export = 0` in data.sql ergänzt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Obsolete-Detector — Erkennung veralteter Projekteinträge nach DB-Import.
|
||||
|
||||
Reine Analyselogik ohne Qt-Abhängigkeit. Findet XslFile-Einträge im Projekt,
|
||||
die nicht mehr in der Datenbank vorhanden sind.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from conf import TreeNode, XslFile
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObsoleteXslEntry:
|
||||
"""Ein XslFile-Eintrag, der nicht mehr in der Datenbank vorhanden ist."""
|
||||
|
||||
xsl_file: XslFile
|
||||
parent_node: TreeNode
|
||||
|
||||
|
||||
@dataclass
|
||||
class ObsoleteGroup:
|
||||
"""Gruppe veralteter XslFile-Einträge unter einem gemeinsamen Eltern-Pfad."""
|
||||
|
||||
node_path: list[str]
|
||||
parent_node: TreeNode
|
||||
xsl_entries: list[ObsoleteXslEntry] = field(default_factory=list)
|
||||
|
||||
|
||||
def extract_db_xsl_ids(new_nodes: list[TreeNode]) -> set[tuple]:
|
||||
"""
|
||||
Extrahiert alle XslFile-IDs aus den frisch aus der DB geladenen Nodes.
|
||||
|
||||
Args:
|
||||
new_nodes: Nodes aus _process_sql_data()
|
||||
|
||||
Returns:
|
||||
Set aller XslFile-IDs (Tupel)
|
||||
"""
|
||||
ids: set[tuple] = set()
|
||||
_collect_ids_recursive(new_nodes, ids)
|
||||
return ids
|
||||
|
||||
|
||||
def _collect_ids_recursive(nodes: list, ids: set[tuple]) -> None:
|
||||
for node in nodes:
|
||||
if isinstance(node, XslFile):
|
||||
ids.add(node.id)
|
||||
elif isinstance(node, TreeNode) and node.children:
|
||||
_collect_ids_recursive(node.children, ids)
|
||||
|
||||
|
||||
def find_obsolete_xsl_entries(
|
||||
project_nodes: list[TreeNode],
|
||||
db_xsl_ids: set[tuple],
|
||||
) -> list[ObsoleteGroup]:
|
||||
"""
|
||||
Findet alle XslFile-Einträge im Projekt, die nicht mehr in der DB vorhanden sind.
|
||||
|
||||
Args:
|
||||
project_nodes: Aktuelle Nodes aus pdf_project.nodes
|
||||
db_xsl_ids: Set aller XslFile-IDs aus der DB (von extract_db_xsl_ids)
|
||||
|
||||
Returns:
|
||||
Liste von ObsoleteGroup, sortiert nach Hierarchiepfad
|
||||
"""
|
||||
groups: dict[int, ObsoleteGroup] = {}
|
||||
|
||||
_find_obsolete_recursive(project_nodes, db_xsl_ids, path=[], groups=groups)
|
||||
|
||||
# Sortieren nach Pfad für stabile Darstellung
|
||||
return sorted(groups.values(), key=lambda g: g.node_path)
|
||||
|
||||
|
||||
def _find_obsolete_recursive(
|
||||
nodes: list,
|
||||
db_xsl_ids: set[tuple],
|
||||
path: list[str],
|
||||
groups: dict[int, ObsoleteGroup],
|
||||
) -> None:
|
||||
for node in nodes:
|
||||
if isinstance(node, TreeNode):
|
||||
_find_obsolete_in_tree_node(node, db_xsl_ids, path, groups)
|
||||
|
||||
|
||||
def _find_obsolete_in_tree_node(
|
||||
node: TreeNode,
|
||||
db_xsl_ids: set[tuple],
|
||||
parent_path: list[str],
|
||||
groups: dict[int, ObsoleteGroup],
|
||||
) -> None:
|
||||
current_path = parent_path + [node.bez]
|
||||
|
||||
for child in node.children:
|
||||
if isinstance(child, XslFile):
|
||||
if child.id not in db_xsl_ids:
|
||||
node_id = id(node)
|
||||
if node_id not in groups:
|
||||
groups[node_id] = ObsoleteGroup(
|
||||
node_path=current_path,
|
||||
parent_node=node,
|
||||
)
|
||||
groups[node_id].xsl_entries.append(ObsoleteXslEntry(xsl_file=child, parent_node=node))
|
||||
elif isinstance(child, TreeNode):
|
||||
_find_obsolete_in_tree_node(child, db_xsl_ids, current_path, groups)
|
||||
|
||||
|
||||
def remove_empty_tree_nodes(nodes: list) -> list:
|
||||
"""
|
||||
Entfernt rekursiv alle TreeNodes, deren children-Liste nach der Bereinigung leer ist.
|
||||
XslFile-Einträge werden immer behalten (sie wurden bereits entfernt oder sind noch gültig).
|
||||
|
||||
Args:
|
||||
nodes: Liste von TreeNode|XslFile
|
||||
|
||||
Returns:
|
||||
Bereinigte Liste ohne leere TreeNodes
|
||||
"""
|
||||
result = []
|
||||
for node in nodes:
|
||||
if isinstance(node, TreeNode):
|
||||
node.children = remove_empty_tree_nodes(node.children)
|
||||
if node.children:
|
||||
result.append(node)
|
||||
# leere TreeNodes stillschweigend verwerfen
|
||||
else:
|
||||
result.append(node)
|
||||
return result
|
||||
|
||||
|
||||
def collect_unused_xml_files(
|
||||
obsolete_groups: list[ObsoleteGroup],
|
||||
project_dir: Path,
|
||||
is_xml_used_elsewhere_fn,
|
||||
) -> list[tuple[Path, Path]]:
|
||||
"""
|
||||
Sammelt XML-Dateien der veralteten XslFiles, die nirgends anders mehr verwendet werden.
|
||||
|
||||
WICHTIG: Muss nach dem Entfernen der XslFiles aus dem Modell aufgerufen werden,
|
||||
damit is_xml_used_elsewhere_fn korrekte Ergebnisse liefert.
|
||||
|
||||
Args:
|
||||
obsolete_groups: Die veralteten Gruppen
|
||||
project_dir: Absoluter Pfad zum Projektverzeichnis
|
||||
is_xml_used_elsewhere_fn: Callable(xml_path, exclude_xsl_file) -> bool
|
||||
|
||||
Returns:
|
||||
Liste von (xml_path_relativ, xml_path_absolut) für nicht mehr verwendete XML-Dateien
|
||||
"""
|
||||
unused: list[tuple[Path, Path]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for group in obsolete_groups:
|
||||
for entry in group.xsl_entries:
|
||||
for xml_file_obj in entry.xsl_file.xmls:
|
||||
xml_path_str = str(xml_file_obj.xml)
|
||||
if xml_path_str in seen:
|
||||
continue
|
||||
seen.add(xml_path_str)
|
||||
xml_abs = project_dir / xml_file_obj.xml
|
||||
if xml_abs.exists():
|
||||
if not is_xml_used_elsewhere_fn(xml_file_obj.xml, entry.xsl_file):
|
||||
unused.append((xml_file_obj.xml, xml_abs))
|
||||
|
||||
return unused
|
||||
Reference in New Issue
Block a user