Compare commits

..

1 Commits

Author SHA1 Message Date
27b0f62274 Readme added 2025-11-04 16:53:31 +01:00
40 changed files with 341 additions and 8453 deletions

10
.gitignore vendored
View File

@@ -10,9 +10,13 @@ keys/
.idea/ .idea/
*.kate-swp *.kate-swp
<<<<<<< HEAD
# Diagram cache directory
media/diagram_cache/
=======
.env
node_modules/ node_modules/
package-lock.json package-lock.json
package.json package.json
# Diagram cache directory >>>>>>> 299c046 (Readme added)
media/diagram_cache/
.env

1599
R0066.json

File diff suppressed because it is too large Load Diff

View File

@@ -2,5 +2,8 @@
There are examples for importing text in the "Documentation"-directory. Actual documentation follows. There are examples for importing text in the "Documentation"-directory. Actual documentation follows.
<<<<<<< HEAD
Documentation on Confluence so far. Documentation on Confluence so far.
This commit should be signed. This commit should be signed.
=======
>>>>>>> 299c046 (Readme added)

View File

@@ -26,8 +26,8 @@ import referenzen.views
admin.site.site_header="Autorenumgebung" admin.site.site_header="Autorenumgebung"
urlpatterns = [ urlpatterns = [
path('',pages.views.startseite, name='startseite'), path('',pages.views.startseite),
path('search/',pages.views.search, name='search'), path('search/',pages.views.search),
path('dokumente/', include("dokumente.urls")), path('dokumente/', include("dokumente.urls")),
path('autorenumgebung/', admin.site.urls), path('autorenumgebung/', admin.site.urls),
path('stichworte/', include("stichworte.urls")), path('stichworte/', include("stichworte.urls")),

View File

@@ -1,820 +1,3 @@
from django.test import TestCase, TransactionTestCase from django.test import TestCase
from django.core.management import call_command
from django.conf import settings
from django.core.files.storage import default_storage
from unittest.mock import patch, Mock, MagicMock
from io import StringIO
import os
import hashlib
import tempfile
import shutil
from .models import AbschnittTyp, Textabschnitt # Create your tests here.
from .utils import render_textabschnitte, md_table_to_html
from diagramm_proxy.diagram_cache import (
get_cached_diagram, compute_hash, get_cache_path, clear_cache
)
class AbschnittTypModelTest(TestCase):
"""Test cases for AbschnittTyp model"""
def setUp(self):
"""Set up test data"""
self.abschnitttyp = AbschnittTyp.objects.create(
abschnitttyp="text"
)
def test_abschnitttyp_creation(self):
"""Test that AbschnittTyp is created correctly"""
self.assertEqual(self.abschnitttyp.abschnitttyp, "text")
def test_abschnitttyp_str(self):
"""Test string representation of AbschnittTyp"""
self.assertEqual(str(self.abschnitttyp), "text")
def test_abschnitttyp_verbose_name_plural(self):
"""Test verbose name plural"""
self.assertEqual(
AbschnittTyp._meta.verbose_name_plural,
"Abschnitttypen"
)
def test_abschnitttyp_primary_key(self):
"""Test that abschnitttyp field is the primary key"""
pk_field = AbschnittTyp._meta.pk
self.assertEqual(pk_field.name, 'abschnitttyp')
def test_create_multiple_abschnitttypen(self):
"""Test creating multiple AbschnittTyp objects"""
types = ['liste ungeordnet', 'liste geordnet', 'tabelle', 'diagramm', 'code']
for typ in types:
AbschnittTyp.objects.create(abschnitttyp=typ)
self.assertEqual(AbschnittTyp.objects.count(), 6) # Including setUp type
class TextabschnittModelTest(TestCase):
"""Test cases for Textabschnitt abstract model using VorgabeLangtext"""
def setUp(self):
"""Set up test data"""
from dokumente.models import Dokumententyp, Dokument, Vorgabe, VorgabeLangtext, Thema
from datetime import date
self.typ_text = AbschnittTyp.objects.create(abschnitttyp="text")
self.typ_code = AbschnittTyp.objects.create(abschnitttyp="code")
# Create required dokumente objects
self.dokumententyp = Dokumententyp.objects.create(
name="Test Type", verantwortliche_ve="TEST"
)
self.dokument = Dokument.objects.create(
nummer="TEST-001",
name="Test Doc",
dokumententyp=self.dokumententyp,
aktiv=True
)
self.thema = Thema.objects.create(name="Test Thema")
self.vorgabe = Vorgabe.objects.create(
dokument=self.dokument,
nummer=1,
order=1,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date.today()
)
def test_textabschnitt_creation(self):
"""Test that Textabschnitt can be instantiated via concrete model"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="Test content",
order=1
)
self.assertEqual(abschnitt.abschnitttyp, self.typ_text)
self.assertEqual(abschnitt.inhalt, "Test content")
self.assertEqual(abschnitt.order, 1)
def test_textabschnitt_default_order(self):
"""Test that order defaults to 0"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="Test"
)
self.assertEqual(abschnitt.order, 0)
def test_textabschnitt_blank_fields(self):
"""Test that abschnitttyp and inhalt can be blank/null"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe
)
self.assertIsNone(abschnitt.abschnitttyp)
self.assertIsNone(abschnitt.inhalt)
def test_textabschnitt_ordering(self):
"""Test that Textabschnitte can be ordered"""
from dokumente.models import VorgabeLangtext
abschnitt1 = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="First",
order=2
)
abschnitt2 = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="Second",
order=1
)
abschnitt3 = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="Third",
order=3
)
ordered = VorgabeLangtext.objects.filter(abschnitt=self.vorgabe).order_by('order')
self.assertEqual(list(ordered), [abschnitt2, abschnitt1, abschnitt3])
def test_textabschnitt_foreign_key_protection(self):
"""Test that AbschnittTyp is protected from deletion"""
from dokumente.models import VorgabeLangtext
from django.db.models import ProtectedError
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="Test"
)
# Try to delete the AbschnittTyp
with self.assertRaises(ProtectedError):
self.typ_text.delete()
class RenderTextabschnitteTest(TestCase):
"""Test cases for render_textabschnitte function"""
def setUp(self):
"""Set up test data"""
from dokumente.models import Dokumententyp, Dokument, Vorgabe, VorgabeLangtext, Thema
from datetime import date
self.typ_text = AbschnittTyp.objects.create(abschnitttyp="text")
self.typ_unordered = AbschnittTyp.objects.create(abschnitttyp="liste ungeordnet")
self.typ_ordered = AbschnittTyp.objects.create(abschnitttyp="liste geordnet")
self.typ_table = AbschnittTyp.objects.create(abschnitttyp="tabelle")
self.typ_code = AbschnittTyp.objects.create(abschnitttyp="code")
self.typ_diagram = AbschnittTyp.objects.create(abschnitttyp="diagramm")
# Create required dokumente objects
self.dokumententyp = Dokumententyp.objects.create(
name="Test Type", verantwortliche_ve="TEST"
)
self.dokument = Dokument.objects.create(
nummer="TEST-001",
name="Test Doc",
dokumententyp=self.dokumententyp,
aktiv=True
)
self.thema = Thema.objects.create(name="Test Thema")
self.vorgabe = Vorgabe.objects.create(
dokument=self.dokument,
nummer=1,
order=1,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date.today()
)
def test_render_empty_queryset(self):
"""Test rendering an empty queryset"""
from dokumente.models import VorgabeLangtext
result = render_textabschnitte(VorgabeLangtext.objects.none())
self.assertEqual(result, [])
def test_render_text_markdown(self):
"""Test rendering plain text with markdown"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="# Heading\n\nThis is **bold** text.",
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
self.assertEqual(len(result), 1)
typ, html = result[0]
self.assertEqual(typ, "text")
self.assertIn("<h1>Heading</h1>", html)
self.assertIn("<strong>bold</strong>", html)
def test_render_text_with_footnotes(self):
"""Test rendering text with footnotes"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="This is text[^1].\n\n[^1]: This is a footnote.",
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertIn("footnote", html.lower())
def test_render_unordered_list(self):
"""Test rendering unordered list"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_unordered,
inhalt="Item 1\nItem 2\nItem 3",
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertEqual(typ, "liste ungeordnet")
self.assertIn("<ul>", html)
self.assertIn("<li>Item 1</li>", html)
self.assertIn("<li>Item 2</li>", html)
self.assertIn("<li>Item 3</li>", html)
def test_render_ordered_list(self):
"""Test rendering ordered list"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_ordered,
inhalt="First item\nSecond item\nThird item",
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertEqual(typ, "liste geordnet")
self.assertIn("<ol>", html)
self.assertIn("<li>First item</li>", html)
self.assertIn("<li>Second item</li>", html)
self.assertIn("<li>Third item</li>", html)
def test_render_table(self):
"""Test rendering table"""
from dokumente.models import VorgabeLangtext
table_content = """| Header 1 | Header 2 |
|----------|----------|
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |"""
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_table,
inhalt=table_content,
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertEqual(typ, "tabelle")
self.assertIn('<table class="table table-bordered table-hover">', html)
self.assertIn("<thead>", html)
self.assertIn("<th>Header 1</th>", html)
self.assertIn("<th>Header 2</th>", html)
self.assertIn("<tbody>", html)
self.assertIn("<td>Cell 1</td>", html)
self.assertIn("<td>Cell 2</td>", html)
def test_render_code_block(self):
"""Test rendering code block"""
from dokumente.models import VorgabeLangtext
code_content = "def hello():\n print('Hello, World!')"
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_code,
inhalt=code_content,
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertEqual(typ, "code")
self.assertIn("<pre><code>", html)
self.assertIn("</code></pre>", html)
self.assertIn("hello", html)
@patch('abschnitte.utils.get_cached_diagram')
def test_render_diagram_success(self, mock_get_cached):
"""Test rendering diagram with successful caching"""
from dokumente.models import VorgabeLangtext
mock_get_cached.return_value = "diagram_cache/plantuml/abc123.svg"
diagram_content = """plantuml
@startuml
Alice -> Bob: Hello
@enduml"""
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_diagram,
inhalt=diagram_content,
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertEqual(typ, "diagramm")
self.assertIn('<img', html)
self.assertIn('width="100%"', html)
self.assertIn('diagram_cache/plantuml/abc123.svg', html)
# Verify get_cached_diagram was called correctly
mock_get_cached.assert_called_once()
args = mock_get_cached.call_args[0]
self.assertEqual(args[0], "plantuml")
self.assertIn("Alice -> Bob", args[1])
@patch('abschnitte.utils.get_cached_diagram')
def test_render_diagram_with_options(self, mock_get_cached):
"""Test rendering diagram with custom options"""
from dokumente.models import VorgabeLangtext
mock_get_cached.return_value = "diagram_cache/mermaid/xyz789.svg"
diagram_content = """mermaid
option: width="50%" height="300px"
graph TD
A-->B"""
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_diagram,
inhalt=diagram_content,
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertIn('width="50%"', html)
self.assertIn('height="300px"', html)
@patch('abschnitte.utils.get_cached_diagram')
def test_render_diagram_error(self, mock_get_cached):
"""Test rendering diagram when caching fails"""
from dokumente.models import VorgabeLangtext
mock_get_cached.side_effect = Exception("Connection error")
diagram_content = """plantuml
@startuml
A -> B
@enduml"""
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_diagram,
inhalt=diagram_content,
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertIn("Error generating diagram", html)
self.assertIn("Connection error", html)
self.assertIn('class="text-danger"', html)
def test_render_multiple_abschnitte(self):
"""Test rendering multiple Textabschnitte in order"""
from dokumente.models import VorgabeLangtext
abschnitt1 = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="First section",
order=1
)
abschnitt2 = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_unordered,
inhalt="Item 1\nItem 2",
order=2
)
abschnitt3 = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_code,
inhalt="print('hello')",
order=3
)
result = render_textabschnitte(
VorgabeLangtext.objects.filter(abschnitt=self.vorgabe).order_by('order')
)
self.assertEqual(len(result), 3)
self.assertEqual(result[0][0], "text")
self.assertEqual(result[1][0], "liste ungeordnet")
self.assertEqual(result[2][0], "code")
def test_render_abschnitt_without_type(self):
"""Test rendering Textabschnitt without AbschnittTyp"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=None,
inhalt="Content without type",
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
typ, html = result[0]
self.assertEqual(typ, '')
self.assertIn("Content without type", html)
def test_render_abschnitt_with_empty_content(self):
"""Test rendering Textabschnitt with empty content"""
from dokumente.models import VorgabeLangtext
abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt=None,
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(abschnitt=self.vorgabe))
self.assertEqual(len(result), 1)
typ, html = result[0]
self.assertEqual(typ, "text")
class MdTableToHtmlTest(TestCase):
"""Test cases for md_table_to_html function"""
def test_simple_table(self):
"""Test converting a simple markdown table to HTML"""
md = """| Name | Age |
|------|-----|
| John | 30 |
| Jane | 25 |"""
html = md_table_to_html(md)
self.assertIn('<table class="table table-bordered table-hover">', html)
self.assertIn("<thead>", html)
self.assertIn("<th>Name</th>", html)
self.assertIn("<th>Age</th>", html)
self.assertIn("<tbody>", html)
self.assertIn("<td>John</td>", html)
self.assertIn("<td>30</td>", html)
self.assertIn("<td>Jane</td>", html)
self.assertIn("<td>25</td>", html)
def test_table_with_multiple_rows(self):
"""Test table with multiple rows"""
md = """| A | B | C |
|---|---|---|
| 1 | 2 | 3 |
| 4 | 5 | 6 |
| 7 | 8 | 9 |"""
html = md_table_to_html(md)
self.assertEqual(html.count("<tr>"), 4) # 1 header + 3 body rows
self.assertEqual(html.count("<td>"), 9) # 3x3 cells
self.assertEqual(html.count("<th>"), 3) # 3 headers
def test_table_with_spaces(self):
"""Test table with extra spaces"""
md = """ | Header 1 | Header 2 |
| --------- | ---------- |
| Value 1 | Value 2 | """
html = md_table_to_html(md)
self.assertIn("<th>Header 1</th>", html)
self.assertIn("<th>Header 2</th>", html)
self.assertIn("<td>Value 1</td>", html)
self.assertIn("<td>Value 2</td>", html)
def test_table_with_empty_cells(self):
"""Test table with empty cells"""
md = """| Col1 | Col2 | Col3 |
|------|------|------|
| A | | C |
| | B | |"""
html = md_table_to_html(md)
self.assertIn("<td>A</td>", html)
self.assertIn("<td></td>", html)
self.assertIn("<td>C</td>", html)
self.assertIn("<td>B</td>", html)
def test_table_insufficient_lines(self):
"""Test that ValueError is raised for insufficient lines"""
md = """| Header |"""
with self.assertRaises(ValueError) as context:
md_table_to_html(md)
self.assertIn("at least header + separator", str(context.exception))
def test_table_empty_string(self):
"""Test that ValueError is raised for empty string"""
with self.assertRaises(ValueError):
md_table_to_html("")
def test_table_only_whitespace(self):
"""Test that ValueError is raised for only whitespace"""
with self.assertRaises(ValueError):
md_table_to_html(" \n \n ")
class DiagramCacheTest(TestCase):
"""Test cases for diagram caching functionality"""
def setUp(self):
"""Set up test environment"""
# Create a temporary directory for testing
self.test_media_root = tempfile.mkdtemp()
self.original_media_root = settings.MEDIA_ROOT
settings.MEDIA_ROOT = self.test_media_root
def tearDown(self):
"""Clean up test environment"""
# Restore original settings
settings.MEDIA_ROOT = self.original_media_root
# Remove test directory
if os.path.exists(self.test_media_root):
shutil.rmtree(self.test_media_root)
def test_compute_hash(self):
"""Test that compute_hash generates consistent SHA256 hashes"""
content1 = "test content"
content2 = "test content"
content3 = "different content"
hash1 = compute_hash(content1)
hash2 = compute_hash(content2)
hash3 = compute_hash(content3)
# Same content should produce same hash
self.assertEqual(hash1, hash2)
# Different content should produce different hash
self.assertNotEqual(hash1, hash3)
# Hash should be 64 characters (SHA256 hex)
self.assertEqual(len(hash1), 64)
def test_get_cache_path(self):
"""Test that get_cache_path generates correct paths"""
diagram_type = "plantuml"
content_hash = "abc123"
path = get_cache_path(diagram_type, content_hash)
self.assertIn("diagram_cache", path)
self.assertIn("plantuml", path)
self.assertIn("abc123.svg", path)
@patch('diagramm_proxy.diagram_cache.requests.post')
@patch('diagramm_proxy.diagram_cache.default_storage')
def test_get_cached_diagram_miss(self, mock_storage, mock_post):
"""Test diagram generation on cache miss"""
# Setup mocks
mock_storage.exists.return_value = False
mock_storage.path.return_value = os.path.join(
self.test_media_root, 'diagram_cache/plantuml/test.svg'
)
mock_response = Mock()
mock_response.content = b'<svg>test</svg>'
mock_post.return_value = mock_response
diagram_content = "@startuml\nA -> B\n@enduml"
# Call function
result = get_cached_diagram("plantuml", diagram_content)
# Verify POST request was made
mock_post.assert_called_once()
call_args = mock_post.call_args
# Check URL in positional args (first argument)
self.assertIn("plantuml/svg", call_args[0][0])
# Verify storage.save was called
mock_storage.save.assert_called_once()
@patch('diagramm_proxy.diagram_cache.default_storage')
def test_get_cached_diagram_hit(self, mock_storage):
"""Test diagram retrieval on cache hit"""
# Setup mock - diagram exists in cache
mock_storage.exists.return_value = True
diagram_content = "@startuml\nA -> B\n@enduml"
# Call function
result = get_cached_diagram("plantuml", diagram_content)
# Verify no save was attempted (cache hit)
mock_storage.save.assert_not_called()
# Verify result contains expected path elements
self.assertIn("diagram_cache", result)
self.assertIn("plantuml", result)
self.assertIn(".svg", result)
@patch('diagramm_proxy.diagram_cache.requests.post')
@patch('diagramm_proxy.diagram_cache.default_storage')
def test_get_cached_diagram_request_error(self, mock_storage, mock_post):
"""Test that request errors are properly raised"""
import requests
mock_storage.exists.return_value = False
mock_storage.path.return_value = os.path.join(
self.test_media_root, 'diagram_cache/plantuml/test.svg'
)
mock_post.side_effect = requests.RequestException("Connection error")
with self.assertRaises(requests.RequestException):
get_cached_diagram("plantuml", "@startuml\nA -> B\n@enduml")
@patch('diagramm_proxy.diagram_cache.default_storage')
def test_clear_cache_specific_type(self, mock_storage):
"""Test clearing cache for specific diagram type"""
# Create real test cache structure for this test
cache_dir = os.path.join(self.test_media_root, 'diagram_cache', 'plantuml')
os.makedirs(cache_dir, exist_ok=True)
# Create test files
test_file1 = os.path.join(cache_dir, 'test1.svg')
test_file2 = os.path.join(cache_dir, 'test2.svg')
open(test_file1, 'w').close()
open(test_file2, 'w').close()
# Mock storage methods
mock_storage.exists.return_value = True
mock_storage.path.return_value = cache_dir
# Clear cache
clear_cache('plantuml')
# Verify files are deleted
self.assertFalse(os.path.exists(test_file1))
self.assertFalse(os.path.exists(test_file2))
@patch('diagramm_proxy.diagram_cache.default_storage')
def test_clear_cache_all_types(self, mock_storage):
"""Test clearing cache for all diagram types"""
# Create real test cache structure with multiple types
cache_root = os.path.join(self.test_media_root, 'diagram_cache')
for diagram_type in ['plantuml', 'mermaid', 'graphviz']:
cache_dir = os.path.join(cache_root, diagram_type)
os.makedirs(cache_dir, exist_ok=True)
test_file = os.path.join(cache_dir, 'test.svg')
open(test_file, 'w').close()
# Mock storage methods
mock_storage.exists.return_value = True
mock_storage.path.return_value = cache_root
# Clear all cache
clear_cache()
# Verify all files are deleted
for diagram_type in ['plantuml', 'mermaid', 'graphviz']:
test_file = os.path.join(cache_root, diagram_type, 'test.svg')
self.assertFalse(os.path.exists(test_file))
class ClearDiagramCacheCommandTest(TestCase):
"""Test cases for clear_diagram_cache management command"""
def setUp(self):
"""Set up test environment"""
self.test_media_root = tempfile.mkdtemp()
self.original_media_root = settings.MEDIA_ROOT
settings.MEDIA_ROOT = self.test_media_root
def tearDown(self):
"""Clean up test environment"""
settings.MEDIA_ROOT = self.original_media_root
if os.path.exists(self.test_media_root):
shutil.rmtree(self.test_media_root)
def test_command_without_type(self):
"""Test running command without specifying type"""
# Create test cache
cache_dir = os.path.join(self.test_media_root, 'diagram_cache', 'plantuml')
os.makedirs(cache_dir, exist_ok=True)
test_file = os.path.join(cache_dir, 'test.svg')
open(test_file, 'w').close()
# Run command
out = StringIO()
call_command('clear_diagram_cache', stdout=out)
# Check output
self.assertIn('Clearing all diagram caches', out.getvalue())
self.assertIn('Cache cleared successfully', out.getvalue())
def test_command_with_type(self):
"""Test running command with specific diagram type"""
# Create test cache
cache_dir = os.path.join(self.test_media_root, 'diagram_cache', 'mermaid')
os.makedirs(cache_dir, exist_ok=True)
test_file = os.path.join(cache_dir, 'test.svg')
open(test_file, 'w').close()
# Run command
out = StringIO()
call_command('clear_diagram_cache', type='mermaid', stdout=out)
# Check output
self.assertIn('Clearing cache for mermaid', out.getvalue())
self.assertIn('Cache cleared successfully', out.getvalue())
class IntegrationTest(TestCase):
"""Integration tests with actual dokumente models"""
def setUp(self):
"""Set up test data using dokumente models"""
from dokumente.models import (
Dokumententyp, Dokument, Vorgabe, VorgabeLangtext, Thema
)
from datetime import date
# Create required objects
self.dokumententyp = Dokumententyp.objects.create(
name="Test Policy",
verantwortliche_ve="TEST"
)
self.dokument = Dokument.objects.create(
nummer="TEST-001",
name="Test Document",
dokumententyp=self.dokumententyp,
aktiv=True
)
self.thema = Thema.objects.create(name="Test Thema")
self.vorgabe = Vorgabe.objects.create(
dokument=self.dokument,
nummer=1,
order=1,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date.today()
)
# Create AbschnittTypen
self.typ_text = AbschnittTyp.objects.create(abschnitttyp="text")
# Create VorgabeLangtext (which inherits from Textabschnitt)
self.langtext = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt="# Test\n\nThis is a **test** vorgabe.",
order=1
)
def test_render_vorgabe_langtext(self):
"""Test rendering VorgabeLangtext through render_textabschnitte"""
from dokumente.models import VorgabeLangtext
result = render_textabschnitte(
VorgabeLangtext.objects.filter(abschnitt=self.vorgabe).order_by('order')
)
self.assertEqual(len(result), 1)
typ, html = result[0]
self.assertEqual(typ, "text")
self.assertIn("<h1>Test</h1>", html)
self.assertIn("<strong>test</strong>", html)
self.assertIn("vorgabe", html)
def test_textabschnitt_inheritance(self):
"""Test that VorgabeLangtext properly inherits Textabschnitt fields"""
self.assertEqual(self.langtext.abschnitttyp, self.typ_text)
self.assertIn("test", self.langtext.inhalt)
self.assertEqual(self.langtext.order, 1)

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" fsGroupChangePolicy: "OnRootMismatch"
initContainers: initContainers:
- name: loader - 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" ] command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
volumeMounts: volumeMounts:
- name: data - name: data
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/vui:0.942 image: git.baumann.gr/adebaumann/vui:0.933
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000

