Compare commits

..

1 Commits

Author SHA1 Message Date
718160c8b7 First version of helm-charts 2025-11-01 01:24:09 +01:00
22 changed files with 384 additions and 924 deletions

View File

@@ -1,102 +0,0 @@
/* Better visual separation for Vorgaben inlines */
.inline-group[data-inline-model="vorgabe"] {
border: 2px solid #ddd;
border-radius: 8px;
margin-bottom: 20px;
padding: 15px;
background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.inline-group[data-inline-model="vorgabe"] .inline-related {
border: 1px solid #ccc;
border-radius: 6px;
margin-bottom: 10px;
background-color: white;
padding: 10px;
}
.inline-group[data-inline-model="vorgabe"] h3 {
background-color: #007cba;
color: white;
padding: 8px 12px;
margin: -15px -15px 10px -15px;
border-radius: 6px 6px 0 0;
font-weight: bold;
}
.inline-group[data-inline-model="vorgabe"] .collapse .inline-related {
border-left: 3px solid #007cba;
}
/* Better spacing for nested inlines */
.inline-group[data-inline-model="vorgabe"] .inline-group {
margin-top: 10px;
}
.inline-group[data-inline-model="vorgabe"] .inline-group h3 {
background-color: #f0f8ff;
color: #333;
padding: 6px 10px;
margin: 0 0 8px 0;
border-left: 3px solid #007cba;
}
/* Highlight active/expanded vorgabe */
.inline-group[data-inline-model="vorgabe"] .inline-related:not(.collapsed) {
border-color: #007cba;
box-shadow: 0 0 8px rgba(0,124,186,0.2);
}
/* Highlight actively edited vorgabe */
.inline-group[data-inline-model="vorgabe"] .inline-related.active-edit {
border-color: #28a745;
box-shadow: 0 0 12px rgba(40,167,69,0.3);
background-color: #f8fff9;
}
/* Toggle hint styling */
.toggle-hint {
font-size: 0.8em;
color: #666;
font-weight: normal;
}
/* Better fieldset styling for vorgabe inlines */
.inline-group[data-inline-model="vorgabe"] .fieldset {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
background-color: #fafafa;
}
.inline-group[data-inline-model="vorgabe"] .fieldset h2 {
background-color: #e3f2fd;
color: #1565c0;
padding: 5px 10px;
margin: -10px -10px 10px -10px;
border-radius: 4px 4px 0 0;
font-size: 0.9em;
font-weight: bold;
}
/* Better form layout */
.inline-group[data-inline-model="vorgabe"] .form-row {
border-bottom: 1px solid #eee;
padding: 8px 0;
}
.inline-group[data-inline-model="vorgabe"] .form-row:last-child {
border-bottom: none;
}
/* Wide fields styling */
.inline-group[data-inline-model="vorgabe"] .wide .form-row > div {
width: 100%;
}
.inline-group[data-inline-model="vorgabe"] .wide textarea {
width: 100%;
min-height: 80px;
}

View File

@@ -1,25 +0,0 @@
(function($) {
'use strict';
$(document).ready(function() {
// Add toggle buttons for each vorgabe inline
$('.inline-group[data-inline-model="vorgabe"]').each(function() {
var $group = $(this);
var $headers = $group.find('h3');
$headers.css('cursor', 'pointer').append(' <span class="toggle-hint">(klicken zum umschalten)</span>');
$headers.on('click', function(e) {
e.preventDefault();
var $inline = $(this).closest('.inline-related');
$inline.find('.collapse').toggleClass('collapsed');
});
});
// Highlight active vorgabe when editing
$('.inline-group[data-inline-model="vorgabe"] .inline-related').on('click', function() {
$('.inline-group[data-inline-model="vorgabe"] .inline-related').removeClass('active-edit');
$(this).addClass('active-edit');
});
});
})(django.jQuery);

View File

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

Binary file not shown.

View File

@@ -21,75 +21,45 @@ from referenzen.models import Referenz
# 'frage': forms.Textarea(attrs={'rows': 1, 'cols': 100}),
# }
class ChecklistenfragenInline(NestedStackedInline):
class ChecklistenfragenInline(NestedTabularInline):
model=Checklistenfrage
extra=0
fk_name="vorgabe"
# form=ChecklistenForm
classes = ['collapse']
verbose_name_plural = "Checklistenfragen"
fieldsets = (
(None, {
'fields': ('frage',),
'classes': ('wide',),
}),
)
class VorgabeKurztextInline(NestedStackedInline):
class VorgabeKurztextInline(NestedTabularInline):
model=VorgabeKurztext
extra=0
sortable_field_name = "order"
show_change_link=True
classes = ['collapse']
verbose_name_plural = "Kurztext-Abschnitte"
fieldsets = (
(None, {
'fields': ('abschnitttyp', 'inhalt', 'order'),
'classes': ('wide',),
}),
)
#inline=inhalt
class VorgabeLangtextInline(NestedStackedInline):
class VorgabeLangtextInline(NestedTabularInline):
model=VorgabeLangtext
extra=0
sortable_field_name = "order"
show_change_link=True
classes = ['collapse']
verbose_name_plural = "Langtext-Abschnitte"
fieldsets = (
(None, {
'fields': ('abschnitttyp', 'inhalt', 'order'),
'classes': ('wide',),
}),
)
#inline=inhalt
class GeltungsbereichInline(NestedStackedInline):
class GeltungsbereichInline(NestedTabularInline):
model=Geltungsbereich
extra=0
sortable_field_name = "order"
show_change_link=True
classes = ['collapse']
verbose_name_plural = "Geltungsbereich-Abschnitte"
fieldsets = (
(None, {
'fields': ('abschnitttyp', 'inhalt', 'order'),
'classes': ('wide',),
}),
)
class EinleitungInline(NestedStackedInline):
model = Einleitung
extra = 0
sortable_field_name = "order"
show_change_link = True
classes = ['collapse']
verbose_name_plural = "Einleitungs-Abschnitte"
fieldsets = (
(None, {
'fields': ('abschnitttyp', 'inhalt', 'order'),
'classes': ('wide',),
}),
)
#inline=inhalt
class EinleitungInline(NestedTabularInline):
model = Einleitung
extra = 0
sortable_field_name = "order"
show_change_link = True
classes = ['collapse']
class VorgabeForm(forms.ModelForm):
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
@@ -97,31 +67,17 @@ class VorgabeForm(forms.ModelForm):
model = Vorgabe
fields = '__all__'
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline):
class VorgabeInline(SortableInlineAdminMixin, NestedTabularInline): # or StackedInline for more vertical layout
model = Vorgabe
form = VorgabeForm
extra = 0
sortable_field_name = "order"
show_change_link = False
can_delete = False
inlines = [VorgabeKurztextInline, VorgabeLangtextInline, ChecklistenfragenInline]
sortable_field_name = "order" # Add this - make sure your Vorgabe model has an 'order' field
#show_change_link = True
inlines = [VorgabeKurztextInline,VorgabeLangtextInline,ChecklistenfragenInline]
autocomplete_fields = ['stichworte','referenzen','relevanz']
# Remove collapse class so Vorgaben show by default
fieldsets = (
('Grunddaten', {
'fields': (('order', 'nummer'), ('thema', 'titel')),
'classes': ('wide',),
}),
('Gültigkeit', {
'fields': (('gueltigkeit_von', 'gueltigkeit_bis'),),
'classes': ('wide',),
}),
('Verknüpfungen', {
'fields': (('referenzen', 'stichworte', 'relevanz'),),
'classes': ('wide',),
}),
)
#search_fields=['nummer','name']ModelAdmin.
list_filter=['stichworte']
#classes=["collapse"]
class StichworterklaerungInline(NestedTabularInline):
model=Stichworterklaerung
@@ -148,31 +104,16 @@ class PersonAdmin(admin.ModelAdmin):
@admin.register(Dokument)
class DokumentAdmin(SortableAdminBase, NestedModelAdmin):
actions_on_top=True
inlines = [EinleitungInline, GeltungsbereichInline, VorgabeInline]
filter_horizontal=['autoren','pruefende']
list_display=['nummer','name','dokumententyp','gueltigkeit_von','gueltigkeit_bis','aktiv']
inlines = [EinleitungInline,GeltungsbereichInline,VorgabeInline]
#filter_horizontal=['autoren','pruefende']
list_display=['nummer','name','dokumententyp']
search_fields=['nummer','name']
list_filter=['dokumententyp','aktiv','gueltigkeit_von']
fieldsets = (
('Grunddaten', {
'fields': ('nummer', 'name', 'dokumententyp', 'aktiv'),
'classes': ('wide',),
}),
('Verantwortlichkeiten', {
'fields': ('autoren', 'pruefende'),
'classes': ('wide', 'collapse'),
}),
('Gültigkeit & Metadaten', {
'fields': ('gueltigkeit_von', 'gueltigkeit_bis', 'signatur_cso', 'anhaenge'),
'classes': ('wide', 'collapse'),
}),
)
class Media:
js = ('admin/js/vorgabe_collapse.js',)
# js = ('admin/js/vorgabe_collapse.js',)
css = {
'all': ('admin/css/vorgabe_border.css',)
'all': ('admin/css/vorgabe_border.css',
# 'admin/css/vorgabe_collapse.css',
)
}

