Compare commits

...

5 Commits

Author SHA1 Message Date
6c2c76859b Version push for django vulnerabilities
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 4m12s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
2026-03-09 10:33:38 +01:00
44de29a9da DocX button added to standards
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 27s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 6s
2026-02-20 16:40:18 +01:00
e39a65d5b2 LibXSLT added to Dockerfile
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 57s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 9s
2026-02-20 16:30:00 +01:00
137ed9d1a0 Deploy docx
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 7m7s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 11s
2026-02-20 16:04:11 +01:00
3faa88fcea DocX-export added - not perfect yet. 2026-02-20 15:59:51 +01:00
11 changed files with 315 additions and 7 deletions

View File

@@ -12,7 +12,11 @@ RUN pip install --no-cache-dir -r requirements.txt && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
FROM python:3.15.0a5-slim-trixie FROM python:3.15.0a5-slim-trixie
RUN useradd -m -r -u 99 appuser && \ RUN apt-get update && \
apt-get install -y --no-install-recommends libxslt1.1 libxml2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
useradd -m -r -u 99 appuser && \
mkdir /app && \ mkdir /app && \
chown -R appuser /app chown -R appuser /app

View File

@@ -18,7 +18,7 @@ data:
MEDIA_URL: "/media/" MEDIA_URL: "/media/"
# Application Version # Application Version
VERSION: "0.990" VERSION: "0.992"
# Database Configuration (for future use) # Database Configuration (for future use)
# DATABASE_ENGINE: "django.db.backends.sqlite3" # DATABASE_ENGINE: "django.db.backends.sqlite3"

View File

@@ -18,7 +18,7 @@ spec:
fsGroupChangePolicy: "OnRootMismatch" fsGroupChangePolicy: "OnRootMismatch"
initContainers: initContainers:
- name: loader - name: loader
image: git.baumann.gr/adebaumann/vui-data-loader:0.11 image: git.baumann.gr/adebaumann/vui-data-loader:0.12
securityContext: securityContext:
runAsUser: 99 runAsUser: 99
command: [ "sh","-c","if [ ! -f /data/db.sqlite3 ] || [ ! -s /data/db.sqlite3 ]; then cp preload/preload.sqlite3 /data/db.sqlite3 && echo 'Database copied from preload'; else echo 'Existing database preserved'; fi" ] command: [ "sh","-c","if [ ! -f /data/db.sqlite3 ] || [ ! -s /data/db.sqlite3 ]; then cp preload/preload.sqlite3 /data/db.sqlite3 && echo 'Database copied from preload'; else echo 'Existing database preserved'; fi" ]
@@ -27,7 +27,7 @@ spec:
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/vui:0.990 image: git.baumann.gr/adebaumann/vui:0.993
imagePullPolicy: Always imagePullPolicy: Always
securityContext: securityContext:
runAsUser: 99 runAsUser: 99

View File

@@ -1,5 +1,6 @@
FROM alpine:3.22.1 FROM alpine:3.22
RUN addgroup -S appuser && \ RUN apk upgrade --no-cache && \
addgroup -S appuser && \
adduser -S appuser -G appuser && \ adduser -S appuser -G appuser && \
mkdir /preload && \ mkdir /preload && \
chown -R appuser:appuser /preload chown -R appuser:appuser /preload

Binary file not shown.

207
dokumente/docx_utils.py Normal file
View File