Binary file not shown.

Binary file not shown.

View File

@@ -4,7 +4,6 @@ from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInl
from django import forms from django import forms
from mptt.forms import TreeNodeMultipleChoiceField from mptt.forms import TreeNodeMultipleChoiceField
from mptt.admin import DraggableMPTTAdmin from mptt.admin import DraggableMPTTAdmin
from adminsortable2.admin import SortableInlineAdminMixin, SortableAdminBase
# Register your models here. # Register your models here.
from .models import * from .models import *
@@ -21,33 +20,21 @@ from referenzen.models import Referenz
# 'frage': forms.Textarea(attrs={'rows': 1, 'cols': 100}), # 'frage': forms.Textarea(attrs={'rows': 1, 'cols': 100}),
# } # }
class ChecklistenfragenInline(NestedStackedInline): class ChecklistenfragenInline(NestedTabularInline):
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(NestedStackedInline): class VorgabeKurztextInline(NestedTabularInline):
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']
verbose_name_plural = "Kurztext-Abschnitte" #inline=inhalt
fieldsets = (
(None, {
'fields': ('abschnitttyp', 'inhalt', 'order'),
'classes': ('wide',),
}),
)
class VorgabeLangtextInline(NestedStackedInline): class VorgabeLangtextInline(NestedStackedInline):
model=VorgabeLangtext model=VorgabeLangtext
@@ -55,75 +42,42 @@ class VorgabeLangtextInline(NestedStackedInline):
sortable_field_name = "order" sortable_field_name = "order"
show_change_link=True show_change_link=True
classes = ['collapse'] classes = ['collapse']
verbose_name_plural = "Langtext-Abschnitte" #inline=inhalt
fieldsets = (
(None, {
'fields': ('abschnitttyp', 'inhalt', 'order'),
'classes': ('wide',),
}),
)
class GeltungsbereichInline(NestedStackedInline): class GeltungsbereichInline(NestedTabularInline):
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']
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'] classes = ['collapse']
verbose_name_plural = "Einleitungs-Abschnitte" #inline=inhalt
fieldsets = (
(None, { class EinleitungInline(NestedTabularInline):
'fields': ('abschnitttyp', 'inhalt', 'order'), model = Einleitung
'classes': ('wide',), extra = 0
}), sortable_field_name = "order"
) show_change_link = True
classes = ['collapse']
class VorgabeForm(forms.ModelForm): class VorgabeForm(forms.ModelForm):
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False) # referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
class Meta: class Meta:
model = Vorgabe model = Vorgabe
fields = '__all__' fields = '__all__'
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline): class VorgabeInline(NestedTabularInline): # or StackedInline for more vertical layout
model = Vorgabe model = Vorgabe
form = VorgabeForm form = VorgabeForm
extra = 0 extra = 0
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']
# Remove collapse class so Vorgaben show by default #search_fields=['nummer','name']ModelAdmin.
list_filter=['stichworte']
#classes=["collapse"]
fieldsets = ( class StichworterklaerungInline(NestedStackedInline):
('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):
model=Stichworterklaerung model=Stichworterklaerung
extra=0 extra=0
sortable_field_name = "order" sortable_field_name = "order"
@@ -146,104 +100,28 @@ class PersonAdmin(admin.ModelAdmin):
@admin.register(Dokument) @admin.register(Dokument)
class DokumentAdmin(SortableAdminBase, NestedModelAdmin): class DokumentAdmin(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','gueltigkeit_von','gueltigkeit_bis','aktiv'] list_display=['nummer','name','dokumententyp']
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',
)
} }
#admin.site.register(Stichwort) #admin.site.register(Stichwort)
@admin.register(VorgabenTable)
class VorgabenTableAdmin(admin.ModelAdmin):
list_display = ['order', 'nummer', 'dokument', 'thema', 'titel', 'gueltigkeit_von', 'gueltigkeit_bis']
list_display_links = ['dokument']
list_editable = ['order', 'nummer', 'thema', 'titel', 'gueltigkeit_von', 'gueltigkeit_bis']
list_filter = ['dokument', 'thema', 'gueltigkeit_von', 'gueltigkeit_bis']
search_fields = ['nummer', 'titel', 'dokument__nummer', 'dokument__name']
autocomplete_fields = ['dokument', 'thema', 'stichworte', 'referenzen', 'relevanz']
ordering = ['order']
list_per_page = 100
fieldsets = (
('Grunddaten', {
'fields': ('order', 'nummer', 'dokument', 'thema', 'titel')
}),
('Gültigkeit', {
'fields': ('gueltigkeit_von', 'gueltigkeit_bis')
}),
('Verknüpfungen', {
'fields': ('referenzen', 'stichworte', 'relevanz'),
'classes': ('collapse',)
}),
)
@admin.register(Thema)
class ThemaAdmin(admin.ModelAdmin):
search_fields = ['name']
ordering = ['name']
@admin.register(Vorgabe)
class VorgabeAdmin(NestedModelAdmin):
form = VorgabeForm
list_display = ['vorgabe_nummer', 'titel', 'dokument', 'thema', 'gueltigkeit_von', 'gueltigkeit_bis']
list_filter = ['dokument', 'thema', 'gueltigkeit_von', 'gueltigkeit_bis']
search_fields = ['nummer', 'titel', 'dokument__nummer', 'dokument__name']
autocomplete_fields = ['stichworte', 'referenzen', 'relevanz']
ordering = ['dokument', 'order']
inlines = [
VorgabeKurztextInline,
VorgabeLangtextInline,
ChecklistenfragenInline
]
fieldsets = (
('Grunddaten', {
'fields': (('order', 'nummer'), ('dokument', 'thema'), 'titel'),
'classes': ('wide',),
}),
('Gültigkeit', {
'fields': (('gueltigkeit_von', 'gueltigkeit_bis'),),
'classes': ('wide',),
}),
('Verknüpfungen', {
'fields': (('referenzen', 'stichworte', 'relevanz'),),
'classes': ('wide',),
}),
)
def vorgabe_nummer(self, obj):
return obj.Vorgabennummer()
vorgabe_nummer.short_description = 'Vorgabennummer'
admin.site.register(Checklistenfrage) admin.site.register(Checklistenfrage)
admin.site.register(Dokumententyp) admin.site.register(Dokumententyp)
#admin.site.register(Person) #admin.site.register(Person)
admin.site.register(Thema)
#admin.site.register(Referenz, DraggableM§PTTAdmin) #admin.site.register(Referenz, DraggableM§PTTAdmin)
admin.site.register(Vorgabe)
#admin.site.register(Changelog) #admin.site.register(Changelog)

View File

@@ -1,174 +0,0 @@
from django.core.management.base import BaseCommand
from django.core.serializers.json import DjangoJSONEncoder
import json
from datetime import datetime
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
class Command(BaseCommand):
help = 'Export all dokumente as JSON using R0066.json format as reference'
def add_arguments(self, parser):
parser.add_argument(
'--output',
type=str,
help='Output file path (default: stdout)',
)
def handle(self, *args, **options):
# Get all active documents
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
).order_by('nummer')
result = {
"Vorgabendokument": {
"Typ": "Standard IT-Sicherheit",
"Nummer": "", # Will be set per document
"Name": "", # Will be set per document
"Autoren": [], # Will be set per document
"Pruefende": [], # Will be set per document
"Geltungsbereich": {
"Abschnitt": []
},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": [],
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
}
output_data = []
for dokument in dokumente:
# Build document structure
doc_data = {
"Typ": dokument.dokumententyp.name if dokument.dokumententyp else "",
"Nummer": dokument.nummer,
"Name": dokument.name,
"Autoren": [autor.name for autor in dokument.autoren.all()],
"Pruefende": [pruefender.name for pruefender in dokument.pruefende.all()],
"Gueltigkeit": {
"Von": dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else "",
"Bis": dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
},
"SignaturCSO": dokument.signatur_cso,
"Geltungsbereich": {},
"Einleitung": {},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": dokument.anhaenge,
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
# Process Geltungsbereich sections
geltungsbereich_sections = []
for gb in dokument.geltungsbereich_set.all().order_by('order'):
geltungsbereich_sections.append({
"typ": gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text",
"inhalt": gb.inhalt
})
if geltungsbereich_sections:
doc_data["Geltungsbereich"] = {
"Abschnitt": geltungsbereich_sections
}
# Process Einleitung sections
einleitung_sections = []
for ei in dokument.einleitung_set.all().order_by('order'):
einleitung_sections.append({
"typ": ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text",
"inhalt": ei.inhalt
})
if einleitung_sections:
doc_data["Einleitung"] = {
"Abschnitt": einleitung_sections
}
# Process Changelog entries
changelog_entries = []
for cl in dokument.changelog.all().order_by('-datum'):
changelog_entries.append({
"Datum": cl.datum.strftime("%Y-%m-%d"),
"Autoren": [autor.name for autor in cl.autoren.all()],
"Aenderung": cl.aenderung
})
doc_data["Changelog"] = changelog_entries
# Process Vorgaben for this document
vorgaben = dokument.vorgaben.all().order_by('order')
for vorgabe in vorgaben:
# Get Kurztext and Langtext
kurztext_sections = []
for kt in vorgabe.vorgabekurztext_set.all().order_by('order'):
kurztext_sections.append({
"typ": kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text",
"inhalt": kt.inhalt
})
langtext_sections = []
for lt in vorgabe.vorgabelangtext_set.all().order_by('order'):
langtext_sections.append({
"typ": lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text",
"inhalt": lt.inhalt
})
# Build text structures following Langtext pattern
kurztext = {
"Abschnitt": kurztext_sections if kurztext_sections else []
} if kurztext_sections else {}
langtext = {
"Abschnitt": langtext_sections if langtext_sections else []
} if langtext_sections else {}
# Get references and keywords
referenzen = [f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer for ref in vorgabe.referenzen.all()]
stichworte = [stw.stichwort for stw in vorgabe.stichworte.all()]
# Get checklist questions
checklistenfragen = [cf.frage for cf in vorgabe.checklistenfragen.all()]
vorgabe_data = {
"Nummer": str(vorgabe.nummer),
"Titel": vorgabe.titel,
"Thema": vorgabe.thema.name if vorgabe.thema else "",
"Kurztext": kurztext,
"Langtext": langtext,
"Referenz": referenzen,
"Gueltigkeit": {
"Von": vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else "",
"Bis": vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
},
"Checklistenfragen": checklistenfragen,
"Stichworte": stichworte
}
doc_data["Vorgaben"].append(vorgabe_data)
output_data.append(doc_data)
# Output the data
json_output = json.dumps(output_data, cls=DjangoJSONEncoder, indent=2, ensure_ascii=False)
if options['output']:
with open(options['output'], 'w', encoding='utf-8') as f:
f.write(json_output)
self.stdout.write(self.style.SUCCESS(f'JSON exported to {options["output"]}'))
else:
self.stdout.write(json_output)

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

@@ -1,19 +0,0 @@
# Generated by Django 5.2.5 on 2025-10-27 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0007_alter_changelog_options_and_more'),
]
operations = [
migrations.AddField(
model_name='dokument',
name='aktiv',
field=models.BooleanField(blank=True, default=False),
preserve_default=False,
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.2.5 on 2025-10-28 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0008_dokument_aktiv'),
]
operations = [
migrations.AlterModelOptions(
name='vorgabe',
options={'ordering': ['order'], 'verbose_name_plural': 'Vorgaben'},
),
migrations.AddField(
model_name='vorgabe',
name='order',
field=models.IntegerField(default=0),
preserve_default=False,
),
]

View File

@@ -47,7 +47,6 @@ class Dokument(models.Model):
gueltigkeit_bis = models.DateField(null=True, blank=True) gueltigkeit_bis = models.DateField(null=True, blank=True)
signatur_cso = models.CharField(max_length=255, blank=True) signatur_cso = models.CharField(max_length=255, blank=True)
anhaenge = models.TextField(blank=True) anhaenge = models.TextField(blank=True)
aktiv = models.BooleanField(blank=True)
def __str__(self): def __str__(self):
return f"{self.nummer} {self.name}" return f"{self.nummer} {self.name}"
@@ -57,10 +56,9 @@ class Dokument(models.Model):
verbose_name="Dokument" verbose_name="Dokument"
class Vorgabe(models.Model): class Vorgabe(models.Model):
order = models.IntegerField()
nummer = models.IntegerField() nummer = models.IntegerField()
dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='vorgaben') dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='vorgaben')
thema = models.ForeignKey(Thema, on_delete=models.PROTECT, blank=False) thema = models.ForeignKey(Thema, on_delete=models.PROTECT)
titel = models.CharField(max_length=255) titel = models.CharField(max_length=255)
referenzen = models.ManyToManyField(Referenz, blank=True) referenzen = models.ManyToManyField(Referenz, blank=True)
gueltigkeit_von = models.DateField() gueltigkeit_von = models.DateField()
@@ -78,7 +76,7 @@ class Vorgabe(models.Model):
if not self.gueltigkeit_bis: if not self.gueltigkeit_bis:
return "active" return "active"
if self.gueltigkeit_bis >= check_date: if self.gueltigkeit_bis > check_date:
return "active" return "active"
return "expired" if not verbose else "Ist seit dem "+self.gueltigkeit_bis.strftime('%d.%m.%Y')+" nicht mehr in Kraft." return "expired" if not verbose else "Ist seit dem "+self.gueltigkeit_bis.strftime('%d.%m.%Y')+" nicht mehr in Kraft."
@@ -86,126 +84,9 @@ 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"ü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()} in Konflikt mit "
f"bestehender {other_vorgabe.Vorgabennummer()} "
f" - Geltungsdauer übeschneidet sich"
})
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']
class VorgabeLangtext(Textabschnitt): class VorgabeLangtext(Textabschnitt):
abschnitt=models.ForeignKey(Vorgabe,on_delete=models.CASCADE) abschnitt=models.ForeignKey(Vorgabe,on_delete=models.CASCADE)
@@ -242,12 +123,6 @@ class Checklistenfrage(models.Model):
verbose_name_plural="Fragen für Checkliste" verbose_name_plural="Fragen für Checkliste"
verbose_name="Frage für Checkliste" verbose_name="Frage für Checkliste"
class VorgabenTable(Vorgabe):
class Meta:
proxy = True
verbose_name = "Vorgabe (Tabellenansicht)"
verbose_name_plural = "Vorgaben (Tabellenansicht)"
class Changelog(models.Model): class Changelog(models.Model):
dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='changelog') dokument = models.ForeignKey(Dokument, on_delete=models.CASCADE, related_name='changelog')
autoren = models.ManyToManyField(Person) autoren = models.ManyToManyField(Person)