View File

@@ -1,70 +0,0 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from dokumente.models import Vorgabe
import datetime
class Command(BaseCommand):
help = 'Run sanity checks on Vorgaben to detect conflicts'
def add_arguments(self, parser):
parser.add_argument(
'--fix',
action='store_true',
help='Attempt to fix conflicts (not implemented yet)',
)
parser.add_argument(
'--verbose',
action='store_true',
help='Show detailed output',
)
def handle(self, *args, **options):
self.verbose = options['verbose']
self.stdout.write(self.style.SUCCESS('Starting Vorgaben sanity check...'))
# Run the sanity check
conflicts = Vorgabe.sanity_check_vorgaben()
if not conflicts:
self.stdout.write(self.style.SUCCESS('✓ No conflicts found in Vorgaben'))
return
self.stdout.write(
self.style.WARNING(f'Found {len(conflicts)} conflicts:')
)
for i, conflict in enumerate(conflicts, 1):
self._display_conflict(i, conflict)
if options['fix']:
self.stdout.write(self.style.ERROR('Auto-fix not implemented yet'))
def _display_conflict(self, index, conflict):
"""Display a single conflict"""
v1 = conflict['vorgabe1']
v2 = conflict['vorgabe2']
self.stdout.write(f"\n{index}. {conflict['message']}")
if self.verbose:
self.stdout.write(f" Vorgabe 1: {v1.Vorgabennummer()}")
self.stdout.write(f" Valid from: {v1.gueltigkeit_von} to {v1.gueltigkeit_bis or 'unlimited'}")
self.stdout.write(f" Title: {v1.titel}")
self.stdout.write(f" Vorgabe 2: {v2.Vorgabennummer()}")
self.stdout.write(f" Valid from: {v2.gueltigkeit_von} to {v2.gueltigkeit_bis or 'unlimited'}")
self.stdout.write(f" Title: {v2.titel}")
# Show the overlapping period
overlap_start = max(v1.gueltigkeit_von, v2.gueltigkeit_von)
overlap_end = min(
v1.gueltigkeit_bis or datetime.date.max,
v2.gueltigkeit_bis or datetime.date.max
)
if overlap_end != datetime.date.max:
self.stdout.write(f" Overlap: {overlap_start} to {overlap_end}")
else:
self.stdout.write(f" Overlap starts: {overlap_start} (no end)")

