Compare commits

..

13 Commits

Author SHA1 Message Date
b7f50ce30f XML export added
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 1m5s
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 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Failing after 8s
2026-01-07 14:43:59 +01:00
d3d0298ad1 scripts added and Dockerfile adjusted to remove them from production 2026-01-07 14:31:54 +01:00
29c1ad1dcf Tests for document import added
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 4m14s
2025-12-09 15:27:49 +01:00
4504e8a2a5 Database updated with collaborators
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 53s
2025-12-08 16:26:25 +01:00
502dd85efb Document import management command updated to work with changed structure 2025-12-08 16:26:03 +01:00
9e6e2b5a03 Workflow: Changed description (old vs new was obsolete)
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 4s
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 4s
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 16:01:07 +01:00
c492b7bda6 New Preload-DB-image
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 4s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 15:59:13 +01:00
0d7e63d3a2 Changed "Historische Version..." to "Zukünftige Version..." when appropriate
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 16s
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 4s
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 15:34:14 +01:00
0866e604bc Documentation change regarding diagram cache
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 15:01:14 +01:00
753c00bc45 Validation for 'Thema' on Vorgabe added
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 14s
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 4s
SonarQube Scan / SonarQube Trigger (push) Successful in 54s
2025-12-08 14:41:02 +01:00
74d2f15d6a Package versions lifted for urllib and django due to trivy scan
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 39s
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 4s
SonarQube Scan / SonarQube Trigger (push) Successful in 59s
2025-12-08 14:23:37 +01:00
615908e569 Password miminum length: 12
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 20s
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 4s
SonarQube Scan / SonarQube Trigger (push) Successful in 56s
2025-12-08 12:34:09 +01:00
55d467ee58 Deployment 963
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 16s
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 4s
SonarQube Scan / SonarQube Trigger (push) Successful in 56s
2025-12-04 14:28:40 +01:00
22 changed files with 1967 additions and 23 deletions

View File

@@ -60,7 +60,7 @@ jobs:
chmod +x /usr/local/bin/yq chmod +x /usr/local/bin/yq
yq --version yq --version
- name: Read ${{ matrix.description }} image from deployment (old vs new) - name: Read ${{ matrix.description }} image from deployment
id: img id: img
shell: bash shell: bash
run: | run: |

View File

