Compare commits
16 Commits
improvemen
...
dd6d0fae46
| Author | SHA1 | Date | |
|---|---|---|---|
| dd6d0fae46 | |||
| e5202d9b2b | |||
| 5535684a45 | |||
| f933b7d99a | |||
| fd729b3019 | |||
| e1c1eafb39 | |||
| 1b016c49f2 | |||
| 4376069b11 | |||
| c285ae81af | |||
| 5bfe4866a4 | |||
| f7799675d5 | |||
| c125427b8d | |||
| a14a80f7bd | |||
| 477143b3ff | |||
| fc404f6755 | |||
| bb01174bd2 |
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ metadata:
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
- ReadWriteMany
|
||||
storageClassName: nfs
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
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 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()}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">×</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 %}
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
|
||||
@@ -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
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
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
- ReadWriteMany
|
||||
storageClassName: nfs
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
urllib3==2.5.0
|
||||
wcwidth==0.2.13
|
||||
bleach==6.1.0
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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