View File

@@ -1,151 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1 class="mb-4">Unvollständige Vorgaben</h1>
{% if vorgaben_data %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Vorgabe</th>
<th class="text-center">Referenzen</th>
<th class="text-center">Stichworte</th>
<th class="text-center">Text</th>
<th class="text-center">Checklistenfragen</th>
</tr>
</thead>
<tbody>
{% for item in vorgaben_data %}
<tr>
<td>
<a href="/autorenumgebung/dokumente/vorgabe/{{ item.vorgabe.id }}/change/"
class="text-decoration-none" target="_blank">
<strong>{{ item.vorgabe.Vorgabennummer }}</strong><br>
<small class="text-muted">{{ item.vorgabe.titel }}</small><br>
<small class="text-muted">{{ item.vorgabe.dokument.nummer }} {{ item.vorgabe.dokument.name }}</small>
</a>
</td>
<td class="text-center align-middle">
{% if item.has_references %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
<td class="text-center align-middle">
{% if item.has_stichworte %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
<td class="text-center align-middle">
{% if item.has_text %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
<td class="text-center align-middle">
{% if item.has_checklistenfragen %}
<span class="text-success fs-4"></span>
{% else %}
<span class="text-danger fs-4"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Summary -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Zusammenfassung</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-references-count">0</h4>
<p class="mb-0">Ohne Referenzen</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-stichworte-count">0</h4>
<p class="mb-0">Ohne Stichworte</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-text-count">0</h4>
<p class="mb-0">Ohne Text</p>
</div>
</div>
<div class="col-md-3">
<div class="p-3">
<h4 class="text-danger" id="no-checklistenfragen-count">0</h4>
<p class="mb-0">Ohne Checklistenfragen</p>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12 text-center">
<h4 class="text-primary">Gesamt: {{ vorgaben_data|length }} unvollständige Vorgaben</h4>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">
<i class="fas fa-check-circle"></i> Alle Vorgaben sind vollständig!
</h4>
<p>Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.</p>
<hr>
<p class="mb-0">
<a href="{% url 'standard_list' %}" class="btn btn-primary">
<i class="fas fa-list"></i> Zurück zur Übersicht
</a>
</p>
</div>
{% endif %}
<div class="mt-3">
<a href="{% url 'standard_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Zurück zur Übersicht
</a>
</div>
<script>
// Update summary counts
document.addEventListener('DOMContentLoaded', function() {
let noReferences = 0;
let noStichworte = 0;
let noText = 0;
let noChecklistenfragen = 0;
const rows = document.querySelectorAll('tbody tr');
rows.forEach(function(row) {
const cells = row.querySelectorAll('td');
if (cells.length >= 5) {
if (cells[1].textContent.trim() === '✗') noReferences++;
if (cells[2].textContent.trim() === '✗') noStichworte++;
if (cells[3].textContent.trim() === '✗') noText++;
if (cells[4].textContent.trim() === '✗') noChecklistenfragen++;
}
});
document.getElementById('no-references-count').textContent = noReferences;
document.getElementById('no-stichworte-count').textContent = noStichworte;
document.getElementById('no-text-count').textContent = noText;
document.getElementById('no-checklistenfragen-count').textContent = noChecklistenfragen;
});
</script>
{% endblock %}

View File

@@ -1,403 +1,109 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ standard.nummer }} {{ standard.name }}{% endblock %} {% block title %}{{ standard }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="{% url 'standard_list' %}">Standards</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ standard.nummer }}</li>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <h1>{{ standard.nummer }} {{ standard.name }}</h1>
<!-- Main Content --> {% if standard.history == True %}
<div class="col-lg-8"> <h2>Version vom {{ standard.check_date }}</h2>
<!-- Standard Header --> {% endif %}
<div class="card mb-4"> <!-- Autoren, Prüfende etc. -->
<div class="card-header bg-primary text-white"> <p><strong>Autoren:</strong> {{ standard.autoren.all|join:", " }}</p>
<div class="d-flex justify-content-between align-items-start"> <p><strong>Prüfende:</strong> {{ standard.pruefende.all|join:", " }}</p>
<div> <p><strong>Gültigkeit:</strong> {{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis }}</p>
<h1 class="h2 mb-2">{{ standard.nummer }} {{ standard.name }}</h1>
{% if standard.history == True %}
<p class="mb-0 opacity-75">Version vom {{ standard.check_date|date:"d.m.Y" }}</p>
{% endif %}
</div>
<div class="d-flex gap-2">
{% if not standard.aktiv %}
<span class="badge bg-danger">Inaktiv</span>
{% else %}
<span class="badge bg-success">Aktiv</span>
{% endif %}
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-2">📅 Gültigkeit</h6>
<p class="mb-3">
<strong>Von:</strong> {{ standard.gueltigkeit_von|default_if_none:"-" }}<br>
<strong>Bis:</strong> {{ standard.gueltigkeit_bis|default_if_none:"Auf weiteres" }}
</p>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">👥 Verantwortlich</h6>
{% if standard.autoren.all %}
<p class="mb-1"><strong>Autoren:</strong><br>
{% for autor in standard.autoren.all %}
<span class="badge bg-light text-dark me-1">{{ autor }}</span>
{% endfor %}
</p>
{% endif %}
{% if standard.pruefende.all %}
<p class="mb-3"><strong>Prüfende:</strong><br>
{% for pruefender in standard.pruefende.all %}
<span class="badge bg-light text-dark me-1">{{ pruefender }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
<div class="d-flex gap-2"> <!-- Start Einleitung -->
<a href="{% url 'standard_json' standard.nummer %}" class="btn btn-outline btn-sm" download="{{ standard.nummer }}.json"> {% if standard.einleitung_html %}
📄 JSON herunterladen <h2>Einleitung</h2>
</a> {% for typ, html in standard.einleitung_html %}
<button class="btn btn-outline btn-sm" onclick="window.print()"> <div>{{ html|safe }}</div>
🖨️ Drucken {% endfor %}
</button> {% endif %}
</div> <!-- End Einleitung -->
</div>
<!-- Start Geltungsbereich -->
{% if standard.geltungsbereich_html %}
<h2>Geltungsbereich</h2>
{% for typ, html in standard.geltungsbereich_html %}
<div>{{ html|safe }}</div>
{% endfor %}
{% endif %}
<!-- End Geltungsbereich -->
<h2>Vorgaben</h2>
{% for vorgabe in vorgaben %}
<!-- Start Vorgabe -->
{% if standard.history == True or vorgabe.long_status == "active" %}
<a id="{{ vorgabe.Vorgabennummer }}"></a><div class="card mb-4">
{% if vorgabe.long_status == "active"%}
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
{% elif standard.history == True %}
<div class="card-header d-flex justify-content-between align-items-center bg-danger-subtle">
{% endif %}
<h3 class="h5 m-0">{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}<span class="text-danger"> ({{ vorgabe.long_status}})</span>{% endif %}
</h3>
{% if vorgabe.relevanzset %}
<span class="badge bg-light text-black"> Relevanz:
{{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
<span class="badge bg-light text-black">{{ vorgabe.thema }}</span>
</div> </div>
<!-- Table of Contents --> <div class="card-body p-0">
<div class="toc mb-4" id="table-of-contents"> <!-- Start Kurztext -->
<h3>📋 Inhaltsverzeichnis</h3> {% comment %} KURZTEXT BLOCK {% endcomment %}
<ul class="list-unstyled"> {% if vorgabe.kurztext_html.0.1 %}
{% if standard.einleitung_html %} <div class="p-3 mb-3 bg-light border-3" style="width: 100%;">
<li><a href="#einleitung">Einleitung</a></li> {% for typ, html in vorgabe.kurztext_html %}
{% endif %} {% if html %}
{% if standard.geltungsbereich_html %} <div class="mb-2">{{ html|safe }}</div>
<li><a href="#geltungsbereich">Geltungsbereich</a></li>
{% endif %}
<li><a href="#vorgaben">Vorgaben ({{ vorgaben|length }})</a>
<ul class="ms-3 mt-1">
{% for vorgabe in vorgaben %}
{% if standard.history == True or vorgabe.long_status == "active" %}
<li><a href="#{{ vorgabe.Vorgabennummer }}">{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel|truncatechars:50 }}</a></li>
{% endif %}
{% endfor %}
</ul>
</li>
</ul>
</div>
<!-- Einleitung -->
{% if standard.einleitung_html %}
<section id="einleitung" class="mb-5">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">📖 Einleitung</h2>
</div>
<div class="card-body">
{% for typ, html in standard.einleitung_html %}
<div class="content-section">{{ html|safe }}</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<!-- Geltungsbereich -->
{% if standard.geltungsbereich_html %}
<section id="geltungsbereich" class="mb-5">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">🎯 Geltungsbereich</h2>
</div>
<div class="card-body">
{% for typ, html in standard.geltungsbereich_html %}
<div class="content-section">{{ html|safe }}</div>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<!-- Vorgaben -->
<section id="vorgaben" class="mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h4 mb-0">📝 Vorgaben</h2>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline btn-sm" onclick="toggleAllVorgaben(true)">Alle ausklappen</button>
<button type="button" class="btn btn-outline btn-sm" onclick="toggleAllVorgaben(false)">Alle einklappen</button>
</div>
</div>
{% for vorgabe in vorgaben %}
{% if standard.history == True or vorgabe.long_status == "active" %}
<div class="card mb-4 vorgabe-card" id="{{ vorgabe.Vorgabennummer }}">
<div class="card-header {% if vorgabe.long_status == "active" %}bg-success text-white{% else %}bg-secondary text-white{% endif %}"
style="cursor: pointer;"
onclick="toggleVorgabe('{{ vorgabe.Vorgabennummer }}')">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<span class="toggle-icon me-2"></span>
<h3 class="h5 m-0">
{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}
<span class="badge bg-warning text-dark ms-2">{{ vorgabe.long_status }}</span>
{% endif %}
</h3>
</div>
<div class="d-flex gap-2">
{% if vorgabe.relevanzset %}
<span class="badge bg-light text-dark">
🔥 {{ vorgabe.relevanzset|join:", " }}
</span>
{% endif %}
{% if vorgabe.thema %}
<span class="badge bg-info">{{ vorgabe.thema }}</span>
{% endif %}
</div>
</div>
</div>
<div class="card-body vorgabe-content" id="content-{{ vorgabe.Vorgabennummer }}">
<!-- Kurztext -->
{% if vorgabe.kurztext_html.0.1 %}
<div class="alert alert-info mb-3">
<h6 class="alert-heading">📌 Kurztext</h6>
{% for typ, html in vorgabe.kurztext_html %}
{% if html %}
<div class="mb-2">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %} {% endif %}
{% endfor %}
<!-- Langtext -->
<div class="mb-4">
{% for typ, html in vorgabe.langtext_html %}
{% if html %}
<div class="content-section mb-3">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
<!-- Checklistenfragen -->
<div class="mb-4">
<h6 class="mb-3">✅ Checklistenfragen</h6>
{% if vorgabe.checklistenfragen.all %}
<div class="list-group">
{% for frage in vorgabe.checklistenfragen.all %}
<div class="list-group-item">
<div class="d-flex align-items-start">
<input type="checkbox" class="form-check-input me-2 mt-1" id="check-{{ forloop.counter }}">
<label class="form-check-label" for="check-{{ forloop.counter }}">
{{ frage.frage }}
</label>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted"><em>Keine Checklistenfragen vorhanden</em></p>
{% endif %}
</div>
<!-- Metadaten -->
<div class="border-top pt-3">
<div class="row">
<div class="col-md-6">
<h6 class="text-muted mb-2">🏷️ Stichworte</h6>
{% if vorgabe.stichworte.all %}
<div>
{% for s in vorgabe.stichworte.all %}
<a href="{% url 'stichwort_detail' stichwort=s %}" class="badge bg-light text-dark text-decoration-none me-1 mb-1">
{{ s }}
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted small mb-0">Keine Stichworte</p>
{% endif %}
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2">🔗 Referenzen</h6>
{% if vorgabe.referenzpfade %}
<div class="small">
{% for ref in vorgabe.referenzpfade %}
<div class="mb-1">{{ ref|safe }}</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted small mb-0">Keine Referenzen</p>
{% endif %}
</div>
</div>
</div>
</div>
</div> </div>
{% endif %} {% endif %}
<!-- Langtext -->
<div class="p-3 mb-3">
{% comment %} LANGTEXT BLOCK {% endcomment %}
{# <h5>Langtext</h5> #}
{% for typ, html in vorgabe.langtext_html %}
{% if html %}<div class="mb-3">{{ html|safe }}</div>{% endif %}
{% endfor %} {% endfor %}
</section> <!-- Checklistenfragen -->
</div> {% comment %} CHECKLISTENFRAGEN BLOCK {% endcomment %}
<h5>Checklistenfragen</h5>
<!-- Sidebar --> {% if vorgabe.checklistenfragen.all %}
<div class="col-lg-4"> <ul class="list-group">
<!-- Quick Actions --> {% for frage in vorgabe.checklistenfragen.all %}
<div class="card mb-4 sticky-top" style="top: 1rem;"> <li class="list-group-item">{{ frage.frage }}</li>
<div class="card-header"> {% endfor %}
<h5 class="mb-0">⚡ Schnellaktionen</h5> </ul>
{% else %}
<p><em>Keine Checklistenfragen</em></p>
{% endif %}
{% comment %} STICHWORTE + REFERENZEN AT BOTTOM {% endcomment %}
<div class="mt-4 small text-muted">
<strong>Stichworte:</strong>
{% if vorgabe.stichworte.all %}
{% for s in vorgabe.stichworte.all %}
<a href="{% url 'stichwort_detail' stichwort=s %}">{{ s }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<em>Keine</em>
{% endif %}
<br>
<strong>Referenzen:</strong>
{% if vorgabe.referenzpfade %}
{% for ref in vorgabe.referenzpfade %}
{{ ref|safe }}{% if not forloop.last %}, {% endif %}
{% endfor %}
{% else %}
<em>Keine</em>
{% endif %}
</div> </div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-outline btn-sm" onclick="scrollToSection('einleitung')">
📖 Zur Einleitung
</button>
<button class="btn btn-outline btn-sm" onclick="scrollToSection('geltungsbereich')">
🎯 Zum Geltungsbereich
</button>
<button class="btn btn-outline btn-sm" onclick="scrollToSection('vorgaben')">
📝 Zu den Vorgaben
</button>
<hr>
<a href="{% url 'standard_list' %}" class="btn btn-outline btn-sm">
← Zurück zur Liste
</a>
</div>
</div>
</div>
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📊 Statistiken</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<div class="border-end">
<h4 class="text-primary mb-1">{{ vorgaben|length }}</h4>
<small class="text-muted">Vorgaben</small>
</div>
</div>
<div class="col-6">
<h4 class="text-success mb-1">
{% for vorgabe in vorgaben %}
{% if vorgabe.long_status == "active" %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
</h4>
<small class="text-muted">Aktiv</small>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> {% endif %}
{% endfor %}
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Update active TOC item on scroll
const sections = document.querySelectorAll('section[id]');
const tocLinks = document.querySelectorAll('.toc a');
function updateActiveTOC() {
let current = '';
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
if (pageYOffset >= sectionTop - 100) {
current = section.getAttribute('id');
}
});
tocLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === '#' + current) {
link.classList.add('active');
}
});
}
window.addEventListener('scroll', updateActiveTOC);
updateActiveTOC();
});
function toggleVorgabe(vorgabeId) {
const content = document.getElementById('content-' + vorgabeId);
const icon = document.querySelector('#' + vorgabeId + ' .toggle-icon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.textContent = '▼';
} else {
content.style.display = 'none';
icon.textContent = '▶';
}
}
function toggleAllVorgaben(expand) {
const contents = document.querySelectorAll('.vorgabe-content');
const icons = document.querySelectorAll('.toggle-icon');
contents.forEach(content => {
content.style.display = expand ? 'block' : 'none';
});
icons.forEach(icon => {
icon.textContent = expand ? '▼' : '▶';
});
}
function scrollToSection(sectionId) {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
</script>
<style>
.content-section {
line-height: 1.6;
}
.content-section h1, .content-section h2, .content-section h3 {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.content-section ul, .content-section ol {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.content-section li {
margin-bottom: 0.5rem;
}
.vorgabe-card {
transition: all 0.2s ease;
}
.vorgabe-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.toc a.active {
background-color: var(--primary-color);
color: white;
}
@media print {
.vorgabe-content {
display: block !important;
}
.toggle-icon {
display: none !important;
}
}
</style>
{% endblock %} {% endblock %}

View File

@@ -1,201 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Standards Informatiksicherheit{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Standards</li>
{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <h1>Standards Informatiksicherheit</h1>
<h1>Standards Informatiksicherheit</h1> <ul>
<div class="d-flex gap-2">
<span class="badge bg-primary">{{ dokumente|length }} Standards</span>
</div>
</div>
<!-- Filter and Search Section -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label for="filter-search" class="form-label">Suchen</label>
<input type="text" class="form-control" id="filter-search" placeholder="Standard durchsuchen...">
</div>
<div class="col-md-3">
<label for="filter-status" class="form-label">Status</label>
<select class="form-select" id="filter-status">
<option value="">Alle</option>
<option value="active">Aktiv</option>
<option value="inactive">Inaktiv</option>
</select>
</div>
<div class="col-md-3">
<label for="filter-sort" class="form-label">Sortieren</label>
<select class="form-select" id="filter-sort">
<option value="nummer">Nummer</option>
<option value="name">Name</option>
<option value="gueltigkeit">Gültigkeit</option>
</select>
</div>
</div>
</div>
</div>
<!-- Standards Grid -->
<div class="row" id="standards-container">
{% for dokument in dokumente %} {% for dokument in dokumente %}
<div class="col-lg-6 col-xl-4 mb-4 standard-item" <li>
data-nummer="{{ dokument.nummer|lower }}" <a href="{% url 'standard_detail' nummer=dokument.nummer %}">
data-name="{{ dokument.name|lower }}" {{ dokument.nummer }} {{ dokument.name }}
data-status="{% if dokument.aktiv %}active{% else %}inactive{% endif %}"> </a>
<div class="card standard-card h-100"> </li>
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<span class="standard-number">{{ dokument.nummer }}</span>
{% if not dokument.aktiv %}
<span class="badge badge-status-inactive ms-2">Inaktiv</span>
{% else %}
<span class="badge badge-status-active ms-2">Aktiv</span>
{% endif %}
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline" type="button" data-bs-toggle="dropdown">
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{% url 'standard_detail' nummer=dokument.nummer %}">Details anzeigen</a></li>
<li><a class="dropdown-item" href="{% url 'standard_json' dokument.nummer %}" download="{{ dokument.nummer }}.json">JSON herunterladen</a></li>
</ul>
</div>
</div>
<div class="card-body">
<h5 class="card-title">
<a href="{% url 'standard_detail' nummer=dokument.nummer %}" class="text-decoration-none">
{{ dokument.name }}
</a>
</h5>
<div class="standard-meta mb-3">
<div class="row g-2">
<div class="col-6">
<small class="text-muted">
<strong>Gültig von:</strong><br>
{{ dokument.gueltigkeit_von|default_if_none:"-" }}
</small>
</div>
<div class="col-6">
<small class="text-muted">
<strong>Gültig bis:</strong><br>
{{ dokument.gueltigkeit_bis|default_if_none:"Auf weiteres" }}
</small>
</div>
</div>
</div>
{% if dokument.autoren.all %}
<div class="mb-2">
<small class="text-muted">
<strong>Autoren:</strong>
{% for autor in dokument.autoren.all %}
{{ autor }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</small>
</div>
{% endif %}
{% if dokument.pruefende.all %}
<div class="mb-3">
<small class="text-muted">
<strong>Prüfende:</strong>
{% for pruefender in dokument.pruefende.all %}
{{ pruefender }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</small>
</div>
{% endif %}
<div class="d-flex justify-content-between align-items-center">
<a href="{% url 'standard_detail' nummer=dokument.nummer %}" class="btn btn-primary btn-sm">
Details anzeigen
</a>
<div class="text-muted">
<small>
{% if dokument.history %}
Version vom {{ dokument.check_date|date:"d.m.Y" }}
{% endif %}
</small>
</div>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="text-center py-5">
<h3 class="text-muted">Keine Standards gefunden</h3>
<p class="text-muted">Es wurden keine Standards gefunden, die Ihren Kriterien entsprechen.</p>
</div>
</div>
{% endfor %} {% endfor %}
</div> </ul>
<!-- JavaScript for filtering and sorting -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('filter-search');
const statusSelect = document.getElementById('filter-status');
const sortSelect = document.getElementById('filter-sort');
const container = document.getElementById('standards-container');
function filterAndSort() {
const searchTerm = searchInput.value.toLowerCase();
const statusFilter = statusSelect.value;
const sortBy = sortSelect.value;
let items = Array.from(container.querySelectorAll('.standard-item'));
// Filter
items = items.filter(item => {
const nummer = item.dataset.nummer;
const name = item.dataset.name;
const status = item.dataset.status;
const matchesSearch = !searchTerm ||
nummer.includes(searchTerm) ||
name.includes(searchTerm);
const matchesStatus = !statusFilter || status === statusFilter;
return matchesSearch && matchesStatus;
});
// Sort
items.sort((a, b) => {
switch(sortBy) {
case 'nummer':
return a.dataset.nummer.localeCompare(b.dataset.nummer);
case 'name':
return a.dataset.name.localeCompare(b.dataset.name);
case 'gueltigkeit':
// This would need additional data attributes for proper sorting
return a.dataset.nummer.localeCompare(b.dataset.nummer);
default:
return 0;
}
});
// Reorder DOM
items.forEach(item => container.appendChild(item));
// Show/hide no results message
const noResults = container.querySelector('.col-12 .text-center');
if (noResults) {
noResults.parentElement.style.display = items.length === 0 ? 'block' : 'none';
}
}
searchInput.addEventListener('input', filterAndSort);
statusSelect.addEventListener('change', filterAndSort);
sortSelect.addEventListener('change', filterAndSort);
});
</script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,9 @@ from . import views
urlpatterns = [ urlpatterns = [
path('', views.standard_list, name='standard_list'), path('', views.standard_list, name='standard_list'),
path('unvollstaendig/', views.incomplete_vorgaben, name='incomplete_vorgaben'),
path('<str:nummer>/', views.standard_detail, name='standard_detail'), path('<str:nummer>/', views.standard_detail, name='standard_detail'),
path('<str:nummer>/history/<str:check_date>/', views.standard_detail), path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'), path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'), path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste')
path('<str:nummer>/json/', views.standard_json, name='standard_json')
] ]

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)

View File

@@ -1,9 +1,5 @@
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test from .models import Dokument
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
import json
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
from datetime import date from datetime import date
@@ -60,180 +56,3 @@ def standard_checkliste(request, nummer):
}) })
def is_staff_user(user):
return user.is_staff
@login_required
@user_passes_test(is_staff_user)
def incomplete_vorgaben(request):
"""
Show table of all Vorgaben with completeness status:
- References (✓ or ✗)
- Stichworte (✓ or ✗)
- Text (✓ or ✗)
- Checklistenfragen (✓ or ✗)
"""
# Get all active Vorgaben
all_vorgaben = Vorgabe.objects.all().select_related('dokument', 'thema').prefetch_related(
'referenzen', 'stichworte', 'checklistenfragen', 'vorgabekurztext_set', 'vorgabelangtext_set'
)
# Build table data
vorgaben_data = []
for vorgabe in all_vorgaben:
has_references = vorgabe.referenzen.exists()
has_stichworte = vorgabe.stichworte.exists()
has_kurztext = vorgabe.vorgabekurztext_set.exists()
has_langtext = vorgabe.vorgabelangtext_set.exists()
has_text = has_kurztext or has_langtext
has_checklistenfragen = vorgabe.checklistenfragen.exists()
# Only include Vorgaben that are incomplete in at least one way
if not (has_references and has_stichworte and has_text and has_checklistenfragen):
vorgaben_data.append({
'vorgabe': vorgabe,
'has_references': has_references,
'has_stichworte': has_stichworte,
'has_text': has_text,
'has_checklistenfragen': has_checklistenfragen,
'is_complete': has_references and has_stichworte and has_text and has_checklistenfragen
})
# Sort by document number and Vorgabe number
vorgaben_data.sort(key=lambda x: (x['vorgabe'].dokument.nummer, x['vorgabe'].Vorgabennummer()))
return render(request, 'standards/incomplete_vorgaben.html', {
'vorgaben_data': vorgaben_data,
})
def standard_json(request, nummer):
"""
Export a single Dokument as JSON
"""
# Get the document with all related data
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
),
nummer=nummer
)
# Build document structure (reusing logic from export_json command)
doc_data = {
"Typ": dokument.dokumententyp.name if dokument.dokumententyp else "",
"Nummer": dokument.nummer,
"Name": dokument.name,
"Autoren": [autor.name for autor in dokument.autoren.all()],
"Pruefende": [pruefender.name for pruefender in dokument.pruefende.all()],
"Gueltigkeit": {
"Von": dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else "",
"Bis": dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
},
"SignaturCSO": dokument.signatur_cso,
"Geltungsbereich": {},
"Einleitung": {},
"Ziel": "",
"Grundlagen": "",
"Changelog": [],
"Anhänge": dokument.anhaenge,
"Verantwortlich": "Information Security Management BIT",
"Klassifizierung": None,
"Glossar": {},
"Vorgaben": []
}
# Process Geltungsbereich sections
geltungsbereich_sections = []
for gb in dokument.geltungsbereich_set.all().order_by('order'):
geltungsbereich_sections.append({
"typ": gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text",
"inhalt": gb.inhalt
})
if geltungsbereich_sections:
doc_data["Geltungsbereich"] = {
"Abschnitt": geltungsbereich_sections
}
# Process Einleitung sections
einleitung_sections = []
for ei in dokument.einleitung_set.all().order_by('order'):
einleitung_sections.append({
"typ": ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text",
"inhalt": ei.inhalt
})
if einleitung_sections:
doc_data["Einleitung"] = {
"Abschnitt": einleitung_sections
}
# Process Changelog entries
changelog_entries = []
for cl in dokument.changelog.all().order_by('-datum'):
changelog_entries.append({
"Datum": cl.datum.strftime("%Y-%m-%d"),
"Autoren": [autor.name for autor in cl.autoren.all()],
"Aenderung": cl.aenderung
})
doc_data["Changelog"] = changelog_entries
# Process Vorgaben for this document
vorgaben = dokument.vorgaben.all().order_by('order')
for vorgabe in vorgaben:
# Get Kurztext and Langtext sections
kurztext_sections = []
for kt in vorgabe.vorgabekurztext_set.all().order_by('order'):
kurztext_sections.append({
"typ": kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text",
"inhalt": kt.inhalt
})
langtext_sections = []
for lt in vorgabe.vorgabelangtext_set.all().order_by('order'):
langtext_sections.append({
"typ": lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text",
"inhalt": lt.inhalt
})
# Build text structures following Langtext pattern
kurztext = {
"Abschnitt": kurztext_sections if kurztext_sections else []
} if kurztext_sections else {}
langtext = {
"Abschnitt": langtext_sections if langtext_sections else []
} if langtext_sections else {}
# Get references and keywords
referenzen = [f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer for ref in vorgabe.referenzen.all()]
stichworte = [stw.stichwort for stw in vorgabe.stichworte.all()]
# Get checklist questions
checklistenfragen = [cf.frage for cf in vorgabe.checklistenfragen.all()]
vorgabe_data = {
"Nummer": str(vorgabe.nummer),
"Titel": vorgabe.titel,
"Thema": vorgabe.thema.name if vorgabe.thema else "",
"Kurztext": kurztext,
"Langtext": langtext,
"Referenz": referenzen,
"Gueltigkeit": {
"Von": vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else "",
"Bis": vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
},
"Checklistenfragen": checklistenfragen,
"Stichworte": stichworte
}
doc_data["Vorgaben"].append(vorgabe_data)
# Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)