View File

@@ -78,7 +78,7 @@ class Vorgabe(models.Model):
if not self.gueltigkeit_bis:
return "active"
if self.gueltigkeit_bis >= check_date:
if self.gueltigkeit_bis > check_date:
return "active"
return "expired" if not verbose else "Ist seit dem "+self.gueltigkeit_bis.strftime('%d.%m.%Y')+" nicht mehr in Kraft."
@@ -86,123 +86,6 @@ class Vorgabe(models.Model):
def __str__(self):
return f"{self.Vorgabennummer()}: {self.titel}"
@staticmethod
def sanity_check_vorgaben():
"""
Sanity check for Vorgaben:
If there are two Vorgaben with the same number, Thema and Dokument,
their valid_from and valid_to date ranges shouldn't intersect.
Returns:
list: List of dictionaries containing conflicts found
"""
conflicts = []
# Group Vorgaben by dokument, thema, and nummer
from django.db.models import Count
from itertools import combinations
# Find Vorgaben with same dokument, thema, and nummer
duplicate_groups = (
Vorgabe.objects.values('dokument', 'thema', 'nummer')
.annotate(count=Count('id'))
.filter(count__gt=1)
)
for group in duplicate_groups:
# Get all Vorgaben in this group
vorgaben = Vorgabe.objects.filter(
dokument=group['dokument'],
thema=group['thema'],
nummer=group['nummer']
)
# Check all pairs for date range intersections
for vorgabe1, vorgabe2 in combinations(vorgaben, 2):
if Vorgabe._date_ranges_intersect(
vorgabe1.gueltigkeit_von, vorgabe1.gueltigkeit_bis,
vorgabe2.gueltigkeit_von, vorgabe2.gueltigkeit_bis
):
conflicts.append({
'vorgabe1': vorgabe1,
'vorgabe2': vorgabe2,
'conflict_type': 'date_range_intersection',
'message': f"Vorgaben {vorgabe1.Vorgabennummer()} and {vorgabe2.Vorgabennummer()} "
f"überschneiden sich in der Geltungsdauer"
})
return conflicts
def clean(self):
"""
Validate the Vorgabe before saving.
"""
from django.core.exceptions import ValidationError
# Check for conflicts with existing Vorgaben
conflicts = self.find_conflicts()
if conflicts:
conflict_messages = [c['message'] for c in conflicts]
raise ValidationError({
'__all__': conflict_messages
})
def find_conflicts(self):
"""
Find conflicts with existing Vorgaben.
Returns:
list: List of conflict dictionaries
"""
conflicts = []
# Find Vorgaben with same dokument, thema, and nummer (excluding self)
existing_vorgaben = Vorgabe.objects.filter(
dokument=self.dokument,
thema=self.thema,
nummer=self.nummer
).exclude(pk=self.pk)
for other_vorgabe in existing_vorgaben:
if self._date_ranges_intersect(
self.gueltigkeit_von, self.gueltigkeit_bis,
other_vorgabe.gueltigkeit_von, other_vorgabe.gueltigkeit_bis
):
conflicts.append({
'vorgabe1': self,
'vorgabe2': other_vorgabe,
'conflict_type': 'date_range_intersection',
'message': f"Vorgabe {self.Vorgabennummer()} conflicts with "
f"existing {other_vorgabe.Vorgabennummer()} "
f"due to overlapping validity periods"
})
return conflicts
@staticmethod
def _date_ranges_intersect(start1, end1, start2, end2):
"""
Check if two date ranges intersect.
None end date means open-ended range.
Args:
start1, start2: Start dates
end1, end2: End dates (can be None for open-ended)
Returns:
bool: True if ranges intersect
"""
# If either start date is None, treat it as invalid case
if not start1 or not start2:
return False
# If end date is None, treat it as far future
end1 = end1 or datetime.date.max
end2 = end2 or datetime.date.max
# Ranges intersect if start1 <= end2 and start2 <= end1
return start1 <= end2 and start2 <= end1
class Meta:
verbose_name_plural="Vorgaben"
ordering = ['order']

