"""
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