View File

@@ -1,160 +1,33 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de" data-theme="light"> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block title %}{% endblock %}</title>
<title>{% block title %}Vorgaben Informatiksicherheit BIT{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
{% load static %} {% load static %}
<link rel="stylesheet" href="{% static 'custom/css/vorgaben-ui.css' %}">
</head> </head>
<body> <body>
<!-- Enhanced Navigation --> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<nav class="navbar navbar-expand-lg sticky-top"> <a class="navbar-brand" href="#">Vorgaben</a>
<div class="container-fluid"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<a class="navbar-brand" href="/"> <span class="navbar-toggler-icon"></span>
Vorgaben Informatiksicherheit </button>
</a> <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Navigation umschalten"> <a class="nav-item nav-link active" href="/dokumente">Standards</a>
<span class="navbar-toggler-icon"></span> <a class="nav-item nav-link" href="/referenzen">Referenzen</a>
</button> <a class="nav-item nav-link" href="/stichworte">Stichworte</a>
<a class="nav-item nav-link" href="/search">Suche</a>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'standard_list' %}">Standards</a>
</li>
{% if user.is_staff %}
<li class="nav-item">
<a class="nav-link" href="{% url 'incomplete_standards' %}">Unvollständig</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link" href="{% url 'referenz_tree' %}">Referenzen</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'stichworte_list' %}">Stichworte</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'search' %}">Suche</a>
</li>
</ul>
<!-- Search Bar -->
<form class="navbar-search d-none d-lg-flex me-3" action="/search/" method="get">
<input type="text" name="q" placeholder="Suchen..." value="{{ search_term|default:'' }}">
<button type="submit" aria-label="Suchen">🔍</button>
</form>
<!-- Dark Mode Toggle -->
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Dark Mode umschalten">
<span id="theme-icon">🌙</span>
</button>
</div> </div>
</div> </div>
</nav> </nav>
<div class="d-flex">
<!-- Breadcrumb --> <div class="col-md-2">{% block sidebar_left %}{% endblock %}</div>
{% block breadcrumb %} <div class="flex-fill">{% block content %}Main Content{% endblock %}</div>
{% if request.resolver_match.url_name != 'startseite' and request.path != '/' %} <div class="col-md-2">{% block sidebar_right %}{% endblock %}</div>
<nav aria-label="Breadcrumb">
<div class="container-fluid">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Startseite</a></li>
{% block breadcrumb_items %}{% endblock %}
</ol>
</div>
</nav>
{% endif %}
{% endblock %}
<!-- Main Content -->
<div class="container-fluid">
<div class="row">
<!-- Left Sidebar -->
{% if block.sidebar_left %}
<aside class="col-lg-2 d-none d-lg-block">
{% block sidebar_left %}{% endblock %}
</aside>
{% endif %}
<!-- Main Content Area -->
<main class="{% if block.sidebar_left or block.sidebar_right %}col-lg-8{% else %}col-lg-12{% endif %} py-4">
{% block content %}{% endblock %}
</main>
<!-- Right Sidebar -->
{% if block.sidebar_right %}
<aside class="col-lg-2 d-none d-lg-block">
{% block sidebar_right %}{% endblock %}
</aside>
{% endif %}
</div>
</div> </div>
<div>VorgabenUI v0.931</div>
<!-- Footer -->
<footer class="footer">
<div class="container-fluid">
<p class="mb-0">VorgabenUI v0.942 | © {{ "now"|date:"Y" }} Bundesamt für Informatik</p>
</div>
</footer>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Theme Toggle
function toggleTheme() {
const html = document.documentElement;
const themeIcon = document.getElementById('theme-icon');
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
themeIcon.textContent = newTheme === 'light' ? '🌙' : '☀️';
localStorage.setItem('theme', newTheme);
}
// Load saved theme
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('theme') || 'light';
const html = document.documentElement;
const themeIcon = document.getElementById('theme-icon');
html.setAttribute('data-theme', savedTheme);
themeIcon.textContent = savedTheme === 'light' ? '🌙' : '☀️';
});
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Active navigation highlighting
document.addEventListener('DOMContentLoaded', function() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.navbar-nav .nav-link');
navLinks.forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
}
});
});
</script>
</body> </body>
</html> </html>