View File

@@ -1,14 +1,11 @@
from django.test import TestCase, Client
from django.urls import reverse
from django.core.management import call_command
from datetime import date, timedelta
from io import StringIO
from .models import (
Dokumententyp, Person, Thema, Dokument, Vorgabe,
VorgabeLangtext, VorgabeKurztext, Geltungsbereich,
Einleitung, Checklistenfrage, Changelog
)
from .utils import check_vorgabe_conflicts, date_ranges_intersect, format_conflict_report
from abschnitte.models import AbschnittTyp
from referenzen.models import Referenz
from stichworte.models import Stichwort
@@ -516,304 +513,3 @@ class URLPatternsTest(TestCase):
"""Test that standard_history URL resolves correctly"""
url = reverse('standard_history', kwargs={'nummer': 'TEST-001'})
self.assertEqual(url, '/dokumente/TEST-001/history/')
class VorgabeSanityCheckTest(TestCase):
"""Test cases for Vorgabe sanity check functionality"""
def setUp(self):
"""Set up test data for sanity check 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")
self.base_date = date(2023, 1, 1)
# Create non-conflicting Vorgaben
self.vorgabe1 = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="First Vorgabe",
gueltigkeit_von=self.base_date,
gueltigkeit_bis=date(2023, 12, 31)
)
self.vorgabe2 = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Second Vorgabe",
gueltigkeit_von=self.base_date,
gueltigkeit_bis=date(2023, 12, 31)
)
def test_date_ranges_intersect_no_overlap(self):
"""Test date_ranges_intersect with non-overlapping ranges"""
# Range 1: 2023-01-01 to 2023-06-30
# Range 2: 2023-07-01 to 2023-12-31
result = date_ranges_intersect(
date(2023, 1, 1), date(2023, 6, 30),
date(2023, 7, 1), date(2023, 12, 31)
)
self.assertFalse(result)
def test_date_ranges_intersect_with_overlap(self):
"""Test date_ranges_intersect with overlapping ranges"""
# Range 1: 2023-01-01 to 2023-06-30
# Range 2: 2023-06-01 to 2023-12-31 (overlaps in June)
result = date_ranges_intersect(
date(2023, 1, 1), date(2023, 6, 30),
date(2023, 6, 1), date(2023, 12, 31)
)
self.assertTrue(result)
def test_date_ranges_intersect_with_none_end_date(self):
"""Test date_ranges_intersect with None end date (open-ended)"""
# Range 1: 2023-01-01 to None (open-ended)
# Range 2: 2023-06-01 to 2023-12-31
result = date_ranges_intersect(
date(2023, 1, 1), None,
date(2023, 6, 1), date(2023, 12, 31)
)
self.assertTrue(result)
def test_date_ranges_intersect_both_none_end_dates(self):
"""Test date_ranges_intersect with both None end dates"""
# Both ranges are open-ended
result = date_ranges_intersect(
date(2023, 1, 1), None,
date(2023, 6, 1), None
)
self.assertTrue(result)
def test_date_ranges_intersect_identical_ranges(self):
"""Test date_ranges_intersect with identical ranges"""
result = date_ranges_intersect(
date(2023, 1, 1), date(2023, 12, 31),
date(2023, 1, 1), date(2023, 12, 31)
)
self.assertTrue(result)
def test_sanity_check_vorgaben_no_conflicts(self):
"""Test sanity_check_vorgaben with no conflicts"""
conflicts = Vorgabe.sanity_check_vorgaben()
self.assertEqual(len(conflicts), 0)
def test_sanity_check_vorgaben_with_conflicts(self):
"""Test sanity_check_vorgaben with conflicting Vorgaben"""
# Create a conflicting Vorgabe (same nummer, thema, dokument with overlapping dates)
conflicting_vorgabe = Vorgabe.objects.create(
order=3,
nummer=1, # Same as vorgabe1
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe",
gueltigkeit_von=date(2023, 6, 1), # Overlaps with vorgabe1
gueltigkeit_bis=date(2023, 8, 31)
)
conflicts = Vorgabe.sanity_check_vorgaben()
self.assertEqual(len(conflicts), 1)
conflict = conflicts[0]
self.assertEqual(conflict['conflict_type'], 'date_range_intersection')
self.assertIn('R0066.O.1', conflict['message'])
self.assertIn('intersecting validity periods', conflict['message'])
self.assertEqual(conflict['vorgabe1'], self.vorgabe1)
self.assertEqual(conflict['vorgabe2'], conflicting_vorgabe)
def test_sanity_check_vorgaben_multiple_conflicts(self):
"""Test sanity_check_vorgaben with multiple conflict groups"""
# Create first conflict group
conflicting_vorgabe1 = Vorgabe.objects.create(
order=3,
nummer=1, # Same as vorgabe1
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe 1",
gueltigkeit_von=date(2023, 6, 1),
gueltigkeit_bis=date(2023, 8, 31)
)
# Create second conflict group with different nummer
conflicting_vorgabe2 = Vorgabe.objects.create(
order=4,
nummer=2, # Same as vorgabe2
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe 2",
gueltigkeit_von=date(2023, 6, 1),
gueltigkeit_bis=date(2023, 8, 31)
)
conflicts = Vorgabe.sanity_check_vorgaben()
self.assertEqual(len(conflicts), 2)
# Check that we have conflicts for both nummer 1 and nummer 2
conflict_messages = [c['message'] for c in conflicts]
self.assertTrue(any('R0066.O.1' in msg for msg in conflict_messages))
self.assertTrue(any('R0066.O.2' in msg for msg in conflict_messages))
def test_find_conflicts_no_conflicts(self):
"""Test find_conflicts method on Vorgabe with no conflicts"""
conflicts = self.vorgabe1.find_conflicts()
self.assertEqual(len(conflicts), 0)
def test_find_conflicts_with_conflicts(self):
"""Test find_conflicts method on Vorgabe with conflicts"""
# Create a conflicting Vorgabe
conflicting_vorgabe = Vorgabe.objects.create(
order=3,
nummer=1, # Same as vorgabe1
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe",
gueltigkeit_von=date(2023, 6, 1), # Overlaps
gueltigkeit_bis=date(2023, 8, 31)
)
conflicts = self.vorgabe1.find_conflicts()
self.assertEqual(len(conflicts), 1)
conflict = conflicts[0]
self.assertEqual(conflict['vorgabe1'], self.vorgabe1)
self.assertEqual(conflict['vorgabe2'], conflicting_vorgabe)
def test_vorgabe_clean_no_conflicts(self):
"""Test Vorgabe.clean() with no conflicts"""
try:
self.vorgabe1.clean()
except Exception as e:
self.fail(f"clean() raised {e} unexpectedly!")
def test_vorgabe_clean_with_conflicts(self):
"""Test Vorgabe.clean() with conflicts raises ValidationError"""
# Create a conflicting Vorgabe
conflicting_vorgabe = Vorgabe(
order=3,
nummer=1, # Same as vorgabe1
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe",
gueltigkeit_von=date(2023, 6, 1), # Overlaps
gueltigkeit_bis=date(2023, 8, 31)
)
with self.assertRaises(Exception) as context:
conflicting_vorgabe.clean()
self.assertIn('conflicts with existing', str(context.exception))
self.assertIn('overlapping validity periods', str(context.exception))
def test_check_vorgabe_conflicts_utility(self):
"""Test check_vorgabe_conflicts utility function"""
# Initially no conflicts
conflicts = check_vorgabe_conflicts()
self.assertEqual(len(conflicts), 0)
# Create a conflicting Vorgabe
conflicting_vorgabe = Vorgabe.objects.create(
order=3,
nummer=1, # Same as vorgabe1
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe",
gueltigkeit_von=date(2023, 6, 1), # Overlaps
gueltigkeit_bis=date(2023, 8, 31)
)
conflicts = check_vorgabe_conflicts()
self.assertEqual(len(conflicts), 1)
def test_format_conflict_report_no_conflicts(self):
"""Test format_conflict_report with no conflicts"""
report = format_conflict_report([])
self.assertEqual(report, "✓ No conflicts found in Vorgaben")
def test_format_conflict_report_with_conflicts(self):
"""Test format_conflict_report with conflicts"""
# Create a conflicting Vorgabe
conflicting_vorgabe = Vorgabe.objects.create(
order=3,
nummer=1, # Same as vorgabe1
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe",
gueltigkeit_von=date(2023, 6, 1), # Overlaps
gueltigkeit_bis=date(2023, 8, 31)
)
conflicts = check_vorgabe_conflicts()
report = format_conflict_report(conflicts)
self.assertIn("Found 1 conflicts:", report)
self.assertIn("R0066.O.1", report)
self.assertIn("intersecting validity periods", report)
class SanityCheckManagementCommandTest(TestCase):
"""Test cases for sanity_check_vorgaben management command"""
def setUp(self):
"""Set up test data for management command 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_sanity_check_command_no_conflicts(self):
"""Test management command with no conflicts"""
out = StringIO()
call_command('sanity_check_vorgaben', stdout=out)
output = out.getvalue()
self.assertIn("Starting Vorgaben sanity check...", output)
self.assertIn("✓ No conflicts found in Vorgaben", output)
def test_sanity_check_command_with_conflicts(self):
"""Test management command with conflicts"""
# Create conflicting Vorgaben
Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="First Vorgabe",
gueltigkeit_von=date(2023, 1, 1),
gueltigkeit_bis=date(2023, 12, 31)
)
Vorgabe.objects.create(
order=2,
nummer=1, # Same nummer, thema, dokument
dokument=self.dokument,
thema=self.thema,
titel="Conflicting Vorgabe",
gueltigkeit_von=date(2023, 6, 1), # Overlaps
gueltigkeit_bis=date(2023, 8, 31)
)
out = StringIO()
call_command('sanity_check_vorgaben', stdout=out)
output = out.getvalue()
self.assertIn("Starting Vorgaben sanity check...", output)
self.assertIn("Found 1 conflicts:", output)
self.assertIn("R0066.O.1", output)
self.assertIn("intersecting validity periods", output)

