""" Utility functions for Vorgaben sanity checking and XML export """ import datetime import xml.etree.ElementTree as ET import xml.dom.minidom from django.db.models import Count from itertools import combinations from dokumente.models import Vorgabe def check_vorgabe_conflicts(): """ Check for conflicts in Vorgaben. Main rule: If there are two Vorgaben with the same number, Thema and Dokument, their valid_from and valid_to date ranges shouldn't intersect. Returns: list: List of conflict dictionaries """ conflicts = [] # Find Vorgaben with same dokument, thema, and nummer duplicate_groups = ( Vorgabe.objects.values('dokument', 'thema', 'nummer') .annotate(count=Count('id')) .filter(count__gt=1) ) for group in duplicate_groups: # Get all Vorgaben in this group vorgaben = Vorgabe.objects.filter( dokument=group['dokument'], thema=group['thema'], nummer=group['nummer'] ) # Check all pairs for date range intersections for vorgabe1, vorgabe2 in combinations(vorgaben, 2): if date_ranges_intersect( vorgabe1.gueltigkeit_von, vorgabe1.gueltigkeit_bis, vorgabe2.gueltigkeit_von, vorgabe2.gueltigkeit_bis ): conflicts.append({ 'vorgabe1': vorgabe1, 'vorgabe2': vorgabe2, 'conflict_type': 'date_range_intersection', 'message': f"Vorgaben {vorgabe1.Vorgabennummer()} and {vorgabe2.Vorgabennummer()} " f"have intersecting validity periods" }) return conflicts def date_ranges_intersect(start1, end1, start2, end2): """ Check if two date ranges intersect. None end date means open-ended range. Args: start1, start2: Start dates end1, end2: End dates (can be None for open-ended) Returns: bool: True if ranges intersect """ # If either start date is None, treat it as invalid case if not start1 or not start2: return False # If end date is None, treat it as far future end1 = end1 or datetime.date.max end2 = end2 or datetime.date.max # Ranges intersect if start1 <= end2 and start2 <= end1 return start1 <= end2 and start2 <= end1 def format_conflict_report(conflicts, verbose=False): """ Format conflicts into a readable report. Args: conflicts: List of conflict dictionaries verbose: Whether to show detailed information Returns: str: Formatted report """ if not conflicts: return "✓ No conflicts found in Vorgaben" lines = [f"Found {len(conflicts)} conflicts:"] for i, conflict in enumerate(conflicts, 1): lines.append(f"\n{i}. {conflict['message']}") if verbose: v1 = conflict['vorgabe1'] v2 = conflict['vorgabe2'] lines.append(f" Vorgabe 1: {v1.Vorgabennummer()}") lines.append(f" Valid from: {v1.gueltigkeit_von} to {v1.gueltigkeit_bis or 'unlimited'}") lines.append(f" Title: {v1.titel}") lines.append(f" Vorgabe 2: {v2.Vorgabennummer()}") lines.append(f" Valid from: {v2.gueltigkeit_von} to {v2.gueltigkeit_bis or 'unlimited'}") lines.append(f" Title: {v2.titel}") # Show the overlapping period v1 = conflict['vorgabe1'] v2 = conflict['vorgabe2'] overlap_start = max(v1.gueltigkeit_von, v2.gueltigkeit_von) overlap_end = min( v1.gueltigkeit_bis or datetime.date.max, v2.gueltigkeit_bis or datetime.date.max ) if overlap_end != datetime.date.max: lines.append(f" Overlap: {overlap_start} to {overlap_end}") else: lines.append(f" Overlap starts: {overlap_start} (no end)") return "\n".join(lines) # XML Export utilities def parse_markdown_table(markdown_content): """ Parse markdown table content and return XML element with
structure """ lines = [line.strip() for line in markdown_content.strip().split('\n') if line.strip()] if not lines: return None # Create table element table = ET.Element('table') # Parse first row as header header_row = [cell.strip() for cell in lines[0].split('|') if cell.strip()] header = ET.SubElement(table, 'header') for cell in header_row: column = ET.SubElement(header, 'column') column.text = cell # Parse remaining rows (skip separator row if it exists) for line in lines[2:] if len(lines) > 1 and all(c in '-| ' for c in lines[1]) else lines[1:]: # Check if this is a separator row if all(c in '-| ' for c in line): continue row = ET.SubElement(table, 'row') row_cells = [cell.strip() for cell in line.split('|') if cell.strip()] for cell in row_cells: column = ET.SubElement(row, 'column') column.text = cell return table def prettify_xml(xml_string): """ Prettify XML string with proper indentation """ dom = xml.dom.minidom.parseString(xml_string) return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8') def build_dokument_xml_element(dokument, parent_element): """ Build XML element for a single Dokument and append it to parent_element. Args: dokument: Dokument instance (should be prefetched with related data) parent_element: Parent XML element to append to Returns: The created document element """ doc_element = ET.SubElement(parent_element, 'Vorgabendokument') ET.SubElement(doc_element, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else "" ET.SubElement(doc_element, 'Nummer').text = dokument.nummer ET.SubElement(doc_element, 'Name').text = dokument.name autoren_element = ET.SubElement(doc_element, 'Autoren') for autor in dokument.autoren.all(): ET.SubElement(autoren_element, 'Autor').text = autor.name pruefende_element = ET.SubElement(doc_element, 'Pruefende') for pruefender in dokument.pruefende.all(): ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name gueltigkeit_element = ET.SubElement(doc_element, 'Gueltigkeit') ET.SubElement(gueltigkeit_element, 'Von').text = dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else "" ET.SubElement(gueltigkeit_element, 'Bis').text = dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None ET.SubElement(doc_element, 'SignaturCSO').text = dokument.signatur_cso geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order') if geltungsbereich_sections: geltungsbereich_element = ET.SubElement(doc_element, 'Geltungsbereich') for gb in geltungsbereich_sections: section_type = gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text" if section_type in ('tabelle', 'table'): table = parse_markdown_table(gb.inhalt) if table is not None: abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt') abschnitt_element.set('typ', section_type) abschnitt_element.append(table) else: abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt') abschnitt_element.set('typ', section_type) abschnitt_element.text = gb.inhalt einleitung_sections = dokument.einleitung_set.all().order_by('order') if einleitung_sections: einleitung_element = ET.SubElement(doc_element, 'Einleitung') for ei in einleitung_sections: section_type = ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text" if section_type in ('tabelle', 'table'): table = parse_markdown_table(ei.inhalt) if table is not None: abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt') abschnitt_element.set('typ', section_type) abschnitt_element.append(table) else: abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt') abschnitt_element.set('typ', section_type) abschnitt_element.text = ei.inhalt ET.SubElement(doc_element, 'Ziel').text = "" ET.SubElement(doc_element, 'Grundlagen').text = "" changelog_element = ET.SubElement(doc_element, 'Changelog') for cl in dokument.changelog.all().order_by('-datum'): entry = ET.SubElement(changelog_element, 'Eintrag') ET.SubElement(entry, 'Datum').text = cl.datum.strftime("%Y-%m-%d") autoren = ET.SubElement(entry, 'Autoren') for autor in cl.autoren.all(): ET.SubElement(autoren, 'Autor').text = autor.name ET.SubElement(entry, 'Aenderung').text = cl.aenderung anhaenge_element = ET.SubElement(doc_element, 'Anhaenge') ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge ET.SubElement(doc_element, 'Verantwortlich').text = "Information Security Management BIT" ET.SubElement(doc_element, 'Klassifizierung').text = "" glossar_element = ET.SubElement(doc_element, 'Glossar') vorgaben_element = ET.SubElement(doc_element, 'Vorgaben') for vorgabe in dokument.vorgaben.all().order_by('order'): vorgabe_el = ET.SubElement(vorgaben_element, 'Vorgabe') ET.SubElement(vorgabe_el, 'Nummer').text = str(vorgabe.nummer) ET.SubElement(vorgabe_el, 'Titel').text = vorgabe.titel ET.SubElement(vorgabe_el, 'Thema').text = vorgabe.thema.name if vorgabe.thema else "" kurztext_sections = vorgabe.vorgabekurztext_set.all().order_by('order') if kurztext_sections: kurztext_element = ET.SubElement(vorgabe_el, 'Kurztext') for kt in kurztext_sections: section_type = kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text" if section_type in ('tabelle', 'table'): table = parse_markdown_table(kt.inhalt) if table is not None: abschnitt = ET.SubElement(kurztext_element, 'Abschnitt') abschnitt.set('typ', section_type) abschnitt.append(table) else: abschnitt = ET.SubElement(kurztext_element, 'Abschnitt') abschnitt.set('typ', section_type) abschnitt.text = kt.inhalt langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order') if langtext_sections: langtext_element = ET.SubElement(vorgabe_el, 'Langtext') for lt in langtext_sections: section_type = lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text" if section_type in ('tabelle', 'table'): table = parse_markdown_table(lt.inhalt) if table is not None: abschnitt = ET.SubElement(langtext_element, 'Abschnitt') abschnitt.set('typ', section_type) abschnitt.append(table) else: abschnitt = ET.SubElement(langtext_element, 'Abschnitt') abschnitt.set('typ', section_type) abschnitt.text = lt.inhalt referenz_element = ET.SubElement(vorgabe_el, 'Referenzen') for ref in vorgabe.referenzen.all(): ref_text = f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer ET.SubElement(referenz_element, 'Referenz').text = ref_text vorgabe_gueltigkeit = ET.SubElement(vorgabe_el, 'Gueltigkeit') ET.SubElement(vorgabe_gueltigkeit, 'Von').text = vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else "" ET.SubElement(vorgabe_gueltigkeit, 'Bis').text = vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None checklistenfragen_element = ET.SubElement(vorgabe_el, 'Checklistenfragen') for cf in vorgabe.checklistenfragen.all(): ET.SubElement(checklistenfragen_element, 'Frage').text = cf.frage stichworte_element = ET.SubElement(vorgabe_el, 'Stichworte') for stw in vorgabe.stichworte.all(): ET.SubElement(stichworte_element, 'Stichwort').text = stw.stichwort return doc_element