Compare commits

...

29 Commits

Author SHA1 Message Date
c1eb2d7871 Deploy 0.933 2025-10-24 01:07:37 +02:00
b29e894b22 Merge branch 'feature/diagram-post-caching' into testing 2025-10-24 01:04:54 +02:00
0f096d18aa Add MEDIA_ROOT, MEDIA_URL and DIAGRAM_CACHE_DIR to docker settings 2025-10-23 23:00:14 +00:00
9b484787a4 Add media file serving for cached diagrams
- Configure MEDIA_URL/MEDIA_ROOT serving in DEBUG mode
- Separate static and media file configurations for clarity
2025-10-23 22:59:54 +00:00
8dd3b4e9af Add MEDIA_ROOT, MEDIA_URL and DIAGRAM_CACHE_DIR settings 2025-10-23 22:59:40 +00:00
0d0199ca62 Wrong branching point - corrected 2025-10-24 00:54:10 +02:00
5f58d660c0 Merge branch 'feature/diagram-post-caching' into testing 2025-10-24 00:47:18 +02:00
e84f25ca1d New DB 2025-10-24 00:37:40 +02:00
dfb8eeef97 Deploy 931 (with diagram branch in container) - readiness probes off temporarily 2025-10-24 00:30:22 +02:00
0225fb3396 Deploy 931 (with diagram branch in container) - 2nd attempt 2025-10-24 00:27:07 +02:00
7377ddaea3 Deploy 931 (with diagram branch in container) 2025-10-24 00:20:34 +02:00
67c393ecf1 Add documentation for diagram POST caching feature 2025-10-23 22:06:30 +00:00
dbb3ecd5bf Add diagram cache directory to gitignore 2025-10-23 22:06:07 +00:00
966cd46228 Add management command to clear diagram cache
Usage:
  python manage.py clear_diagram_cache
  python manage.py clear_diagram_cache --type plantuml
2025-10-23 22:05:36 +00:00
1ee9b3c46f Add commands package 2025-10-23 22:05:26 +00:00
8f57f5fc5b Add management module 2025-10-23 22:05:21 +00:00
cd7195b3aa Update diagram rendering to use POST with caching
- Replace URL-encoded GET approach with POST requests
- Use local filesystem cache for generated diagrams
- Add error handling with fallback message
- Serve diagrams from MEDIA_URL instead of proxy
2025-10-23 22:05:13 +00:00
020dff0871 Add diagram caching module with POST support
- Implement content-based hashing for cache keys
- POST diagram content to Kroki server instead of URL encoding
- Store generated SVGs in local filesystem cache
- Add cache clearing functionality
2025-10-23 22:04:19 +00:00
1dbdbc7f3c Initialize diagramm_proxy module 2025-10-23 22:03:39 +00:00
4d1232b764 Tests - not runnable yet. 2025-10-23 16:35:31 +02:00
fe2e02934a README added, first try at signing commits. 2025-10-23 09:27:25 +02:00
add1a88ce4 README added, first try at signing commits. 2025-10-23 09:26:44 +02:00
3c23918e1f No-clobber back on for database in ArgoCD 2025-10-23 09:01:45 +02:00
fa0a2a9df9 new data structure due to renaming - clobbering database temporarily in init-container 2025-10-23 08:25:12 +00:00
Adrian A. Baumann
9feaf6686f Deploy 930 2025-10-23 09:42:41 +02:00
7087be672a Added "Geltungsbereich" back into search function and corrected it; Changed "standards" page to "dokumente" internally 2025-10-23 09:35:23 +02:00
Adrian A. Baumann
969141601d Merge branch 'rename_standards' into development 2025-10-22 15:14:48 +02:00
d46d937e93 Geltungsbereich removed from search scope for now. Maybe check Haystack or other dedicated search engines 2025-10-21 16:16:42 +02:00
4d713b3763 Suche konsolidiert, unterscheidet nicht mehr nach Abschnittstyp. Möglicherweise optimierungswürdig 2025-10-21 15:55:49 +02:00
21 changed files with 298 additions and 69 deletions