View File

@@ -1,123 +0,0 @@
"""
Utility functions for Vorgaben sanity checking
"""
import datetime
from django.db.models import Count
from itertools import combinations
from dokumente.models import Vorgabe
def check_vorgabe_conflicts():
"""
Check for conflicts in Vorgaben.
Main rule: If there are two Vorgaben with the same number, Thema and Dokument,
their valid_from and valid_to date ranges shouldn't intersect.
Returns:
list: List of conflict dictionaries
"""
conflicts = []
# Find Vorgaben with same dokument, thema, and nummer
duplicate_groups = (
Vorgabe.objects.values('dokument', 'thema', 'nummer')
.annotate(count=Count('id'))
.filter(count__gt=1)
)
for group in duplicate_groups:
# Get all Vorgaben in this group
vorgaben = Vorgabe.objects.filter(
dokument=group['dokument'],
thema=group['thema'],
nummer=group['nummer']
)
# Check all pairs for date range intersections
for vorgabe1, vorgabe2 in combinations(vorgaben, 2):
if date_ranges_intersect(
vorgabe1.gueltigkeit_von, vorgabe1.gueltigkeit_bis,
vorgabe2.gueltigkeit_von, vorgabe2.gueltigkeit_bis
):
conflicts.append({
'vorgabe1': vorgabe1,
'vorgabe2': vorgabe2,
'conflict_type': 'date_range_intersection',
'message': f"Vorgaben {vorgabe1.Vorgabennummer()} and {vorgabe2.Vorgabennummer()} "
f"have intersecting validity periods"
})
return conflicts
def date_ranges_intersect(start1, end1, start2, end2):
"""
Check if two date ranges intersect.
None end date means open-ended range.
Args:
start1, start2: Start dates
end1, end2: End dates (can be None for open-ended)
Returns:
bool: True if ranges intersect
"""
# If either start date is None, treat it as invalid case
if not start1 or not start2:
return False
# If end date is None, treat it as far future
end1 = end1 or datetime.date.max
end2 = end2 or datetime.date.max
# Ranges intersect if start1 <= end2 and start2 <= end1
return start1 <= end2 and start2 <= end1
def format_conflict_report(conflicts, verbose=False):
"""
Format conflicts into a readable report.
Args:
conflicts: List of conflict dictionaries
verbose: Whether to show detailed information
Returns:
str: Formatted report
"""
if not conflicts:
return "✓ No conflicts found in Vorgaben"
lines = [f"Found {len(conflicts)} conflicts:"]
for i, conflict in enumerate(conflicts, 1):
lines.append(f"\n{i}. {conflict['message']}")
if verbose:
v1 = conflict['vorgabe1']
v2 = conflict['vorgabe2']
lines.append(f" Vorgabe 1: {v1.Vorgabennummer()}")
lines.append(f" Valid from: {v1.gueltigkeit_von} to {v1.gueltigkeit_bis or 'unlimited'}")
lines.append(f" Title: {v1.titel}")
lines.append(f" Vorgabe 2: {v2.Vorgabennummer()}")
lines.append(f" Valid from: {v2.gueltigkeit_von} to {v2.gueltigkeit_bis or 'unlimited'}")
lines.append(f" Title: {v2.titel}")
# Show the overlapping period
v1 = conflict['vorgabe1']
v2 = conflict['vorgabe2']
overlap_start = max(v1.gueltigkeit_von, v2.gueltigkeit_von)
overlap_end = min(
v1.gueltigkeit_bis or datetime.date.max,
v2.gueltigkeit_bis or datetime.date.max
)
if overlap_end != datetime.date.max:
lines.append(f" Overlap: {overlap_start} to {overlap_end}")
else:
lines.append(f" Overlap starts: {overlap_start} (no end)")
return "\n".join(lines)

