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