Compare commits

..

27 Commits

Author SHA1 Message Date
aca9a2f307 Removed "Ändern" and "Löschen"-Links 2025-11-03 12:36:39 +01:00
081ea4de1c background of Vorgaben changed - looks better in dark mode. 2025-11-01 01:09:40 +01:00
a075811173 Collapsing and drag/drop implemented 2025-11-01 00:34:21 +01:00
d4143da9fc Horizontal fieldsets OK 2025-11-01 00:21:13 +01:00
b0c9b89e94 Borders work, collapsing doesn't yet 2025-11-01 00:18:29 +01:00
Adrian A. Baumann
94363d49ce Deploy 939 2025-10-31 12:35:26 +01:00
Adrian A. Baumann
8bca1bb3c7 Tabular view for Vorgaben added 2025-10-31 11:43:34 +01:00
Adrian A. Baumann
1ce8eb15c0 Merge branch 'feature/textabschnitte-comprehensive-tests' into development 2025-10-29 14:26:23 +01:00
Adrian A. Baumann
4d2ffeea27 .gitignore extended by npm stuff 2025-10-29 14:11:53 +01:00
Adrian A. Baumann
8860947d38 Add comprehensive tests for Textabschnitte app
- Add 41 comprehensive test cases covering all functionality
- Test AbschnittTyp model creation and validation
- Test Textabschnitt abstract model through VorgabeLangtext
- Test all rendering types: text, lists, tables, code, diagrams
- Test markdown rendering with footnotes and formatting
- Test table conversion from markdown to Bootstrap HTML
- Test diagram caching with mocked external service calls
- Test diagram error handling and custom options
- Test clear_diagram_cache management command
- Test integration with dokumente models
- All tests passing (41/41)
2025-10-29 14:09:02 +01:00
Adrian A. Baumann
6df72c95cb Tests for documents fixed (Vorgabe-Order added) 2025-10-29 13:47:16 +01:00
2afada0bce Date 'bis None' changed to 'bis auf weiteres' 2025-10-29 13:30:46 +01:00
Adrian A. Baumann
a42a65b40f Make Vorgaben draggable; Deploy 938 2025-10-28 16:19:37 +01:00
5609a735f4 Deploy 937 2025-10-28 13:41:13 +01:00
6654779e67 Corrections on Dokumente-Admin; Homepage now only shows active documents 2025-10-28 13:36:26 +01:00
7befde104d Added 'aktiv' to document tests 2025-10-27 21:24:23 +01:00
96819a7427 Merge branch 'feature/dokumente-unit-tests' into development 2025-10-27 21:18:53 +01:00
a437af554b Deploy 936 2025-10-27 20:53:13 +01:00
650fe0a87b added 'aktiv' to dokument (so people can play around with standards) 2025-10-27 20:49:22 +01:00
Adrian A. Baumann
ddf035c50f Deploy 935 2025-10-27 16:57:35 +01:00
Adrian A. Baumann
886baa163e Increase whitespace between Vorgabe boxes
Increased margin-bottom from 30px to 50px for better visual separation between Vorgaben.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 16:47:19 +01:00
Adrian A. Baumann
1146506ca2 Fix selector for tabular Vorgabe identifiers with tbody target
Changed selector to target tbody.djn-dynamic-form-dokumente-vorgabe and added !important to override existing styles.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 16:46:05 +01:00
Adrian A. Baumann
9610024739 Make Vorgabe identifier text in tabular view prominent
Styled the td.original cell containing Vorgabe identifiers (e.g., "R0066.O.3: Dateninhaber"):
- Font size: 16px
- Font weight: 700 (bold)
- Blue color matching border (#2c5aa0)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 16:44:30 +01:00
Adrian A. Baumann
c8755e4339 Make Vorgabe titles bigger and more prominent
- Increased font size to 18px with bold weight (700)
- Blue color (#2c5aa0) matching the border
- Light blue gradient background
- Bottom border separator
- Extends full width with negative margins

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 16:42:34 +01:00
Adrian A. Baumann
0bc1fe7413 Add prominent border boxes around each Vorgabe
- 3px solid blue border (#2c5aa0)
- Increased margin between Vorgaben (30px)
- Added subtle box shadow
- Support both Standards and dokumente class names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 16:41:14 +01:00
Adrian A. Baumann
8ce761c248 Deploy 934 2025-10-27 14:00:21 +01:00
Adrian A. Baumann
39a2021cc3 Attempt at improving reference choice in documents 2025-10-27 13:50:14 +01:00
19 changed files with 1236 additions and 70 deletions

4
.gitignore vendored
View File

@@ -10,6 +10,8 @@ keys/
.idea/ .idea/
*.kate-swp *.kate-swp
node_modules/
package-lock.json
package.json
# Diagram cache directory # Diagram cache directory
media/diagram_cache/ media/diagram_cache/

View File

@@ -1,3 +1,820 @@
from django.test import TestCase from django.test import TestCase, TransactionTestCase
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
# Create your tests here. from .models import AbschnittTyp, Textabschnitt
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

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

View File

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

View File

@@ -25,7 +25,7 @@ spec:
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/vui:0.933 image: git.baumann.gr/adebaumann/vui:0.939
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -0,0 +1,19 @@
# 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

@@ -0,0 +1,23 @@
# 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,6 +47,7 @@ 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}"
@@ -56,6 +57,7 @@ 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) thema = models.ForeignKey(Thema, on_delete=models.PROTECT)
@@ -86,7 +88,7 @@ class Vorgabe(models.Model):
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)
@@ -123,6 +125,12 @@ 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

@@ -8,7 +8,7 @@
<!-- Autoren, Prüfende etc. --> <!-- Autoren, Prüfende etc. -->
<p><strong>Autoren:</strong> {{ standard.autoren.all|join:", " }}</p> <p><strong>Autoren:</strong> {{ standard.autoren.all|join:", " }}</p>
<p><strong>Prüfende:</strong> {{ standard.pruefende.all|join:", " }}</p> <p><strong>Prüfende:</strong> {{ standard.pruefende.all|join:", " }}</p>
<p><strong>Gültigkeit:</strong> {{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis }}</p> <p><strong>Gültigkeit:</strong> {{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</p>
<!-- Start Einleitung --> <!-- Start Einleitung -->
{% if standard.einleitung_html %} {% if standard.einleitung_html %}

View File

@@ -114,7 +114,8 @@ class DokumentModelTest(TestCase):
name="Security Policy", name="Security Policy",
gueltigkeit_von=date.today(), gueltigkeit_von=date.today(),
signatur_cso="CSO-123", signatur_cso="CSO-123",
anhaenge="Appendix A, B" anhaenge="Appendix A, B",
aktiv=True
) )
self.dokument.autoren.add(self.autor) self.dokument.autoren.add(self.autor)
self.dokument.pruefende.add(self.pruefer) self.dokument.pruefende.add(self.pruefer)
@@ -124,6 +125,7 @@ class DokumentModelTest(TestCase):
self.assertEqual(self.dokument.nummer, "DOC-001") self.assertEqual(self.dokument.nummer, "DOC-001")
self.assertEqual(self.dokument.name, "Security Policy") self.assertEqual(self.dokument.name, "Security Policy")
self.assertEqual(self.dokument.dokumententyp, self.dokumententyp) self.assertEqual(self.dokument.dokumententyp, self.dokumententyp)
self.assertEqual(self.dokument.aktiv, True)
def test_dokument_str(self): def test_dokument_str(self):
"""Test string representation of Dokument""" """Test string representation of Dokument"""
@@ -139,7 +141,8 @@ class DokumentModelTest(TestCase):
dokument = Dokument.objects.create( dokument = Dokument.objects.create(
nummer="DOC-002", nummer="DOC-002",
dokumententyp=self.dokumententyp, dokumententyp=self.dokumententyp,
name="Test Document" name="Test Document",
aktiv=True
) )
self.assertIsNone(dokument.gueltigkeit_von) self.assertIsNone(dokument.gueltigkeit_von)
self.assertIsNone(dokument.gueltigkeit_bis) self.assertIsNone(dokument.gueltigkeit_bis)
@@ -158,10 +161,12 @@ class VorgabeModelTest(TestCase):
self.dokument = Dokument.objects.create( self.dokument = Dokument.objects.create(
nummer="R01234", nummer="R01234",
dokumententyp=self.dokumententyp, dokumententyp=self.dokumententyp,
name="IT Standard" name="IT Standard",
aktiv=True
) )
self.thema = Thema.objects.create(name="Security") self.thema = Thema.objects.create(name="Security")
self.vorgabe = Vorgabe.objects.create( self.vorgabe = Vorgabe.objects.create(
order=1,
nummer=1, nummer=1,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,
@@ -171,6 +176,7 @@ class VorgabeModelTest(TestCase):
def test_vorgabe_creation(self): def test_vorgabe_creation(self):
"""Test that Vorgabe is created correctly""" """Test that Vorgabe is created correctly"""
self.assertEqual(self.vorgabe.order, 1)
self.assertEqual(self.vorgabe.nummer, 1) self.assertEqual(self.vorgabe.nummer, 1)
self.assertEqual(self.vorgabe.dokument, self.dokument) self.assertEqual(self.vorgabe.dokument, self.dokument)
self.assertEqual(self.vorgabe.thema, self.thema) self.assertEqual(self.vorgabe.thema, self.thema)
@@ -193,6 +199,7 @@ class VorgabeModelTest(TestCase):
def test_get_status_future(self): def test_get_status_future(self):
"""Test get_status returns 'future' for future vorgabe""" """Test get_status returns 'future' for future vorgabe"""
future_vorgabe = Vorgabe.objects.create( future_vorgabe = Vorgabe.objects.create(
order=2,
nummer=2, nummer=2,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,
@@ -205,6 +212,7 @@ class VorgabeModelTest(TestCase):
def test_get_status_expired(self): def test_get_status_expired(self):
"""Test get_status returns 'expired' for expired vorgabe""" """Test get_status returns 'expired' for expired vorgabe"""
expired_vorgabe = Vorgabe.objects.create( expired_vorgabe = Vorgabe.objects.create(
order=3,
nummer=3, nummer=3,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,
@@ -218,6 +226,7 @@ class VorgabeModelTest(TestCase):
def test_get_status_verbose(self): def test_get_status_verbose(self):
"""Test get_status with verbose=True""" """Test get_status with verbose=True"""
future_vorgabe = Vorgabe.objects.create( future_vorgabe = Vorgabe.objects.create(
order=4,
nummer=4, nummer=4,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,
@@ -231,6 +240,7 @@ class VorgabeModelTest(TestCase):
def test_get_status_with_custom_check_date(self): def test_get_status_with_custom_check_date(self):
"""Test get_status with custom check_date""" """Test get_status with custom check_date"""
vorgabe = Vorgabe.objects.create( vorgabe = Vorgabe.objects.create(
order=5,
nummer=5, nummer=5,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,
@@ -254,10 +264,12 @@ class VorgabeTextAbschnitteTest(TestCase):
self.dokument = Dokument.objects.create( self.dokument = Dokument.objects.create(
nummer="R01234", nummer="R01234",
dokumententyp=self.dokumententyp, dokumententyp=self.dokumententyp,
name="Test Standard" name="Test Standard",
aktiv=True
) )
self.thema = Thema.objects.create(name="Testing") self.thema = Thema.objects.create(name="Testing")
self.vorgabe = Vorgabe.objects.create( self.vorgabe = Vorgabe.objects.create(
order=1,
nummer=1, nummer=1,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,
@@ -302,7 +314,8 @@ class DokumentTextAbschnitteTest(TestCase):
self.dokument = Dokument.objects.create( self.dokument = Dokument.objects.create(
nummer="POL-001", nummer="POL-001",
dokumententyp=self.dokumententyp, dokumententyp=self.dokumententyp,
name="Test Policy" name="Test Policy",
aktiv=True
) )
self.abschnitttyp = AbschnittTyp.objects.create( self.abschnitttyp = AbschnittTyp.objects.create(
abschnitttyp="Paragraph" abschnitttyp="Paragraph"
@@ -342,10 +355,12 @@ class ChecklistenfrageModelTest(TestCase):
self.dokument = Dokument.objects.create( self.dokument = Dokument.objects.create(
nummer="QA-001", nummer="QA-001",
dokumententyp=self.dokumententyp, dokumententyp=self.dokumententyp,
name="QA Standard" name="QA Standard",
aktiv=True
) )
self.thema = Thema.objects.create(name="Quality") self.thema = Thema.objects.create(name="Quality")
self.vorgabe = Vorgabe.objects.create( self.vorgabe = Vorgabe.objects.create(
order=1,
nummer=1, nummer=1,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,
@@ -382,7 +397,8 @@ class ChangelogModelTest(TestCase):
self.dokument = Dokument.objects.create( self.dokument = Dokument.objects.create(
nummer="R01234", nummer="R01234",
dokumententyp=self.dokumententyp, dokumententyp=self.dokumententyp,
name="IT Standard" name="IT Standard",
aktiv=True
) )
self.autor = Person.objects.create( self.autor = Person.objects.create(
name="John Doe", name="John Doe",
@@ -419,10 +435,12 @@ class ViewsTestCase(TestCase):
self.dokument = Dokument.objects.create( self.dokument = Dokument.objects.create(
nummer="R01234", nummer="R01234",
dokumententyp=self.dokumententyp, dokumententyp=self.dokumententyp,
name="Test Standard" name="Test Standard",
aktiv=True
) )
self.thema = Thema.objects.create(name="Testing") self.thema = Thema.objects.create(name="Testing")
self.vorgabe = Vorgabe.objects.create( self.vorgabe = Vorgabe.objects.create(
order=1,
nummer=1, nummer=1,
dokument=self.dokument, dokument=self.dokument,
thema=self.thema, thema=self.thema,

View File

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

View File

@@ -6,7 +6,7 @@ import datetime
import pprint import pprint
def startseite(request): def startseite(request):
standards=list(Dokument.objects.all()) standards=list(Dokument.objects.filter(aktiv=True))
return render(request, 'startseite.html', {"dokumente":standards,}) return render(request, 'startseite.html', {"dokumente":standards,})
def search(request): def search(request):

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",) search_fields=("referenz","path")

View File

@@ -6,6 +6,7 @@ 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,14 +1,39 @@
/* Style each Vorgabe inline block */ /* Style each Vorgabe inline block */
.djn-dynamic-form-Standards-vorgabe { .djn-dynamic-form-Standards-vorgabe,
border: 2px solid #ccc; .djn-dynamic-form-dokumente-vorgabe {
border: 3px solid #2c5aa0;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
margin-bottom: 20px; margin-bottom: 50px;
background-color: #f9f9f9; box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
/* 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,21 +1,58 @@
window.addEventListener('load', function () { window.addEventListener('load', function () {
setTimeout(() => { setTimeout(() => {
const vorgabenBlocks = document.querySelectorAll('.djn-dynamic-form-Standards-vorgabe'); // Try different selectors for nested admin vorgabe elements
console.log("Found", vorgabenBlocks.length, "Vorgaben blocks"); const selectors = [
'.djn-dynamic-form-dokumente-vorgabe',
'.djn-dynamic-form-Standards-vorgabe',
'.inline-related[data-inline-type="stacked"]',
'.nested-inline'
];
let vorgabenBlocks = [];
for (const selector of selectors) {
vorgabenBlocks = document.querySelectorAll(selector);
if (vorgabenBlocks.length > 0) {
console.log("Found", vorgabenBlocks.length, "Vorgaben blocks with selector:", selector);
break;
}
}
if (vorgabenBlocks.length === 0) {
console.log("No Vorgaben blocks found, trying fallback...");
// Fallback: look for any inline with vorgabe in the class
vorgabenBlocks = document.querySelectorAll('[class*="vorgabe"]');
}
vorgabenBlocks.forEach((block, index) => { vorgabenBlocks.forEach((block, index) => {
const header = document.createElement('div'); // Find the existing title/header within the vorgabe block
header.className = 'vorgabe-toggle-header'; const existingHeader = block.querySelector('h3, .inline-label, .module h2, .djn-inline-header');
header.innerHTML = `▼ Vorgabe ${index + 1}`;
header.style.cursor = 'pointer';
block.parentNode.insertBefore(header, block); if (existingHeader) {
// Make the existing header clickable for collapse/expand
existingHeader.style.cursor = 'pointer';
existingHeader.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
header.addEventListener('click', () => { // Find all content to collapse - everything except the header itself
const isHidden = block.style.display === 'none'; const allChildren = Array.from(block.children);
block.style.display = isHidden ? '' : 'none'; const contentElements = allChildren.filter(child => child !== existingHeader && !child.contains(existingHeader));
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}`;
}
}); });
}, 500); // wait 500ms to allow nested inlines to render }, 1000); // wait longer to allow nested inlines to render
}); });