@@ -30,6 +30,7 @@ RUN rm -rvf /app/Dockerfile* \
/app/requirements.txt \ /app/requirements.txt \
/app/node_modules \ /app/node_modules \
/app/*.json \ /app/*.json \
/app/scripts
/app/test_*.py && \ /app/test_*.py && \
python3 /app/manage.py collectstatic --noinput python3 /app/manage.py collectstatic --noinput
CMD ["gunicorn","--bind","0.0.0.0:8000","--workers","3","VorgabenUI.wsgi:application"] CMD ["gunicorn","--bind","0.0.0.0:8000","--workers","3","VorgabenUI.wsgi:application"]

View File

@@ -540,5 +540,5 @@ digraph {
Bei Fragen oder Problemen mit Diagrammen: Bei Fragen oder Problemen mit Diagrammen:
1. Code auf https://kroki.io/ testen 1. Code auf https://kroki.io/ testen
2. Syntax-Dokumentation des jeweiligen Diagrammtyps konsultieren 2. Syntax-Dokumentation des jeweiligen Diagrammtyps konsultieren
3. Diagramm-Cache leeren: `python manage.py clear_diagram_cache` 3. (Nur mit Shell-Zugriff auf Kubernetes-Pod möglich): Diagramm-Cache leeren: `python manage.py clear_diagram_cache`
4. Bei technischen Problemen: Information Security Management BIT kontaktieren 4. Bei technischen Problemen: Information Security Management BIT kontaktieren

View File

@@ -151,6 +151,24 @@ LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = 'login' LOGOUT_REDIRECT_URL = 'login'
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {
"min_length": 12,
},
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
#LOGGING = { #LOGGING = {
# "version": 1, # "version": 1,
# "handlers" :{ # "handlers" :{

View File

@@ -18,14 +18,14 @@ spec:
fsGroupChangePolicy: "OnRootMismatch" fsGroupChangePolicy: "OnRootMismatch"
initContainers: initContainers:
- name: loader - name: loader
image: git.baumann.gr/adebaumann/vui-data-loader:0.10 image: git.baumann.gr/adebaumann/vui-data-loader:0.11
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ] command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
volumeMounts: volumeMounts:
- name: data - name: data
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/vui:0.968 image: git.baumann.gr/adebaumann/vui:0.974
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000

Binary file not shown.

Binary file not shown.

View File

@@ -94,9 +94,17 @@ class EinleitungInline(NestedStackedInline):
class VorgabeForm(forms.ModelForm): class VorgabeForm(forms.ModelForm):
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False) referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
class Meta: class Meta:
model = Vorgabe model = Vorgabe
fields = '__all__' fields = '__all__'
def clean_thema(self):
"""Validate that thema is provided."""
thema = self.cleaned_data.get('thema')
if not thema:
raise forms.ValidationError('Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.')
return thema
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline): class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline):
model = Vorgabe model = Vorgabe

View File

@@ -0,0 +1,144 @@
from django.core.management.base import BaseCommand
import xml.etree.ElementTree as ET
from datetime import datetime
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
class Command(BaseCommand):
help = 'Export all dokumente as XML'
def add_arguments(self, parser):
parser.add_argument(
'--output',
type=str,
help='Output file path (default: stdout)',
)
def handle(self, *args, **options):
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
).order_by('nummer')
root = ET.Element('Vorgabendokumente')
for dokument in dokumente:
doc_element = ET.SubElement(root, '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')
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
for gb in geltungsbereich_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text")
section.text = gb.inhalt
einleitung_sections = dokument.einleitung_set.all().order_by('order')
if einleitung_sections:
einleitung_element = ET.SubElement(doc_element, 'Einleitung')
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
for ei in einleitung_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text")
section.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')
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
for kt in kurztext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text")
teil.text = kt.inhalt
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
if langtext_sections:
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
for lt in langtext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text")
teil.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
xml_str = ET.tostring(root, encoding='unicode', method='xml')
xml_output = self._prettify_xml(xml_str)
if options['output']:
with open(options['output'], 'w', encoding='utf-8') as f:
f.write(xml_output)
self.stdout.write(self.style.SUCCESS(f'XML exported to {options["output"]}'))
else:
self.stdout.write(xml_output)
def _prettify_xml(self, xml_string):
import xml.dom.minidom
dom = xml.dom.minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')

View File

@@ -71,6 +71,7 @@ class Command(BaseCommand):
"name": name, "name": name,
"gueltigkeit_von": options["gueltigkeit_von"], "gueltigkeit_von": options["gueltigkeit_von"],
"gueltigkeit_bis": options["gueltigkeit_bis"], "gueltigkeit_bis": options["gueltigkeit_bis"],
"aktiv":False,
}, },
) )
if created: if created:
@@ -319,6 +320,7 @@ class Command(BaseCommand):
thema=thema, thema=thema,
titel=v["titel"], titel=v["titel"],
gueltigkeit_von=timezone.now().date(), gueltigkeit_von=timezone.now().date(),
order=0,
) )
# Stichworte # Stichworte

View File

@@ -169,6 +169,12 @@ class Vorgabe(models.Model):
""" """
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
# Check that thema is provided
if not self.thema_id:
raise ValidationError({
'thema': 'Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.'
})
# Check for conflicts with existing Vorgaben # Check for conflicts with existing Vorgaben
conflicts = self.find_conflicts() conflicts = self.find_conflicts()
if conflicts: if conflicts:

View File

@@ -16,7 +16,11 @@
{% if standard.history == True %} {% if standard.history == True %}
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
{% if standard.is_future %}
<strong>Zukünftige Version vom {{ standard.check_date }}</strong>
{% else %}
<strong>Historische Version vom {{ standard.check_date }}</strong> <strong>Historische Version vom {{ standard.check_date }}</strong>
{% endif %}
</div> </div>
{% endif %} {% endif %}
@@ -189,23 +193,28 @@
<h2>Metadaten</h2> <h2>Metadaten</h2>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-12"> <div class="col-md-12">
<dl class="row"> <dl class="row">
<dt class="col-sm-3">Autoren:</dt> <dt class="col-sm-3">Autoren:</dt>
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd> <dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
<dt class="col-sm-3">Prüfende:</dt> <dt class="col-sm-3">Prüfende:</dt>
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd> <dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
<dt class="col-sm-3">Gültigkeit:</dt> <dt class="col-sm-3">Gültigkeit:</dt>
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd> <dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
</dl> </dl>
<p> <p>
<a href="{% url 'standard_json' standard.nummer %}" <a href="{% url 'standard_json' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download" class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.json"> download="{{ standard.nummer }}.json">
JSON herunterladen JSON herunterladen
</a> </a>
</p> <a href="{% url 'standard_xml' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.xml">
XML herunterladen
</a>
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,960 @@
"""
Tests for the import-document management command.
This test suite covers:
- Basic import functionality
- Dry-run mode
- Purge functionality
- Error handling (missing file, dokumententyp, thema, abschnitttyp)
- Context switching (einleitung → geltungsbereich → vorgabe)
- Header normalization
- Vorgaben with Kurztext, Langtext, Stichworte, Checklistenfragen
- Edge cases and malformed input
"""
import os
import tempfile
from io import StringIO
from pathlib import Path
from django.test import TestCase
from django.core.management import call_command
from django.core.management.base import CommandError
from dokumente.models import (
Dokumententyp,
Dokument,
Thema,
Vorgabe,
VorgabeKurztext,
VorgabeLangtext,
Geltungsbereich,
Einleitung,
Checklistenfrage,
)
from abschnitte.models import AbschnittTyp
from stichworte.models import Stichwort
class ImportDocumentCommandTestCase(TestCase):
"""Test cases for the import-document management command"""
def setUp(self):
"""Set up test fixtures"""
# Create required Dokumententyp
self.dokumententyp = Dokumententyp.objects.create(
name="IT-Sicherheit",
verantwortliche_ve="TEST-VE"
)
# Create required AbschnittTyp instances
self.text_typ = AbschnittTyp.objects.create(abschnitttyp="text")
self.liste_geordnet_typ = AbschnittTyp.objects.create(
abschnitttyp="liste geordnet"
)
self.liste_ungeordnet_typ = AbschnittTyp.objects.create(
abschnitttyp="liste ungeordnet"
)
# Create test Themen
self.thema_organisation = Thema.objects.create(
name="Organisation",
erklaerung="Organisatorische Anforderungen"
)
self.thema_technik = Thema.objects.create(
name="Technik",
erklaerung="Technische Anforderungen"
)
# Additional Themen for r009.txt example
self.thema_informationen = Thema.objects.create(
name="Informationen",
erklaerung="Informationssicherheit"
)
self.thema_systeme = Thema.objects.create(
name="Systeme",
erklaerung="Systemanforderungen"
)
self.thema_anwendungen = Thema.objects.create(
name="Anwendungen",
erklaerung="Anwendungsanforderungen"
)
self.thema_zonen = Thema.objects.create(
name="Zonen",
erklaerung="Zonenanforderungen"
)
def create_test_file(self, content):
"""Helper to create a temporary test file with given content"""
fd, path = tempfile.mkstemp(suffix=".txt", text=True)
with os.fdopen(fd, 'w', encoding='utf-8') as f:
f.write(content)
return path
def test_basic_import_creates_document(self):
"""Test that basic import creates a document"""
test_content = """>>>Einleitung
>>>text
This is the introduction.
>>>geltungsbereich
>>>text
This is the scope.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Test Requirement
>>>Kurztext
>>>Text
Short description.
>>>Langtext
>>>Text
Long description.
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-001',
'--name', 'Test Document',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
# Check document was created
dokument = Dokument.objects.get(nummer='TEST-001')
self.assertEqual(dokument.name, 'Test Document')
self.assertEqual(dokument.dokumententyp, self.dokumententyp)
# Check output message
output = out.getvalue()
self.assertIn('Created Document TEST-001', output)
self.assertIn('Imported document TEST-001', output)
finally:
os.unlink(test_file)
def test_import_creates_einleitung(self):
"""Test that Einleitung sections are created"""
test_content = """>>>Einleitung
>>>text
This is the introduction text.
>>>geltungsbereich
>>>text
Scope text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-002',
'--name', 'Test Document 2',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-002')
einleitung = Einleitung.objects.filter(einleitung=dokument)
self.assertEqual(einleitung.count(), 1)
self.assertEqual(einleitung.first().inhalt, 'This is the introduction text.')
self.assertEqual(einleitung.first().abschnitttyp, self.text_typ)
finally:
os.unlink(test_file)
def test_import_creates_geltungsbereich(self):
"""Test that Geltungsbereich sections are created"""
test_content = """>>>geltungsbereich
>>>text
This standard applies to all servers.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-003',
'--name', 'Test Document 3',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-003')
geltungsbereich = Geltungsbereich.objects.filter(geltungsbereich=dokument)
self.assertEqual(geltungsbereich.count(), 1)
self.assertEqual(
geltungsbereich.first().inhalt,
'This standard applies to all servers.'
)
self.assertEqual(geltungsbereich.first().abschnitttyp, self.text_typ)
finally:
os.unlink(test_file)
def test_import_creates_vorgabe_with_all_fields(self):
"""Test creating a Vorgabe with all fields"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Complete Requirement
>>>Kurztext
>>>Text
Short text here.
>>>Langtext
>>>Text
Long text here.
>>>Stichworte
Testing, Management, Security
>>>Checkliste
Is the requirement met?
Has documentation been provided?
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-004',
'--name', 'Test Document 4',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-004')
vorgabe = Vorgabe.objects.get(dokument=dokument, nummer=1)
# Check basic fields
self.assertEqual(vorgabe.titel, 'Complete Requirement')
self.assertEqual(vorgabe.thema, self.thema_organisation)
# Check Kurztext
kurztext = VorgabeKurztext.objects.filter(abschnitt=vorgabe)
self.assertEqual(kurztext.count(), 1)
self.assertEqual(kurztext.first().inhalt, 'Short text here.')
# Check Langtext
langtext = VorgabeLangtext.objects.filter(abschnitt=vorgabe)
self.assertEqual(langtext.count(), 1)
self.assertEqual(langtext.first().inhalt, 'Long text here.')
# Check Stichworte
stichworte = vorgabe.stichworte.all()
self.assertEqual(stichworte.count(), 3)
stichwort_names = {s.stichwort for s in stichworte}
self.assertEqual(stichwort_names, {'Testing', 'Management', 'Security'})
# Check Checklistenfragen
fragen = Checklistenfrage.objects.filter(vorgabe=vorgabe)
self.assertEqual(fragen.count(), 2)
frage_texts = {f.frage for f in fragen}
self.assertEqual(frage_texts, {
'Is the requirement met?',
'Has documentation been provided?'
})
finally:
os.unlink(test_file)
def test_import_multiple_vorgaben(self):
"""Test importing multiple Vorgaben"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
First Requirement
>>>Kurztext
>>>Text
First requirement text.
>>>Vorgabe Technik
>>>Nummer 2
>>>Titel
Second Requirement
>>>Kurztext
>>>Text
Second requirement text.
>>>Vorgabe Organisation
>>>Nummer 3
>>>Titel
Third Requirement
>>>Kurztext
>>>Text
Third requirement text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-005',
'--name', 'Test Document 5',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-005')
vorgaben = Vorgabe.objects.filter(dokument=dokument).order_by('nummer')
self.assertEqual(vorgaben.count(), 3)
self.assertEqual(vorgaben[0].nummer, 1)
self.assertEqual(vorgaben[0].thema, self.thema_organisation)
self.assertEqual(vorgaben[1].nummer, 2)
self.assertEqual(vorgaben[1].thema, self.thema_technik)
self.assertEqual(vorgaben[2].nummer, 3)
self.assertEqual(vorgaben[2].thema, self.thema_organisation)
finally:
os.unlink(test_file)
def test_dry_run_creates_no_data(self):
"""Test that dry-run mode creates no database records"""
test_content = """>>>Einleitung
>>>text
Introduction text.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Test Requirement
>>>Kurztext
>>>Text
Short text.
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-DRY',
'--name', 'Dry Run Test',
'--dokumententyp', 'IT-Sicherheit',
'--dry-run',
stdout=out
)
# Document is created (for counting purposes) but not saved
output = out.getvalue()
self.assertIn('Dry run: no database changes will be made', output)
self.assertIn('Dry run complete', output)
# Check that Einleitung and Vorgabe were NOT created
dokument = Dokument.objects.get(nummer='TEST-DRY')
self.assertEqual(Einleitung.objects.filter(einleitung=dokument).count(), 0)
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 0)
finally:
os.unlink(test_file)
def test_dry_run_verbose_shows_details(self):
"""Test that dry-run with verbose shows detailed output"""
test_content = """>>>Einleitung
>>>text
Introduction.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Test
>>>Kurztext
>>>Text
Short.
>>>Langtext
>>>Text
Long.
>>>Stichworte
Keyword1, Keyword2
>>>Checkliste
Question 1?
Question 2?
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-VERBOSE',
'--name', 'Verbose Test',
'--dokumententyp', 'IT-Sicherheit',
'--dry-run',
'--verbose',
stdout=out
)
output = out.getvalue()
self.assertIn('[DRY RUN] Einleitung Abschnitt', output)
self.assertIn('[DRY RUN] Would create Vorgabe 1', output)
self.assertIn('Stichworte: Keyword1, Keyword2', output)
self.assertIn('Checkliste: Question 1?', output)
self.assertIn('Checkliste: Question 2?', output)
self.assertIn('Kurztext', output)
self.assertIn('Langtext', output)
finally:
os.unlink(test_file)
def test_purge_deletes_existing_content(self):
"""Test that --purge deletes existing content before import"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
New Requirement
>>>Kurztext
>>>Text
New text.
"""
test_file = self.create_test_file(test_content)
try:
# First import
call_command(
'import-document',
test_file,
'--nummer', 'TEST-PURGE',
'--name', 'Purge Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-PURGE')
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 1)
# Second import with different content and --purge
test_content_2 = """>>>Vorgabe Technik
>>>Nummer 2
>>>Titel
Replacement Requirement
>>>Kurztext
>>>Text
Replacement text.
"""
test_file_2 = self.create_test_file(test_content_2)
try:
out = StringIO()
call_command(
'import-document',
test_file_2,
'--nummer', 'TEST-PURGE',
'--name', 'Purge Test',
'--dokumententyp', 'IT-Sicherheit',
'--purge',
stdout=out
)
# Old Vorgabe should be deleted, only new one exists
vorgaben = Vorgabe.objects.filter(dokument=dokument)
self.assertEqual(vorgaben.count(), 1)
self.assertEqual(vorgaben.first().nummer, 2)
self.assertEqual(vorgaben.first().thema, self.thema_technik)
output = out.getvalue()
self.assertIn('Purged', output)
finally:
os.unlink(test_file_2)
finally:
os.unlink(test_file)
def test_purge_dry_run_shows_what_would_be_deleted(self):
"""Test that --purge with --dry-run shows deletion counts"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Original
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
# First import to create data
call_command(
'import-document',
test_file,
'--nummer', 'TEST-PURGE-DRY',
'--name', 'Purge Dry Test',
'--dokumententyp', 'IT-Sicherheit'
)
# Dry run with purge
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-PURGE-DRY',
'--name', 'Purge Dry Test',
'--dokumententyp', 'IT-Sicherheit',
'--purge',
'--dry-run',
stdout=out
)
output = out.getvalue()
self.assertIn('[DRY RUN] Would purge:', output)
self.assertIn('1 Vorgaben', output)
finally:
os.unlink(test_file)
def test_header_normalization(self):
"""Test that headers with hyphens are normalized correctly"""
test_content = """>>>geltungsbereich
>>>Liste-ungeordnet
Item 1
Item 2
Item 3
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-NORM',
'--name', 'Normalization Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-NORM')
geltungsbereich = Geltungsbereich.objects.get(geltungsbereich=dokument)
# Should have normalized "Liste-ungeordnet" to "liste ungeordnet"
self.assertEqual(geltungsbereich.abschnitttyp, self.liste_ungeordnet_typ)
finally:
os.unlink(test_file)
def test_missing_file_raises_error(self):
"""Test that missing file raises CommandError"""
with self.assertRaises(CommandError) as cm:
call_command(
'import-document',
'/nonexistent/file.txt',
'--nummer', 'TEST-ERR',
'--name', 'Error Test',
'--dokumententyp', 'IT-Sicherheit'
)
self.assertIn('does not exist', str(cm.exception))
def test_missing_dokumententyp_raises_error(self):
"""Test that missing Dokumententyp raises CommandError"""
test_content = """>>>geltungsbereich
>>>text
Text.
"""
test_file = self.create_test_file(test_content)
try:
with self.assertRaises(CommandError) as cm:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-ERR',
'--name', 'Error Test',
'--dokumententyp', 'NonExistentType'
)
self.assertIn('does not exist', str(cm.exception))
finally:
os.unlink(test_file)
def test_missing_thema_skips_vorgabe(self):
"""Test that missing Thema causes Vorgabe to be skipped with warning"""
test_content = """>>>Vorgabe NonExistentThema
>>>Nummer 1
>>>Titel
Test
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-SKIP',
'--name', 'Skip Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='TEST-SKIP')
# Vorgabe should NOT be created
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 0)
output = out.getvalue()
self.assertIn('not found, skipping Vorgabe', output)
finally:
os.unlink(test_file)
def test_missing_abschnitttyp_defaults_to_text(self):
"""Test that missing AbschnittTyp defaults to 'text' with warning"""
# Delete all but text type
AbschnittTyp.objects.exclude(abschnitttyp='text').delete()
test_content = """>>>geltungsbereich
>>>liste geordnet
Item 1
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-DEFAULT',
'--name', 'Default Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='TEST-DEFAULT')
geltungsbereich = Geltungsbereich.objects.get(geltungsbereich=dokument)
# Should default to 'text' type
self.assertEqual(geltungsbereich.abschnitttyp.abschnitttyp, 'text')
output = out.getvalue()
self.assertIn("not found; defaulting to 'text'", output)
finally:
os.unlink(test_file)
def test_inline_titel(self):
"""Test that inline title (on same line as header) is parsed"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Inline Title Here
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-INLINE',
'--name', 'Inline Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-INLINE')
vorgabe = Vorgabe.objects.get(dokument=dokument)
self.assertEqual(vorgabe.titel, 'Inline Title Here')
finally:
os.unlink(test_file)
def test_inline_stichworte(self):
"""Test that inline Stichworte (on same line as header) are parsed"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Test
>>>Stichworte Security, Testing, Compliance
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-INLINE-STW',
'--name', 'Inline Stichwort Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-INLINE-STW')
vorgabe = Vorgabe.objects.get(dokument=dokument)
stichworte = {s.stichwort for s in vorgabe.stichworte.all()}
self.assertEqual(stichworte, {'Security', 'Testing', 'Compliance'})
finally:
os.unlink(test_file)
def test_gueltigkeit_dates(self):
"""Test that validity dates are set correctly"""
test_content = """>>>geltungsbereich
>>>text
Scope.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-DATES',
'--name', 'Date Test',
'--dokumententyp', 'IT-Sicherheit',
'--gueltigkeit_von', '2024-01-01',
'--gueltigkeit_bis', '2024-12-31'
)
dokument = Dokument.objects.get(nummer='TEST-DATES')
self.assertEqual(str(dokument.gueltigkeit_von), '2024-01-01')
self.assertEqual(str(dokument.gueltigkeit_bis), '2024-12-31')
finally:
os.unlink(test_file)
def test_existing_document_updates(self):
"""Test that importing to existing document number shows warning"""
test_content = """>>>geltungsbereich
>>>text
First version.
"""
test_file = self.create_test_file(test_content)
try:
# First import
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-EXISTS',
'--name', 'Existing Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
output1 = out.getvalue()
self.assertIn('Created Document TEST-EXISTS', output1)
# Second import with same number
out2 = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-EXISTS',
'--name', 'Existing Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out2
)
output2 = out2.getvalue()
self.assertIn('already exists', output2)
finally:
os.unlink(test_file)
def test_multiple_kurztext_sections(self):
"""Test Vorgabe with multiple Kurztext sections"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Multiple Sections
>>>Kurztext
>>>Text
First kurztext section.
>>>Liste ungeordnet
Item A
Item B
>>>Langtext
>>>Text
Langtext.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-MULTI',
'--name', 'Multi Section Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-MULTI')
vorgabe = Vorgabe.objects.get(dokument=dokument)
kurztext_sections = VorgabeKurztext.objects.filter(abschnitt=vorgabe).order_by('id')
self.assertEqual(kurztext_sections.count(), 2)
self.assertEqual(kurztext_sections[0].abschnitttyp.abschnitttyp, 'text')
self.assertEqual(kurztext_sections[1].abschnitttyp.abschnitttyp, 'liste ungeordnet')
finally:
os.unlink(test_file)
def test_empty_file(self):
"""Test importing an empty file"""
test_content = ""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-EMPTY',
'--name', 'Empty Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='TEST-EMPTY')
# Document created but no content
self.assertEqual(Einleitung.objects.filter(einleitung=dokument).count(), 0)
self.assertEqual(Geltungsbereich.objects.filter(geltungsbereich=dokument).count(), 0)
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 0)
output = out.getvalue()
self.assertIn('with 0 Vorgaben', output)
finally:
os.unlink(test_file)
def test_unicode_content(self):
"""Test that Unicode characters (German umlauts, etc.) are handled correctly"""
test_content = """>>>Einleitung
>>>text
Übersicht über die Sicherheitsanforderungen für IT-Systeme.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Überprüfung der Systemkonfiguration
>>>Kurztext
>>>Text
Die Konfiguration muss regelmäßig überprüft werden.
>>>Stichworte
Überprüfung, Sicherheit, Qualität
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-UNICODE',
'--name', 'Unicode Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-UNICODE')
# Check Einleitung
einleitung = Einleitung.objects.get(einleitung=dokument)
self.assertIn('Übersicht', einleitung.inhalt)
# Check Vorgabe
vorgabe = Vorgabe.objects.get(dokument=dokument)
self.assertEqual(vorgabe.titel, 'Überprüfung der Systemkonfiguration')
# Check Kurztext
kurztext = VorgabeKurztext.objects.get(abschnitt=vorgabe)
self.assertIn('regelmäßig', kurztext.inhalt)
# Check Stichworte
stichworte = {s.stichwort for s in vorgabe.stichworte.all()}
self.assertIn('Überprüfung', stichworte)
finally:
os.unlink(test_file)
def test_context_switching(self):
"""Test that context switches correctly between sections"""
test_content = """>>>Einleitung
>>>text
Intro text 1.
>>>text
Intro text 2.
>>>geltungsbereich
>>>text
Scope text 1.
>>>text
Scope text 2.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Test
>>>Kurztext
>>>text
Kurztext 1.
>>>text
Kurztext 2.
>>>Langtext
>>>text
Langtext 1.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-CONTEXT',
'--name', 'Context Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-CONTEXT')
# Check Einleitung has 2 sections
einleitung = Einleitung.objects.filter(einleitung=dokument)
self.assertEqual(einleitung.count(), 2)
# Check Geltungsbereich has 2 sections
geltungsbereich = Geltungsbereich.objects.filter(geltungsbereich=dokument)
self.assertEqual(geltungsbereich.count(), 2)
# Check Vorgabe has correct Kurztext and Langtext counts
vorgabe = Vorgabe.objects.get(dokument=dokument)
kurztext = VorgabeKurztext.objects.filter(abschnitt=vorgabe)
langtext = VorgabeLangtext.objects.filter(abschnitt=vorgabe)
self.assertEqual(kurztext.count(), 2)
self.assertEqual(langtext.count(), 1)
finally:
os.unlink(test_file)
def test_real_world_example(self):
"""Test importing the real r009.txt example document"""
# Use the actual example file
example_file = Path(__file__).parent.parent / 'Documentation' / 'import formats' / 'r009.txt'
if not example_file.exists():
self.skipTest("r009.txt example file not found")
out = StringIO()
call_command(
'import-document',
str(example_file),
'--nummer', 'R009',
'--name', 'IT-Sicherheit Serversysteme',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='R009')
# Check that Einleitung was created
self.assertGreater(Einleitung.objects.filter(einleitung=dokument).count(), 0)
# Check that Geltungsbereich was created
self.assertGreater(Geltungsbereich.objects.filter(geltungsbereich=dokument).count(), 0)
# Check that multiple Vorgaben were created (r009.txt has 23 Vorgaben)
vorgaben = Vorgabe.objects.filter(dokument=dokument)
self.assertGreaterEqual(vorgaben.count(), 20)
# Verify output message
output = out.getvalue()
self.assertIn('Imported document R009', output)

View File

@@ -493,6 +493,38 @@ class ViewsTestCase(TestCase):
url = reverse('standard_history', kwargs={'nummer': 'R01234'}) url = reverse('standard_history', kwargs={'nummer': 'R01234'})
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_standard_history_past_date_shows_historische(self):
"""Test that past dates show 'Historische Version'"""
past_date = (date.today() - timedelta(days=30)).strftime('%Y-%m-%d')
url = f'/dokumente/R01234/history/{past_date}/'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Historische Version vom')
self.assertNotContains(response, 'Zukünftige Version vom')
# Verify is_future flag is False
self.assertFalse(response.context['standard'].is_future)
def test_standard_history_future_date_shows_zukuenftige(self):
"""Test that future dates show 'Zukünftige Version'"""
future_date = (date.today() + timedelta(days=30)).strftime('%Y-%m-%d')
url = f'/dokumente/R01234/history/{future_date}/'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Zukünftige Version vom')
self.assertNotContains(response, 'Historische Version vom')
# Verify is_future flag is True
self.assertTrue(response.context['standard'].is_future)
def test_standard_detail_current_has_no_version_label(self):
"""Test that current view (no history) has no version label"""
url = reverse('standard_detail', kwargs={'nummer': 'R01234'})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, 'Historische Version vom')
self.assertNotContains(response, 'Zukünftige Version vom')
# Verify history flag is False
self.assertFalse(response.context['standard'].history)
class URLPatternsTest(TestCase): class URLPatternsTest(TestCase):
@@ -761,6 +793,134 @@ class VorgabeSanityCheckTest(TestCase):
self.assertIn("intersecting validity periods", report) self.assertIn("intersecting validity periods", report)
class VorgabeThemaValidationTest(TestCase):
"""Test cases for Vorgabe Thema validation"""
def setUp(self):
"""Set up test data for Thema validation tests"""
self.dokumententyp = Dokumententyp.objects.create(
name="Standard IT-Sicherheit",
verantwortliche_ve="SR-SUR-SEC"
)
self.dokument = Dokument.objects.create(
nummer="R0066",
dokumententyp=self.dokumententyp,
name="IT Security Standard",
aktiv=True
)
self.thema = Thema.objects.create(name="Organisation")
def test_vorgabe_with_thema_passes_validation(self):
"""Test that Vorgabe with a valid Thema passes clean() validation"""
vorgabe = Vorgabe(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date.today()
)
# Should not raise any exception
try:
vorgabe.clean()
except Exception as e:
self.fail(f"clean() raised {e} unexpectedly!")
def test_vorgabe_without_thema_fails_validation(self):
"""Test that Vorgabe without Thema fails clean() validation"""
from django.core.exceptions import ValidationError
vorgabe = Vorgabe(
order=1,
nummer=1,
dokument=self.dokument,
thema=None, # No Thema
titel="Test Vorgabe",
gueltigkeit_von=date.today()
)
with self.assertRaises(ValidationError) as context:
vorgabe.clean()
# Check that the error message is about thema
self.assertIn('thema', context.exception.message_dict)
self.assertIn('Thema ist ein Pflichtfeld', str(context.exception))
def test_vorgabe_form_with_thema_is_valid(self):
"""Test that VorgabeForm with Thema is valid"""
from dokumente.admin import VorgabeForm
form_data = {
'order': 1,
'nummer': 1,
'dokument': self.dokument.pk,
'thema': self.thema.pk,
'titel': 'Test Vorgabe',
'gueltigkeit_von': date.today(),
}
form = VorgabeForm(data=form_data)
self.assertTrue(form.is_valid(), f"Form errors: {form.errors}")
def test_vorgabe_form_without_thema_is_invalid(self):
"""Test that VorgabeForm without Thema is invalid"""
from dokumente.admin import VorgabeForm
form_data = {
'order': 1,
'nummer': 1,
'dokument': self.dokument.pk,
'thema': '', # Empty/missing Thema
'titel': 'Test Vorgabe',
'gueltigkeit_von': date.today(),
}
form = VorgabeForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('thema', form.errors)
def test_vorgabe_form_thema_error_message_is_german(self):
"""Test that VorgabeForm shows German error message for missing Thema"""
from dokumente.admin import VorgabeForm
form_data = {
'order': 1,
'nummer': 1,
'dokument': self.dokument.pk,
'thema': '', # Empty/missing Thema
'titel': 'Test Vorgabe',
'gueltigkeit_von': date.today(),
}
form = VorgabeForm(data=form_data)
form.is_valid()
# Check that the error message is in German
thema_errors = form.errors.get('thema', [])
error_messages = ' '.join(thema_errors)
self.assertTrue(
'Pflichtfeld' in error_messages or 'pflichtfeld' in error_messages.lower(),
f"Expected German error message about Pflichtfeld, got: {thema_errors}"
)
def test_vorgabe_model_clean_error_message_is_german(self):
"""Test that Vorgabe.clean() shows German error message for missing Thema"""
from django.core.exceptions import ValidationError
vorgabe = Vorgabe(
order=1,
nummer=1,
dokument=self.dokument,
thema=None,
titel="Test Vorgabe",
gueltigkeit_von=date.today()
)
with self.assertRaises(ValidationError) as context:
vorgabe.clean()
# Check error message is in German
error_str = str(context.exception)
self.assertIn('Thema ist ein Pflichtfeld', error_str)
class SanityCheckManagementCommandTest(TestCase): class SanityCheckManagementCommandTest(TestCase):
"""Test cases for sanity_check_vorgaben management command""" """Test cases for sanity_check_vorgaben management command"""
@@ -1323,6 +1483,200 @@ class JSONExportManagementCommandTest(TestCase):
self.assertIn('"Test Standard"', output) self.assertIn('"Test Standard"', output)
class ExportXMLCommandTest(TestCase):
"""Test cases for export_xml management command"""
def setUp(self):
"""Set up test data for XML export"""
# Create test data
self.dokumententyp = Dokumententyp.objects.create(
name="Standard IT-Sicherheit",
verantwortliche_ve="SR-SUR-SEC"
)
self.autor1 = Person.objects.create(
name="Max Mustermann",
funktion="Security Analyst"
)
self.autor2 = Person.objects.create(
name="Erika Mustermann",
funktion="Security Manager"
)
self.thema = Thema.objects.create(
name="Access Control",
erklaerung="Zugangskontrolle"
)
self.dokument = Dokument.objects.create(
nummer="TEST-001",
dokumententyp=self.dokumententyp,
name="Test Standard",
gueltigkeit_von=date(2023, 1, 1),
gueltigkeit_bis=date(2025, 12, 31),
signatur_cso="CSO-123",
anhaenge="Anhang1.pdf, Anhang2.pdf",
aktiv=True
)
self.dokument.autoren.add(self.autor1, self.autor2)
self.vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date(2023, 1, 1),
gueltigkeit_bis=date(2025, 12, 31)
)
# Create text sections
self.abschnitttyp_text = AbschnittTyp.objects.create(abschnitttyp="text")
self.abschnitttyp_table = AbschnittTyp.objects.create(abschnitttyp="table")
self.geltungsbereich = Geltungsbereich.objects.create(
geltungsbereich=self.dokument,
abschnitttyp=self.abschnitttyp_text,
inhalt="Dies ist der Geltungsbereich",
order=1
)
self.einleitung = Einleitung.objects.create(
einleitung=self.dokument,
abschnitttyp=self.abschnitttyp_text,
inhalt="Dies ist die Einleitung",
order=1
)
self.kurztext = VorgabeKurztext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.abschnitttyp_text,
inhalt="Dies ist der Kurztext",
order=1
)
self.langtext = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.abschnitttyp_table,
inhalt="Spalte1|Spalte2\nWert1|Wert2",
order=1
)
self.checklistenfrage = Checklistenfrage.objects.create(
vorgabe=self.vorgabe,
frage="Ist die Zugriffskontrolle implementiert?"
)
self.changelog = Changelog.objects.create(
dokument=self.dokument,
datum=date(2023, 6, 1),
aenderung="Erste Version erstellt"
)
self.changelog.autoren.add(self.autor1)
def test_export_xml_command_stdout(self):
"""Test export_xml command output to stdout"""
out = StringIO()
call_command('export_xml', stdout=out)
output = out.getvalue()
# Check that output contains expected XML structure
self.assertIn('<Vorgabendokumente>', output)
self.assertIn('<Vorgabendokument>', output)
self.assertIn('<Typ>Standard IT-Sicherheit</Typ>', output)
self.assertIn('<Nummer>TEST-001</Nummer>', output)
self.assertIn('<Name>Test Standard</Name>', output)
self.assertIn('<Autor>Max Mustermann</Autor>', output)
self.assertIn('<Autor>Erika Mustermann</Autor>', output)
self.assertIn('<Von>2023-01-01</Von>', output)
self.assertIn('<Bis>2025-12-31</Bis>', output)
self.assertIn('<SignaturCSO>CSO-123</SignaturCSO>', output)
self.assertIn('Dies ist der Geltungsbereich', output)
self.assertIn('Dies ist die Einleitung', output)
self.assertIn('Dies ist der Kurztext', output)
self.assertIn('<Frage>Ist die Zugriffskontrolle implementiert?</Frage>', output)
self.assertIn('Erste Version erstellt', output)
def test_export_xml_command_to_file(self):
"""Test export_xml command output to file"""
import tempfile
import os
import xml.etree.ElementTree as ET
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.xml') as tmp_file:
tmp_filename = tmp_file.name
try:
call_command('export_xml', output=tmp_filename)
# Read file content
with open(tmp_filename, 'r', encoding='utf-8') as f:
content = f.read()
# Parse XML to ensure it's valid
root = ET.fromstring(content)
# Verify structure
self.assertEqual(root.tag, 'Vorgabendokumente')
docs = root.findall('Vorgabendokument')
self.assertEqual(len(docs), 1)
doc = docs[0]
self.assertEqual(doc.find('Nummer').text, 'TEST-001')
self.assertEqual(doc.find('Name').text, 'Test Standard')
self.assertEqual(doc.find('Typ').text, 'Standard IT-Sicherheit')
autoren = doc.find('Autoren').findall('Autor')
self.assertEqual(len(autoren), 2)
autor_names = [autor.text for autor in autoren]
self.assertIn('Max Mustermann', autor_names)
self.assertIn('Erika Mustermann', autor_names)
finally:
# Clean up temporary file
if os.path.exists(tmp_filename):
os.unlink(tmp_filename)
def test_export_xml_command_empty_database(self):
"""Test export_xml command with no documents"""
# Delete all documents
Dokument.objects.all().delete()
out = StringIO()
call_command('export_xml', stdout=out)
output = out.getvalue()
# Should output empty XML structure (accepts both formats)
self.assertIn('Vorgabendokumente', output)
self.assertNotIn('<Vorgabendokument>', output)
def test_export_xml_command_inactive_documents(self):
"""Test export_xml command filters inactive documents"""
# Create inactive document
inactive_doc = Dokument.objects.create(
nummer="INACTIVE-001",
dokumententyp=self.dokumententyp,
name="Inactive Document",
aktiv=False
)
out = StringIO()
call_command('export_xml', stdout=out)
output = out.getvalue()
# Should not contain inactive document
self.assertNotIn('INACTIVE-001', output)
self.assertNotIn('Inactive Document', output)
# Should still contain active document
self.assertIn('TEST-001', output)
self.assertIn('Test Standard', output)
class StandardJSONViewTest(TestCase): class StandardJSONViewTest(TestCase):
"""Test cases for standard_json view""" """Test cases for standard_json view"""
@@ -1508,6 +1862,195 @@ class StandardJSONViewTest(TestCase):
self.assertIn(' ', response.content.decode()) # Check for indentation self.assertIn(' ', response.content.decode()) # Check for indentation
class StandardXMLViewTest(TestCase):
"""Test cases for standard_xml view"""
def setUp(self):
"""Set up test data for XML view"""
self.client = Client()
# Create test data
self.dokumententyp = Dokumententyp.objects.create(
name="Standard IT-Sicherheit",
verantwortliche_ve="SR-SUR-SEC"
)
self.autor = Person.objects.create(
name="Test Autor",
funktion="Security Analyst"
)
self.pruefender = Person.objects.create(
name="Test Pruefender",
funktion="Security Manager"
)
self.thema = Thema.objects.create(
name="Access Control",
erklaerung="Zugangskontrolle"
)
self.dokument = Dokument.objects.create(
nummer="XML-001",
dokumententyp=self.dokumententyp,
name="XML Test Standard",
gueltigkeit_von=date(2023, 1, 1),
gueltigkeit_bis=date(2025, 12, 31),
signatur_cso="CSO-456",
anhaenge="test.pdf",
aktiv=True
)
self.dokument.autoren.add(self.autor)
self.dokument.pruefende.add(self.pruefender)
self.vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="XML Test Vorgabe",
gueltigkeit_von=date(2023, 1, 1),
gueltigkeit_bis=date(2025, 12, 31)
)
# Create text sections
self.abschnitttyp_text = AbschnittTyp.objects.create(abschnitttyp="text")
self.geltungsbereich = Geltungsbereich.objects.create(
geltungsbereich=self.dokument,
abschnitttyp=self.abschnitttyp_text,
inhalt="XML Geltungsbereich",
order=1
)
self.kurztext = VorgabeKurztext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.abschnitttyp_text,
inhalt="XML Kurztext",
order=1
)
self.langtext = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.abschnitttyp_text,
inhalt="XML Langtext",
order=1
)
self.checklistenfrage = Checklistenfrage.objects.create(
vorgabe=self.vorgabe,
frage="XML Checklistenfrage?"
)
self.changelog = Changelog.objects.create(
dokument=self.dokument,
datum=date(2023, 6, 1),
aenderung="XML Changelog Eintrag"
)
self.changelog.autoren.add(self.autor)
def test_standard_xml_view_success(self):
"""Test standard_xml view returns correct XML"""
url = reverse('standard_xml', kwargs={'nummer': 'XML-001'})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/xml; charset=utf-8')
self.assertIn('attachment', response['Content-Disposition'])
self.assertIn('XML-001.xml', response['Content-Disposition'])
# Parse XML response
import xml.etree.ElementTree as ET
root = ET.fromstring(response.content)
# Verify document structure
self.assertEqual(root.tag, 'Vorgabendokument')
self.assertEqual(root.find('Nummer').text, 'XML-001')
self.assertEqual(root.find('Name').text, 'XML Test Standard')
self.assertEqual(root.find('Typ').text, 'Standard IT-Sicherheit')
self.assertEqual(root.find('Autoren').find('Autor').text, 'Test Autor')
self.assertEqual(root.find('Pruefende').find('Pruefender').text, 'Test Pruefender')
self.assertEqual(root.find('Gueltigkeit').find('Von').text, '2023-01-01')
self.assertEqual(root.find('Gueltigkeit').find('Bis').text, '2025-12-31')
self.assertEqual(root.find('SignaturCSO').text, 'CSO-456')
self.assertEqual(root.find('Anhaenge').find('Anhang').text, 'test.pdf')
self.assertEqual(root.find('Verantwortlich').text, 'Information Security Management BIT')
self.assertIn(root.find('Klassifizierung').text, ['', None]) # Empty string or None
def test_standard_xml_view_not_found(self):
"""Test standard_xml view returns 404 for non-existent document"""
url = reverse('standard_xml', kwargs={'nummer': 'NONEXISTENT'})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_standard_xml_view_empty_sections(self):
"""Test standard_xml view handles empty sections correctly"""
# Create document without sections
empty_doc = Dokument.objects.create(
nummer="EMPTY-XML-001",
dokumententyp=self.dokumententyp,
name="Empty Document",
aktiv=True
)
url = reverse('standard_xml', kwargs={'nummer': 'EMPTY-XML-001'})
response = self.client.get(url)
# Parse XML response
import xml.etree.ElementTree as ET
root = ET.fromstring(response.content)
# Verify empty sections are handled correctly
self.assertIsNone(root.find('Geltungsbereich'))
self.assertIsNone(root.find('Einleitung'))
self.assertEqual(len(root.find('Vorgaben')), 0)
self.assertEqual(len(root.find('Changelog')), 0)
def test_standard_xml_view_null_dates(self):
"""Test standard_xml view handles null dates correctly"""
# Create document with null dates
null_doc = Dokument.objects.create(
nummer="NULL-XML-001",
dokumententyp=self.dokumententyp,
name="Null Dates Document",
gueltigkeit_von=None,
gueltigkeit_bis=None,
aktiv=True
)
url = reverse('standard_xml', kwargs={'nummer': 'NULL-XML-001'})
response = self.client.get(url)
# Parse XML response
import xml.etree.ElementTree as ET
root = ET.fromstring(response.content)
# Verify null dates are handled correctly
self.assertIn(root.find('Gueltigkeit').find('Von').text, ['', None])
self.assertIsNone(root.find('Gueltigkeit').find('Bis').text)
def test_standard_xml_view_xml_formatting(self):
"""Test standard_xml view returns properly formatted XML"""
url = reverse('standard_xml', kwargs={'nummer': 'XML-001'})
response = self.client.get(url)
# Check that response is valid XML
import xml.etree.ElementTree as ET
try:
ET.fromstring(response.content)
xml_valid = True
except ET.ParseError:
xml_valid = False
self.assertTrue(xml_valid)
# Check that XML is properly indented (should be formatted)
self.assertIn('<?xml version', response.content.decode())
self.assertIn('\n', response.content.decode())
self.assertIn(' ', response.content.decode()) # Check for indentation
class VorgabeCommentModelTest(TestCase): class VorgabeCommentModelTest(TestCase):
"""Test cases for VorgabeComment model""" """Test cases for VorgabeComment model"""

View File

@@ -11,6 +11,7 @@ urlpatterns = [
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'), path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
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('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

@@ -1,12 +1,13 @@
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse from django.http import JsonResponse, HttpResponse
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.views.decorators.http import require_POST 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 json import json
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 abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
@@ -29,9 +30,11 @@ def standard_detail(request, nummer,check_date=""):
if check_date: if check_date:
check_date = calendar.parseDT(check_date)[0].date() check_date = calendar.parseDT(check_date)[0].date()
standard.history = True standard.history = True
standard.is_future = check_date > date.today()
else: else:
check_date = date.today() check_date = date.today()
standard.history = False standard.history = False
standard.is_future = False
standard.check_date=check_date standard.check_date=check_date
vorgaben = list(standard.vorgaben.order_by("thema","nummer").select_related("thema","dokument")) # convert queryset to list so we can attach attributes vorgaben = list(standard.vorgaben.order_by("thema","nummer").select_related("thema","dokument")) # convert queryset to list so we can attach attributes
@@ -252,6 +255,138 @@ 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_xml(request, nummer):
"""
Export a single Dokument as XML
"""
# Get the document with all related data
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
),
nummer=nummer
)
root = ET.Element('Vorgabendokument')
ET.SubElement(root, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else ""
ET.SubElement(root, 'Nummer').text = dokument.nummer
ET.SubElement(root, 'Name').text = dokument.name
autoren_element = ET.SubElement(root, 'Autoren')
for autor in dokument.autoren.all():
ET.SubElement(autoren_element, 'Autor').text = autor.name
pruefende_element = ET.SubElement(root, 'Pruefende')
for pruefender in dokument.pruefende.all():
ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name
gueltigkeit_element = ET.SubElement(root, '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(root, 'SignaturCSO').text = dokument.signatur_cso
geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order')
if geltungsbereich_sections:
geltungsbereich_element = ET.SubElement(root, 'Geltungsbereich')
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
for gb in geltungsbereich_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text")
section.text = gb.inhalt
einleitung_sections = dokument.einleitung_set.all().order_by('order')
if einleitung_sections:
einleitung_element = ET.SubElement(root, 'Einleitung')
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
for ei in einleitung_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text")
section.text = ei.inhalt
ET.SubElement(root, 'Ziel').text = ""
ET.SubElement(root, 'Grundlagen').text = ""
changelog_element = ET.SubElement(root, '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(root, 'Anhaenge')
ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge
ET.SubElement(root, 'Verantwortlich').text = "Information Security Management BIT"
ET.SubElement(root, 'Klassifizierung').text = ""
glossar_element = ET.SubElement(root, 'Glossar')
vorgaben_element = ET.SubElement(root, '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')
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
for kt in kurztext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text")
teil.text = kt.inhalt
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
if langtext_sections:
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
for lt in langtext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text")
teil.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
xml_str = ET.tostring(root, encoding='unicode', method='xml')
xml_output = _prettify_xml(xml_str)
response = HttpResponse(xml_output, content_type='application/xml; charset=utf-8')
response['Content-Disposition'] = f'attachment; filename="{dokument.nummer}.xml"'
return response
def _prettify_xml(xml_string):
import xml.dom.minidom
dom = xml.dom.minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')
@login_required @login_required
def get_vorgabe_comments(request, vorgabe_id): def get_vorgabe_comments(request, vorgabe_id):
"""Get comments for a specific Vorgabe""" """Get comments for a specific Vorgabe"""

View File

@@ -219,7 +219,7 @@
</p> </p>
</div> </div>
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.968" }}</p> <p class="text-muted">Version {{ version|default:"0.973" }}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@ certifi==2025.8.3
charset-normalizer==3.4.3 charset-normalizer==3.4.3
curtsies==0.4.3 curtsies==0.4.3
cwcwidth==0.1.10 cwcwidth==0.1.10
Django==5.2.8 Django==5.2.9
django-admin-sortable2==2.2.8 django-admin-sortable2==2.2.8
django-js-asset==3.1.2 django-js-asset==3.1.2
django-mptt==0.17.0 django-mptt==0.17.0
@@ -30,7 +30,7 @@ pyxdg==0.28
requests==2.32.5 requests==2.32.5
six==1.17.0 six==1.17.0
sqlparse==0.5.3 sqlparse==0.5.3
urllib3==2.5.0 urllib3==2.6.0
wcwidth==0.2.13 wcwidth==0.2.13
bleach==6.1.0 bleach==6.1.0
coverage==7.6.1 coverage==7.6.1

45
scripts/deploy_secret.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Generate and deploy Django secret key to Kubernetes
NAMESPACE="vorgabenui"
SECRET_NAME="django-secret"
SECRET_FILE="argocd/secret.yaml"
# Check if secret file exists
if [ ! -f "$SECRET_FILE" ]; then
echo "Error: $SECRET_FILE not found"
exit 1
fi
# Generate random secret key
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")
# Create temporary secret file with generated key
TEMP_SECRET_FILE=$(mktemp)
cat "$SECRET_FILE" | sed "s/CHANGE_ME_TO_RANDOM_STRING/$SECRET_KEY/g" > "$TEMP_SECRET_FILE"
# Check if secret already exists
if kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" &>/dev/null; then
echo "Secret $SECRET_NAME already exists in namespace $NAMESPACE"
read -p "Do you want to replace it? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted"
rm "$TEMP_SECRET_FILE"
exit 0
fi
kubectl apply -f "$TEMP_SECRET_FILE"
echo "Secret updated successfully"
else
kubectl apply -f "$TEMP_SECRET_FILE"
echo "Secret created successfully"
fi
# Clean up
rm "$TEMP_SECRET_FILE"
echo ""
echo "Secret deployed:"
echo " Name: $SECRET_NAME"
echo " Namespace: $NAMESPACE"
echo " Key: secret-key"

45
scripts/full_deploy.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Full deployment script - bumps both container versions by 0.001 and copies database
DEPLOYMENT_FILE="argocd/deployment.yaml"
DB_SOURCE="data/db.sqlite3"
DB_DEST="data-loader/preload.sqlite3"
# Check if deployment file exists
if [ ! -f "$DEPLOYMENT_FILE" ]; then
echo "Error: $DEPLOYMENT_FILE not found"
exit 1
fi
# Check if source database exists
if [ ! -f "$DB_SOURCE" ]; then
echo "Error: $DB_SOURCE not found"
exit 1
fi
# Extract current version of data-loader
LOADER_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vgui-data-loader:[0-9]" "$DEPLOYMENT_FILE" | sed -E 's/.*:([0-9.]+)/\1/')
# Extract current version of main container
MAIN_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vgui:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
if [ -z "$LOADER_VERSION" ] || [ -z "$MAIN_VERSION" ]; then
echo "Error: Could not find current versions"
exit 1
fi
# Calculate new versions (add 0.001), preserve leading zero
NEW_LOADER_VERSION=$(echo "$LOADER_VERSION + 0.001" | bc | sed 's/^\./0./')
NEW_MAIN_VERSION=$(echo "$MAIN_VERSION + 0.001" | bc | sed 's/^\./0./')
# Update the deployment file
sed -i "s|image: git.baumann.gr/adebaumann/labhelper-data-loader:$LOADER_VERSION|image: git.baumann.gr/adebaumann/labhelper-data-loader:$NEW_LOADER_VERSION|" "$DEPLOYMENT_FILE"
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE"
# Copy database
cp "$DB_SOURCE" "$DB_DEST"
echo "Full deployment prepared:"
echo " Data loader: $LOADER_VERSION -> $NEW_LOADER_VERSION"
echo " Main container: $MAIN_VERSION -> $NEW_MAIN_VERSION"
echo " Database copied to $DB_DEST"

27
scripts/partial_deploy.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Partial deployment script - bumps main container version by 0.001
DEPLOYMENT_FILE="argocd/deployment.yaml"
# Check if file exists
if [ ! -f "$DEPLOYMENT_FILE" ]; then
echo "Error: $DEPLOYMENT_FILE not found"
exit 1
fi
# Extract current version of main container (labhelper, not labhelper-data-loader)
CURRENT_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vui:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
if [ -z "$CURRENT_VERSION" ]; then
echo "Error: Could not find current version"
exit 1
fi
# Calculate new version (add 0.001), preserve leading zero
NEW_VERSION=$(echo "$CURRENT_VERSION + 0.001" | bc | sed 's/^\./0./')
# Update the deployment file (only the main container, not the data-loader)
sed -i "s|image: git.baumann.gr/adebaumann/vui:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/vui:$NEW_VERSION|" "$DEPLOYMENT_FILE"
echo "Partial deployment prepared:"
echo " Main container: $CURRENT_VERSION -> $NEW_VERSION"

0
test Normal file
View File