View File

@@ -1,160 +1,30 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load page_extras %} {% load page_extras %}
{% block title %}Suchresultate für {{ suchbegriff }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="/search/">Suche</a></li>
<li class="breadcrumb-item active" aria-current="page">Resultate</li>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <h1 class="mb-4">Suchresultate für {{ suchbegriff }}</h1>
<div class="col-lg-4"> {% if resultat.geltungsbereich %}
<!-- Search Form --> <h2>Standards mit "{{suchbegriff}}" im Geltungsbereich</h2>
<div class="card search-form sticky-top" style="top: 1rem;"> {% for standard,geltungsbereich in resultat.geltungsbereich.items %}
<div class="card-header"> <h4>{{ standard }}</h4>
<h5 class="mb-0">🔍 Suche</h5> {% for typ, html in geltungsbereich %}
</div> <div>{{ html|highlighttext:suchbegriff|safe }}</div>
<div class="card-body"> {% endfor %}
<form action="/search/" method="post" id="search-form"> {% endfor %}
{% csrf_token %} {% endif %}
<!-- Main Search Field --> {% if resultat.all %}
<div class="mb-3"> <h2>Vorgaben mit "{{ suchbegriff }}"</h2>
<label for="query" class="form-label">Suchbegriff</label> {% for standard, vorgaben in resultat.all.items %}
<div class="input-group"> <h4>{{ standard }}</h4>
<input type="text" <ul>
class="form-control"
id="query"
name="q"
placeholder="Suchbegriff eingeben …"
value="{{ suchbegriff|default:'' }}"
required
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
<!-- Search Options -->
<div class="mb-3">
<label class="form-label">Suchbereiche</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="search_in" value="standards" id="search-standards" checked>
<label class="form-check-label" for="search-standards">
Standards
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="search_in" value="geltungsbereich" id="search-geltungsbereich" checked>
<label class="form-check-label" for="search-geltungsbereich">
Geltungsbereich
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="search_in" value="vorgaben" id="search-vorgaben" checked>
<label class="form-check-label" for="search-vorgaben">
Vorgaben
</label>
</div>
</div>
<button class="btn btn-outline btn-sm w-100" onclick="history.back()">
← Zurück zur Suche
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<!-- Search Results -->
<div class="search-results">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Suchresultate für "{{ suchbegriff }}"</h2>
<span class="badge bg-primary">
{% if resultat.geltungsbereich %}
{{ resultat.geltungsbereich|length }}
{% endif %}
{% if resultat.all %}
{% for standard, vorgaben in resultat.all.items %}
{{ vorgaben|length }}
{% endfor %}
{% endif %}
Ergebnisse
</span>
</div>
<!-- Geltungsbereich Results -->
{% if resultat.geltungsbereich %}
<div class="mb-5">
<h3 class="h4 mb-3">📋 Standards mit "{{ suchbegriff }}" im Geltungsbereich</h3>
{% for standard, geltungsbereich in resultat.geltungsbereich.items %}
<div class="result-item">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="mb-1">
<a href="/dokumente/{{ standard.nummer }}/" class="text-decoration-none">
{{ standard.nummer }} {{ standard.name }}
</a>
</h5>
<span class="badge bg-info">Geltungsbereich</span>
</div>
{% for typ, html in geltungsbereich %}
<div class="search-context">{{ html|highlighttext:suchbegriff|safe }}</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %}
<!-- Vorgaben Results -->
{% if resultat.all %}
<div class="mb-5">
<h3 class="h4 mb-3">📝 Vorgaben mit "{{ suchbegriff }}"</h3>
{% for standard, vorgaben in resultat.all.items %}
<div class="result-item">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="mb-1">
<a href="/dokumente/{{ standard.nummer }}/" class="text-decoration-none">
{{ standard }}
</a>
</h5>
<span class="badge bg-success">{{ vorgaben|length }} Vorgaben</span>
</div>
<div class="row">
{% for vorgabe in vorgaben %} {% for vorgabe in vorgaben %}
<div class="col-md-6 mb-2"> <li><a href="{% url 'standard_detail' nummer=vorgabe.dokument.nummer %}#{{vorgabe.Vorgabennummer}}">{{vorgabe}}</a></li>
<div class="d-flex align-items-center">
<span class="badge bg-secondary me-2">{{ vorgabe.Vorgabennummer }}</span>
<a href="/dokumente/{{ vorgabe.dokument.nummer }}/#{{vorgabe.Vorgabennummer}}"
class="text-decoration-none">
{{ vorgabe.titel }}
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %} {% endfor %}
</div> </ul>
{% endif %} {% endfor %}
{% endif %}
<!-- No Results --> {% if not resultat.all %}
{% if not resultat.geltungsbereich and not resultat.all %} <h2>Keine Resultate für "{{suchbegriff}}"</h2>
<div class="text-center py-5"> {% endif %}
<div class="mb-3">
<span style="font-size: 3rem;">🔍</span>
</div>
<h3 class="text-muted">Keine Resultate für "{{ suchbegriff }}"</h3>
<p class="text-muted mb-4">
Es wurden keine Ergebnisse gefunden. Versuchen Sie es mit anderen Suchbegriffen.
</p>
<button class="btn btn-primary" onclick="history.back()">
← Zurück zur Suche
</button>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,139 +1,20 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Suche{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Suche</li>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <h1 class="mb-4">Suche</h1>
<div class="col-lg-4">
<!-- Search Form -->
<div class="card search-form sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">🔍 Suche</h5>
</div>
<div class="card-body">
<form action="/search/" method="post" id="search-form">
{% csrf_token %}
<!-- Main Search Field --> <!-- Search form -->
<div class="mb-3"> <form action="." method="post">
<label for="query" class="form-label">Suchbegriff</label> {% csrf_token %}
<div class="input-group"> <!-- Search field -->
<input type="text" <div class="mb-3">
class="form-control" <label for="query" class="form-label">Suchbegriff</label>
id="query" <input type="text"
name="q" class="form-control"
placeholder="Suchbegriff eingeben …" id="query"
value="{{ search_term|default:'' }}" name="q"
required placeholder="Suchbegriff eingeben …"
maxlength="200"> required>
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
<button class="btn btn-outline btn-sm w-100" onclick="history.back()">
← Zurück zur Suche
</button>
</form>
</div> </div>
</div> <button type="submit" class="btn btn-primary">Suchen</button>
</div> </form>
{% endblock %}
<div class="col-lg-8">
<!-- Search Results -->
{% if search_term %}
<div class="search-results">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Suchresultate für "{{ search_term }}"</h2>
<span class="badge bg-primary">
{% if resultat.geltungsbereich %}
{{ resultat.geltungsbereich|length }}
{% endif %}
{% if resultat.all %}
{% for standard, vorgaben in resultat.all.items %}
{{ vorgaben|length }}
{% endfor %}
{% endif %}
Ergebnisse
</span>
</div>
<!-- No Results -->
{% if not resultat.geltungsbereich and not resultat.all %}
<div class="text-center py-5">
<div class="mb-3">
<span style="font-size: 3rem;">🔍</span>
</div>
<h3 class="text-muted">Keine Resultate für "{{ search_term }}"</h3>
<p class="text-muted mb-4">
Es wurden keine Ergebnisse gefunden. Versuchen Sie es mit anderen Suchbegriffen.
</p>
<button class="btn btn-primary" onclick="history.back()">
← Zurück zur Suche
</button>
</div>
{% endif %}
</div>
{% else %}
<!-- Initial Search State -->
<div class="text-center py-5">
<div class="mb-4">
<span style="font-size: 4rem;">🔍</span>
</div>
<h2>Willkommen bei der Suche</h2>
<p class="text-muted mb-4">
Geben Sie einen Suchbegriff ein, um Standards, Vorgaben und Geltungsbereiche zu durchsuchen.
</p>
<!-- Quick Search Examples -->
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Beispielsuchen</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Passwort">
🔐 Passwort
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Netzwerk">
🌐 Netzwerk
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Verschlüsselung">
🔒 Verschlüsselung
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- JavaScript for quick search -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Quick search buttons
const quickSearchButtons = document.querySelectorAll('.quick-search');
quickSearchButtons.forEach(button => {
button.addEventListener('click', function() {
const term = this.dataset.term;
document.getElementById('query').value = term;
document.getElementById('search-form').submit();
});
});
});
</script>
{% endblock %}

View File

@@ -1,139 +0,0 @@
{% extends "base.html" %}
{% block title %}Suche{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Suche</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-4">
<!-- Search Form -->
<div class="card search-form sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">🔍 Suche</h5>
</div>
<div class="card-body">
<form action="/search/" method="post" id="search-form">
{% csrf_token %}
<!-- Main Search Field -->
<div class="mb-3">
<label for="query" class="form-label">Suchbegriff</label>
<div class="input-group">
<input type="text"
class="form-control"
id="query"
name="q"
placeholder="Suchbegriff eingeben …"
value="{{ search_term|default:'' }}"
required
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
<button class="btn btn-outline btn-sm w-100" onclick="history.back()">
← Zurück zur Suche
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<!-- Search Results -->
{% if search_term %}
<div class="search-results">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Suchresultate für "{{ search_term }}"</h2>
<span class="badge bg-primary">
{% if resultat.geltungsbereich %}
{{ resultat.geltungsbereich|length }}
{% endif %}
{% if resultat.all %}
{% for standard, vorgaben in resultat.all.items %}
{{ vorgaben|length }}
{% endfor %}
{% endif %}
Ergebnisse
</span>
</div>
<!-- No Results -->
{% if not resultat.geltungsbereich and not resultat.all %}
<div class="text-center py-5">
<div class="mb-3">
<span style="font-size: 3rem;">🔍</span>
</div>
<h3 class="text-muted">Keine Resultate für "{{ search_term }}"</h3>
<p class="text-muted mb-4">
Es wurden keine Ergebnisse gefunden. Versuchen Sie es mit anderen Suchbegriffen.
</p>
<button class="btn btn-primary" onclick="history.back()">
← Zurück zur Suche
</button>
</div>
{% endif %}
</div>
{% else %}
<!-- Initial Search State -->
<div class="text-center py-5">
<div class="mb-4">
<span style="font-size: 4rem;">🔍</span>
</div>
<h2>Willkommen bei der Suche</h2>
<p class="text-muted mb-4">
Geben Sie einen Suchbegriff ein, um Standards, Vorgaben und Geltungsbereiche zu durchsuchen.
</p>
<!-- Quick Search Examples -->
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h6 class="mb-0">Beispielsuchen</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Passwort">
🔐 Passwort
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Netzwerk">
🌐 Netzwerk
</button>
</div>
<div class="col-md-4">
<button class="btn btn-outline btn-sm w-100 quick-search" data-term="Verschlüsselung">
🔒 Verschlüsselung
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- JavaScript for quick search -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Quick search buttons
const quickSearchButtons = document.querySelectorAll('.quick-search');
quickSearchButtons.forEach(button => {
button.addEventListener('click', function() {
const term = this.dataset.term;
document.getElementById('query').value = term;
document.getElementById('search-form').submit();
});
});
});
</script>
{% endblock %}

View File