14
k8s/helm-chart/Chart.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v2
name: vorgabenui
description: Helm chart for VorgabenUI Django application with Kroki diagram service
type: application
version: 0.1.0
appVersion: "0.939"
keywords:
- django
- kroki
- diagrams
- vorgabenui
maintainers:
- name: adebaumann
email: adebaumann@baumann.gr

View File

@@ -0,0 +1,51 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "vorgabenui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "vorgabenui.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "vorgabenui.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "vorgabenui.labels" -}}
helm.sh/chart: {{ include "vorgabenui.chart" . }}
{{ include "vorgabenui.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "vorgabenui.selectorLabels" -}}
app.kubernetes.io/name: {{ include "vorgabenui.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@@ -0,0 +1,59 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "vorgabenui.fullname" . }}
namespace: {{ .Values.global.namespace }}
labels:
{{- include "vorgabenui.labels" . | nindent 4 }}
app.kubernetes.io/component: django
spec:
replicas: {{ .Values.django.replicaCount }}
selector:
matchLabels:
{{- include "vorgabenui.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: django
template:
metadata:
labels:
{{- include "vorgabenui.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: django
spec:
securityContext:
{{- toYaml .Values.django.securityContext | nindent 8 }}
initContainers:
- name: loader
image: "{{ .Values.django.dataLoader.image.repository }}:{{ .Values.django.dataLoader.image.tag }}"
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: web
image: "{{ .Values.django.image.repository }}:{{ .Values.django.image.tag }}"
imagePullPolicy: {{ .Values.django.image.pullPolicy }}
ports:
- containerPort: {{ .Values.django.service.port }}
volumeMounts:
- name: data
mountPath: /app/data
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ include "vorgabenui.fullname" . }}-data-pvc

View File

@@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "vorgabenui.fullname" . }}
namespace: {{ .Values.global.namespace }}
labels:
{{- include "vorgabenui.labels" . | nindent 4 }}
app.kubernetes.io/component: django
spec:
type: {{ .Values.django.service.type }}
selector:
{{- include "vorgabenui.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: django
ports:
- port: {{ .Values.django.service.port }}
targetPort: {{ .Values.django.service.port }}

View File

@@ -0,0 +1,33 @@
{{- if .Values.ingress.enabled }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "vorgabenui.fullname" . }}
namespace: {{ .Values.global.namespace }}
labels:
{{- include "vorgabenui.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: {{ .Values.ingress.path }}
pathType: {{ .Values.ingress.pathType }}
backend:
service:
name: {{ include "vorgabenui.fullname" . }}
port:
number: {{ .Values.django.service.port }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,48 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "vorgabenui.fullname" . }}-kroki
namespace: {{ .Values.global.namespace }}
labels:
{{- include "vorgabenui.labels" . | nindent 4 }}
app.kubernetes.io/component: kroki
spec:
replicas: {{ .Values.kroki.replicaCount }}
selector:
matchLabels:
{{- include "vorgabenui.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: kroki
template:
metadata:
labels:
{{- include "vorgabenui.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: kroki
spec:
containers:
- name: kroki
image: "{{ .Values.kroki.image.repository }}:{{ .Values.kroki.image.tag }}"
ports:
- containerPort: {{ .Values.kroki.service.port }}
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
{{- range $service := .Values.kroki.services }}
- name: {{ $service }}
image: "{{ $service.image.repository }}:{{ $service.image.tag }}"
ports:
- containerPort: {{ $service.port }}
{{- end }}

View File

@@ -0,0 +1,16 @@
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "vorgabenui.fullname" . }}-kroki
namespace: {{ .Values.global.namespace }}
labels:
{{- include "vorgabenui.labels" . | nindent 4 }}
app.kubernetes.io/component: kroki
spec:
selector:
{{- include "vorgabenui.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: kroki
ports:
- port: {{ .Values.kroki.service.port }}
targetPort: {{ .Values.kroki.service.port }}

View File

@@ -0,0 +1,19 @@
{{- if .Values.persistence.enabled }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "vorgabenui.fullname" . }}-data-pvc
namespace: {{ .Values.global.namespace }}
labels:
{{- include "vorgabenui.labels" . | nindent 4 }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,77 @@
# Global settings
global:
namespace: vorgabenui
# Django application settings
django:
replicaCount: 1
image:
repository: git.baumann.gr/adebaumann/vui
tag: "0.939"
pullPolicy: Always
# Data loader init container
dataLoader:
image:
repository: git.baumann.gr/adebaumann/vui-data-loader
tag: "0.8"
# Security context
securityContext:
fsGroup: 999
fsGroupChangePolicy: "OnRootMismatch"
# Service settings
service:
type: ClusterIP
port: 8000
# Kroki diagram service settings
kroki:
replicaCount: 1
# Main kroki service
image:
repository: git.baumann.gr/adebaumann/kroki
tag: "0.026"
# Additional diagram services
services:
mermaid:
image:
repository: git.baumann.gr/adebaumann/kroki-mermaid
tag: "0.026"
port: 8002
bpmn:
image:
repository: git.baumann.gr/adebaumann/kroki-bpmn
tag: "0.026"
port: 8003
excalidraw:
image:
repository: git.baumann.gr/adebaumann/kroki-excalidraw
tag: "0.026"
port: 8004
# Service settings
service:
port: 8000
# Persistent storage
persistence:
enabled: true
storageClass: ""
accessModes:
- ReadWriteOnce
size: 2Gi
# Ingress settings
ingress:
enabled: true
className: ""
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
host: vorgabenportal.knowyoursecurity.com
path: /
pathType: Prefix
tls: []

View File

@@ -28,6 +28,6 @@
<div class="flex-fill">{% block content %}Main Content{% endblock %}</div>
<div class="col-md-2">{% block sidebar_right %}{% endblock %}</div>
</div>
<div>VorgabenUI v0.941</div>
<div>VorgabenUI v0.939</div>
</body>
</html>

View File

@@ -5,6 +5,7 @@
border-radius: 8px;
padding: 15px;
margin-bottom: 50px;
background-color: #f9f9f9;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

View File

@@ -1,58 +1,21 @@
window.addEventListener('load', function () {
setTimeout(() => {
// Try different selectors for nested admin vorgabe elements
const selectors = [
'.djn-dynamic-form-dokumente-vorgabe',
'.djn-dynamic-form-Standards-vorgabe',
'.inline-related[data-inline-type="stacked"]',
'.nested-inline'
];
let vorgabenBlocks = [];
for (const selector of selectors) {
vorgabenBlocks = document.querySelectorAll(selector);
if (vorgabenBlocks.length > 0) {
console.log("Found", vorgabenBlocks.length, "Vorgaben blocks with selector:", selector);
break;
}
}
if (vorgabenBlocks.length === 0) {
console.log("No Vorgaben blocks found, trying fallback...");
// Fallback: look for any inline with vorgabe in the class
vorgabenBlocks = document.querySelectorAll('[class*="vorgabe"]');
}
const vorgabenBlocks = document.querySelectorAll('.djn-dynamic-form-Standards-vorgabe');
console.log("Found", vorgabenBlocks.length, "Vorgaben blocks");
vorgabenBlocks.forEach((block, index) => {
// Find the existing title/header within the vorgabe block
const existingHeader = block.querySelector('h3, .inline-label, .module h2, .djn-inline-header');
const header = document.createElement('div');
header.className = 'vorgabe-toggle-header';
header.innerHTML = `▼ Vorgabe ${index + 1}`;
header.style.cursor = 'pointer';
if (existingHeader) {
// Make the existing header clickable for collapse/expand
existingHeader.style.cursor = 'pointer';
existingHeader.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
block.parentNode.insertBefore(header, block);
// Find all content to collapse - everything except the header itself
const allChildren = Array.from(block.children);
const contentElements = allChildren.filter(child => child !== existingHeader && !child.contains(existingHeader));
contentElements.forEach(element => {
const isHidden = element.style.display === 'none';
element.style.display = isHidden ? '' : 'none';
});
// Update the header text to show collapse state
const originalText = existingHeader.textContent.replace(/[▼▶]\s*/, '');
const anyHidden = contentElements.some(el => el.style.display === 'none');
existingHeader.innerHTML = `${anyHidden ? '▶' : '▼'} ${originalText}`;
});
// Add initial collapse indicator
const originalText = existingHeader.textContent;
existingHeader.innerHTML = `${originalText}`;
}
header.addEventListener('click', () => {
const isHidden = block.style.display === 'none';
block.style.display = isHidden ? '' : 'none';
header.innerHTML = `${isHidden ? '▼' : '▶'} Vorgabe ${index + 1}`;
});
});
}, 1000); // wait longer to allow nested inlines to render
}, 500); // wait 500ms to allow nested inlines to render
});

View File

@@ -1,38 +0,0 @@
#!/usr/bin/env python
"""
Simple script to test Vorgaben sanity checking
"""
import os
import sys
import django
# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'VorgabenUI.settings')
django.setup()
from dokumente.utils import check_vorgabe_conflicts, format_conflict_report
def main():
print("Running Vorgaben sanity check...")
print("=" * 50)
# Check for conflicts
conflicts = check_vorgabe_conflicts()
# Generate and display report
report = format_conflict_report(conflicts, verbose=True)
print(report)
print("=" * 50)
if conflicts:
print(f"\n⚠️ Found {len(conflicts)} conflicts that need attention!")
sys.exit(1)
else:
print("✅ All Vorgaben are valid!")
sys.exit(0)
if __name__ == "__main__":
main()