3
.gitignore vendored
View File

@@ -10,3 +10,6 @@ keys/
.idea/ .idea/
*.kate-swp *.kate-swp
# Diagram cache directory
media/diagram_cache/

View File

@@ -0,0 +1,105 @@
# Diagram POST Caching Implementation
This feature replaces the URL-encoded GET approach for diagram generation with POST requests and local filesystem caching.
## Changes Overview
### New Files
- `diagramm_proxy/__init__.py` - Module initialization
- `diagramm_proxy/diagram_cache.py` - Caching logic and POST request handling
- `abschnitte/management/commands/clear_diagram_cache.py` - Management command for cache clearing
### Modified Files
- `abschnitte/utils.py` - Updated `render_textabschnitte()` to use caching
- `.gitignore` - Added cache directory exclusion
## Configuration Required
Add to your Django settings file (e.g., `VorgabenUI/settings.py`):
```python
# Diagram cache settings
DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT
# Ensure MEDIA_ROOT and MEDIA_URL are configured
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
```
### URL Configuration
Ensure media files are served in development. In your main `urls.py`:
```python
from django.conf import settings
from django.conf.urls.static import static
# ... existing urlpatterns ...
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
```
## How It Works
1. When a diagram is rendered, the system computes a SHA256 hash of the diagram content
2. It checks if a cached SVG exists for that hash
3. If cached: serves the existing file
4. If not cached: POSTs content to Kroki server, saves the response, and serves it
5. Diagrams are served from `MEDIA_URL/diagram_cache/{type}/{hash}.svg`
## Benefits
- **No URL length limitations** - Content is POSTed instead of URL-encoded
- **Improved performance** - Cached diagrams are served directly from filesystem
- **Reduced server load** - Kroki server is only called once per unique diagram
- **Persistent cache** - Survives application restarts
- **Better error handling** - Graceful fallback on generation failures
## Usage
### Viewing Diagrams
No changes required - diagrams will be automatically cached on first render.
### Clearing Cache
Clear all cached diagrams:
```bash
python manage.py clear_diagram_cache
```
Clear diagrams of a specific type:
```bash
python manage.py clear_diagram_cache --type plantuml
python manage.py clear_diagram_cache --type mermaid
```
## Testing
1. Create or view a page with diagrams
2. Verify diagrams render correctly
3. Check that `media/diagram_cache/` directory is created with cached SVGs
4. Refresh the page - second load should be faster (cache hit)
5. Check logs for cache hit/miss messages
6. Test cache clearing command
## Migration Notes
- Existing diagrams will be regenerated on first view after deployment
- The old URL-based approach is completely replaced
- No database migrations needed
- Ensure `requests` library is installed (already in requirements.txt)
## Troubleshooting
### Diagrams not rendering
- Check that MEDIA_ROOT and MEDIA_URL are configured correctly
- Verify Kroki server is accessible at `http://svckroki:8000`
- Check application logs for error messages
- Ensure media directory is writable
### Cache not working
- Verify Django storage configuration
- Check file permissions on media/diagram_cache directory
- Review logs for cache-related errors

View File

@@ -2,3 +2,5 @@
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.
Documentation on Confluence so far.
This commit should be signed.

View File

@@ -126,6 +126,13 @@ STATICFILES_DIRS= (
os.path.join(BASE_DIR,"static"), os.path.join(BASE_DIR,"static"),
) )
# Media files (User-uploaded content)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Diagram cache settings
DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

View File

@@ -139,6 +139,13 @@ STATICFILES_DIRS= (
os.path.join(BASE_DIR,"static"), os.path.join(BASE_DIR,"static"),
) )
# Media files (User-uploaded content)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Diagram cache settings
DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

View File

