Compare commits

...

16 Commits

Author SHA1 Message Date
dd6d0fae46 Comments migrated into database and data-loader-container. Deploying as soon as merged.
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 1m15s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 30s
2025-11-27 23:23:51 +01:00
e5202d9b2b Comment function added 2025-11-27 23:11:59 +01:00
5535684a45 Deploy
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 33s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
2025-11-27 15:47:31 +01:00
f933b7d99a XSS prevention added (with tests) 2025-11-27 15:43:41 +01:00
fd729b3019 Merge pull request 'feature/nfs-storage' (#14) from feature/nfs-storage into development
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 14s
Reviewed-on: #14
2025-11-24 15:35:02 +00:00
e1c1eafb39 openspec updated 2025-11-24 16:32:27 +01:00
1b016c49f2 ArgoCD-Documentation added 2025-11-24 15:55:27 +01:00
4376069b11 NFS pointed to wrong place 2025-11-24 15:37:12 +01:00
c285ae81af Test with NFS
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 15:20:31 +01:00
5bfe4866a4 Deploy version 0.955
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 14s
2025-11-24 13:48:35 +01:00
f7799675d5 Typo in template fixed 2025-11-24 13:46:13 +01:00
c125427b8d ArgoCD resolved 2025-11-24 13:43:00 +01:00
a14a80f7bd Design tweaks
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 13:38:53 +01:00
477143b3ff Merge pull request 'fix: add argocd ignore-healthcheck and ingressClassName to Ingress' (#13) from improvements/argocd-service-fix into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Reviewed-on: #13
2025-11-24 11:02:52 +00:00
fc404f6755 Merge pull request 'troubleshooting ingress' (#12) from improvements/frontend into development
Reviewed-on: #12
2025-11-24 10:56:18 +00:00
bb01174bd2 fix: add argocd ignore-healthcheck and ingressClassName to Ingress
- Add ignore-healthcheck annotation to prevent 'Processing' state
- Add ingressClassName: traefik for proper ingress controller binding
2025-11-24 11:34:20 +01:00
29 changed files with 940 additions and 28 deletions

241
Documentation/ArgoCD.md Normal file
View File

@@ -0,0 +1,241 @@
# ArgoCD Configuration Documentation
## Overview
This directory contains the ArgoCD application manifests for deploying the VorgabenUI application and its dependencies to Kubernetes.
## Files
### Application Manifests
#### `001_pvc.yaml`
- **Purpose**: PersistentVolumeClaim for Django application data
- **Storage**: 2Gi storage with ReadWriteMany access mode
- **Storage Class**: Uses NFS storage class for shared storage across multiple pods
- **Namespace**: vorgabenui
#### `deployment.yaml`
- **Purpose**: Main application deployment configuration
- **Contains**: Django application container, environment variables, resource limits
- **Replicas**: Configurable replica count for high availability
#### `ingress.yaml`
- **Purpose**: External access configuration
- **Host**: Configurable hostname for the application
- **TLS**: SSL/TLS termination configuration
- **Backend**: Routes traffic to the Django application service
#### `nfs-pv.yaml`
- **Purpose**: PersistentVolume definition for NFS storage
- **Server**: 192.168.17.199
- **Path**: /mnt/user/vorgabenui
- **Access**: ReadWriteMany for multi-pod access
- **Reclaim Policy**: Retain (data preserved after PVC deletion)
#### `nfs-storageclass.yaml`
- **Purpose**: StorageClass definition for NFS volumes
- **Provisioner**: kubernetes.io/no-provisioner (static provisioning)
- **Volume Expansion**: Enabled for growing storage capacity
- **Binding Mode**: Immediate (binds PV to PVC as soon as possible)
#### `diagrammer.yaml`
- **Purpose**: Deployment configuration for the diagram generation service
- **Function**: Handles diagram creation and caching for the application
## NFS Storage Configuration
### Prerequisites
1. NFS server must be running at 192.168.17.199
2. The directory `/mnt/user/vorgabenui` must exist and be exported
3. Kubernetes nodes must have NFS client utilities installed
4. For MicroK8s: `microk8s enable nfs`
## MicroK8s Addons Required
### Required Addons
Enable the following MicroK8s addons before deployment:
```bash
# Enable storage and NFS support
sudo microk8s enable storage
sudo microk8s enable nfs
# Enable ingress for external access
sudo microk8s enable ingress
# Enable DNS for service discovery
sudo microk8s enable dns
# Optional: Enable metrics for monitoring
sudo microk8s enable metrics-server
```
### Addon Descriptions
#### `storage`
- **Purpose**: Provides default storage class for persistent volumes
- **Required for**: Basic PVC functionality
- **Note**: Works alongside our custom NFS storage class
#### `nfs`
- **Purpose**: Installs NFS client utilities on all MicroK8s nodes
- **Required for**: Mounting NFS volumes in pods
- **Components**: Installs `nfs-common` package with mount helpers
#### `ingress`
- **Purpose**: Provides Ingress controller for external HTTP/HTTPS access
- **Required for**: `ingress.yaml` to function properly
- **Implementation**: Uses NGINX Ingress Controller
#### `dns`
- **Purpose**: Provides DNS service for service discovery within cluster
- **Required for**: Inter-service communication
- **Note**: Usually enabled by default in MicroK8s
#### `metrics-server` (Optional)
- **Purpose**: Enables resource usage monitoring
- **Required for**: `kubectl top` commands and HPA (Horizontal Pod Autoscaling)
- **Recommended for**: Production monitoring
### Addon Verification
After enabling addons, verify they are running:
```bash
# Check addon status
microk8s status
# Check pods in kube-system namespace
microk8s kubectl get pods -n kube-system
# Check storage classes
microk8s kubectl get storageclass
# Check ingress controller
microk8s kubectl get pods -n ingress
```
### Troubleshooting Addons
#### NFS Addon Issues
```bash
# Check if NFS utilities are installed
which mount.nfs
# Manually install if addon fails
sudo apt update && sudo apt install nfs-common
# Restart MicroK8s after manual installation
sudo microk8s restart
```
#### Ingress Issues
```bash
# Check ingress controller pods
microk8s kubectl get pods -n ingress
# Check ingress services
microk8s kubectl get svc -n ingress
# Test ingress connectivity
curl -k https://your-domain.com
```
#### Storage Issues
```bash
# List available storage classes
microk8s kubectl get storageclass
# Check default storage class
microk8s kubectl get storageclass -o yaml
```
### Storage Architecture
- **Storage Class**: `nfs` - Static provisioning for NFS shares
- **Persistent Volume**: Pre-provisioned PV pointing to NFS server
- **Persistent Volume Claim**: Claims the NFS storage for application use
- **Access Mode**: ReadWriteMany allows multiple pods to access the same data
### NFS Server Setup
On the NFS server (192.168.17.199), ensure the following:
```bash
# Create the shared directory
sudo mkdir -p /mnt/user/vorgabenui
sudo chmod 755 /mnt/user/vorgabenui
# Add to /etc/exports
echo "/mnt/user/vorgabenui *(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
# Export the directory
sudo exportfs -a
sudo systemctl restart nfs-kernel-server
```
## Deployment Order
1. **StorageClass** (`nfs-storageclass.yaml`) - Defines NFS storage class
2. **PersistentVolume** (`nfs-pv.yaml`) - Creates the NFS volume
3. **PersistentVolumeClaim** (`001_pvc.yaml`) - Claims storage for application
4. **Application Deployments** (`deployment.yaml`, `diagrammer.yaml`) - Deploy application services
5. **Ingress** (`ingress.yaml`) - Configure external access
## Configuration Notes
### Namespace
All resources are deployed to the `vorgabenui` namespace.
### Storage Sizing
- Current allocation: 2Gi
- Volume expansion is enabled through the StorageClass
- Monitor usage and adjust PVC size as needed
### Access Control
- NFS export uses `no_root_squash` for container root access
- Ensure proper network security between Kubernetes nodes and NFS server
- Consider implementing network policies for additional security
## Troubleshooting
### Common Issues
#### Mount Failures
- **Error**: "bad option; for several filesystems you might need a /sbin/mount.<type> helper program"
- **Solution**: Install NFS client utilities or enable NFS addon in MicroK8s
#### Permission Issues
- **Error**: Permission denied when accessing mounted volume
- **Solution**: Check NFS export permissions and ensure `no_root_squash` is set
#### Network Connectivity
- **Error**: Connection timeout to NFS server
- **Solution**: Verify network connectivity and firewall rules between nodes and NFS server
### Debug Commands
```bash
# Check PVC status
kubectl get pvc -n vorgabenui
# Check PV status
kubectl get pv
# Describe PVC for detailed information
kubectl describe pvc django-data-pvc -n vorgabenui
# Check pod mount status
kubectl describe pod <pod-name> -n vorgabenui
```
## Maintenance
### Backup Strategy
- The NFS server should have regular backups of `/mnt/user/vorgabenui`
- Consider snapshot capabilities if using enterprise NFS solutions
### Monitoring
- Monitor NFS server performance and connectivity
- Track storage usage and plan capacity upgrades
- Monitor pod restarts related to storage issues
### Updates
- When updating storage configuration, update PV first, then PVC
- Test changes in non-production environment first
- Ensure backward compatibility when modifying NFS exports

View File

@@ -0,0 +1,92 @@
# Modelle (App: dokumente)
Kurzbeschreibungen der Modelle in dokumente/models.py.
## Dokumententyp
- Zweck: Kategorisierung von Dokumenten (z. B. Richtlinie, Verfahren).
- Wichtige Felder: `name` (CharField, PK), `verantwortliche_ve` (CharField).
- Besonderheiten: `__str__()` gibt `name` zurück.
- Meta: `verbose_name` und `verbose_name_plural` gesetzt.
## Person
- Zweck: Repräsentiert Personen (Autoren, Prüfer).
- Wichtige Felder: `name` (CharField, PK), `funktion` (CharField).
- Beziehungen: Many-to-many mit Dokument über `autoren` und `pruefende`.
- Besonderheiten: `__str__()` gibt `name` zurück; `ordering = ['name']`.
- Meta: `verbose_name_plural = "Personen"`.
## Thema
- Zweck: Thematische Einordnung von Vorgaben.
- Wichtige Felder: `name` (CharField, PK), `erklaerung` (TextField, optional).
- Besonderheiten: `__str__()` gibt `name` zurück.
## Dokument
- Zweck: Hauptobjekt; ein einzelnes Dokument mit Metadaten.
- Wichtige Felder:
- `nummer` (CharField, PK)
- `dokumententyp` (FK → Dokumententyp, on_delete=PROTECT)
- `name` (CharField)
- `autoren`, `pruefende` (ManyToManyField → Person)
- `gueltigkeit_von`, `gueltigkeit_bis` (DateField, optional)
- `aktiv` (BooleanField)
- `signatur_cso`, `anhaenge` (Metadaten)
- Besonderheiten: `__str__()` formatiert als "nummer name".
- Meta: `verbose_name` / `verbose_name_plural`.
## Vorgabe
- Zweck: Einzelne Vorgabe / Anforderung innerhalb eines Dokuments.
- Wichtige Felder:
- `order` (IntegerField) — Sortierreihenfolge
- `nummer` (IntegerField) — Nummer innerhalb Thema/Dokument
- `dokument` (FK → Dokument, CASCADE, related_name='vorgaben')
- `thema` (FK → Thema, PROTECT)
- `titel` (CharField)
- `referenzen` (M2M → Referenz, optional)
- `stichworte` (M2M → Stichwort, optional)
- `relevanz` (M2M → Rolle, optional)
- `gueltigkeit_von`, `gueltigkeit_bis` (Datum/Felder)
- Beziehungen: zu Dokument, Thema, Referenzen, Stichworte, Rollen.
- Wichtige Methoden:
- `Vorgabennummer()` — generiert eine lesbare Kennung (z. B. "DOK. T. N").
- `get_status(check_date, verbose)` — liefert "future", "active" oder "expired" oder eine deutsche Statusbeschreibung, abhängig von Gültigkeitsdaten.
- `sanity_check_vorgaben()` (static) — findet Konflikte zwischen Vorgaben mit gleicher Nummer/Thema/Dokument, deren Zeiträume sich überschneiden.
- `clean()` — ruft `find_conflicts()` auf und wirft ValidationError bei Konflikten.
- `find_conflicts()` — prüft Konflikte mit bestehenden Vorgaben (ohne sich selbst).
- `_date_ranges_intersect(...)` (static) — prüft, ob sich zwei Datumsbereiche überschneiden (None = offen).
- Besonderheiten: `__str__()` gibt "Vorgabennummer: titel" zurück.
- Meta: `ordering = ['order']`, `verbose_name_plural = "Vorgaben"`.
## VorgabeLangtext, VorgabeKurztext
- Zweck: Textabschnitts-Modelle, erben von `Textabschnitt` (aus abschnitte.models).
- Wichtige Felder: je ein FK `abschnitt` → Vorgabe.
- Besonderheit: konkrete Untertypen für Lang- und Kurztexte; Meta-`verbose_name` gesetzt.
## Geltungsbereich, Einleitung
- Zweck: Dokumentbezogene Textabschnitte (erben von `Textabschnitt`).
- Wichtige Felder: FK zum `Dokument` (`geltungsbereich` bzw. `einleitung`).
- Meta: `verbose_name`/`verbose_name_plural` gesetzt.
## Checklistenfrage
- Zweck: Einzelne Frage für Checklisten zu einer Vorgabe.
- Wichtige Felder: `vorgabe` (FK → Vorgabe, related_name="checklistenfragen"), `frage` (CharField).
- Besonderheiten: `__str__()` gibt `frage` zurück.
## VorgabenTable
- Zweck: Proxy-Modell für Vorgabe zur Darstellung (Tabellenansicht).
- Besonderheiten: kein eigenes Schema; nur Meta-Attribute (`proxy = True`, `verbose_name`).
## Changelog
- Zweck: Änderungsverzeichnis-Eintrag für ein Dokument.
- Wichtige Felder:
- `dokument` (FK → Dokument, related_name='changelog')
- `autoren` (M2M → Person)
- `datum` (DateField)
- `aenderung` (TextField)
- Besonderheiten: `__str__()` formatiert als "datum dokumentnummer".
- Meta: `verbose_name` / `verbose_name_plural`.
Hinweise zur Pflege
- Wichtige Relationen nutzen häufig on_delete=PROTECT, um versehentliche Löschungen zu vermeiden.
- Viele Modelle haben CharField-Primärschlüssel (z. B. `nummer`, `name`).
- Validierungslogik für zeitliche Konflikte ist in Vorgabe implementiert (clean / find_conflicts).
- Textabschnitt-Modelle erben Verhalten aus `abschnitte.models.Textabschnitt` — dort sind Anzeige- und Inhaltsregeln definiert.

View File

@@ -15,7 +15,7 @@ Dieses Dokument bietet einen umfassenden Überblick über alle Tests im vgui-cic
## abschnitte App Tests
Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-Caching und Management-Befehle abdecken.
Die abschnitte App enthält 33 Tests, die Modelle, Utility-Funktionen, Diagram-Caching, Management-Befehle und Sicherheit abdecken.
### Modell-Tests
@@ -58,6 +58,7 @@ Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-C
- **test_render_text_with_footnotes**: Verarbeitet Text, der Fußnoten enthält
- **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp
- **test_render_abschnitt_with_empty_content**: Behandelt Textabschnitte mit leerem Inhalt
- **test_render_textabschnitte_xss_prevention**: Überprüft, dass bösartiger HTML-Code und Skript-Tags aus gerenderten Inhalten bereinigt werden, um XSS-Angriffe zu verhindern
### Diagram-Caching-Tests
@@ -332,8 +333,8 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
## Test-Statistiken
- **Gesamt-Tests**: 206
- **abschnitte**: 32 Tests
- **Gesamt-Tests**: 207
- **abschnitte**: 33 Tests (einschließlich XSS-Prävention)
- **dokumente**: 116 Tests (98 in tests.py + 9 in test_json.py + 9 JSON-Tests in Haupt-tests.py)
- **pages**: 4 Tests
- **referenzen**: 18 Tests
@@ -348,6 +349,7 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung
5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung
6. **Integration**: App-übergreifende Funktionalität und Datenfluss
7. **Sicherheit**: XSS-Prävention durch HTML-Bereinigung beim Rendern von Inhalten
## Ausführen der Tests

View File

@@ -15,7 +15,7 @@ This document provides a comprehensive overview of all tests in the vgui-cicd Dj
## abschnitte App Tests
The abschnitte app contains 32 tests covering models, utility functions, diagram caching, and management commands.
The abschnitte app contains 33 tests covering models, utility functions, diagram caching, management commands, and security.
### Model Tests
@@ -58,6 +58,7 @@ The abschnitte app contains 32 tests covering models, utility functions, diagram
- **test_render_text_with_footnotes**: Processes text containing footnotes
- **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp
- **test_render_abschnitt_with_empty_content**: Handles Textabschnitte with empty content
- **test_render_textabschnitte_xss_prevention**: Verifies that malicious HTML and script tags are sanitized from rendered content to prevent XSS attacks
### Diagram Caching Tests
@@ -332,8 +333,8 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
## Test Statistics
- **Total Tests**: 206
- **abschnitte**: 32 tests
- **Total Tests**: 207
- **abschnitte**: 33 tests (including XSS prevention)
- **dokumente**: 116 tests (98 in tests.py + 9 in test_json.py + 9 JSON tests in main tests.py)
- **pages**: 4 tests
- **referenzen**: 18 tests
@@ -348,6 +349,7 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
4. **Utility Functions**: Text processing, caching, formatting
5. **Management Commands**: CLI interface and output handling
6. **Integration**: Cross-app functionality and data flow
7. **Security**: XSS prevention through HTML sanitization in content rendering
## Running the Tests

View File

@@ -28,12 +28,6 @@ DEBUG = True
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
TEMPLATES = [
{"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
}
]
# Application definition
INSTALLED_APPS = [

View File

@@ -467,6 +467,32 @@ A -> B
typ, html = result[0]
self.assertEqual(typ, "text")
def test_render_textabschnitte_xss_prevention(self):
"""Test that malicious HTML is sanitized in rendered content"""
from dokumente.models import VorgabeLangtext
# Create content with malicious HTML
malicious_abschnitt = VorgabeLangtext.objects.create(
abschnitt=self.vorgabe,
abschnitttyp=self.typ_text,
inhalt='<script>alert("xss")</script><img src=x onerror=alert(1)>Normal text',
order=1
)
result = render_textabschnitte(VorgabeLangtext.objects.filter(pk=malicious_abschnitt.pk))
self.assertEqual(len(result), 1)
typ, html = result[0]
self.assertEqual(typ, "text")
# Dangerous tags and attributes should be removed or sanitized
self.assertNotIn('<script>', html) # Script tags should not be present unescaped
self.assertNotIn('onerror', html) # Dangerous attributes removed
# Note: 'alert' may still be present in escaped script tags, which is safe
# Safe content should remain
self.assertIn('Normal text', html)
class MdTableToHtmlTest(TestCase):
"""Test cases for md_table_to_html function"""

View File

@@ -4,12 +4,34 @@ import zlib
import re
from textwrap import dedent
from django.conf import settings
import bleach
# Import the caching function
from diagramm_proxy.diagram_cache import get_cached_diagram
DIAGRAMMSERVER="/diagramm"
# Allowed HTML tags for bleach sanitization
ALLOWED_TAGS = [
'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'blockquote', 'code', 'pre', 'hr',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'img', 'a', 'sup', 'sub', 'span', 'div'
]
ALLOWED_ATTRIBUTES = {
'img': ['src', 'alt', 'width', 'height'],
'a': ['href', 'title'],
'span': ['class'],
'div': ['class'],
'p': ['class'],
'table': ['class'],
'th': ['colspan', 'rowspan', 'class'],
'td': ['colspan', 'rowspan', 'class'],
'pre': ['class'],
'code': ['class'],
}
def render_textabschnitte(queryset):
"""
Converts a queryset of Textabschnitt-like models into a list of (typ, html) tuples.
@@ -52,6 +74,8 @@ def render_textabschnitte(queryset):
html += "</code></pre>"
else:
html = markdown(inhalt, extensions=['tables', 'attr_list','footnotes'])
# Sanitize HTML to prevent XSS
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
output.append((typ, html))
return output

View File

@@ -5,7 +5,8 @@ metadata:
namespace: vorgabenui
spec:
accessModes:
- ReadWriteOnce
- ReadWriteMany
storageClassName: nfs
resources:
requests:
storage: 2Gi

View File

@@ -18,14 +18,14 @@ spec:
fsGroupChangePolicy: "OnRootMismatch"
initContainers:
- name: loader
image: git.baumann.gr/adebaumann/vui-data-loader:0.9
image: git.baumann.gr/adebaumann/vui-data-loader:0.10
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.953-ingressfixed
image: git.baumann.gr/adebaumann/vui:0.958-comments
imagePullPolicy: Always
ports:
- containerPort: 8000
@@ -63,6 +63,8 @@ spec:
selector:
app: django
ports:
- port: 8000
- name: http
protocol: TCP
port: 8000
targetPort: 8000

View File

@@ -3,7 +3,10 @@ kind: Ingress
metadata:
name: django
namespace: vorgabenui
annotations:
argocd.argoproj.io/ignore-healthcheck: "true"
spec:
ingressClassName: traefik
rules:
- host: vorgabenportal.knowyoursecurity.com
http:

15
argocd/nfs-pv.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: django-data-pv
namespace: vorgabenui
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
server: 192.168.17.199
path: /mnt/user/vorgabenui

View File

@@ -0,0 +1,8 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: Immediate

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,49 @@
# Generated by Django 5.2.5 on 2025-11-27 22:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0009_alter_vorgabe_options_vorgabe_order'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='VorgabenTable',
fields=[
],
options={
'verbose_name': 'Vorgabe (Tabellenansicht)',
'verbose_name_plural': 'Vorgaben (Tabellenansicht)',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('dokumente.vorgabe',),
),
migrations.AlterModelOptions(
name='person',
options={'ordering': ['name'], 'verbose_name_plural': 'Personen'},
),
migrations.CreateModel(
name='VorgabeComment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('vorgabe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='dokumente.vorgabe')),
],
options={
'verbose_name': 'Vorgabe-Kommentar',
'verbose_name_plural': 'Vorgabe-Kommentare',
'ordering': ['-created_at'],
},
),
]

View File

@@ -1,5 +1,6 @@
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from django.contrib.auth.models import User
from abschnitte.models import Textabschnitt
from stichworte.models import Stichwort
from referenzen.models import Referenz
@@ -261,3 +262,19 @@ class Changelog(models.Model):
class Meta:
verbose_name_plural="Changelog"
verbose_name="Changelog-Eintrag"
class VorgabeComment(models.Model):
vorgabe = models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name='comments')
user = models.ForeignKey(User, on_delete=models.CASCADE)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Vorgabe-Kommentar"
verbose_name_plural = "Vorgabe-Kommentare"
ordering = ['-created_at']
def __str__(self):
return f"Kommentar von {self.user.username} zu {self.vorgabe.Vorgabennummer()}"

View File

@@ -105,13 +105,13 @@
{% else %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">
<i class="fas fa-check-circle"></i> Alle Vorgaben sind vollständig!
<span class="emoji-icon"></span> 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
<span class="emoji-icon">📋</span> Zurück zur Übersicht
</a>
</p>
</div>
@@ -119,7 +119,7 @@
<div class="mt-3">
<a href="{% url 'standard_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Zurück zur Übersicht
<span class="emoji-icon"></span> Zurück zur Übersicht
</a>
</div>

View File

@@ -25,7 +25,7 @@
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">Einleitung</h2>
<h2>Einleitung</h2>
</div>
<div class="card-body">
{% for typ, html in standard.einleitung_html %}
@@ -43,7 +43,7 @@
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">Geltungsbereich</h2>
<h2>Geltungsbereich</h2>
</div>
<div class="card-body">
{% for typ, html in standard.geltungsbereich_html %}
@@ -73,7 +73,7 @@
<a id="{{ vorgabe.Vorgabennummer }}"></a>
<div class="card mb-4">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<h3 class="h5 mb-0">
<h3>
{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %}
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
@@ -123,7 +123,7 @@
{% endif %}
<!-- Stichworte und Referenzen -->
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6;">
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6; padding-left: 0.5en;">
<p class="mb-2">
<strong>Stichworte:</strong>
{% if vorgabe.stichworte.all %}
@@ -145,6 +145,20 @@
{% endif %}
</p>
</div>
<!-- Comment Button -->
{% if user.is_authenticated %}
<div class="mt-3 text-right">
<button class="btn btn-sm btn-outline-primary comment-btn"
data-vorgabe-id="{{ vorgabe.id }}"
data-vorgabe-nummer="{{ vorgabe.Vorgabennummer }}">
<span class="emoji-icon">💬</span> Kommentare
{% if vorgabe.comment_count > 0 %}
<span class="comment-count">{{ vorgabe.comment_count }}</span>
{% endif %}
</button>
</div>
{% endif %}
</div>
</div>
</div>
@@ -176,4 +190,180 @@
</div>
</div>
</div>
<!-- Comment Modal -->
<div class="modal fade" id="commentModal" tabindex="-1" role="dialog" aria-labelledby="commentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="commentModalLabel">Kommentare für <span id="modalVorgabeNummer"></span></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="commentsContainer">
<!-- Comments will be loaded here -->
</div>
<!-- Add Comment Form -->
<div class="mt-4">
<h6>Neuen Kommentar hinzufügen:</h6>
<textarea id="newCommentText" class="form-control" rows="3" placeholder="Ihr Kommentar..."></textarea>
<button id="addCommentBtn" class="btn btn-primary btn-sm mt-2">Kommentar hinzufügen</button>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript for Comments -->
<script>
document.addEventListener('DOMContentLoaded', function() {
let currentVorgabeId = null;
let currentVorgabeNummer = null;
// Comment button click handler
document.querySelectorAll('.comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentVorgabeId = this.dataset.vorgabeId;
currentVorgabeNummer = this.dataset.vorgabeNummer;
document.getElementById('modalVorgabeNummer').textContent = currentVorgabeNummer;
document.getElementById('newCommentText').value = '';
loadComments();
$('#commentModal').modal('show');
});
});
// Load comments function
function loadComments() {
fetch(`/dokumente/comments/${currentVorgabeId}/`)
.then(response => response.json())
.then(data => {
renderComments(data.comments);
})
.catch(error => {
console.error('Error loading comments:', error);
document.getElementById('commentsContainer').innerHTML =
'<div class="alert alert-danger">Fehler beim Laden der Kommentare</div>';
});
}
// Render comments function
function renderComments(comments) {
const container = document.getElementById('commentsContainer');
if (comments.length === 0) {
container.innerHTML = '<p class="text-muted">Noch keine Kommentare vorhanden.</p>';
return;
}
let html = '';
comments.forEach(comment => {
const canDelete = comment.is_own || {% if user.is_authenticated %}'{{ user.is_staff|yesno:"true,false" }}'{% else %}'false'{% endif %} === 'true';
html += `
<div class="comment-item border-bottom pb-2 mb-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<strong>${comment.user}</strong>
<small class="text-muted">(${comment.created_at})</small>
${comment.updated_at !== comment.created_at ? `<small class="text-muted">(bearbeitet: ${comment.updated_at})</small>` : ''}
<div class="mt-1">${comment.text.replace(/\n/g, '<br>')}</div>
</div>
${canDelete ? `
<button class="btn btn-sm btn-outline-danger ml-2 delete-comment-btn" data-comment-id="${comment.id}">
<span aria-hidden="true">&times;</span>
</button>
` : ''}
</div>
</div>
`;
});
container.innerHTML = html;
// Add delete handlers
document.querySelectorAll('.delete-comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
if (confirm('Möchten Sie diesen Kommentar wirklich löschen?')) {
deleteComment(this.dataset.commentId);
}
});
});
}
// Add comment function
document.getElementById('addCommentBtn').addEventListener('click', function() {
const text = document.getElementById('newCommentText').value.trim();
if (!text) {
alert('Bitte geben Sie einen Kommentar ein.');
return;
}
fetch(`/dokumente/comments/${currentVorgabeId}/add/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ text: text })
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('newCommentText').value = '';
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error adding comment:', error);
alert('Fehler beim Hinzufügen des Kommentars');
});
});
// Delete comment function
function deleteComment(commentId) {
fetch(`/dokumente/comments/delete/${commentId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error deleting comment:', error);
alert('Fehler beim Löschen des Kommentars');
});
}
// CSRF token helper
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});
</script>
{% endblock %}

View File

@@ -8,6 +8,9 @@ urlpatterns = [
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>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
path('<str:nummer>/json/', views.standard_json, name='standard_json')
path('<str:nummer>/json/', views.standard_json, name='standard_json'),
path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),
]

View File

@@ -2,8 +2,10 @@ from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
import json
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from abschnitte.utils import render_textabschnitte
from datetime import date
@@ -44,6 +46,15 @@ def standard_detail(request, nummer,check_date=""):
for r in vorgabe.referenzen.all():
referenz_items.append(r.Path())
vorgabe.referenzpfade = referenz_items
# Add comment count
if request.user.is_authenticated:
if request.user.is_staff:
vorgabe.comment_count = vorgabe.comments.count()
else:
vorgabe.comment_count = vorgabe.comments.filter(user=request.user).count()
else:
vorgabe.comment_count = 0
return render(request, 'standards/standard_detail.html', {
'standard': standard,
@@ -237,3 +248,83 @@ def standard_json(request, nummer):
# Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
@login_required
def get_vorgabe_comments(request, vorgabe_id):
"""Get comments for a specific Vorgabe"""
vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
if request.user.is_staff:
# Staff can see all comments
comments = vorgabe.comments.all().select_related('user')
else:
# Regular users can only see their own comments
comments = vorgabe.comments.filter(user=request.user).select_related('user')
comments_data = []
for comment in comments:
comments_data.append({
'id': comment.id,
'text': comment.text,
'user': comment.user.username,
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
'is_own': comment.user == request.user
})
return JsonResponse({'comments': comments_data})
@require_POST
@login_required
def add_vorgabe_comment(request, vorgabe_id):
"""Add a new comment to a Vorgabe"""
vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
try:
data = json.loads(request.body)
text = data.get('text', '').strip()
if not text:
return JsonResponse({'error': 'Kommentar darf nicht leer sein'}, status=400)
comment = VorgabeComment.objects.create(
vorgabe=vorgabe,
user=request.user,
text=text
)
return JsonResponse({
'success': True,
'comment': {
'id': comment.id,
'text': comment.text,
'user': comment.user.username,
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
'is_own': True
}
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Ungültige Daten'}, status=400)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
@require_POST
@login_required
def delete_vorgabe_comment(request, comment_id):
"""Delete a comment (only own comments or staff can delete)"""
comment = get_object_or_404(VorgabeComment, id=comment_id)
# Check if user can delete this comment
if comment.user != request.user and not request.user.is_staff:
return JsonResponse({'error': 'Keine Berechtigung zum Löschen dieses Kommentars'}, status=403)
try:
comment.delete()
return JsonResponse({'success': True})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)

15
k8s/nfs-pv.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: django-data-pv
namespace: vorgabenui
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
server: 192.168.17.199
path: /mnt/user/vorgabenui

View File

@@ -0,0 +1,8 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: Immediate

View File

@@ -5,7 +5,8 @@ metadata:
namespace: vorgabenui
spec:
accessModes:
- ReadWriteOnce
- ReadWriteMany
storageClassName: nfs
resources:
requests:
storage: 2Gi

View File

@@ -102,6 +102,7 @@
<li><a href="/dokumente">Standards</a></li>
{% if user.is_staff %}
<li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li>
<li><a href="/autorenumgebung/">Autorenumgebung</a></li>
{% endif %}
<li><a href="/referenzen">Referenzen</a></li>
<li><a href="/stichworte">Stichworte</a></li>
@@ -131,6 +132,9 @@
<li class="dropdown {% if 'unvollstaendig' in request.path %}current{% endif %}">
<a href="/dokumente/unvollstaendig/">Unvollständig</a>
</li>
<li class="dropdown {% if 'autorenumgebung' in request.path %}current{% endif %}">
<a href="/autorenumgebung/">Autorenumgebung</a>
</li>
{% endif %}
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
<a href="/referenzen">Referenzen</a>
@@ -211,7 +215,7 @@
</p>
</div>
<div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.953" }}</p>
<p class="text-muted">Version {{ version|default:"0.957-xss" }}</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.5 on 2025-11-27 22:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('referenzen', '0002_alter_referenz_table_alter_referenzerklaerung_table'),
]
operations = [
migrations.AlterModelOptions(
name='referenzerklaerung',
options={'verbose_name': 'Erklärung', 'verbose_name_plural': 'Erklärungen'},
),
]

View File

@@ -32,3 +32,4 @@ six==1.17.0
sqlparse==0.5.3
urllib3==2.5.0
wcwidth==0.2.13
bleach==6.1.0

View File

@@ -11,4 +11,93 @@
margin-bottom: 1em;
border: 1px solid #ccc;
padding: 0;
}
}
/* Comment System Styles */
.comment-btn {
position: relative;
}
.comment-btn .comment-count {
position: absolute;
top: -8px;
right: -8px;
background-color: #dc3545;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 11px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.comment-item {
max-width: 100%;
word-wrap: break-word;
overflow-wrap: break-word;
}
.comment-item .text-muted {
font-size: 0.85em;
}
#commentModal .modal-body {
max-height: 60vh;
overflow-y: auto;
}
#commentsContainer {
min-height: 100px;
}
.delete-comment-btn {
opacity: 0.7;
transition: opacity 0.2s;
}
.delete-comment-btn:hover {
opacity: 1;
}
.delete-comment-btn {
font-size: 18px;
font-weight: bold;
line-height: 1;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.delete-comment-btn:hover {
opacity: 1;
background-color: #f8d7da;
border-color: #f5c6cb;
}
/* Icon styling for emoji replacements */
.emoji-icon {
font-size: 1.1em;
margin-right: 0.3em;
vertical-align: middle;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.comment-item .d-flex {
flex-direction: column;
}
.delete-comment-btn {
margin-left: 0 !important;
margin-top: 0.5rem;
}
}

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.5 on 2025-11-27 22:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stichworte', '0002_stichworterklaerung_order'),
]
operations = [
migrations.AlterModelOptions(
name='stichworterklaerung',
options={'verbose_name': 'Erklärung', 'verbose_name_plural': 'Erklärungen'},
),
]