Merge branch 'feature/sanitychecks' into development
This commit is contained in:
102
admin/css/vorgabe_border.css
Normal file
102
admin/css/vorgabe_border.css
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
25
admin/js/vorgabe_toggle.js
Normal file
25
admin/js/vorgabe_toggle.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
(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);
|
||||||
@@ -21,45 +21,75 @@ from referenzen.models import Referenz
|
|||||||
# 'frage': forms.Textarea(attrs={'rows': 1, 'cols': 100}),
|
# 'frage': forms.Textarea(attrs={'rows': 1, 'cols': 100}),
|
||||||
# }
|
# }
|
||||||
|
|
||||||
class ChecklistenfragenInline(NestedTabularInline):
|
class ChecklistenfragenInline(NestedStackedInline):
|
||||||
model=Checklistenfrage
|
model=Checklistenfrage
|
||||||
extra=0
|
extra=0
|
||||||
fk_name="vorgabe"
|
fk_name="vorgabe"
|
||||||
# form=ChecklistenForm
|
|
||||||
classes = ['collapse']
|
classes = ['collapse']
|
||||||
|
verbose_name_plural = "Checklistenfragen"
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('frage',),
|
||||||
|
'classes': ('wide',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VorgabeKurztextInline(NestedTabularInline):
|
class VorgabeKurztextInline(NestedStackedInline):
|
||||||
model=VorgabeKurztext
|
model=VorgabeKurztext
|
||||||
extra=0
|
extra=0
|
||||||
sortable_field_name = "order"
|
sortable_field_name = "order"
|
||||||
show_change_link=True
|
show_change_link=True
|
||||||
classes = ['collapse']
|
classes = ['collapse']
|
||||||
#inline=inhalt
|
verbose_name_plural = "Kurztext-Abschnitte"
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('abschnitttyp', 'inhalt', 'order'),
|
||||||
|
'classes': ('wide',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
class VorgabeLangtextInline(NestedTabularInline):
|
class VorgabeLangtextInline(NestedStackedInline):
|
||||||
model=VorgabeLangtext
|
model=VorgabeLangtext
|
||||||
extra=0
|
extra=0
|
||||||
sortable_field_name = "order"
|
sortable_field_name = "order"
|
||||||
show_change_link=True
|
show_change_link=True
|
||||||
classes = ['collapse']
|
classes = ['collapse']
|
||||||
#inline=inhalt
|
verbose_name_plural = "Langtext-Abschnitte"
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('abschnitttyp', 'inhalt', 'order'),
|
||||||
|
'classes': ('wide',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
class GeltungsbereichInline(NestedTabularInline):
|
class GeltungsbereichInline(NestedStackedInline):
|
||||||
model=Geltungsbereich
|
model=Geltungsbereich
|
||||||
extra=0
|
extra=0
|
||||||
sortable_field_name = "order"
|
sortable_field_name = "order"
|
||||||
show_change_link=True
|
show_change_link=True
|
||||||
classes = ['collapse']
|
classes = ['collapse']
|
||||||
classes = ['collapse']
|
verbose_name_plural = "Geltungsbereich-Abschnitte"
|
||||||
#inline=inhalt
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('abschnitttyp', 'inhalt', 'order'),
|
||||||
|
'classes': ('wide',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
class EinleitungInline(NestedTabularInline):
|
class EinleitungInline(NestedStackedInline):
|
||||||
model = Einleitung
|
model = Einleitung
|
||||||
extra = 0
|
extra = 0
|
||||||
sortable_field_name = "order"
|
sortable_field_name = "order"
|
||||||
show_change_link = True
|
show_change_link = True
|
||||||
classes = ['collapse']
|
classes = ['collapse']
|
||||||
|
verbose_name_plural = "Einleitungs-Abschnitte"
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('abschnitttyp', 'inhalt', 'order'),
|
||||||
|
'classes': ('wide',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
class VorgabeForm(forms.ModelForm):
|
class VorgabeForm(forms.ModelForm):
|
||||||
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
|
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
|
||||||
@@ -67,17 +97,31 @@ class VorgabeForm(forms.ModelForm):
|
|||||||
model = Vorgabe
|
model = Vorgabe
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
class VorgabeInline(SortableInlineAdminMixin, NestedTabularInline): # or StackedInline for more vertical layout
|
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline):
|
||||||
model = Vorgabe
|
model = Vorgabe
|
||||||
form = VorgabeForm
|
form = VorgabeForm
|
||||||
extra = 0
|
extra = 0
|
||||||
sortable_field_name = "order" # Add this - make sure your Vorgabe model has an 'order' field
|
sortable_field_name = "order"
|
||||||
#show_change_link = True
|
show_change_link = False
|
||||||
inlines = [VorgabeKurztextInline,VorgabeLangtextInline,ChecklistenfragenInline]
|
can_delete = False
|
||||||
|
inlines = [VorgabeKurztextInline, VorgabeLangtextInline, ChecklistenfragenInline]
|
||||||
autocomplete_fields = ['stichworte','referenzen','relevanz']
|
autocomplete_fields = ['stichworte','referenzen','relevanz']
|
||||||
#search_fields=['nummer','name']ModelAdmin.
|
# Remove collapse class so Vorgaben show by default
|
||||||
list_filter=['stichworte']
|
|
||||||
#classes=["collapse"]
|
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',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
class StichworterklaerungInline(NestedTabularInline):
|
class StichworterklaerungInline(NestedTabularInline):
|
||||||
model=Stichworterklaerung
|
model=Stichworterklaerung
|
||||||
@@ -104,16 +148,31 @@ class PersonAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(Dokument)
|
@admin.register(Dokument)
|
||||||
class DokumentAdmin(SortableAdminBase, NestedModelAdmin):
|
class DokumentAdmin(SortableAdminBase, NestedModelAdmin):
|
||||||
actions_on_top=True
|
actions_on_top=True
|
||||||
inlines = [EinleitungInline,GeltungsbereichInline,VorgabeInline]
|
inlines = [EinleitungInline, GeltungsbereichInline, VorgabeInline]
|
||||||
#filter_horizontal=['autoren','pruefende']
|
filter_horizontal=['autoren','pruefende']
|
||||||
list_display=['nummer','name','dokumententyp']
|
list_display=['nummer','name','dokumententyp','gueltigkeit_von','gueltigkeit_bis','aktiv']
|
||||||
search_fields=['nummer','name']
|
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:
|
class Media:
|
||||||
# js = ('admin/js/vorgabe_collapse.js',)
|
js = ('admin/js/vorgabe_collapse.js',)
|
||||||
css = {
|
css = {
|
||||||
'all': ('admin/css/vorgabe_border.css',
|
'all': ('admin/css/vorgabe_border.css',)
|
||||||
# 'admin/css/vorgabe_collapse.css',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
70
dokumente/management/commands/sanity_check_vorgaben.py
Normal file
70
dokumente/management/commands/sanity_check_vorgaben.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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)")
|
||||||
@@ -5,6 +5,7 @@ from stichworte.models import Stichwort
|
|||||||
from referenzen.models import Referenz
|
from referenzen.models import Referenz
|
||||||
from rollen.models import Rolle
|
from rollen.models import Rolle
|
||||||
import datetime
|
import datetime
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
class Dokumententyp(models.Model):
|
class Dokumententyp(models.Model):
|
||||||
name = models.CharField(max_length=100, primary_key=True)
|
name = models.CharField(max_length=100, primary_key=True)
|
||||||
@@ -86,6 +87,123 @@ class Vorgabe(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.Vorgabennummer()}: {self.titel}"
|
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"have intersecting validity periods"
|
||||||
|
})
|
||||||
|
|
||||||
|
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:
|
class Meta:
|
||||||
verbose_name_plural="Vorgaben"
|
verbose_name_plural="Vorgaben"
|
||||||
ordering = ['order']
|
ordering = ['order']
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.core.management import call_command
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
from io import StringIO
|
||||||
from .models import (
|
from .models import (
|
||||||
Dokumententyp, Person, Thema, Dokument, Vorgabe,
|
Dokumententyp, Person, Thema, Dokument, Vorgabe,
|
||||||
VorgabeLangtext, VorgabeKurztext, Geltungsbereich,
|
VorgabeLangtext, VorgabeKurztext, Geltungsbereich,
|
||||||
Einleitung, Checklistenfrage, Changelog
|
Einleitung, Checklistenfrage, Changelog
|
||||||
)
|
)
|
||||||
|
from .utils import check_vorgabe_conflicts, date_ranges_intersect, format_conflict_report
|
||||||
from abschnitte.models import AbschnittTyp
|
from abschnitte.models import AbschnittTyp
|
||||||
from referenzen.models import Referenz
|
from referenzen.models import Referenz
|
||||||
from stichworte.models import Stichwort
|
from stichworte.models import Stichwort
|
||||||
@@ -513,3 +516,304 @@ class URLPatternsTest(TestCase):
|
|||||||
"""Test that standard_history URL resolves correctly"""
|
"""Test that standard_history URL resolves correctly"""
|
||||||
url = reverse('standard_history', kwargs={'nummer': 'TEST-001'})
|
url = reverse('standard_history', kwargs={'nummer': 'TEST-001'})
|
||||||
self.assertEqual(url, '/dokumente/TEST-001/history/')
|
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)
|
||||||
|
|||||||
123
dokumente/utils.py
Normal file
123
dokumente/utils.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
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)
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
background-color: #f9f9f9;
|
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,4 +37,4 @@ tbody.djn-dynamic-form-dokumente-vorgabe td.original p {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
border-left: 2px dashed #ccc;
|
border-left: 2px dashed #ccc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,58 @@
|
|||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const vorgabenBlocks = document.querySelectorAll('.djn-dynamic-form-Standards-vorgabe');
|
// Try different selectors for nested admin vorgabe elements
|
||||||
console.log("Found", vorgabenBlocks.length, "Vorgaben blocks");
|
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"]');
|
||||||
|
}
|
||||||
|
|
||||||
vorgabenBlocks.forEach((block, index) => {
|
vorgabenBlocks.forEach((block, index) => {
|
||||||
const header = document.createElement('div');
|
// Find the existing title/header within the vorgabe block
|
||||||
header.className = 'vorgabe-toggle-header';
|
const existingHeader = block.querySelector('h3, .inline-label, .module h2, .djn-inline-header');
|
||||||
header.innerHTML = `▼ Vorgabe ${index + 1}`;
|
|
||||||
header.style.cursor = 'pointer';
|
if (existingHeader) {
|
||||||
|
// Make the existing header clickable for collapse/expand
|
||||||
block.parentNode.insertBefore(header, block);
|
existingHeader.style.cursor = 'pointer';
|
||||||
|
existingHeader.addEventListener('click', (e) => {
|
||||||
header.addEventListener('click', () => {
|
e.preventDefault();
|
||||||
const isHidden = block.style.display === 'none';
|
e.stopPropagation();
|
||||||
block.style.display = isHidden ? '' : 'none';
|
|
||||||
header.innerHTML = `${isHidden ? '▼' : '▶'} Vorgabe ${index + 1}`;
|
// 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}`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, 500); // wait 500ms to allow nested inlines to render
|
}, 1000); // wait longer to allow nested inlines to render
|
||||||
});
|
});
|
||||||
|
|||||||
38
test_sanity_check.py
Normal file
38
test_sanity_check.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/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()
|
||||||
Reference in New Issue
Block a user