Compare commits
17 Commits
improvemen
...
dd6d0fae46
| Author | SHA1 | Date | |
|---|---|---|---|
| dd6d0fae46 | |||
| e5202d9b2b | |||
| 5535684a45 | |||
| f933b7d99a | |||
| fd729b3019 | |||
| e1c1eafb39 | |||
| 1b016c49f2 | |||
| 4376069b11 | |||
| c285ae81af | |||
| 5bfe4866a4 | |||
| f7799675d5 | |||
| c125427b8d | |||
| a14a80f7bd | |||
| 477143b3ff | |||
| fc404f6755 | |||
| fe7c55eceb | |||
| 38ce55d8fd |
241
Documentation/ArgoCD.md
Normal file
241
Documentation/ArgoCD.md
Normal 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
|
||||||
92
Documentation/modelle_dokumente.md
Normal file
92
Documentation/modelle_dokumente.md
Normal 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.
|
||||||
@@ -15,7 +15,7 @@ Dieses Dokument bietet einen umfassenden Überblick über alle Tests im vgui-cic
|
|||||||
|
|
||||||
## abschnitte App Tests
|
## 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
|
### 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_text_with_footnotes**: Verarbeitet Text, der Fußnoten enthält
|
||||||
- **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp
|
- **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp
|
||||||
- **test_render_abschnitt_with_empty_content**: Behandelt Textabschnitte mit leerem Inhalt
|
- **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
|
### Diagram-Caching-Tests
|
||||||
|
|
||||||
@@ -332,8 +333,8 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
|
|||||||
|
|
||||||
## Test-Statistiken
|
## Test-Statistiken
|
||||||
|
|
||||||
- **Gesamt-Tests**: 206
|
- **Gesamt-Tests**: 207
|
||||||
- **abschnitte**: 32 Tests
|
- **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)
|
- **dokumente**: 116 Tests (98 in tests.py + 9 in test_json.py + 9 JSON-Tests in Haupt-tests.py)
|
||||||
- **pages**: 4 Tests
|
- **pages**: 4 Tests
|
||||||
- **referenzen**: 18 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
|
4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung
|
||||||
5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung
|
5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung
|
||||||
6. **Integration**: App-übergreifende Funktionalität und Datenfluss
|
6. **Integration**: App-übergreifende Funktionalität und Datenfluss
|
||||||
|
7. **Sicherheit**: XSS-Prävention durch HTML-Bereinigung beim Rendern von Inhalten
|
||||||
|
|
||||||
## Ausführen der Tests
|
## Ausführen der Tests
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ This document provides a comprehensive overview of all tests in the vgui-cicd Dj
|
|||||||
|
|
||||||
## abschnitte App Tests
|
## 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
|
### 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_text_with_footnotes**: Processes text containing footnotes
|
||||||
- **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp
|
- **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp
|
||||||
- **test_render_abschnitt_with_empty_content**: Handles Textabschnitte with empty content
|
- **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
|
### Diagram Caching Tests
|
||||||
|
|
||||||
@@ -332,8 +333,8 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
|
|||||||
|
|
||||||
## Test Statistics
|
## Test Statistics
|
||||||
|
|
||||||
- **Total Tests**: 206
|
- **Total Tests**: 207
|
||||||
- **abschnitte**: 32 tests
|
- **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)
|
- **dokumente**: 116 tests (98 in tests.py + 9 in test_json.py + 9 JSON tests in main tests.py)
|
||||||
- **pages**: 4 tests
|
- **pages**: 4 tests
|
||||||
- **referenzen**: 18 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
|
4. **Utility Functions**: Text processing, caching, formatting
|
||||||
5. **Management Commands**: CLI interface and output handling
|
5. **Management Commands**: CLI interface and output handling
|
||||||
6. **Integration**: Cross-app functionality and data flow
|
6. **Integration**: Cross-app functionality and data flow
|
||||||
|
7. **Security**: XSS prevention through HTML sanitization in content rendering
|
||||||
|
|
||||||
## Running the Tests
|
## Running the Tests
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,6 @@ DEBUG = True
|
|||||||
|
|
||||||
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
|
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
||||||
"APP_DIRS": True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
|||||||
@@ -467,6 +467,32 @@ A -> B
|
|||||||
typ, html = result[0]
|
typ, html = result[0]
|
||||||
self.assertEqual(typ, "text")
|
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):
|
class MdTableToHtmlTest(TestCase):
|
||||||
"""Test cases for md_table_to_html function"""
|
"""Test cases for md_table_to_html function"""
|
||||||
|
|||||||
@@ -4,12 +4,34 @@ import zlib
|
|||||||
import re
|
import re
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import bleach
|
||||||
|
|
||||||
# Import the caching function
|
# Import the caching function
|
||||||
from diagramm_proxy.diagram_cache import get_cached_diagram
|
from diagramm_proxy.diagram_cache import get_cached_diagram
|
||||||
|
|
||||||
DIAGRAMMSERVER="/diagramm"
|
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):
|
def render_textabschnitte(queryset):
|
||||||
"""
|
"""
|
||||||
Converts a queryset of Textabschnitt-like models into a list of (typ, html) tuples.
|
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>"
|
html += "</code></pre>"
|
||||||
else:
|
else:
|
||||||
html = markdown(inhalt, extensions=['tables', 'attr_list','footnotes'])
|
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))
|
output.append((typ, html))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ metadata:
|
|||||||
namespace: vorgabenui
|
namespace: vorgabenui
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteMany
|
||||||
|
storageClassName: nfs
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ spec:
|
|||||||
fsGroupChangePolicy: "OnRootMismatch"
|
fsGroupChangePolicy: "OnRootMismatch"
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: loader
|
- name: loader
|
||||||
image: git.baumann.gr/adebaumann/vui-data-loader:0.9
|
image: git.baumann.gr/adebaumann/vui-data-loader:0.10
|
||||||
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.953-ingressfixed
|
image: git.baumann.gr/adebaumann/vui:0.958-comments
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
|
|||||||
15
argocd/nfs-pv.yaml
Normal file
15
argocd/nfs-pv.yaml
Normal 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
|
||||||
8
argocd/nfs-storageclass.yaml
Normal file
8
argocd/nfs-storageclass.yaml
Normal 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.
BIN
data/db.sqlite3
BIN
data/db.sqlite3
Binary file not shown.
@@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
from mptt.models import MPTTModel, TreeForeignKey
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from abschnitte.models import Textabschnitt
|
from abschnitte.models import Textabschnitt
|
||||||
from stichworte.models import Stichwort
|
from stichworte.models import Stichwort
|
||||||
from referenzen.models import Referenz
|
from referenzen.models import Referenz
|
||||||
@@ -261,3 +262,19 @@ class Changelog(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural="Changelog"
|
verbose_name_plural="Changelog"
|
||||||
verbose_name="Changelog-Eintrag"
|
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()}"
|
||||||
|
|||||||
@@ -105,13 +105,13 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-success" role="alert">
|
<div class="alert alert-success" role="alert">
|
||||||
<h4 class="alert-heading">
|
<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>
|
</h4>
|
||||||
<p>Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.</p>
|
<p>Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
<a href="{% url 'standard_list' %}" class="btn btn-primary">
|
<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>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<a href="{% url 'standard_list' %}" class="btn btn-secondary">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="h4 mb-0">Einleitung</h2>
|
<h2>Einleitung</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% for typ, html in standard.einleitung_html %}
|
{% for typ, html in standard.einleitung_html %}
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="h4 mb-0">Geltungsbereich</h2>
|
<h2>Geltungsbereich</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% for typ, html in standard.geltungsbereich_html %}
|
{% for typ, html in standard.geltungsbereich_html %}
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<a id="{{ vorgabe.Vorgabennummer }}"></a>
|
<a id="{{ vorgabe.Vorgabennummer }}"></a>
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<h3 class="h5 mb-0">
|
<h3>
|
||||||
{{ vorgabe.Vorgabennummer }} – {{ vorgabe.titel }}
|
{{ vorgabe.Vorgabennummer }} – {{ vorgabe.titel }}
|
||||||
{% if vorgabe.long_status != "active" and standard.history == True %}
|
{% if vorgabe.long_status != "active" and standard.history == True %}
|
||||||
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
|
<span class="badge badge-danger">{{ vorgabe.long_status }}</span>
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Stichworte und Referenzen -->
|
<!-- 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">
|
<p class="mb-2">
|
||||||
<strong>Stichworte:</strong>
|
<strong>Stichworte:</strong>
|
||||||
{% if vorgabe.stichworte.all %}
|
{% if vorgabe.stichworte.all %}
|
||||||
@@ -145,6 +145,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,4 +190,180 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">×</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">×</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ urlpatterns = [
|
|||||||
path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
|
path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
|
||||||
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
|
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
|
||||||
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
|
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
|
||||||
path('<str:nummer>/json/', views.standard_json, name='standard_json')
|
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'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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.contrib.auth.decorators import login_required, user_passes_test
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
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
|
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 abschnitte.utils import render_textabschnitte
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -45,6 +47,15 @@ def standard_detail(request, nummer,check_date=""):
|
|||||||
referenz_items.append(r.Path())
|
referenz_items.append(r.Path())
|
||||||
vorgabe.referenzpfade = referenz_items
|
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', {
|
return render(request, 'standards/standard_detail.html', {
|
||||||
'standard': standard,
|
'standard': standard,
|
||||||
'vorgaben': vorgaben,
|
'vorgaben': vorgaben,
|
||||||
@@ -237,3 +248,83 @@ def standard_json(request, nummer):
|
|||||||
|
|
||||||
# Return JSON response
|
# Return JSON response
|
||||||
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
|
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
15
k8s/nfs-pv.yaml
Normal 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
|
||||||
8
k8s/nfs-storageclass.yaml
Normal file
8
k8s/nfs-storageclass.yaml
Normal 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
|
||||||
@@ -5,7 +5,8 @@ metadata:
|
|||||||
namespace: vorgabenui
|
namespace: vorgabenui
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteMany
|
||||||
|
storageClassName: nfs
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 2Gi
|
storage: 2Gi
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
<li><a href="/dokumente">Standards</a></li>
|
<li><a href="/dokumente">Standards</a></li>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
<li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li>
|
<li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li>
|
||||||
|
<li><a href="/autorenumgebung/">Autorenumgebung</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="/referenzen">Referenzen</a></li>
|
<li><a href="/referenzen">Referenzen</a></li>
|
||||||
<li><a href="/stichworte">Stichworte</a></li>
|
<li><a href="/stichworte">Stichworte</a></li>
|
||||||
@@ -131,6 +132,9 @@
|
|||||||
<li class="dropdown {% if 'unvollstaendig' in request.path %}current{% endif %}">
|
<li class="dropdown {% if 'unvollstaendig' in request.path %}current{% endif %}">
|
||||||
<a href="/dokumente/unvollstaendig/">Unvollständig</a>
|
<a href="/dokumente/unvollstaendig/">Unvollständig</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="dropdown {% if 'autorenumgebung' in request.path %}current{% endif %}">
|
||||||
|
<a href="/autorenumgebung/">Autorenumgebung</a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
|
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
|
||||||
<a href="/referenzen">Referenzen</a>
|
<a href="/referenzen">Referenzen</a>
|
||||||
@@ -211,7 +215,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6 text-right">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -32,3 +32,4 @@ six==1.17.0
|
|||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
wcwidth==0.2.13
|
wcwidth==0.2.13
|
||||||
|
bleach==6.1.0
|
||||||
|
|||||||
@@ -12,3 +12,92 @@
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'},
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user