Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 56s
312 lines
12 KiB
Python
312 lines
12 KiB
Python
"""
|
|
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 <table><header><row><column> 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 |