@@ -34,5 +34,11 @@ urlpatterns = [
path('referenzen/', referenzen.views.tree, name="referenz_tree"), path('referenzen/', referenzen.views.tree, name="referenz_tree"),
path('referenzen/<str:refid>/', referenzen.views.detail, name="referenz_detail"), path('referenzen/<str:refid>/', referenzen.views.detail, name="referenz_detail"),
re_path(r'^diagramm/(?P<path>.*)$', DiagrammProxyView.as_view()), re_path(r'^diagramm/(?P<path>.*)$', DiagrammProxyView.as_view()),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ]
# Serve static files
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Serve media files (including cached diagrams)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -0,0 +1 @@
# Management commands

View File

@@ -0,0 +1 @@
# Commands package

View File

@@ -0,0 +1,23 @@
from django.core.management.base import BaseCommand
from diagramm_proxy.diagram_cache import clear_cache
class Command(BaseCommand):
help = 'Clear cached diagrams'
def add_arguments(self, parser):
parser.add_argument(
'--type',
type=str,
help='Diagram type to clear (e.g., plantuml, mermaid)',
)
def handle(self, *args, **options):
diagram_type = options.get('type')
if diagram_type:
self.stdout.write(f'Clearing cache for {diagram_type}...')
clear_cache(diagram_type)
else:
self.stdout.write('Clearing all diagram caches...')
clear_cache()
self.stdout.write(self.style.SUCCESS('Cache cleared successfully'))

View File

@@ -3,6 +3,10 @@ import base64
import zlib import zlib
import re import re
from textwrap import dedent from textwrap import dedent
from django.conf import settings
# Import the caching function
from diagramm_proxy.diagram_cache import get_cached_diagram
DIAGRAMMSERVER="/diagramm" DIAGRAMMSERVER="/diagramm"
@@ -25,15 +29,23 @@ def render_textabschnitte(queryset):
elif typ == "tabelle": elif typ == "tabelle":
html = md_table_to_html(inhalt) html = md_table_to_html(inhalt)
elif typ == "diagramm": elif typ == "diagramm":
temp=inhalt.splitlines() temp = inhalt.splitlines()
diagramtype=temp.pop(0) diagramtype = temp.pop(0)
diagramoptions='width="100%"' diagramoptions = 'width="100%"'
if temp[0][0:6].lower() == "option": if temp and temp[0][0:6].lower() == "option":
diagramoptions=temp.pop(0).split(":",1)[1] diagramoptions = temp.pop(0).split(":", 1)[1]
rest="\n".join(temp) rest = "\n".join(temp)
html = '<p><img '+diagramoptions+' src="'+DIAGRAMMSERVER+"/"+diagramtype+"/svg/"
html += base64.urlsafe_b64encode(zlib.compress(rest.encode("utf-8"),9)).decode() # Use caching instead of URL encoding
html += '"></p>' try:
cache_path = get_cached_diagram(diagramtype, rest)
# Generate URL to serve from media/static
diagram_url = settings.MEDIA_URL + cache_path
html = f'<p><img {diagramoptions} src="{diagram_url}"></p>'
except Exception as e:
# Fallback to error message
html = f'<p class="text-danger">Error generating diagram: {str(e)}</p>'
elif typ == "code": elif typ == "code":
html = "<pre><code>" html = "<pre><code>"
html += markdown(inhalt, extensions=['tables', 'attr_list']) html += markdown(inhalt, extensions=['tables', 'attr_list'])

View File

@@ -18,14 +18,14 @@ spec:
fsGroupChangePolicy: "OnRootMismatch" fsGroupChangePolicy: "OnRootMismatch"
initContainers: initContainers:
- name: loader - name: loader
image: git.baumann.gr/adebaumann/vgui-data-loader:0.5 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.929 image: git.baumann.gr/adebaumann/vui:0.933
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000

Binary file not shown.

View File

@@ -0,0 +1 @@
# Diagram proxy module

View File

