Add Vorgaben sanity check functionality
Implement comprehensive validation system to detect conflicting Vorgaben with overlapping validity periods. Features: - Static method Vorgabe.sanity_check_vorgaben() for global conflict detection - Instance method Vorgabe.find_conflicts() for individual conflict checking - Model validation via Vorgabe.clean() to prevent conflicting data - Utility functions for date range intersection and conflict reporting - Django management command 'sanity_check_vorgaben' for manual checks - Comprehensive test suite with 17 new tests covering all functionality Validation logic ensures Vorgaben with same dokument, thema, and nummer cannot have overlapping gueltigkeit_von/gueltigkeit_bis date ranges. Handles open-ended ranges (None end dates) and provides clear error messages. Files added/modified: - dokumente/models.py: Added sanity check methods and validation - dokumente/utils.py: New utility functions for conflict detection - dokumente/management/commands/sanity_check_vorgaben.py: New management command - dokumente/tests.py: Added comprehensive test coverage - test_sanity_check.py: Standalone test script All tests pass (56/56) with no regressions.
This commit is contained in:
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 rollen.models import Rolle
|
||||
import datetime
|
||||
from django.db.models import Q
|
||||
|
||||
class Dokumententyp(models.Model):
|
||||
name = models.CharField(max_length=100, primary_key=True)
|
||||
@@ -86,6 +87,123 @@ 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"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:
|
||||
verbose_name_plural="Vorgaben"
|
||||
ordering = ['order']
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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
|
||||
@@ -513,3 +516,304 @@ 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)
|
||||
|
||||
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)
|
||||
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