@@ -0,0 +1,207 @@
"""
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

View File

@@ -0,0 +1,55 @@
from django.core.management.base import BaseCommand, CommandError
from dokumente.models import Dokument
from dokumente.docx_utils import build_standard_docx
import os
class Command(BaseCommand):
help = 'Export a Dokument (or all active Dokumente) as Word (.docx) files'
def add_arguments(self, parser):
parser.add_argument(
'--nummer',
type=str,
help='Document number to export (e.g. R0129). Omit to export all active documents.',
)
parser.add_argument(
'--output',
type=str,
default='.',
help='Output directory (default: current directory)',
)
def handle(self, *args, **options):
output_dir = options['output']
os.makedirs(output_dir, exist_ok=True)
if options['nummer']:
try:
dokumente = [Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
'vorgaben__vorgabekurztext_set__abschnitttyp',
'vorgaben__vorgabelangtext_set__abschnitttyp',
'geltungsbereich_set__abschnitttyp',
'einleitung_set__abschnitttyp',
'changelog__autoren',
).get(nummer=options['nummer'])]
except Dokument.DoesNotExist:
raise CommandError(f"Dokument with nummer '{options['nummer']}' not found.")
else:
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
'vorgaben__vorgabekurztext_set__abschnitttyp',
'vorgaben__vorgabelangtext_set__abschnitttyp',
'geltungsbereich_set__abschnitttyp',
'einleitung_set__abschnitttyp',
'changelog__autoren',
).order_by('nummer')
for dokument in dokumente:
doc = build_standard_docx(dokument)
filename = os.path.join(output_dir, f'{dokument.nummer}.docx')
doc.save(filename)
self.stdout.write(self.style.SUCCESS(f'Exported {dokument.nummer}{filename}'))

View File

@@ -214,6 +214,11 @@
download="{{ standard.nummer }}.xml"> download="{{ standard.nummer }}.xml">
XML herunterladen XML herunterladen
</a> </a>
<a href="{% url 'standard_docx' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.docx">
DOCX herunterladen (Prototyp)
</a>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -12,6 +12,7 @@ urlpatterns = [
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'), path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
path('<str:nummer>/json/', views.standard_json, name='standard_json'), path('<str:nummer>/json/', views.standard_json, name='standard_json'),
path('<str:nummer>/xml/', views.standard_xml, name='standard_xml'), path('<str:nummer>/xml/', views.standard_xml, name='standard_xml'),
path('<str:nummer>/docx/', views.standard_docx, name='standard_docx'),
path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'), path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'), path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'), path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),

View File

@@ -6,10 +6,12 @@ from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils.html import escape, mark_safe from django.utils.html import escape, mark_safe
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
import io
import json import json
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from .utils import build_dokument_xml_element, prettify_xml from .utils import build_dokument_xml_element, prettify_xml
from .docx_utils import build_standard_docx
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
from datetime import date from datetime import date
@@ -256,6 +258,37 @@ def standard_json(request, nummer):
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder) return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
def standard_docx(request, nummer):
"""
Export a single Dokument as a Word (.docx) file.
"""
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
'vorgaben__vorgabekurztext_set__abschnitttyp',
'vorgaben__vorgabelangtext_set__abschnitttyp',
'geltungsbereich_set__abschnitttyp',
'einleitung_set__abschnitttyp',
'changelog__autoren',
),
nummer=nummer,
)
doc = build_standard_docx(dokument)
buffer = io.BytesIO()
doc.save(buffer)
buffer.seek(0)
response = HttpResponse(
buffer.read(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
)
response['Content-Disposition'] = f'attachment; filename="{dokument.nummer}.docx"'
return response
def standard_xml(request, nummer): def standard_xml(request, nummer):
""" """
Export a single Dokument as XML Export a single Dokument as XML

View File

@@ -7,7 +7,7 @@ charset-normalizer==3.4.4
coverage==7.13.1 coverage==7.13.1
curtsies==0.4.3 curtsies==0.4.3
cwcwidth==0.1.12 cwcwidth==0.1.12
Django==6.0.1 Django==6.0.3
django-admin-sortable2==2.3 django-admin-sortable2==2.3
django-js-asset==3.1.2 django-js-asset==3.1.2
django-mptt==0.18.0 django-mptt==0.18.0
@@ -19,6 +19,7 @@ greenlet==3.3.0
gunicorn==23.0.0 gunicorn==23.0.0
idna==3.11 idna==3.11
jedi==0.19.2 jedi==0.19.2
lxml==6.0.2
jproperties==2.1.2 jproperties==2.1.2
Markdown==3.10 Markdown==3.10
packaging==25.0 packaging==25.0
@@ -30,6 +31,7 @@ pyfakefs==5.9.3
Pygments==2.19.2 Pygments==2.19.2
pysonar==1.2.1.3951 pysonar==1.2.1.3951
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-docx==1.2.0
python-monkey-business==1.1.0 python-monkey-business==1.1.0
pyxdg==0.28 pyxdg==0.28
PyYAML==6.0.3 PyYAML==6.0.3