@@ -0,0 +1,91 @@
import hashlib
import os
import requests
from pathlib import Path
from django.conf import settings
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
import logging
logger = logging.getLogger(__name__)
# Configure cache directory
CACHE_DIR = getattr(settings, 'DIAGRAM_CACHE_DIR', 'diagram_cache')
KROKI_UPSTREAM = "http://svckroki:8000"
def get_cache_path(diagram_type, content_hash):
"""Generate cache file path for a diagram."""
return os.path.join(CACHE_DIR, diagram_type, f"{content_hash}.svg")
def compute_hash(content):
"""Compute SHA256 hash of diagram content."""
return hashlib.sha256(content.encode('utf-8')).hexdigest()
def get_cached_diagram(diagram_type, diagram_content):
"""
Retrieve diagram from cache or generate it via POST.
Args:
diagram_type: Type of diagram (e.g., 'plantuml', 'mermaid')
diagram_content: Raw diagram content
Returns:
Path to cached diagram file (relative to MEDIA_ROOT)
"""
content_hash = compute_hash(diagram_content)
cache_path = get_cache_path(diagram_type, content_hash)
# Check if diagram exists in cache
if default_storage.exists(cache_path):
logger.debug(f"Cache hit for {diagram_type} diagram: {content_hash[:8]}")
return cache_path
# Generate diagram via POST request
logger.info(f"Cache miss for {diagram_type} diagram: {content_hash[:8]}, generating...")
try:
url = f"{KROKI_UPSTREAM}/{diagram_type}/svg"
response = requests.post(
url,
data=diagram_content.encode('utf-8'),
headers={'Content-Type': 'text/plain'},
timeout=30
)
response.raise_for_status()
# Ensure cache directory exists
cache_dir = os.path.dirname(default_storage.path(cache_path))
os.makedirs(cache_dir, exist_ok=True)
# Save to cache
default_storage.save(cache_path, ContentFile(response.content))
logger.info(f"Diagram cached successfully: {cache_path}")
return cache_path
except requests.RequestException as e:
logger.error(f"Error generating diagram: {e}")
raise
def clear_cache(diagram_type=None):
"""
Clear cached diagrams.
Args:
diagram_type: If specified, only clear diagrams of this type
"""
if diagram_type:
cache_path = os.path.join(CACHE_DIR, diagram_type)
else:
cache_path = CACHE_DIR
if default_storage.exists(cache_path):
full_path = default_storage.path(cache_path)
# Walk through and delete files
for root, dirs, files in os.walk(full_path):
for file in files:
file_path = os.path.join(root, file)
try:
os.remove(file_path)
logger.info(f"Deleted cached diagram: {file_path}")
except OSError as e:
logger.error(f"Error deleting {file_path}: {e}")

View File