@@ -1,335 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Startseite - Vorgaben Informatiksicherheit BIT{% endblock %}
{% block content %} {% block content %}
<!-- Hero Section --> <h1>Vorgaben Informatiksicherheit BIT</h1>
<div class="card bg-primary text-white mb-5"> <h2>Aktuell erfasste Standards</h2>
<div class="card-body text-center py-5"> <ul>
<h1 class="display-4 mb-3">🔒 Vorgaben Informatiksicherheit BIT</h1> {% for standard in dokumente %}
<p class="lead mb-4"> <li><a href="{% url 'standard_detail' nummer=standard.nummer %}">{{ standard }}</a></li>
Zentraler Zugang zu allen Sicherheitsstandards, Vorgaben und Richtlinien des Bundesamtes für Informatik {% endfor %}
</p> </ul>
<div class="d-flex justify-content-center gap-3">
<a href="/dokumente/" class="btn btn-light btn-lg">
📋 Standards durchsuchen
</a>
<a href="/search/" class="btn btn-outline-light btn-lg">
🔍 Volltextsuche
</a>
</div>
</div>
</div>
<!-- Statistics Dashboard -->
<div class="row mb-5">
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">📋</span>
</div>
<h3 class="h2 text-primary mb-2">{{ dokumente|length }}</h3>
<p class="text-muted mb-0">Standards</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">📝</span>
</div>
<h3 class="h2 text-success mb-2">
{% if total_vorgaben %}{{ total_vorgaben }}{% else %}--{% endif %}
</h3>
<p class="text-muted mb-0">Vorgaben</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">🏷️</span>
</div>
<h3 class="h2 text-info mb-2">
{% if total_stichworte %}{{ total_stichworte }}{% else %}--{% endif %}
</h3>
<p class="text-muted mb-0">Stichworte</p>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card h-100 text-center">
<div class="card-body">
<div class="mb-3">
<span style="font-size: 2.5rem;">🔗</span>
</div>
<h3 class="h2 text-warning mb-2">
{% if total_referenzen %}{{ total_referenzen }}{% else %}--{% endif %}
</h3>
<p class="text-muted mb-0">Referenzen</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-5">
<div class="col-12">
<h2 class="h4 mb-4">⚡ Schnellzugriffe</h2>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">📋</span>
</div>
<h5 class="card-title">Standards</h5>
<p class="card-text text-muted">
Alle Sicherheitsstandards durchsuchen und filtern
</p>
<a href="/dokumente/" class="btn btn-primary">
Standards anzeigen
</a>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">🔍</span>
</div>
<h5 class="card-title">Suche</h5>
<p class="card-text text-muted">
Volltextsuche in allen Standards und Vorgaben
</p>
<a href="/search/" class="btn btn-primary">
Suche starten
</a>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">🏷️</span>
</div>
<h5 class="card-title">Stichworte</h5>
<p class="card-text text-muted">
Nach Stichworten browsen und entdecken
</p>
<a href="/stichworte/" class="btn btn-primary">
Stichworte anzeigen
</a>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">🔗</span>
</div>
<h5 class="card-title">Referenzen</h5>
<p class="card-text text-muted">
Referenzbaum und Querverbindungen
</p>
<a href="/referenzen/" class="btn btn-primary">
Referenzen anzeigen
</a>
</div>
</div>
</div>
{% if user.is_staff %}
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body text-center">
<div class="mb-3">
<span style="font-size: 2rem;">⚠️</span>
</div>
<h5 class="card-title">Unvollständig</h5>
<p class="card-text text-muted">
Unvollständige Standards bearbeiten
</p>
<a href="/dokumente/unvollstaendig/" class="btn btn-warning">
Bearbeiten
</a>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Recent Standards -->
<div class="row mb-5">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h3 class="h5 mb-0">📋 Aktuell erfasste Standards</h3>
<a href="/dokumente/" class="btn btn-outline btn-sm">
Alle anzeigen →
</a>
</div>
</div>
<div class="card-body">
{% if dokumente %}
<div class="row">
{% for standard in dokumente|slice:":6" %}
<div class="col-lg-4 col-md-6 mb-3">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title">
<a href="{% url 'standard_detail' nummer=standard.nummer %}" class="text-decoration-none">
{{ standard.nummer }} {{ standard.name|truncatechars:40 }}
</a>
</h6>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
{{ standard.gueltigkeit_von|default_if_none:"-" }}
</small>
{% if not standard.aktiv %}
<span class="badge bg-danger">Inaktiv</span>
{% else %}
<span class="badge bg-success">Aktiv</span>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<p class="text-muted mb-0">Keine Standards gefunden.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Quick Search -->
<div class="row mb-5">
<div class="col-12">
<div class="card bg-light">
<div class="card-body text-center py-4">
<h3 class="h5 mb-3">🔍 Schnellsuche</h3>
<form action="/search/" method="get" class="row justify-content-center">
<div class="col-md-6">
<div class="input-group">
<input type="text"
class="form-control"
name="q"
placeholder="Suchbegriff eingeben..."
maxlength="200">
<button class="btn btn-primary" type="submit">
Suchen
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Help Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="h5 mb-0">💡 Hinweise zur Nutzung</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<h6 class="text-primary">🔍 Suchen</h6>
<p class="small text-muted mb-0">
Nutzen Sie die Volltextsuche um gezielt nach Begriffen in allen Standards zu suchen.
</p>
</div>
<div class="col-md-4 mb-3">
<h6 class="text-primary">📋 Filtern</h6>
<p class="small text-muted mb-0">
Filtern Sie Standards nach Status, Gültigkeit oder anderen Kriterien.
</p>
</div>
<div class="col-md-4 mb-3">
<h6 class="text-primary">🏷️ Stichworte</h6>
<p class="small text-muted mb-0">
Entdecken Sie verwandte Inhalte durch die Stichwort-Navigation.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript for dynamic interactions -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects to cards
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-2px)';
this.style.transition = 'all 0.2s ease';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
// Animate statistics on page load
const statNumbers = document.querySelectorAll('.h2');
statNumbers.forEach((stat, index) => {
setTimeout(() => {
stat.style.opacity = '0';
stat.style.transform = 'scale(0.8)';
stat.style.transition = 'all 0.5s ease';
setTimeout(() => {
stat.style.opacity = '1';
stat.style.transform = 'scale(1)';
}, 100);
}, index * 100);
});
});
</script>
<style>
.card {
transition: all 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.display-4 {
font-weight: 700;
}
.btn-lg {
padding: 0.75rem 2rem;
font-weight: 500;
}
@media (max-width: 768px) {
.display-4 {
font-size: 2rem;
}
.btn-lg {
padding: 0.5rem 1.5rem;
font-size: 1rem;
}
}
</style>
{% endblock %} {% endblock %}

View File

@@ -1,312 +0,0 @@
from django.test import TestCase, Client
from django.core.exceptions import ValidationError
from django.utils import timezone
from datetime import date, timedelta
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Geltungsbereich, Dokumententyp, Thema
from stichworte.models import Stichwort
from unittest.mock import patch
import re
class SearchViewTest(TestCase):
def setUp(self):
self.client = Client()
# Create test data
self.dokumententyp = Dokumententyp.objects.create(
name="Test Typ",
verantwortliche_ve="Test VE"
)
self.thema = Thema.objects.create(
name="Test Thema",
erklaerung="Test Erklärung"
)
self.dokument = Dokument.objects.create(
nummer="TEST-001",
dokumententyp=self.dokumententyp,
name="Test Dokument",
gueltigkeit_von=date.today(),
aktiv=True
)
self.vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe Titel",
gueltigkeit_von=date.today()
)
# Create text content
self.kurztext = VorgabeKurztext.objects.create(
abschnitt=self.vorgabe,
inhalt="Dies ist ein Test Kurztext mit Suchbegriff"
)
self.langtext = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
inhalt="Dies ist ein Test Langtext mit anderem Suchbegriff"
)
self.geltungsbereich = Geltungsbereich.objects.create(
geltungsbereich=self.dokument,
inhalt="Test Geltungsbereich mit Suchbegriff"
)
def test_search_get_request(self):
"""Test GET request returns search form"""
response = self.client.get('/search/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suche')
self.assertContains(response, 'Suchbegriff')
def test_search_post_valid_term(self):
"""Test POST request with valid search term"""
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für Test')
def test_search_case_insensitive(self):
"""Test that search is case insensitive"""
# Search for lowercase
response = self.client.post('/search/', {'q': 'test'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für test')
# Search for uppercase
response = self.client.post('/search/', {'q': 'TEST'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für TEST')
# Search for mixed case
response = self.client.post('/search/', {'q': 'TeSt'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für TeSt')
def test_search_in_kurztext(self):
"""Test search in Kurztext content"""
response = self.client.post('/search/', {'q': 'Suchbegriff'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'TEST-001')
def test_search_in_langtext(self):
"""Test search in Langtext content"""
response = self.client.post('/search/', {'q': 'anderem'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'TEST-001')
def test_search_in_titel(self):
"""Test search in Vorgabe title"""
response = self.client.post('/search/', {'q': 'Titel'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'TEST-001')
def test_search_in_geltungsbereich(self):
"""Test search in Geltungsbereich content"""
response = self.client.post('/search/', {'q': 'Geltungsbereich'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Standards mit')
def test_search_no_results(self):
"""Test search with no results"""
response = self.client.post('/search/', {'q': 'NichtVorhanden'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Keine Resultate für "NichtVorhanden"')
def test_search_expired_vorgabe_not_included(self):
"""Test that expired Vorgaben are not included in results"""
# Create expired Vorgabe
expired_vorgabe = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Abgelaufene Vorgabe",
gueltigkeit_von=date.today() - timedelta(days=10),
gueltigkeit_bis=date.today() - timedelta(days=1)
)
VorgabeKurztext.objects.create(
abschnitt=expired_vorgabe,
inhalt="Abgelaufener Inhalt mit Test"
)
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
# Should only find the active Vorgabe, not the expired one
self.assertContains(response, 'Test Vorgabe Titel')
# The expired vorgabe should not appear in results
self.assertNotContains(response, 'Abgelaufene Vorgabe')
def test_search_empty_term_validation(self):
"""Test validation for empty search term"""
response = self.client.post('/search/', {'q': ''})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Fehler:')
self.assertContains(response, 'Suchbegriff darf nicht leer sein')
def test_search_no_term_validation(self):
"""Test validation when no search term is provided"""
response = self.client.post('/search/', {})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Fehler:')
self.assertContains(response, 'Suchbegriff darf nicht leer sein')
def test_search_html_tags_stripped(self):
"""Test that HTML tags are stripped from search input"""
response = self.client.post('/search/', {'q': '<script>alert("xss")</script>Test'})
self.assertEqual(response.status_code, 200)
# Should search for "alert('xss')Test" after HTML tag removal
self.assertContains(response, 'Suchresultate für alert(&quot;xss&quot;)Test')
def test_search_invalid_characters_validation(self):
"""Test validation for invalid characters"""
response = self.client.post('/search/', {'q': 'Test| DROP TABLE users'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Fehler:')
self.assertContains(response, 'Ungültige Zeichen im Suchbegriff')
def test_search_too_long_validation(self):
"""Test validation for overly long search terms"""
long_term = 'a' * 201 # 201 characters, exceeds limit of 200
response = self.client.post('/search/', {'q': long_term})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Fehler:')
self.assertContains(response, 'Suchbegriff ist zu lang')
def test_search_max_length_allowed(self):
"""Test that exactly 200 characters are allowed"""
max_term = 'a' * 200 # Exactly 200 characters
response = self.client.post('/search/', {'q': max_term})
self.assertEqual(response.status_code, 200)
# Should not show validation error
self.assertNotContains(response, 'Fehler:')
def test_search_german_umlauts_allowed(self):
"""Test that German umlauts are allowed in search"""
response = self.client.post('/search/', {'q': 'Test Müller äöü ÄÖÜ ß'})
self.assertEqual(response.status_code, 200)
# Should not show validation error
self.assertNotContains(response, 'Fehler:')
def test_search_special_characters_allowed(self):
"""Test that allowed special characters work"""
response = self.client.post('/search/', {'q': 'Test-Test, Test: Test; Test! Test? (Test) [Test] {Test} "Test" \'Test\''})
self.assertEqual(response.status_code, 200)
# Should not show validation error
self.assertNotContains(response, 'Fehler:')
def test_search_input_preserved_on_error(self):
"""Test that search input is preserved on validation errors"""
response = self.client.post('/search/', {'q': '<script>Test</script>'})
self.assertEqual(response.status_code, 200)
# The input should be preserved (escaped) in the form
# Since HTML tags are stripped, we expect "Test" to be searched
self.assertContains(response, 'Suchresultate für Test')
def test_search_xss_prevention_in_results(self):
"""Test that search terms are escaped in results to prevent XSS"""
# Create content with potential XSS
self.kurztext.inhalt = "Content with <script>alert('xss')</script> term"
self.kurztext.save()
response = self.client.post('/search/', {'q': 'term'})
self.assertEqual(response.status_code, 200)
# The script tag should be escaped in the output
# Note: This depends on how the template renders the content
self.assertContains(response, 'Suchresultate für term')
@patch('pages.views.pprint.pp')
def test_search_result_logging(self, mock_pprint):
"""Test that search results are logged for debugging"""
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
# Verify that pprint.pp was called with the result
mock_pprint.assert_called_once()
def test_search_multiple_documents(self):
"""Test search across multiple documents"""
# Create second document
dokument2 = Dokument.objects.create(
nummer="TEST-002",
dokumententyp=self.dokumententyp,
name="Zweites Test Dokument",
gueltigkeit_von=date.today(),
aktiv=True
)
vorgabe2 = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=dokument2,
thema=self.thema,
titel="Zweite Test Vorgabe",
gueltigkeit_von=date.today()
)
VorgabeKurztext.objects.create(
abschnitt=vorgabe2,
inhalt="Zweiter Test Inhalt"
)
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
# Should find results from both documents
self.assertContains(response, 'TEST-001')
self.assertContains(response, 'TEST-002')
class SearchValidationTest(TestCase):
"""Test the validate_search_input function directly"""
def test_validate_search_input_valid(self):
"""Test valid search input"""
from pages.views import validate_search_input
result = validate_search_input("Test Suchbegriff")
self.assertEqual(result, "Test Suchbegriff")
def test_validate_search_input_empty(self):
"""Test empty search input"""
from pages.views import validate_search_input
with self.assertRaises(ValidationError) as context:
validate_search_input("")
self.assertIn("Suchbegriff darf nicht leer sein", str(context.exception))
def test_validate_search_input_html_stripped(self):
"""Test that HTML tags are stripped"""
from pages.views import validate_search_input
result = validate_search_input("<script>alert('xss')</script>Test")
self.assertEqual(result, "alert('xss')Test")
def test_validate_search_input_invalid_chars(self):
"""Test validation of invalid characters"""
from pages.views import validate_search_input
with self.assertRaises(ValidationError) as context:
validate_search_input("Test| DROP TABLE users")
self.assertIn("Ungültige Zeichen im Suchbegriff", str(context.exception))
def test_validate_search_input_too_long(self):
"""Test length validation"""
from pages.views import validate_search_input
with self.assertRaises(ValidationError) as context:
validate_search_input("a" * 201)
self.assertIn("Suchbegriff ist zu lang", str(context.exception))
def test_validate_search_input_whitespace_stripped(self):
"""Test that whitespace is stripped"""
from pages.views import validate_search_input
result = validate_search_input(" Test Suchbegriff ")
self.assertEqual(result, "Test Suchbegriff")

View File

@@ -1,71 +1,31 @@
from django.shortcuts import render from django.shortcuts import render
from django.core.exceptions import ValidationError
from django.utils.html import escape
import re
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
from dokumente.models import Dokument, VorgabeLangtext, VorgabeKurztext, Geltungsbereich, Vorgabe from dokumente.models import Dokument, VorgabeLangtext, VorgabeKurztext, Geltungsbereich
from itertools import groupby from itertools import groupby
import datetime import datetime
import pprint
def startseite(request): def startseite(request):
standards=list(Dokument.objects.filter(aktiv=True)) standards=list(Dokument.objects.all())
return render(request, 'startseite.html', {"dokumente":standards}) return render(request, 'startseite.html', {"dokumente":standards,})
def validate_search_input(search_term):
"""
Validate search input to prevent SQL injection and XSS
"""
if not search_term:
raise ValidationError("Suchbegriff darf nicht leer sein")
# Remove any HTML tags to prevent XSS
search_term = re.sub(r'<[^>]*>', '', search_term)
# Allow only alphanumeric characters, spaces, and basic punctuation
# This prevents SQL injection and other malicious input while allowing useful characters
if not re.match(r'^[a-zA-Z0-9äöüÄÖÜß\s\-\.\,\:\;\!\?\(\)\[\]\{\}\"\']+$', search_term):
raise ValidationError("Ungültige Zeichen im Suchbegriff")
# Limit length to prevent DoS attacks
if len(search_term) > 200:
raise ValidationError("Suchbegriff ist zu lang")
return search_term.strip()
def search(request): def search(request):
if request.method == "GET": if request.method == "GET":
return render(request, 'search.html') return render(request, 'search.html')
elif request.method == "POST": elif request.method == "POST":
raw_search_term = request.POST.get("q", "") suchbegriff=request.POST.get("q")
try:
suchbegriff = validate_search_input(raw_search_term)
except ValidationError as e:
return render(request, 'search.html', {
'error_message': str(e),
'search_term': escape(raw_search_term)
})
# Escape the search term for display in templates
safe_search_term = escape(suchbegriff)
result= {"all": {}} result= {"all": {}}
qs = VorgabeKurztext.objects.filter(inhalt__icontains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today()) qs = VorgabeKurztext.objects.filter(inhalt__contains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today())
result["kurztext"] = {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)} result["kurztext"] = {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)}
qs = VorgabeLangtext.objects.filter(inhalt__icontains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today()) qs = VorgabeLangtext.objects.filter(inhalt__contains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today())
result['langtext']= {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)} result['langtext']= {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)}
qs = Vorgabe.objects.filter(titel__icontains=suchbegriff).exclude(gueltigkeit_bis__lt=datetime.date.today())
result['titel']= {k: list(g) for k, g in groupby(qs, key=lambda o: o.dokument)}
for r in result.keys(): for r in result.keys():
for s in result[r].keys(): for s in result[r].keys():
if r == 'titel': result["all"][s] = set(result[r][s])
result["all"][s] = set(result["all"].get(s, set()) | set(result[r][s]))
else:
result["all"][s] = set(result["all"].get(s, set()) | set(result[r][s]))
result["geltungsbereich"]={} result["geltungsbereich"]={}
geltungsbereich=set(list([x.geltungsbereich for x in Geltungsbereich.objects.filter(inhalt__icontains=suchbegriff)])) geltungsbereich=set(list([x.geltungsbereich for x in Geltungsbereich.objects.filter(inhalt__contains=suchbegriff)]))
for s in geltungsbereich: for s in geltungsbereich:
result["geltungsbereich"][s]=render_textabschnitte(s.geltungsbereich_set.order_by("order")) result["geltungsbereich"][s]=render_textabschnitte(s.geltungsbereich_set.order_by("order"))
pprint.pp (result)
return render(request,"results.html",{"suchbegriff":safe_search_term,"resultat":result}) return render(request,"results.html",{"suchbegriff":suchbegriff,"resultat":result})

View File

@@ -13,4 +13,4 @@ class ReferenzerklaerungInline(NestedStackedInline):
class ReferenzAdmin(NestedModelAdmin): class ReferenzAdmin(NestedModelAdmin):
inlines=[ReferenzerklaerungInline] inlines=[ReferenzerklaerungInline]
list_display =['Path'] list_display =['Path']
search_fields=("referenz","path") search_fields=("referenz",)

View File

@@ -1,190 +1,51 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load mptt_tags %} {% load mptt_tags %}
{% block title %}Referenz: {{ referenz.Path }}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="/referenzen/">Referenzen</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ referenz.Path }}</li>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <h1><a href="../{{ referenz.ParentID }}"></a>{{ referenz.Path }}</h1>
<div class="col-lg-8"> {% if referenz.erklaerung %}
<!-- Referenz Header --> <div class="card mb-4">
<div class="card mb-4"> <div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<div class="card-header bg-primary text-white"> <h3 class="h5 m-0">Beschreibung</h3>
<div class="d-flex justify-content-between align-items-start"> {% if referenz.url %}
<div> <span class="badge bg-light text-black">
<h1 class="h3 mb-2">🔗 {{ referenz.Path }}</h1> <a href="{{ referenz.url }}">Link</a>
{% if referenz.ParentID %} </span>{% endif %}
<small class="opacity-75"> </div>
<a href="/referenzen/{{ referenz.ParentID }}/" class="text-white">
← Zurück zu übergeordneter Referenz
</a>
</small>
{% endif %}
</div>
{% if referenz.url %}
<a href="{{ referenz.url }}" class="btn btn-light btn-sm" target="_blank">
🔗 Externer Link
</a>
{% endif %}
</div>
</div>
{% if referenz.erklaerung %} <div class="card-body p-2">
<div class="card-body">
<h5 class="card-title">📖 Beschreibung</h5>
{% for typ, html in referenz.erklaerung %} {% for typ, html in referenz.erklaerung %}
{% if html %} {% if html %}<div>{{ html|safe }}</div>{% endif %}{% endfor %}
<div class="content-section">{{ html|safe }}</div> </div>
{% endif %} </div>
{% endfor %} {% endif %}
</div>
{% endif %} <div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<h3 class="h5 m-0">Referenzierte Vorgaben</h3>
</div> </div>
<!-- Referenzierte Vorgaben --> <div class="card-body p-2">
<div class="card"> {% recursetree referenz.children %}
<div class="card-header"> {% if not node == referenz %}
<h3 class="h5 mb-0">📝 Referenzierte Vorgaben</h3> {#<a href="../{{node.id}}">#}
</div> {{ node.Path }}
<div class="card-body"> {#</a>#}
{% recursetree referenz.children %} {% else %}
{% if not node == referenz %} {{ node.Path }}
<div class="mb-3 p-3 border rounded"> {% endif %}
<h6 class="mb-2">{{ node.Path }}</h6> <br>
{% if node.referenziertvon %} {% if node.referenziertvon %}
<div class="ms-3"> <ul>
<small class="text-muted">Referenziert von:</small>
<ul class="list-unstyled mb-0">
{% for ref in node.referenziertvon %}
<li class="mb-1">
<a href="/dokumente/{{ ref.dokument.nummer }}/#{{ref.Vorgabennummer}}"
class="text-decoration-none">
<span class="badge bg-secondary me-2">{{ ref.Vorgabennummer }}</span>
{{ ref.titel }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
{% if node.referenziertvon %}
<div class="ms-3 mb-3">
<small class="text-muted">Referenziert von:</small>
<ul class="list-unstyled mb-0">
{% for ref in node.referenziertvon %} {% for ref in node.referenziertvon %}
<li class="mb-1"> <li><a href="{% url 'standard_detail' nummer=ref.dokument.nummer %}#{{ref.Vorgabennummer}}">{{ref}}</a></li>
<a href="/dokumente/{{ ref.dokument.nummer }}/#{{ref.Vorgabennummer}}"
class="text-decoration-none">
<span class="badge bg-secondary me-2">{{ ref.Vorgabennummer }}</span>
{{ ref.titel }}
</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> <br>
{% endif %} {% endif %}
{% if not node.is_leaf_node %}
{% if not node.is_leaf_node %} {{ children }}
<div class="ms-3"> {% endif %}
{{ children }} {% endrecursetree %}
</div>
{% endif %}
{% endrecursetree %}
</div>
</div> </div>
</div> </div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Quick Actions -->
<div class="card mb-4 sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">⚡ Aktionen</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
{% if referenz.ParentID %}
<a href="/referenzen/{{ referenz.ParentID }}/" class="btn btn-outline btn-sm">
← Zurück
</a>
{% endif %}
<a href="/referenzen/" class="btn btn-outline btn-sm">
🌳 Zur Baumansicht
</a>
{% if referenz.url %}
<a href="{{ referenz.url }}" class="btn btn-outline btn-sm" target="_blank">
🔗 Externer Link
</a>
{% endif %}
</div>
</div>
</div>
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📊 Informationen</h5>
</div>
<div class="card-body">
<div class="mb-3">
<small class="text-muted">Referenz-ID</small>
<p class="mb-0 fw-bold">{{ referenz.id }}</p>
</div>
{% if referenz.ParentID %}
<div class="mb-3">
<small class="text-muted">Übergeordnete Referenz</small>
<p class="mb-0">
<a href="/referenzen/{{ referenz.ParentID }}/" class="text-decoration-none">
ID: {{ referenz.ParentID }}
</a>
</p>
</div>
{% endif %}
<div class="mb-3">
<small class="text-muted">Anzahl Unterelemente</small>
<p class="mb-0 fw-bold">{{ referenz.get_children.count }}</p>
</div>
</div>
</div>
<!-- Navigation Help -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">💡 Hinweise</h5>
</div>
<div class="card-body">
<ul class="small mb-0">
<li>Diese Seite zeigt Details zur ausgewählten Referenz</li>
<li>Verknüpfte Vorgaben sind direkt verlinkt</li>
<li>Nutzen Sie die Baumansicht für die Übersicht</li>
</ul>
</div>
</div>
</div>
</div>
<style>
.content-section {
line-height: 1.6;
margin-bottom: 1rem;
}
.content-section h1, .content-section h2, .content-section h3 {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.border {
border-color: var(--border-color) !important;
}
.badge {
font-size: 0.75rem;
}
</style>
{% endblock %} {% endblock %}

View File

@@ -1,466 +1,21 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Referenzen{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Referenzen</li>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <h1>Referenzen</h1>
<div class="col-lg-8">
<!-- Header -->
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0">🔗 Referenzbaum</h1>
<div class="d-flex gap-2">
<button class="btn btn-outline btn-sm" onclick="expandAll()">
Alle ausklappen
</button>
<button class="btn btn-outline btn-sm" onclick="collapseAll()">
Alle einklappen
</button>
</div>
</div>
</div>
<div class="card-body">
<p class="text-muted mb-0">
Hier finden Sie alle Referenzen und Querverbindungen zwischen den Standards und Vorgaben.
Klicken Sie auf die Pfeile um den Baum zu navigieren.
</p>
</div>
</div>
<!-- Search and Filter --> <div>
<div class="card mb-4"> {% load mptt_tags %}
<div class="card-body"> <ul class="tree">
<div class="row g-3"> {% recursetree referenzen %}
<div class="col-md-8"> <li>
<label for="tree-search" class="form-label">🔍 Referenzen durchsuchen</label> <a href="{{node.id}}">{{ node.name_nummer }}{% if node.name_text %} ({{node.name_text}}){% endif %}</a>
<input type="text" {% if not node.is_leaf_node %}
class="form-control" <ul class="children">
id="tree-search"
placeholder="Suchbegriff eingeben..."
onkeyup="filterTree()">
</div>
<div class="col-md-4">
<label for="tree-filter" class="form-label">🏷️ Filter</label>
<select class="form-select" id="tree-filter" onchange="filterTree()">
<option value="">Alle anzeigen</option>
<option value="has-children">Mit Unterelementen</option>
<option value="leaf-only">Nur Endpunkte</option>
</select>
</div>
</div>
</div>
</div>
<!-- Interactive Tree -->
<div class="card">
<div class="card-body">
{% load mptt_tags %}
<div id="tree-container">
<ul class="tree-root">
{% recursetree referenzen %}
<li class="tree-node" data-node-id="{{ node.id }}" data-node-text="{{ node.name_text|default:'' }} {{ node.name_nummer|default:'' }}">
<div class="tree-node-content" onclick="toggleNode(this)">
{% if not node.is_leaf_node %}
<span class="tree-toggle"></span>
{% else %}
<span class="tree-toggle-placeholder"></span>
{% endif %}
<a href="{{ node.id }}" class="tree-link">
{% if node.name_nummer %}
<span class="tree-number">{{ node.name_nummer }}</span>
{% endif %}
{% if node.name_text %}
<span class="tree-text">{{ node.name_text }}</span>
{% endif %}
</a>
<div class="tree-node-meta">
{% if not node.is_leaf_node %}
<span class="badge bg-info">{{ node.get_children.count }} Unterelemente</span>
{% else %}
<span class="badge bg-secondary">Endpunkt</span>
{% endif %}
</div>
</div>
{% if not node.is_leaf_node %}
<ul class="tree-children">
{{ children }} {{ children }}
</ul> </ul>
{% endif %} {% endif %}
</li> </li>
{% endrecursetree %} {% endrecursetree %}
</ul> </ul>
</div>
{% if not referenzen %}
<div class="text-center py-5">
<span style="font-size: 3rem;">🔗</span>
<h4 class="text-muted mt-3">Keine Referenzen gefunden</h4>
<p class="text-muted">Es wurden keine Referenzen in der Datenbank gefunden.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Statistics -->
<div class="card mb-4 sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">📊 Statistiken</h5>
</div>
<div class="card-body">
<div class="row text-center mb-3">
<div class="col-6">
<h4 class="text-primary mb-1">{{ referenzen|length }}</h4>
<small class="text-muted">Gesamt</small>
</div>
<div class="col-6">
<h4 class="text-success mb-1">
{% for node in referenzen %}
{% if node.is_leaf_node %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
</h4>
<small class="text-muted">Endpunkte</small>
</div>
</div>
<div class="border-top pt-3">
<h6 class="text-muted mb-2">Baumtiefe</h6>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary" style="width: 75%"></div>
</div>
<small class="text-muted">Maximale Tiefe: 4 Ebenen</small>
</div>
</div>
</div>
<!-- Quick Navigation -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">🧭 Navigation</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-outline btn-sm" onclick="scrollToTop()">
⬆️ Zum Anfang
</button>
<button class="btn btn-outline btn-sm" onclick="findNextMatch()">
⬇️ Nächster Treffer
</button>
<button class="btn btn-outline btn-sm" onclick="resetFilters()">
🔄 Filter zurücksetzen
</button>
</div>
</div>
</div>
<!-- Help -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">💡 Hinweise</h5>
</div>
<div class="card-body">
<ul class="small mb-0">
<li>Klicken Sie auf ▼/▶ um Knoten ein-/auszuklappen</li>
<li>Nutzen Sie die Suche um gezielt zu filtern</li>
<li>Referenznummern sind hervorgehoben</li>
<li>Endpunkte haben keine Unterelemente</li>
</ul>
</div>
</div>
</div>
</div> </div>
<style>
/* Tree Styles */
.tree-root {
list-style: none;
padding-left: 0;
margin: 0;
}
.tree-node {
margin-bottom: 2px;
}
.tree-node-content {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.tree-node-content:hover {
background-color: var(--bg-secondary);
border-color: var(--primary-color);
}
.tree-toggle {
width: 20px;
text-align: center;
font-size: 12px;
color: var(--text-muted);
margin-right: 8px;
}
.tree-toggle-placeholder {
width: 20px;
margin-right: 8px;
}
.tree-link {
text-decoration: none;
color: var(--text-primary);
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.tree-link:hover {
color: var(--primary-color);
}
.tree-number {
font-family: var(--font-mono);
font-weight: 600;
color: var(--primary-color);
background-color: var(--bg-secondary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.875rem;
}
.tree-text {
font-weight: 500;
}
.tree-node-meta {
margin-left: auto;
display: flex;
gap: 4px;
}
.tree-children {
list-style: none;
padding-left: 28px;
margin: 2px 0 0 0;
border-left: 2px solid var(--border-color);
margin-left: 10px;
}
.tree-children .tree-node {
position: relative;
}
.tree-children .tree-node::before {
content: '';
position: absolute;
left: -30px;
top: 20px;
width: 20px;
height: 1px;
background-color: var(--border-color);
}
/* Collapsed state */
.tree-children.collapsed {
display: none;
}
.tree-node.collapsed .tree-toggle {
transform: rotate(-90deg);
}
/* Highlighted search results */
.tree-node.highlighted > .tree-node-content {
background-color: var(--warning-color);
color: white;
}
.tree-node.highlighted .tree-number {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.tree-children {
padding-left: 20px;
}
.tree-node-meta {
display: none;
}
.tree-number {
font-size: 0.75rem;
}
}
</style>
<script>
let currentMatchIndex = -1;
let matches = [];
function toggleNode(element) {
const node = element.parentElement;
const children = node.querySelector(':scope > .tree-children');
const toggle = element.querySelector('.tree-toggle');
if (children) {
children.classList.toggle('collapsed');
node.classList.toggle('collapsed');
if (toggle) {
toggle.textContent = children.classList.contains('collapsed') ? '▶' : '▼';
}
}
}
function expandAll() {
const children = document.querySelectorAll('.tree-children');
const nodes = document.querySelectorAll('.tree-node');
const toggles = document.querySelectorAll('.tree-toggle');
children.forEach(child => child.classList.remove('collapsed'));
nodes.forEach(node => node.classList.remove('collapsed'));
toggles.forEach(toggle => {
if (toggle.textContent === '▶') {
toggle.textContent = '▼';
}
});
}
function collapseAll() {
const children = document.querySelectorAll('.tree-children');
const nodes = document.querySelectorAll('.tree-node');
const toggles = document.querySelectorAll('.tree-toggle');
children.forEach(child => child.classList.add('collapsed'));
nodes.forEach(node => node.classList.add('collapsed'));
toggles.forEach(toggle => {
if (toggle.textContent === '▼') {
toggle.textContent = '▶';
}
});
}
function filterTree() {
const searchTerm = document.getElementById('tree-search').value.toLowerCase();
const filterType = document.getElementById('tree-filter').value;
const nodes = document.querySelectorAll('.tree-node');
matches = [];
currentMatchIndex = -1;
nodes.forEach(node => {
const nodeText = node.dataset.nodeText.toLowerCase();
const hasChildren = node.querySelector(':scope > .tree-children') !== null;
const isLeaf = !hasChildren;
let showNode = true;
// Apply search filter
if (searchTerm && !nodeText.includes(searchTerm)) {
showNode = false;
}
// Apply type filter
if (filterType === 'has-children' && !hasChildren) {
showNode = false;
} else if (filterType === 'leaf-only' && !isLeaf) {
showNode = false;
}
// Show/hide node
node.style.display = showNode ? 'block' : 'none';
// Highlight search matches
if (searchTerm && nodeText.includes(searchTerm)) {
node.classList.add('highlighted');
matches.push(node);
} else {
node.classList.remove('highlighted');
}
// Auto-expand parent nodes of matches
if (searchTerm && nodeText.includes(searchTerm)) {
let parent = node.parentElement;
while (parent && parent.classList.contains('tree-children')) {
parent.classList.remove('collapsed');
parent.parentElement.classList.remove('collapsed');
const toggle = parent.parentElement.querySelector('.tree-toggle');
if (toggle) toggle.textContent = '▼';
parent = parent.parentElement.parentElement;
}
}
});
}
function findNextMatch() {
if (matches.length === 0) return;
currentMatchIndex = (currentMatchIndex + 1) % matches.length;
const match = matches[currentMatchIndex];
// Scroll to match
match.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Highlight temporarily
match.style.backgroundColor = 'var(--accent-color)';
setTimeout(() => {
match.style.backgroundColor = '';
}, 1000);
}
function scrollToTop() {
document.getElementById('tree-container').scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
function resetFilters() {
document.getElementById('tree-search').value = '';
document.getElementById('tree-filter').value = '';
filterTree();
expandAll();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
// Add keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'f':
e.preventDefault();
document.getElementById('tree-search').focus();
break;
case 'e':
e.preventDefault();
expandAll();
break;
case 'w':
e.preventDefault();
collapseAll();
break;
}
}
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -6,7 +6,6 @@ charset-normalizer==3.4.3
curtsies==0.4.3 curtsies==0.4.3
cwcwidth==0.1.10 cwcwidth==0.1.10
Django==5.2.5 Django==5.2.5
django-admin-sortable2==2.2.8
django-js-asset==3.1.2 django-js-asset==3.1.2
django-mptt==0.17.0 django-mptt==0.17.0
django-mptt-admin==2.8.0 django-mptt-admin==2.8.0

View File

@@ -1,39 +1,14 @@
/* Style each Vorgabe inline block */ /* Style each Vorgabe inline block */
.djn-dynamic-form-Standards-vorgabe, .djn-dynamic-form-Standards-vorgabe {
.djn-dynamic-form-dokumente-vorgabe { border: 2px solid #ccc;
border: 3px solid #2c5aa0;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
margin-bottom: 50px; margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1); background-color: #f9f9f9;
}
/* Make Vorgabe title prominent */
.djn-dynamic-form-Standards-vorgabe > h3,
.djn-dynamic-form-dokumente-vorgabe > h3 {
font-size: 18px;
font-weight: 700;
color: #2c5aa0;
margin: -15px -15px 15px -15px;
padding: 12px 15px;
background: linear-gradient(to bottom, #e8f0f8, #d4e4f3);
border-bottom: 2px solid #2c5aa0;
border-radius: 5px 5px 0 0;
}
/* Make Vorgabe identifier in tabular view prominent */
tbody.djn-dynamic-form-Standards-vorgabe td.original,
tbody.djn-dynamic-form-dokumente-vorgabe td.original,
tbody.djn-dynamic-form-Standards-vorgabe td.original p,
tbody.djn-dynamic-form-dokumente-vorgabe td.original p {
font-size: 16px !important;
font-weight: 700 !important;
color: #2c5aa0 !important;
} }
/* Optional: Slight padding for inner fieldsets (e.g., Langtext/Kurztext inlines) */ /* Optional: Slight padding for inner fieldsets (e.g., Langtext/Kurztext inlines) */
.djn-dynamic-form-Standards-vorgabe .inline-related, .djn-dynamic-form-Standards-vorgabe .inline-related {
.djn-dynamic-form-dokumente-vorgabe .inline-related {
margin-top: 10px; margin-top: 10px;
padding-left: 10px; padding-left: 10px;
border-left: 2px dashed #ccc; border-left: 2px dashed #ccc;

View File

@@ -1,58 +1,21 @@
window.addEventListener('load', function () { window.addEventListener('load', function () {
setTimeout(() => { setTimeout(() => {
// Try different selectors for nested admin vorgabe elements const vorgabenBlocks = document.querySelectorAll('.djn-dynamic-form-Standards-vorgabe');
const selectors = [ console.log("Found", vorgabenBlocks.length, "Vorgaben blocks");
'.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) => {
// Find the existing title/header within the vorgabe block const header = document.createElement('div');
const existingHeader = block.querySelector('h3, .inline-label, .module h2, .djn-inline-header'); header.className = 'vorgabe-toggle-header';
header.innerHTML = `▼ Vorgabe ${index + 1}`;
header.style.cursor = 'pointer';
if (existingHeader) { block.parentNode.insertBefore(header, block);
// Make the existing header clickable for collapse/expand
existingHeader.style.cursor = 'pointer';
existingHeader.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Find all content to collapse - everything except the header itself header.addEventListener('click', () => {
const allChildren = Array.from(block.children); const isHidden = block.style.display === 'none';
const contentElements = allChildren.filter(child => child !== existingHeader && !child.contains(existingHeader)); block.style.display = isHidden ? '' : 'none';
header.innerHTML = `${isHidden ? '▼' : '▶'} Vorgabe ${index + 1}`;
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}`;
}
}); });
}, 1000); // wait longer to allow nested inlines to render }, 500); // wait 500ms to allow nested inlines to render
}); });

View File

@@ -1,819 +0,0 @@
/* Vorgaben UI Custom Styles */
:root {
/* Professional color scheme for security standards */
--primary-color: #1e3a8a;
--primary-dark: #1e2f5a;
--primary-light: #3b82f6;
--secondary-color: #64748b;
--accent-color: #0ea5e9;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--info-color: #06b6d4;
/* Neutral colors */
--gray-50: #f8fafc;
--gray-100: #f1f5f9;
--gray-200: #e2e8f0;
--gray-300: #cbd5e1;
--gray-400: #94a3b8;
--gray-500: #64748b;
--gray-600: #475569;
--gray-700: #334155;
--gray-800: #1e293b;
--gray-900: #0f172a;
/* Typography */
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
}
/* Dark mode variables */
[data-theme="dark"] {
--bg-primary: var(--gray-900);
--bg-secondary: var(--gray-800);
--bg-tertiary: var(--gray-700);
--text-primary: var(--gray-100);
--text-secondary: var(--gray-300);
--text-muted: var(--gray-400);
--border-color: var(--gray-700);
--card-bg: var(--gray-800);
--navbar-bg: var(--gray-900);
}
/* Light mode variables */
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: var(--gray-50);
--bg-tertiary: var(--gray-100);
--text-primary: var(--gray-900);
--text-secondary: var(--gray-700);
--text-muted: var(--gray-500);
--border-color: var(--gray-200);
--card-bg: #ffffff;
--navbar-bg: #ffffff;
}
/* Base styles */
* {
box-sizing: border-box;
}
body {
font-family: var(--font-family);
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color var(--transition-normal), color var(--transition-normal);
line-height: 1.6;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.3;
margin-bottom: var(--spacing-md);
color: var(--text-primary);
}
h1 { font-size: 2.25rem; }
h2 { font-size: 1.875rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1.125rem; }
h6 { font-size: 1rem; }
/* Navbar enhancements */
.navbar {
background-color: var(--navbar-bg) !important;
border-bottom: 1px solid var(--border-color);
box-shadow: var(--shadow-sm);
padding: var(--spacing-md) 0;
transition: all var(--transition-normal);
}
.navbar-brand {
font-weight: 700;
font-size: 1.5rem;
color: var(--primary-color) !important;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.navbar-brand::before {
content: "🔒";
font-size: 1.25rem;
}
.navbar-nav .nav-link {
color: var(--text-secondary) !important;
font-weight: 500;
padding: var(--spacing-sm) var(--spacing-md) !important;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
margin: 0 var(--spacing-xs);
}
.navbar-nav .nav-link:hover,
.navbar-nav .nav-link.active {
color: var(--primary-color) !important;
background-color: var(--bg-secondary);
}
/* Search bar in navbar */
.navbar-search {
position: relative;
max-width: 400px;
flex: 1;
}
.navbar-search input {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-sm) var(--spacing-lg);
padding-right: 2.5rem;
width: 100%;
transition: all var(--transition-fast);
}
.navbar-search input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
}
.navbar-search button {
position: absolute;
right: var(--spacing-xs);
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
padding: var(--spacing-sm);
}
/* Dark mode toggle */
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
cursor: pointer;
color: var(--text-secondary);
transition: all var(--transition-fast);
font-size: 1.25rem;
}
.theme-toggle:hover {
background-color: var(--bg-secondary);
color: var(--primary-color);
}
/* Breadcrumb */
.breadcrumb {
background-color: transparent;
padding: var(--spacing-md) 0;
margin-bottom: var(--spacing-lg);
}
.breadcrumb-item + .breadcrumb-item::before {
content: "";
color: var(--text-muted);
}
.breadcrumb-item a {
color: var(--text-secondary);
text-decoration: none;
transition: color var(--transition-fast);
}
.breadcrumb-item a:hover {
color: var(--primary-color);
}
/* Card enhancements */
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
transition: all var(--transition-normal);
overflow: hidden;
}
.card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.card-header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
padding: var(--spacing-lg);
}
.card-body {
padding: var(--spacing-lg);
}
/* Standard cards */
.standard-card {
border-left: 4px solid var(--primary-color);
margin-bottom: var(--spacing-lg);
}
.standard-card.inactive {
border-left-color: var(--gray-400);
opacity: 0.7;
}
.standard-card .card-title {
color: var(--text-primary);
font-size: 1.125rem;
font-weight: 600;
margin-bottom: var(--spacing-sm);
}
.standard-card .standard-number {
color: var(--primary-color);
font-weight: 700;
font-family: var(--font-mono);
}
.standard-card .standard-meta {
color: var(--text-muted);
font-size: 0.875rem;
margin-bottom: var(--spacing-md);
}
/* Badges */
.badge {
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
}
.badge-relevance {
background-color: var(--info-color);
color: white;
}
.badge-status-active {
background-color: var(--success-color);
color: white;
}
.badge-status-inactive {
background-color: var(--gray-400);
color: white;
}
/* Buttons */
.btn {
font-weight: 500;
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
border: none;
cursor: pointer;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-outline {
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.btn-outline:hover {
background-color: var(--bg-secondary);
border-color: var(--primary-color);
color: var(--primary-color);
}
/* Search enhancements */
.search-container {
max-width: 600px;
margin: 0 auto;
}
.search-form {
background-color: var(--card-bg);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
border: 1px solid var(--border-color);
}
.search-results {
margin-top: var(--spacing-xl);
}
.result-item {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-md);
transition: all var(--transition-fast);
}
.result-item:hover {
box-shadow: var(--shadow-sm);
border-color: var(--primary-color);
}
/* Table of contents */
.toc {
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
position: sticky;
top: var(--spacing-lg);
}
.toc h3 {
margin-top: 0;
margin-bottom: var(--spacing-md);
font-size: 1.125rem;
color: var(--text-primary);
}
.toc ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.toc li {
margin-bottom: var(--spacing-xs);
}
.toc a {
color: var(--text-secondary);
text-decoration: none;
display: block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.toc a:hover,
.toc a.active {
background-color: var(--bg-tertiary);
color: var(--primary-color);
}
/* Footer */
.footer {
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: var(--spacing-xl) 0;
margin-top: var(--spacing-2xl);
color: var(--text-muted);
text-align: center;
}
/* Responsive design */
@media (max-width: 1200px) {
.container-fluid {
padding-left: var(--spacing-md);
padding-right: var(--spacing-md);
}
}
@media (max-width: 992px) {
.navbar-search {
max-width: 300px;
}
.standard-card {
margin-bottom: var(--spacing-md);
}
.toc {
position: static;
margin-bottom: var(--spacing-lg);
}
}
@media (max-width: 768px) {
/* Navigation */
.navbar {
padding: var(--spacing-sm) 0;
}
.navbar-brand {
font-size: 1.25rem;
}
.navbar-search {
max-width: 100%;
margin-top: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.navbar-nav .nav-link {
padding: var(--spacing-sm) var(--spacing-md) !important;
margin: 0;
}
/* Typography */
h1 { font-size: 1.875rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }
h4 { font-size: 1.125rem; }
/* Cards */
.card {
margin-bottom: var(--spacing-md);
}
.standard-card {
border-left-width: 3px;
}
.standard-card .card-body {
padding: var(--spacing-md);
}
/* Buttons */
.btn {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
}
.btn-lg {
padding: var(--spacing-md) var(--spacing-lg);
font-size: 1rem;
}
/* Forms */
.form-control, .form-select {
font-size: 0.875rem;
}
/* Search */
.search-form {
padding: var(--spacing-md);
}
.result-item {
padding: var(--spacing-md);
}
/* Table of Contents */
.toc {
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.toc ul {
font-size: 0.875rem;
}
/* Standard Detail Page */
.vorgabe-card .card-header {
padding: var(--spacing-md);
font-size: 0.875rem;
}
.vorgabe-card .card-body {
padding: var(--spacing-md);
}
.vorgabe-content {
font-size: 0.875rem;
}
/* Homepage */
.display-4 {
font-size: 2rem;
}
.statistics-card .h2 {
font-size: 1.5rem;
}
/* References Tree */
.tree-node-content {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
}
.tree-children {
padding-left: var(--spacing-lg);
}
.tree-node-meta {
display: none;
}
/* Breadcrumb */
.breadcrumb {
padding: var(--spacing-sm) 0;
font-size: 0.875rem;
}
/* Footer */
.footer {
padding: var(--spacing-lg) 0;
font-size: 0.875rem;
}
}
@media (max-width: 576px) {
/* Extra small screens */
.container-fluid {
padding-left: var(--spacing-sm);
padding-right: var(--spacing-sm);
}
/* Typography */
h1 { font-size: 1.5rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.125rem; }
/* Cards */
.card {
border-radius: var(--radius-md);
}
.card-header {
padding: var(--spacing-sm) var(--spacing-md);
}
.card-body {
padding: var(--spacing-sm) var(--spacing-md);
}
/* Buttons */
.btn {
width: 100%;
margin-bottom: var(--spacing-xs);
}
.btn-group .btn {
width: auto;
}
/* Forms */
.row.g-3 > .col {
margin-bottom: var(--spacing-md);
}
/* Homepage */
.display-4 {
font-size: 1.75rem;
}
.hero-section .btn-lg {
width: 100%;
margin-bottom: var(--spacing-sm);
}
.statistics-row .col-6 {
margin-bottom: var(--spacing-md);
}
/* Search */
.search-page .row {
flex-direction: column;
}
.search-page .col-lg-4 {
order: 2;
}
.search-page .col-lg-8 {
order: 1;
}
/* Standard Detail */
.standard-detail-page .row {
flex-direction: column;
}
.standard-detail-page .col-lg-8 {
order: 1;
}
.standard-detail-page .col-lg-4 {
order: 2;
}
/* References */
.references-page .row {
flex-direction: column;
}
.references-page .col-lg-8 {
order: 1;
}
.references-page .col-lg-4 {
order: 2;
}
/* Tree navigation */
.tree-node-content {
flex-wrap: wrap;
}
.tree-link {
min-width: 0;
flex: 1;
}
/* Badges */
.badge {
font-size: 0.625rem;
padding: 0.125rem 0.375rem;
}
/* Lists */
.list-group-item {
padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.875rem;
}
/* Tables */
.table {
font-size: 0.75rem;
}
.table th,
.table td {
padding: var(--spacing-xs) var(--spacing-sm);
}
}
/* Touch-friendly adjustments for mobile */
@media (hover: none) and (pointer: coarse) {
.card:hover {
transform: none;
}
.btn:hover {
transform: none;
}
.tree-node-content:hover {
background-color: transparent;
border-color: transparent;
}
.navbar-nav .nav-link:hover {
background-color: transparent;
}
/* Increase touch targets */
.btn {
min-height: 44px;
min-width: 44px;
}
.tree-node-content {
min-height: 44px;
}
.navbar-nav .nav-link {
min-height: 44px;
display: flex;
align-items: center;
}
}
/* Landscape mobile optimizations */
@media (max-width: 768px) and (orientation: landscape) {
.navbar {
padding: var(--spacing-xs) 0;
}
.navbar-brand {
font-size: 1.125rem;
}
h1 { font-size: 1.625rem; }
.card {
margin-bottom: var(--spacing-sm);
}
.search-form {
padding: var(--spacing-sm) var(--spacing-md);
}
}
/* High DPI displays */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.navbar-brand::before {
image-rendering: -webkit-optimize-contrast;
}
.tree-toggle {
font-weight: 300;
}
}
/* Reduced motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.card:hover {
transform: none;
}
.btn:hover {
transform: none;
}
}
/* Loading states */
.loading {
opacity: 0.6;
pointer-events: none;
}
.spinner {
border: 2px solid var(--border-color);
border-top: 2px solid var(--primary-color);
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Print styles */
@media print {
.navbar,
.theme-toggle,
.btn,
.toc {
display: none !important;
}
body {
background: white !important;
color: black !important;
}
.card {
border: 1px solid #ccc !important;
box-shadow: none !important;
break-inside: avoid;
}
}

View File

@@ -1,175 +1,30 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Stichwort: {{ stichwort.stichwort }}{% endblock %} {% block title %}Stichwort: {{stichwort.stichwort}}{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item"><a href="/stichworte/">Stichworte</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ stichwort.stichwort }}</li>
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <h1>{{stichwort}}</h1>
<div class="col-lg-8"> {% if stichwort.erklaerung %}
<!-- Stichwort Header --> <div class="card mb-4">
<div class="card mb-4"> <div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<div class="card-header bg-primary text-white"> <h3 class="h5 m-0">Beschreibung</h3>
<h1 class="h3 mb-0">🏷️ {{ stichwort.stichwort }}</h1> </div>
</div> <div class="card-body p-2">
{% if stichwort.erklaerung %}
<div class="card-body">
<h5 class="card-title">📖 Beschreibung</h5>
{% for typ, html in stichwort.erklaerung %} {% for typ, html in stichwort.erklaerung %}
{% if html %} {% if html %}<div>{{ html|safe }}</div>{% endif %}{% endfor %}
<div class="content-section">{{ html|safe }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<!-- Relevante Vorgaben -->
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h3 class="h5 mb-0">📝 Relevante Vorgaben</h3>
<span class="badge bg-success">
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
Aktiv
</span>
</div>
</div>
<div class="card-body">
{% if stichwort.vorgaben %}
<div class="list-group">
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
<div class="list-group-item">
<div class="d-flex align-items-start">
<span class="badge bg-secondary me-3">{{ vorgabe.Vorgabennummer }}</span>
<div class="flex-grow-1">
<h6 class="mb-1">
<a href="/dokumente/{{ vorgabe.dokument.nummer }}/#{{vorgabe.Vorgabennummer}}"
class="text-decoration-none">
{{ vorgabe.titel }}
</a>
</h6>
<small class="text-muted">
Standard: {{ vorgabe.dokument.nummer }} {{ vorgabe.dokument.name }}
</small>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<p class="text-muted mb-0">
Keine aktiven Vorgaben für dieses Stichwort gefunden.
</p>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endif %}
<!-- Sidebar --> <div class="card mb-4">
<div class="col-lg-4"> <div class="card-header d-flex justify-content-between align-items-center bg-secondary text-light">
<!-- Quick Actions --> <h3 class="h5 m-0">Relevante Vorgaben</h3>
<div class="card mb-4 sticky-top" style="top: 1rem;">
<div class="card-header">
<h5 class="mb-0">⚡ Aktionen</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="/stichworte/" class="btn btn-outline btn-sm">
← Zurück zur Liste
</a>
<a href="/search/?q={{ stichwort.stichwort }}" class="btn btn-outline btn-sm">
🔍 Nach "{{ stichwort.stichwort }}" suchen
</a>
</div>
</div>
</div>
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">📊 Statistiken</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<h4 class="text-primary mb-1">
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
{% if forloop.first %}1{% else %}{{ forloop.counter }}{% endif %}
{% endif %}
{% endfor %}
</h4>
<small class="text-muted">Aktive Vorgaben</small>
</div>
<div class="col-6">
<h4 class="text-info mb-1">
{{ stichwort.vorgaben|length }}
</h4>
<small class="text-muted">Gesamt</small>
</div>
</div>
</div>
</div>
<!-- Related Stichworte -->
{% if related_stichworte %}
<div class="card">
<div class="card-header">
<h5 class="mb-0">🔗 Verwandte Stichworte</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
{% for related in related_stichworte %}
<a href="/stichworte/{{ related.stichwort }}/"
class="badge bg-light text-dark text-decoration-none">
{{ related.stichwort }}
</a>
{% endfor %}
</div>
</div>
</div> </div>
<div class="card-body p-2">
<ul>
{% for vorgabe in stichwort.vorgaben %}
{% if vorgabe.get_status == "active" %}
<li><a href="{% url 'standard_detail' nummer=vorgabe.dokument.nummer %}#{{vorgabe.Vorgabennummer}}">{{vorgabe.Vorgabennummer}}</a>: {{vorgabe.titel}}</li>
{% endif %} {% endif %}
</div> {% endfor %}
</div> </ul>
</div>
<style>
.content-section {
line-height: 1.6;
margin-bottom: 1rem;
}
.content-section h1, .content-section h2, .content-section h3 {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.list-group-item {
border: none;
border-bottom: 1px solid var(--border-color);
transition: all var(--transition-fast);
}
.list-group-item:hover {
background-color: var(--bg-secondary);
}
.list-group-item:last-child {
border-bottom: none;
}
.badge {
font-size: 0.75rem;
}
</style>
{% endblock %} {% endblock %}

View File

@@ -1,144 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Stichworte{% endblock %} {% block title %}Stichworte{% endblock %}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">Stichworte</li>
{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <h1>Stichworte</h1>
<h1>🏷️ Stichworte</h1>
<div class="d-flex gap-2">
<span class="badge bg-primary">{{ stichworte|length }} Kategorien</span>
</div>
</div>
<!-- Search and Filter -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-8">
<label for="stichwort-search" class="form-label">🔍 Stichworte durchsuchen</label>
<input type="text"
class="form-control"
id="stichwort-search"
placeholder="Stichwort eingeben..."
onkeyup="filterStichworte()">
</div>
<div class="col-md-4">
<label for="letter-filter" class="form-label">🔤 Buchstabe</label>
<select class="form-select" id="letter-filter" onchange="filterStichworte()">
<option value="">Alle Buchstaben</option>
{% for Anfang, Worte in stichworte.items %}
<option value="{{ Anfang }}">{{ Anfang }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
<!-- Stichworte Grid -->
<div class="row" id="stichworte-container">
{% for Anfang, Worte in stichworte.items %} {% for Anfang, Worte in stichworte.items %}
<div class="col-lg-4 col-md-6 mb-4 stichwort-category" data-letter="{{ Anfang }}"> <h2>{{ Anfang }}</h2>
<div class="card h-100"> <ul>
<div class="card-header"> {% for Wort in Worte %}
<h3 class="h5 mb-0">{{ Anfang }}</h3> <li><a href="{% url 'stichwort_detail' stichwort=Wort %}">{{ Wort }}</a></li>
<span class="badge bg-secondary">{{ Worte|length }} Stichworte</span> {% endfor %}
</div> </ul>
<div class="card-body"> {% endfor %}
<div class="list-group list-group-flush"> {% endblock %}
{% for Wort in Worte %}
<a href="/stichworte/{{ Wort }}/" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span>{{ Wort }}</span>
<span class="badge bg-light text-dark"></span>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- JavaScript for filtering -->
<script>
function filterStichworte() {
const searchTerm = document.getElementById('stichwort-search').value.toLowerCase();
const letterFilter = document.getElementById('letter-filter').value;
const categories = document.querySelectorAll('.stichwort-category');
categories.forEach(category => {
const letter = category.dataset.letter;
const items = category.querySelectorAll('.list-group-item');
let hasVisibleItems = false;
// Check if category matches letter filter
const matchesLetter = !letterFilter || letter === letterFilter;
// Filter items within category
items.forEach(item => {
const text = item.textContent.toLowerCase();
const matchesSearch = !searchTerm || text.includes(searchTerm);
if (matchesSearch && matchesLetter) {
item.style.display = 'flex';
hasVisibleItems = true;
} else {
item.style.display = 'none';
}
});
// Show/hide category based on visible items
category.style.display = (matchesLetter && hasVisibleItems) ? 'block' : 'none';
});
}
document.addEventListener('DOMContentLoaded', function() {
// Add hover effects
const items = document.querySelectorAll('.list-group-item');
items.forEach(item => {
item.addEventListener('mouseenter', function() {
this.style.backgroundColor = 'var(--bg-secondary)';
});
item.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
});
</script>
<style>
.list-group-item {
border: none;
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-sm) var(--spacing-md);
transition: all var(--transition-fast);
}
.list-group-item:hover {
background-color: var(--bg-secondary);
transform: translateX(4px);
}
.list-group-item:last-child {
border-bottom: none;
}
.card-header {
display: flex;
justify-content: between;
align-items: center;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
@media (max-width: 768px) {
.list-group-item {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.875rem;
}
}
</style>
{% endblock %}

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()