Files
vgui-cicd/dokumente/docx_utils.py

208 lines
7.1 KiB
Python

"""
Utility functions for exporting Dokument instances as Word (.docx) files.
"""
import os
from docx import Document
from docx.shared import Pt
from docx.oxml.ns import qn
TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docx_template', 'template.docx')
def build_standard_docx(dokument):
"""
Generate a Word Document object for the given Dokument.
Opens the template .docx to inherit custom styles (Hermes, Kurztext,
Tabelle, etc.), clears the body, then writes the document structure.
Args:
dokument: a Dokument instance (should be prefetched with related data)
Returns:
docx.Document instance ready to be saved
"""
doc = Document(TEMPLATE_PATH)
# Clear all body content while preserving page-layout section properties
body = doc.element.body
for element in list(body):
if element.tag != qn('w:sectPr'):
body.remove(element)
# 1. Title
title_para = doc.add_paragraph(style='Normal')
run = title_para.add_run(f'Standard\nIT-Sicherheit-{dokument.name}')
run.bold = True
run.font.size = Pt(22)
# 2. Metadata table (uses the "Hermes" table style from the template)
meta_table = doc.add_table(rows=0, cols=2, style='Hermes')
meta_rows = [
('Identifikation:', dokument.nummer),
('Dokumentationsklasse:', dokument.dokumententyp.name if dokument.dokumententyp else ''),
('Gültig ab:', dokument.gueltigkeit_von.strftime('%d.%m.%Y') if dokument.gueltigkeit_von else ''),
('Gültig bis:', dokument.gueltigkeit_bis.strftime('%d.%m.%Y') if dokument.gueltigkeit_bis else ''),
('Klassifizierung:', ''),
('Verantwortliche Stelle:', 'Information Security Management BIT'),
('Autoren:', ', '.join(a.name for a in dokument.autoren.all())),
('Prüfende:', ', '.join(p.name for p in dokument.pruefende.all())),
]
for label, value in meta_rows:
row = meta_table.add_row()
row.cells[0].text = label
row.cells[1].text = value
# 3. Einleitung
doc.add_heading('Einleitung', level=1)
for abschnitt in dokument.einleitung_set.order_by('order'):
_add_abschnitt(doc, abschnitt)
# 4. Geltungsbereich
doc.add_heading('Geltungsbereich', level=1)
for abschnitt in dokument.geltungsbereich_set.order_by('order'):
_add_abschnitt(doc, abschnitt)
# 5. Vorgaben
doc.add_heading('Vorgaben', level=1)
vorgaben = list(
dokument.vorgaben
.order_by('thema__name', 'nummer')
.select_related('thema')
.prefetch_related(
'vorgabekurztext_set__abschnitttyp',
'vorgabelangtext_set__abschnitttyp',
'checklistenfragen',
'referenzen',
)
)
current_thema = None
for vorgabe in vorgaben:
thema_name = vorgabe.thema.name if vorgabe.thema else ''
if thema_name != current_thema:
current_thema = thema_name
doc.add_heading(thema_name, level=2)
doc.add_heading(f'{vorgabe.Vorgabennummer()} \u2013 {vorgabe.titel}', level=4)
for kt in vorgabe.vorgabekurztext_set.order_by('order'):
_add_abschnitt(doc, kt, default_style='Kurztext')
for lt in vorgabe.vorgabelangtext_set.order_by('order'):
_add_abschnitt(doc, lt)
fragen = list(vorgabe.checklistenfragen.all())
if fragen:
doc.add_paragraph('Checklistenfragen', style='Normal')
for frage in fragen:
doc.add_paragraph(frage.frage, style='Normal')
refs = list(vorgabe.referenzen.all())
if refs:
doc.add_paragraph('Referenzen: ' + ', '.join(r.Path() for r in refs), style='Normal')
# 6. Checkliste
doc.add_heading('Checkliste', level=1)
all_fragen = [
(vorgabe.Vorgabennummer(), frage.frage)
for vorgabe in vorgaben
for frage in vorgabe.checklistenfragen.all()
]
if all_fragen:
checklist_table = doc.add_table(rows=1, cols=3, style='Normal Table')
header = checklist_table.rows[0].cells
header[0].text = '#'
header[1].text = 'Bezeichnung (WAS)'
header[2].text = 'Richtlinieregel (WIE)'
for vorgabe_num, frage_text in all_fragen:
row = checklist_table.add_row()
row.cells[0].text = vorgabe_num
row.cells[1].text = ''
row.cells[2].text = frage_text
# 7. Changelog
changelog_entries = list(
dokument.changelog.order_by('-datum').prefetch_related('autoren')
)
if changelog_entries:
doc.add_heading('Änderungskontrolle', level=1)
changelog_table = doc.add_table(rows=1, cols=4, style='Tabelle')
header = changelog_table.rows[0].cells
header[0].text = 'Wann'
header[1].text = 'Version'
header[2].text = 'Wer'
header[3].text = 'Beschreibung'
for cl in changelog_entries:
row = changelog_table.add_row()
row.cells[0].text = cl.datum.strftime('%d.%m.%Y') if cl.datum else ''
row.cells[1].text = ''
row.cells[2].text = ', '.join(a.name for a in cl.autoren.all())
row.cells[3].text = cl.aenderung
return doc
def _add_abschnitt(doc, abschnitt, default_style='Normal'):
"""
Add a single Textabschnitt to the document, respecting its section type.
"""
typ = abschnitt.abschnitttyp.abschnitttyp if abschnitt.abschnitttyp else 'text'
inhalt = abschnitt.inhalt or ''
if not inhalt.strip():
return
if typ == 'text':
doc.add_paragraph(inhalt, style=default_style)
elif typ in ('liste ungeordnet', 'liste geordnet'):
for line in inhalt.strip().split('\n'):
line = line.strip().lstrip('- ').lstrip('* ')
if line:
doc.add_paragraph(line, style='Normal')
elif typ == 'tabelle':
_add_markdown_table(doc, inhalt)
else:
doc.add_paragraph(inhalt, style='Normal')
def _add_markdown_table(doc, markdown_content):
"""
Parse a markdown table and add it to the document as a Word table.
Falls back to a plain paragraph if parsing fails.
"""
lines = [line.strip() for line in markdown_content.strip().split('\n') if line.strip()]
if len(lines) < 2:
doc.add_paragraph(markdown_content, style='Normal')
return
header_cells = [c.strip() for c in lines[0].split('|') if c.strip()]
if not header_cells:
doc.add_paragraph(markdown_content, style='Normal')
return
# Skip the separator row (dashes), collect data rows
data_lines = [
line for line in lines[2:]
if not all(c in '-|: ' for c in line)
]
table = doc.add_table(rows=1, cols=len(header_cells), style='Table Grid')
header_row = table.rows[0].cells
for i, cell_text in enumerate(header_cells):
if i < len(header_row):
header_row[i].text = cell_text
for line in data_lines:
row_cells = [c.strip() for c in line.split('|') if c.strip()]
row = table.add_row()
for i, cell_text in enumerate(row_cells):
if i < len(row.cells):
row.cells[i].text = cell_text