@@ -2,10 +2,10 @@
{% block content %} {% block content %}
<h1>Standards Informatiksicherheit</h1> <h1>Standards Informatiksicherheit</h1>
<ul> <ul>
{% for standard in standards %} {% for dokument in dokumente %}
<li> <li>
<a href="{% url 'standard_detail' nummer=standard.nummer %}"> <a href="{% url 'standard_detail' nummer=dokument.nummer %}">
{{ standard.nummer }} {{ standard.name }} {{ dokument.nummer }} {{ dokument.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@@ -1,3 +1,8 @@
from django.test import TestCase from django.test import TestCase
from myapp.models import Dokument
# Create your tests here. # Create your tests here.
class DokumentTestCase (TestCase):
def setUp(self):
Document.objects.create(name)

View File

@@ -9,9 +9,9 @@ calendar=parsedatetime.Calendar()
def standard_list(request): def standard_list(request):
standards = Dokument.objects.all() dokumente = Dokument.objects.all()
return render(request, 'standards/standard_list.html', return render(request, 'standards/standard_list.html',
{'dokumente': standards} {'dokumente': dokumente}
) )

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.927</div> <div>VorgabenUI v0.931</div>
</body> </body>
</html> </html>

View File

@@ -12,9 +12,9 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if resultat.kurztext %} {% if resultat.all %}
<h2>Vorgaben mit "{{ suchbegriff }}" im Kurztext</h2> <h2>Vorgaben mit "{{ suchbegriff }}"</h2>
{% for standard, vorgaben in resultat.kurztext.items %} {% for standard, vorgaben in resultat.all.items %}
<h4>{{ standard }}</h4> <h4>{{ standard }}</h4>
<ul> <ul>
{% for vorgabe in vorgaben %} {% for vorgabe in vorgaben %}
@@ -24,18 +24,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if resultat.langtext %} {% if not resultat.all %}
<h2>Vorgaben mit "{{ suchbegriff }}" im Langtext</h2>
{% for standard, vorgaben in resultat.langtext.items %}
<h4>{{ standard }}</h4>
<ul>
{% for vorgabe in vorgaben %}
<li><a href="{% url 'standard_detail' nummer=vorgabe.dokument.nummer %}#{{vorgabe.Vorgabennummer}}">{{vorgabe}}</a></li>
{% endfor %}
</ul>
{% endfor %}
{% endif %}
{% if not resultat.langtext and not resultat.kurztext and not resultat.geltungsbereich %}
<h2>Keine Resultate für "{{suchbegriff}}"</h2> <h2>Keine Resultate für "{{suchbegriff}}"</h2>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -15,27 +15,6 @@
placeholder="Suchbegriff eingeben …" placeholder="Suchbegriff eingeben …"
required> required>
</div> </div>
<!-- Check-box group -->
<fieldset class="mb-4">
<legend class="h6 mb-2">In folgenden Bereichen suchen:</legend>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="kurztext" id="kurztext" name="suchbereich[]" checked>
<label class="form-check-label" for="kurztext">Kurztext</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="langtext" id="langtext" name="suchbereich[]" checked>
<label class="form-check-label" for="langtext">Langtext</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="geltungsbereich" id="geltungsbereich" name="suchbereich[]">
<label class="form-check-label" for="geltungsbereich">Geltungsbereich</label>
</div>
</fieldset>
<button type="submit" class="btn btn-primary">Suchen</button> <button type="submit" class="btn btn-primary">Suchen</button>
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -3,6 +3,7 @@ from abschnitte.utils import render_textabschnitte
from dokumente.models import Dokument, VorgabeLangtext, VorgabeKurztext, Geltungsbereich 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.all()) standards=list(Dokument.objects.all())
@@ -13,22 +14,18 @@ def search(request):
return render(request, 'search.html') return render(request, 'search.html')
elif request.method == "POST": elif request.method == "POST":
suchbegriff=request.POST.get("q") suchbegriff=request.POST.get("q")
areas=request.POST.getlist("suchbereich[]") result= {"all": {}}
result= {} qs = VorgabeKurztext.objects.filter(inhalt__contains=suchbegriff).exclude(abschnitt__gueltigkeit_bis__lt=datetime.date.today())
geltungsbereich=set() result["kurztext"] = {k: [o.abschnitt for o in g] for k, g in groupby(qs, key=lambda o: o.abschnitt.dokument)}
if "kurztext" in areas: qs = VorgabeLangtext.objects.filter(inhalt__contains=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['langtext']= {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)}
if "langtext" in areas:
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)}
if "geltungsbereich" in areas:
result["geltungsbereich"]={}
geltungsbereich=set(list([x.geltungsbereich for x in Geltungsbereich.objects.filter(inhalt__contains=suchbegriff)]))
for s in geltungsbereich:
result["geltungsbereich"][s]=render_textabschnitte(s.geltungsbereich_set.order_by("order"))
for r in result.keys(): for r in result.keys():
for s in result[r].keys(): for s in result[r].keys():
result[r][s]=set(result[r][s]) result["all"][s] = set(result[r][s])
result["geltungsbereich"]={}
geltungsbereich=set(list([x.geltungsbereich for x in Geltungsbereich.objects.filter(inhalt__contains=suchbegriff)]))
for s in geltungsbereich:
result["geltungsbereich"][s]=render_textabschnitte(s.geltungsbereich_set.order_by("order"))
pprint.pp (result)
return render(request,"results.html",{"suchbegriff":suchbegriff,"resultat":result}) return render(request,"results.html",{"suchbegriff":suchbegriff,"resultat":result})