Compare commits

..

33 Commits

Author SHA1 Message Date
1af50c45ff Testing new workflow
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 13s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 8s
2025-12-01 14:29:10 +01:00
40551094e6 New Workflow to check if image is present on gitea server
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 10s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 9s
2025-12-01 14:28:02 +01:00
4297c2d8bf Documentation of all models added 2025-12-01 14:15:42 +01:00
07ba717de9 Display name changed from username to full name 2025-11-28 14:41:24 +01:00
5535684a45 Deploy
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 33s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
2025-11-27 15:47:31 +01:00
f933b7d99a XSS prevention added (with tests) 2025-11-27 15:43:41 +01:00
fd729b3019 Merge pull request 'feature/nfs-storage' (#14) from feature/nfs-storage into development
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 14s
Reviewed-on: #14
2025-11-24 15:35:02 +00:00
e1c1eafb39 openspec updated 2025-11-24 16:32:27 +01:00
1b016c49f2 ArgoCD-Documentation added 2025-11-24 15:55:27 +01:00
4376069b11 NFS pointed to wrong place 2025-11-24 15:37:12 +01:00
c285ae81af Test with NFS
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 15:20:31 +01:00
5bfe4866a4 Deploy version 0.955
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 14s
2025-11-24 13:48:35 +01:00
f7799675d5 Typo in template fixed 2025-11-24 13:46:13 +01:00
c125427b8d ArgoCD resolved 2025-11-24 13:43:00 +01:00
a14a80f7bd Design tweaks
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 13:38:53 +01:00
477143b3ff Merge pull request 'fix: add argocd ignore-healthcheck and ingressClassName to Ingress' (#13) from improvements/argocd-service-fix into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Reviewed-on: #13
2025-11-24 11:02:52 +00:00
fc404f6755 Merge pull request 'troubleshooting ingress' (#12) from improvements/frontend into development
Reviewed-on: #12
2025-11-24 10:56:18 +00:00
fe7c55eceb Merge branch 'development' into improvements/frontend 2025-11-24 10:56:10 +00:00
bb01174bd2 fix: add argocd ignore-healthcheck and ingressClassName to Ingress
- Add ignore-healthcheck annotation to prevent 'Processing' state
- Add ingressClassName: traefik for proper ingress controller binding
2025-11-24 11:34:20 +01:00
38ce55d8fd troubleshooting ingress 2025-11-24 10:22:09 +00:00
d439741339 Merge pull request 'feature/login' (#11) from feature/login into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 19s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 3s
Reviewed-on: #11
2025-11-24 10:20:48 +00:00
bc75cac6cd Merge branch 'development' into feature/login 2025-11-24 10:20:32 +00:00
47c264e8e1 fix: update search tests to match actual template output
- Change expected 'Suchresultate' to 'Suchergebnisse' matching results.html
- Change expected 'Keine Resultate' to 'Keine Ergebnisse gefunden'
- Replace test_search_result_logging with test_search_result_structure
- Remove unused unittest.mock import
2025-11-24 11:19:09 +01:00
4d0ed116dd test: add comprehensive authentication test suite
- Add 21 test cases covering login, logout, and password change functionality
- Test both success and failure scenarios for authentication flows
- Verify proper redirects to main page instead of admin
- Test user menu display for authenticated vs anonymous users
- Test CSRF protection and POST requirement for logout
- Test password validation and error handling
- All tests passing, ensuring authentication feature works correctly
2025-11-24 10:46:10 +01:00
ceb6e13447 fix: resolve logout 405 error by using POST method
- Change logout link from GET anchor to POST form
- Add CSRF token for security
- Style button to match dropdown menu appearance
2025-11-24 10:39:40 +01:00
7e9059a9aa feat: implement user authentication with login/logout functionality
- Add user login screen with German interface
- Add user icon and dropdown menu in header for authenticated users
- Add password change functionality with proper redirects
- Configure authentication URLs and settings
- Ensure all auth functions redirect to main page instead of admin
- Complete openspec change proposal for login feature
2025-11-24 10:37:23 +01:00
ccf31e4ef4 troubleshooting ingress 2025-11-21 19:14:22 +01:00
94e047c7ff Ingress troubleshooting
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 19s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
2025-11-21 16:39:32 +01:00
57f2210c77 Rootmismatch - redeploying
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 30s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
2025-11-21 16:20:37 +01:00
1745596d14 Deploy after Kubernetes fuckup
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 28s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
2025-11-20 15:32:24 +01:00
e923624aec Metadata moved to end of document 2025-11-19 13:49:45 +01:00
3649878b7d Ignore file maintenance 2025-11-18 11:36:06 +01:00
179e7d41b3 Merge pull request 'feature/oblique' (#10) from feature/oblique into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 3m19s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 8s
Reviewed-on: #10
2025-11-18 08:18:49 +00:00
31 changed files with 1582 additions and 73 deletions

View File

@@ -231,12 +231,51 @@ jobs:
if: steps.img.outputs.changed != 'true'
run: echo "${{ matrix.description }} image tag unchanged; skipping build."
- name: Set up Buildx
- name: Check if image exists on registry
if: steps.img.outputs.changed == 'true'
id: check_image
shell: bash
run: |
set -euo pipefail
new_repo="${{ steps.img.outputs.new_repo }}"
new_tag="${{ steps.img.outputs.new_tag }}"
registry_user="${{ secrets.REGISTRY_USER }}"
registry_password="${{ secrets.REGISTRY_PASSWORD }}"
# Extract registry host and image name
registry_host=$(echo "$new_repo" | cut -d/ -f1)
image_path=$(echo "$new_repo" | cut -d/ -f2-)
echo "Checking if $new_repo:$new_tag exists on registry $registry_host"
# Use Docker Registry API v2 to check manifest
# Format: https://registry/v2/{image_path}/manifests/{tag}
manifest_url="https://${registry_host}/v2/${image_path}/manifests/${new_tag}"
# Check with authentication
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-u "${registry_user}:${registry_password}" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json,application/vnd.docker.distribution.manifest.list.v2+json" \
"$manifest_url" || echo "000")
if [ "$http_code" = "200" ]; then
echo "Image already exists on registry (HTTP $http_code)"
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "Image does not exist on registry (HTTP $http_code)"
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Skip if image already exists
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'true'
run: echo "${{ matrix.description }} image ${{ steps.img.outputs.new_image }} already exists on registry; skipping build."
- name: Set up Buildx
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'false'
uses: docker/setup-buildx-action@v3
- name: Log in to registry
if: steps.img.outputs.changed == 'true'
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'false'
uses: docker/login-action@v3
with:
registry: ${{ steps.img.outputs.registry }}
@@ -244,7 +283,7 @@ jobs:
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push ${{ matrix.description }} (exact tag from deployment)
if: steps.img.outputs.changed == 'true'
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'false'
uses: docker/build-push-action@v6
with:
context: ${{ matrix.build_context }}

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ keys/
node_modules/
package-lock.json
package.json
AGENT*.md
# Diagram cache directory
media/diagram_cache/
.env

241
Documentation/ArgoCD.md Normal file
View File

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

544
Documentation/modelle.md Normal file
View File

@@ -0,0 +1,544 @@
# Alle Modelle der vgui-cicd Django-Anwendung
Dieses Dokument beschreibt alle Datenmodelle in der vgui-cicd Anwendung mit ihren Eigenschaften, Beziehungen und Verwendungszwecken.
---
## App: dokumente
Die Hauptmodelle für die Verwaltung von Dokumenten, Vorgaben und deren Metadaten.
### Dokumententyp
**Zweck**: Kategorisierung von Dokumenten (z. B. Richtlinie, Standard).
**Wichtige Felder**:
- `name` (CharField, max_length=100, **PRIMARY KEY**)
- `verantwortliche_ve` (CharField, max_length=255): Die verantwortliche Verwaltungseinheit
**Besonderheiten**:
- `__str__()` gibt den Namen zurück
- Dient als Klassifizierungskategorie für Dokumente
**Meta**:
- `verbose_name = "Dokumententyp"`
- `verbose_name_plural = "Dokumententypen"`
---
### Person
**Zweck**: Repräsentiert Personen, die als Autoren, Prüfer oder in anderen Rollen tätig sind.
**Wichtige Felder**:
- `name` (CharField, max_length=100, **PRIMARY KEY**)
- `funktion` (CharField, max_length=255): Funktionsbezeichnung der Person
**Beziehungen**:
- Many-to-Many mit `Dokument` über `verfasste_dokumente` (Autoren)
- Many-to-Many mit `Dokument` über `gepruefte_dokumente` (Prüfer)
**Besonderheiten**:
- `__str__()` gibt den Namen zurück
- `ordering = ['name']`: Alphabetische Sortierung
**Meta**:
- `verbose_name_plural = "Personen"`
---
### Thema
**Zweck**: Thematische Einordnung und Kategorisierung von Vorgaben innerhalb von Dokumenten.
**Wichtige Felder**:
- `name` (CharField, max_length=100, **PRIMARY KEY**)
- `erklaerung` (TextField, blank=True): Optionale Erklärung des Themas
**Besonderheiten**:
- `__str__()` gibt den Namen zurück
- Der erste Buchstabe des Themas wird in Vorgabennummern verwendet
**Meta**:
- `verbose_name_plural = "Themen"`
---
### Dokument
**Zweck**: Hauptmodell für ein einzelnes Dokument mit allen zugehörigen Metadaten und Inhalten.
**Wichtige Felder**:
- `nummer` (CharField, max_length=50, **PRIMARY KEY**): Eindeutige Dokumentennummer
- `dokumententyp` (ForeignKey → Dokumententyp, on_delete=PROTECT): Klassifizierung
- `name` (CharField, max_length=255): Dokumenttitel
- `autoren` (ManyToManyField → Person, related_name='verfasste_dokumente')
- `pruefende` (ManyToManyField → Person, related_name='gepruefte_dokumente')
- `gueltigkeit_von` (DateField, null=True, blank=True): Gültig ab Datum
- `gueltigkeit_bis` (DateField, null=True, blank=True): Gültig bis Datum
- `signatur_cso` (CharField, max_length=255, blank=True): CSO-Signatur
- `anhaenge` (TextField, blank=True): Beschreibung von Anhängen
- `aktiv` (BooleanField, blank=True): Aktivierungsstatus
**Beziehungen**:
- 1-to-Many mit `Vorgabe` (über related_name='vorgaben')
- 1-to-Many mit `Geltungsbereich`
- 1-to-Many mit `Einleitung`
- 1-to-Many mit `Changelog`
**Besonderheiten**:
- `__str__()` formatiert als "nummer name"
**Meta**:
- `verbose_name = "Dokument"`
- `verbose_name_plural = "Dokumente"`
---
### Vorgabe
**Zweck**: Repräsentiert eine einzelne Vorgabe oder Anforderung innerhalb eines Dokuments.
**Wichtige Felder**:
- `order` (IntegerField): Sortierreihenfolge für die Darstellung
- `nummer` (IntegerField): Nummer innerhalb eines Themas/Dokuments. Muss nicht eindeutig sein (z.B. für geänderte Vorgaben)
- `dokument` (ForeignKey → Dokument, on_delete=CASCADE, related_name='vorgaben')
- `thema` (ForeignKey → Thema, on_delete=PROTECT): Thematische Einordnung
- `titel` (CharField, max_length=255): Titel der Vorgabe
- `referenzen` (ManyToManyField → Referenz, blank=True): Verweise auf externe Referenzen
- `gueltigkeit_von` (DateField): Gültig ab Datum
- `gueltigkeit_bis` (DateField, blank=True, null=True): Gültig bis Datum (offen = unbegrenzt)
- `stichworte` (ManyToManyField → Stichwort, blank=True): Tags zur Kategorisierung
- `relevanz` (ManyToManyField → Rolle, blank=True): Relevante Rollen
**Beziehungen**:
- Foreign Key zu `Dokument` und `Thema`
- Many-to-Many zu `Referenz`, `Stichwort`, `Rolle`
- 1-to-Many zu `VorgabeLangtext`, `VorgabeKurztext`
- 1-to-Many zu `Checklistenfrage`
**Wichtige Methoden**:
- `Vorgabennummer()` → str
- Generiert eine eindeutige, lesbare Kennummer
- Format: "{dokument.nummer}.{thema.name[0]}.{nummer}"
- Beispiel: "R0066.A.1"
- `get_status(check_date=None, verbose=False)` → str
- Bestimmt den Status einer Vorgabe zu einem gegebenen Datum
- Parameter: `check_date` (Default: heute), `verbose` (Deutsche Beschreibung ja/nein)
- Rückgabewerte:
- "future": Vorgabe ist noch nicht gültig
- "active": Vorgabe ist aktuell gültig
- "expired": Vorgabe ist nicht mehr gültig
- Verbose-Ausgaben enthalten Datumsangaben
- `sanity_check_vorgaben()` (statisch) → list
- Findet zeitliche Konflikte zwischen Vorgaben mit gleicher Nummer/Thema/Dokument
- Überprüft, ob sich Geltungszeiträume überschneiden
- Gibt Liste mit Konflikt-Dictionaries zurück
- `clean()`
- Validiert die Vorgabe vor dem Speichern
- Ruft `find_conflicts()` auf
- Wirft `ValidationError` bei erkannten Konflikten
- `find_conflicts()` → list
- Findet Konflikte mit bestehenden Vorgaben (ausgenommen self)
- Überprüft auf zeitliche Überschneidungen
- Gibt Liste mit Konflikt-Details zurück
- `_date_ranges_intersect(start1, end1, start2, end2)` (statisch) → bool
- Prüft, ob zwei Datumsbereiche sich überschneiden
- `None` als Enddatum = unbegrenzter Bereich
- Gibt `True` bei Überschneidung zurück
**Besonderheiten**:
- `__str__()` gibt "Vorgabennummer: titel" zurück
- Validierung von Gültigkeitszeiträumen ist implementiert
- Sehr wichtiges Modell im Geschäftslogik-Kontext
**Meta**:
- `ordering = ['order']`
- `verbose_name_plural = "Vorgaben"`
---
### VorgabeLangtext
**Zweck**: Speichert ausführliche Textinhalte (Langtext) einer Vorgabe.
**Wichtige Felder**:
- `abschnitt` (ForeignKey → Vorgabe, on_delete=CASCADE): Referenz zur Vorgabe
- Erbt von `Textabschnitt` (siehe App: abschnitte):
- `abschnitttyp` (ForeignKey → AbschnittTyp, optional)
- `inhalt` (TextField, blank=True, null=True)
- `order` (PositiveIntegerField, default=0)
**Meta**:
- `verbose_name = "Langtext-Abschnitt"`
- `verbose_name_plural = "Langtext"`
---
### VorgabeKurztext
**Zweck**: Speichert kurze Textinhalte (Kurztext) einer Vorgabe.
**Wichtige Felder**:
- `abschnitt` (ForeignKey → Vorgabe, on_delete=CASCADE): Referenz zur Vorgabe
- Erbt von `Textabschnitt` (siehe App: abschnitte):
- `abschnitttyp` (ForeignKey → AbschnittTyp, optional)
- `inhalt` (TextField, blank=True, null=True)
- `order` (PositiveIntegerField, default=0)
**Meta**:
- `verbose_name = "Kurztext-Abschnitt"`
- `verbose_name_plural = "Kurztext"`
---
### Geltungsbereich
**Zweck**: Speichert den Geltungsbereich-Abschnitt eines Dokuments.
**Wichtige Felder**:
- `geltungsbereich` (ForeignKey → Dokument, on_delete=CASCADE): Referenz zum Dokument
- Erbt von `Textabschnitt` (siehe App: abschnitte):
- `abschnitttyp` (ForeignKey → AbschnittTyp, optional)
- `inhalt` (TextField, blank=True, null=True)
- `order` (PositiveIntegerField, default=0)
**Meta**:
- `verbose_name = "Geltungsbereichs-Abschnitt"`
- `verbose_name_plural = "Geltungsbereich"`
---
### Einleitung
**Zweck**: Speichert die Einleitungs-Abschnitte eines Dokuments.
**Wichtige Felder**:
- `einleitung` (ForeignKey → Dokument, on_delete=CASCADE): Referenz zum Dokument
- Erbt von `Textabschnitt` (siehe App: abschnitte):
- `abschnitttyp` (ForeignKey → AbschnittTyp, optional)
- `inhalt` (TextField, blank=True, null=True)
- `order` (PositiveIntegerField, default=0)
**Meta**:
- `verbose_name = "Einleitungs-Abschnitt"`
- `verbose_name_plural = "Einleitung"`
---
### Checklistenfrage
**Zweck**: Repräsentiert eine Frage für die Checkliste zu einer Vorgabe.
**Wichtige Felder**:
- `vorgabe` (ForeignKey → Vorgabe, on_delete=CASCADE, related_name='checklistenfragen')
- `frage` (CharField, max_length=255): Text der Checklistenfrage
**Besonderheiten**:
- `__str__()` gibt den Fragetext zurück
**Meta**:
- `verbose_name = "Frage für Checkliste"`
- `verbose_name_plural = "Fragen für Checkliste"`
---
### VorgabenTable
**Zweck**: Proxy-Modell für `Vorgabe` für die Darstellung von Vorgaben in Tabellenform.
**Besonderheiten**:
- Proxy-Modell (kein eigenes Datenbankschema)
- Ermöglicht alternative Django-Admin-Ansicht
- Erbt alle Felder und Methoden von `Vorgabe`
**Meta**:
- `proxy = True`
- `verbose_name = "Vorgabe (Tabellenansicht)"`
- `verbose_name_plural = "Vorgaben (Tabellenansicht)"`
---
### Changelog
**Zweck**: Dokumentiert Änderungen und Versionshistorie für Dokumente.
**Wichtige Felder**:
- `dokument` (ForeignKey → Dokument, on_delete=CASCADE, related_name='changelog'): Referenz zum Dokument
- `autoren` (ManyToManyField → Person): Personen, die die Änderung vorgenommen haben
- `datum` (DateField): Datum der Änderung
- `aenderung` (TextField): Beschreibung der Änderung
**Beziehungen**:
- Foreign Key zu `Dokument`
- Many-to-Many zu `Person`
**Besonderheiten**:
- `__str__()` formatiert als "datum dokumentnummer"
**Meta**:
- `verbose_name = "Changelog-Eintrag"`
- `verbose_name_plural = "Changelog"`
---
## App: abschnitte
Modelle für die Verwaltung von Textabschnitten, die von mehreren Modellen geerbt werden.
### AbschnittTyp
**Zweck**: Klassifizierung von Textabschnitten (z. B. "Beschreibung", "Erklärung", "Anleitung").
**Wichtige Felder**:
- `abschnitttyp` (CharField, max_length=100, **PRIMARY KEY**): Name des Abschnitttyps
**Besonderheiten**:
- `__str__()` gibt den Namen zurück
**Meta**:
- `verbose_name_plural = "Abschnitttypen"`
---
### Textabschnitt (abstrakt)
**Zweck**: Abstrakte Basisklasse für Textinhalte, die mit anderen Modellen verknüpft sind.
**Wichtige Felder**:
- `abschnitttyp` (ForeignKey → AbschnittTyp, on_delete=PROTECT, optional)
- `inhalt` (TextField, blank=True, null=True): Der Textinhalt
- `order` (PositiveIntegerField, default=0): Sortierreihenfolge
**Besonderheiten**:
- Abstrakte Klasse (wird nicht direkt in der Datenbank gespeichert)
- Wird von anderen Modellen geerbt: `VorgabeLangtext`, `VorgabeKurztext`, `Geltungsbereich`, `Einleitung`, `Referenzerklaerung`, `Stichworterklaerung`, `RollenBeschreibung`
**Meta**:
- `abstract = True`
- `verbose_name = "Abschnitt"`
- `verbose_name_plural = "Abschnitte"`
---
## App: referenzen
Modelle für die Verwaltung von Referenzen und Verweisen auf externe Standards.
### Referenz (MPTT-Tree)
**Zweck**: Hierarchische Verwaltung von Referenzen und externen Normen (z. B. ISO-Standards, Gesetze, übergeordnete Vorgaben).
**Wichtige Felder**:
- `id` (AutoField, **PRIMARY KEY**)
- `name_nummer` (CharField, max_length=100): Nummer/Kennung der Referenz (z. B. "ISO 27001")
- `name_text` (CharField, max_length=255, blank=True): Ausführlicher Name/Beschreibung
- `oberreferenz` (TreeForeignKey zu self, optional): Parent-Referenz für Hierarchien
- `url` (URLField, blank=True): Link zur Referenz
**Beziehungen**:
- Many-to-Many mit `Vorgabe`
- MPTT Tree-Struktur für hierarchische Referenzen
**Wichtige Methoden**:
- `Path()` → str
- Gibt die vollständige Pfad-Hierarchie als String zurück
- Format: "Referenz → Subreferenz → Unterreferenz (Beschreibung)"
- Beispiel: "ISO → 27000 → 27001 (Information Security Management)"
**Besonderheiten**:
- Verwendet MPPT (Modified Preorder Tree Traversal) für Baumoperationen
- `get_ancestors(include_self=True)`: Gibt alle Vorfahren zurück
- `unterreferenzen`: Related_name für Kindreferenzen
- Sortierung: Nach `name_nummer`
**Meta**:
- `verbose_name_plural = "Referenzen"`
- **MPTTMeta**:
- `parent_attr = 'oberreferenz'`
- `order_insertion_by = ['name_nummer']`
---
### Referenzerklaerung
**Zweck**: Speichert Erklärungen und zusätzliche Informationen zu einer Referenz.
**Wichtige Felder**:
- `erklaerung` (ForeignKey → Referenz, on_delete=CASCADE): Referenz zur Referenz
- Erbt von `Textabschnitt`:
- `abschnitttyp` (ForeignKey → AbschnittTyp, optional)
- `inhalt` (TextField, blank=True, null=True)
- `order` (PositiveIntegerField, default=0)
**Meta**:
- `verbose_name = "Erklärung"`
- `verbose_name_plural = "Erklärungen"`
---
## App: stichworte
Modelle für die Verwaltung von Stichworte und Tags.
### Stichwort
**Zweck**: Einfache Tag/Keyword-Modell zur Kategorisierung von Vorgaben.
**Wichtige Felder**:
- `stichwort` (CharField, max_length=50, **PRIMARY KEY**): Das Stichwort
**Beziehungen**:
- Many-to-Many mit `Vorgabe`
**Besonderheiten**:
- `__str__()` gibt das Stichwort zurück
**Meta**:
- `verbose_name_plural = "Stichworte"`
---
### Stichworterklaerung
**Zweck**: Speichert Erklärungen zu Stichworten.
**Wichtige Felder**:
- `erklaerung` (ForeignKey → Stichwort, on_delete=CASCADE): Referenz zum Stichwort
- Erbt von `Textabschnitt`:
- `abschnitttyp` (ForeignKey → AbschnittTyp, optional)
- `inhalt` (TextField, blank=True, null=True)
- `order` (PositiveIntegerField, default=0)
**Meta**:
- `verbose_name = "Erklärung"`
- `verbose_name_plural = "Erklärungen"`
---
## App: rollen
Modelle für die Verwaltung von Rollen und deren Beschreibungen.
### Rolle
**Zweck**: Definiert Rollen/Positionen im Unternehmen (z. B. "Geschäftsleiter", "IT-Sicherheit", "Datenschutzbeauftragter").
**Wichtige Felder**:
- `name` (CharField, max_length=100, **PRIMARY KEY**): Name der Rolle
**Beziehungen**:
- Many-to-Many mit `Vorgabe` (über `relevanz`)
**Besonderheiten**:
- `__str__()` gibt den Namen zurück
- Wird verwendet, um Rollen zu markieren, die von einer Vorgabe betroffen sind
**Meta**:
- `verbose_name_plural = "Rollen"`
---
### RollenBeschreibung
**Zweck**: Speichert detaillierte Beschreibungen und Informationen zu einer Rolle.
**Wichtige Felder**:
- `abschnitt` (ForeignKey → Rolle, on_delete=CASCADE): Referenz zur Rolle
- Erbt von `Textabschnitt`:
- `abschnitttyp` (ForeignKey → AbschnittTyp, optional)
- `inhalt` (TextField, blank=True, null=True)
- `order` (PositiveIntegerField, default=0)
**Meta**:
- `verbose_name = "Rollenbeschreibungs-Abschnitt"`
- `verbose_name_plural = "Rollenbeschreibung"`
---
## Allgemeine Hinweise zur Modellverwaltung
### Primärschlüssel-Strategie
- Viele Modelle verwenden CharField-basierte Primärschlüssel (`name`, `nummer`, `stichwort`)
- Dies ermöglicht direkte Verwendung von Strings als Identifikatoren
- Vorteil: Lesbarkeit; Nachteil: Umbenennungen sind kritisch
### On-Delete-Strategien
- **PROTECT**: Verwendet für wichtige Beziehungen (z. B. Dokumententyp, Thema, AbschnittTyp)
- Verhindert versehentliches Löschen von Daten, auf die verwiesen wird
- **CASCADE**: Verwendet für Unterkomponenten (z. B. Vorgabe → Dokument)
- Löscht abhängige Datensätze automatisch
- **SET_NULL**: Nur bei optionalen Referenzen (z. B. Oberreferenz in Referenz-Tree)
### Validierungsmechanismen
- **Vorgabe.clean()**: Validiert Gültigkeitszeiträume
- **Vorgabe.find_conflicts()**: Prüft zeitliche Überschneidungen
- Wird von Django-Admin automatisch aufgerufen vor dem Speichern
### MPTT (Modified Preorder Tree Traversal)
- Verwendet in `Referenz` für hierarchische Strukturen
- Ermöglicht effiziente Abfragen von Vorfahren und Nachkommen
- Zusätzliche Datenbank-Felder für Tree-Management (automatisch verwaltet)
### Textabschnitt-Vererbung
- Mehrere Modelle erben von `Textabschnitt`
- Wird verwendet für Lang-/Kurztexte, Erklärungen, Beschreibungen
- `order`-Feld ermöglicht Sortierung mehrerer Abschnitte
### Datumsverwaltung
- `gueltigkeit_von`: Immer erforderlich für Vorgaben
- `gueltigkeit_bis`: Optional; `None` bedeutet unbegrenzte Gültigkeit
- `_date_ranges_intersect()` prüft korrekt auf Überschneidungen mit None-Werten
### Many-to-Many-Beziehungen
- Vielfach verwendet für flexible Zuordnungen (Autoren, Stichworte, Rollen, Referenzen)
- `related_name`-Attribute ermöglichen rückwärts Zugriff
- Beispiel: `dokument.vorgaben.all()`, `person.verfasste_dokumente.all()`
---
## Zusammenfassung der Beziehungen
```
Dokumententyp ← Dokument
Person ← Dokument (Autoren/Prüfer)
Dokument → Vorgabe (1-to-Many)
Dokument → Geltungsbereich (1-to-Many)
Dokument → Einleitung (1-to-Many)
Dokument → Changelog (1-to-Many)
Thema ← Vorgabe
Vorgabe → VorgabeLangtext (1-to-Many)
Vorgabe → VorgabeKurztext (1-to-Many)
Vorgabe → Checklistenfrage (1-to-Many)
Vorgabe ← Referenz (Many-to-Many)
Vorgabe ← Stichwort (Many-to-Many)
Vorgabe ← Rolle (Many-to-Many)
Referenz → Referenz (Hierarchie via MPPT)
Referenz → Referenzerklaerung (1-to-Many)
Stichwort → Stichworterklaerung (1-to-Many)
Rolle → RollenBeschreibung (1-to-Many)
AbschnittTyp ← Textabschnitt (von verschiedenen Modellen geerbt)
```
---
## Entwicklungsrichtlinien
- Alle Modelle sollten aussagekräftige `__str__()`-Methoden haben
- `verbose_name` und `verbose_name_plural` sollten auf Deutsch sein (für Django-Admin)
- Validierungslogik (z. B. `clean()`) sollte implementiert werden für komplexe Business-Logic
- Related-Names sollten aussagekräftig und konsistent sein
- Textinhalte sollten die `Textabschnitt`-Basisklasse erben
- Datumsverwaltung: Immer auf None-Werte bei `gueltigkeit_bis` achten, wenn Vorgaben noch aktiv sind.

View File

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

View File

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

View File

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

View File

@@ -28,12 +28,6 @@ DEBUG = True
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
TEMPLATES = [
{"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
}
]
# Application definition
INSTALLED_APPS = [
@@ -152,6 +146,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DATA_UPLOAD_MAX_NUMBER_FIELDS=10250
NESTED_ADMIN_LAZY_INLINES = True
# Authentication settings
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = 'login'
#LOGGING = {
# "version": 1,
# "handlers" :{

View File

@@ -18,6 +18,7 @@ from django.contrib import admin
from django.urls import include, path, re_path
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth import views as auth_views
import dokumente.views
import pages.views
import referenzen.views
@@ -32,6 +33,11 @@ urlpatterns = [
path('stichworte/', include("stichworte.urls")),
path('referenzen/', referenzen.views.tree, name="referenz_tree"),
path('referenzen/<str:refid>/', referenzen.views.detail, name="referenz_detail"),
# Authentication URLs
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
path('password_change/', auth_views.PasswordChangeView.as_view(template_name='registration/password_change.html', success_url='/'), name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(template_name='registration/password_change_done.html'), name='password_change_done'),
]
# Serve static files

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ spec:
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.952-oblique
image: git.baumann.gr/adebaumann/vui:0.960
imagePullPolicy: Always
ports:
- containerPort: 8000
@@ -63,6 +63,8 @@ spec:
selector:
app: django
ports:
- port: 8000
- name: http
protocol: TCP
port: 8000
targetPort: 8000

View File

@@ -4,8 +4,9 @@ metadata:
name: django
namespace: vorgabenui
annotations:
traefik.ingress.kubernetes.io/router.middlewares: "vorgabenui-vorgabenui-rewrite@kubernetescrd"
argocd.argoproj.io/ignore-healthcheck: "true"
spec:
ingressClassName: traefik
rules:
- host: vorgabenportal.knowyoursecurity.com
http:

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

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

View File

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

View File

@@ -1,9 +0,0 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: vorgabenui-rewrite
namespace: vorgabenui
spec:
stripPrefix:
prefixes:
- "/"

Binary file not shown.

View File

@@ -19,37 +19,13 @@
<strong>Historische Version vom {{ standard.check_date }}</strong>
</div>
{% endif %}
<!-- Metadata -->
<div class="row mb-4">
<div class="col-md-12">
<dl class="row">
<dt class="col-sm-3">Autoren:</dt>
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
<dt class="col-sm-3">Prüfende:</dt>
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
<dt class="col-sm-3">Gültigkeit:</dt>
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
</dl>
<p>
<a href="{% url 'standard_json' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.json">
JSON herunterladen
</a>
</p>
</div>
</div>
<!-- Einleitung -->
{% if standard.einleitung_html %}
<div class="row mb-4">
<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 %}
@@ -67,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 %}
@@ -97,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>
@@ -147,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 %}
@@ -175,5 +151,29 @@
</div>
{% endif %}
{% endfor %}
<!-- Metadata -->
<h2>Metadaten</h2>
<div class="row mb-4">
<div class="col-md-12">
<dl class="row">
<dt class="col-sm-3">Autoren:</dt>
<dd class="col-sm-9">{{ standard.autoren.all|join:", " }}</dd>
<dt class="col-sm-3">Prüfende:</dt>
<dd class="col-sm-9">{{ standard.pruefende.all|join:", " }}</dd>
<dt class="col-sm-3">Gültigkeit:</dt>
<dd class="col-sm-9">{{ standard.gueltigkeit_von }} bis {{ standard.gueltigkeit_bis|default_if_none:"auf weiteres" }}</dd>
</dl>
<p>
<a href="{% url 'standard_json' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.json">
JSON herunterladen
</a>
</p>
</div>
</div>
</div>
{% endblock %}

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

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
## 1. Add user login functionality
- [x] add a login screen for users
- [x] add an icon for logged in user on the top right corner of all page
- [x] add a menu to log out and change password on the user icon
- [x] all functions should go back to the main page, not the django admin page

63
openspec/project.md Normal file
View File

@@ -0,0 +1,63 @@
# Project Context
## Purpose
This is a Django-based document management system for regulatory documents (Dokumente) and their provisions (Vorgaben). It manages validity periods, conflicts between overlapping provisions, references, keywords, and roles. The system supports importing documents, checking for compliance, and maintaining changelogs.
## Tech Stack
- Python 3.x
- Django 5.2.5
- SQLite (development), PostgreSQL (production)
- Django MPTT for tree structures
- Django Nested Admin for inline editing
- Kubernetes for deployment
- ArgoCD for continuous deployment
- Traefik for ingress
- Gunicorn for WSGI server
## Project Conventions
### Code Style
- Language: German for user-facing strings and model names, English for code comments and internal naming
- Imports: Standard library first, then Django, then third-party, then local apps
- Model naming: German nouns (Dokument, Vorgabe, Person)
- Field naming: German for field names, English Django conventions
- Class naming: PascalCase for models, snake_case for functions/variables
- All models have __str__ methods returning meaningful German strings
- Use verbose_name and verbose_name_plural in Meta classes (German)
### Architecture Patterns
- Django apps: abschnitte, dokumente, referenzen, rollen, stichworte, pages
- MPTT for hierarchical text sections
- Foreign keys with on_delete=models.PROTECT for important relationships
- Many-to-many with descriptive related_name
- Proxy models for different views (e.g., VorgabenTable)
- Management commands for data operations
### Testing Strategy
- Django test framework
- Test class names in English, methods in English
- Comprehensive model tests
- Test both success and error cases
- Run with `python manage.py test`
### Git Workflow
- Standard Git workflow
- Commits in English
- Use Gitea workflows for CI/CD
## Domain Context
The system manages regulatory documents with numbered provisions that have validity dates. Provisions can conflict if they have overlapping date ranges for the same document, theme, and number. The system includes sanity checks for conflicts, diagram caching for visualization, and JSON export functionality.
## Important Constraints
- German language for all user interfaces and data
- Strict validation of date ranges to prevent overlapping provisions
- Documents have types, authors, reviewers, and validity periods
- Provisions linked to themes, references, keywords, and relevant roles
- Active/inactive status for documents
## External Dependencies
- Django ecosystem: MPTT, nested-admin, revproxy
- Kubernetes cluster for deployment
- ArgoCD for GitOps
- Traefik for load balancing
- External diagram services (diagramm_proxy)

View File

@@ -41,6 +41,37 @@
alt="Zur Startseite" />
<h1>Vorgaben Informatiksicherheit BIT</h1>
</a>
<!-- User Menu -->
{% if user.is_authenticated %}
<div class="user-menu" style="position: absolute; top: 20px; right: 20px; z-index: 1000;">
<div class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" style="text-decoration: none; color: #000; display: flex; align-items: center;">
<span style="font-size: 24px; margin-right: 8px;">👤</span>
<span class="hidden-xs" style="margin-left: 0;">{{ user.first_name }} {{ user.last_name }}</span>
<span class="caret" style="margin-left: 8px;"></span>
</a>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a href="{% url 'password_change' %}">Passwort ändern</a></li>
<li class="divider"></li>
<li>
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" style="background: none; border: none; color: inherit; padding: 3px 20px; width: 100%; text-align: left; cursor: pointer;">
Abmelden
</button>
</form>
</li>
</ul>
</div>
</div>
{% else %}
<div class="user-menu" style="position: absolute; top: 20px; right: 20px; z-index: 1000;">
<a href="{% url 'login' %}" class="btn btn-sm btn-primary" style="text-decoration: none;">
Anmelden
</a>
</div>
{% endif %}
</header>
<!-- Main Navigation -->
@@ -71,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>
@@ -100,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>
@@ -180,7 +215,7 @@
</p>
</div>
<div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.951" }}</p>
<p class="text-muted">Version {{ version|default:"0.958" }}</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Anmelden{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Anmelden</h3>
</div>
<div class="panel-body">
<form method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
<p>Ihr Benutzername und Passwort stimmen nicht überein. Bitte versuchen Sie es erneut.</p>
</div>
{% endif %}
<div class="form-group">
<label for="id_username">Benutzername:</label>
<input type="text" name="username" class="form-control" id="id_username" required autofocus>
</div>
<div class="form-group">
<label for="id_password">Passwort:</label>
<input type="password" name="password" class="form-control" id="id_password" required>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Anmelden</button>
</div>
<input type="hidden" name="next" value="{{ next }}">
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Passwort ändern{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Passwort ändern</h3>
</div>
<div class="panel-body">
<form method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger">
<p>Bitte korrigieren Sie die Fehler unten.</p>
</div>
{% endif %}
<div class="form-group">
<label for="id_old_password">Aktuelles Passwort:</label>
<input type="password" name="old_password" class="form-control" id="id_old_password" required>
{% if form.old_password.errors %}
<div class="text-danger">{{ form.old_password.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="id_new_password1">Neues Passwort:</label>
<input type="password" name="new_password1" class="form-control" id="id_new_password1" required>
{% if form.new_password1.errors %}
<div class="text-danger">{{ form.new_password1.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="id_new_password2">Neues Passwort bestätigen:</label>
<input type="password" name="new_password2" class="form-control" id="id_new_password2" required>
{% if form.new_password2.errors %}
<div class="text-danger">{{ form.new_password2.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Passwort ändern</button>
<a href="/" class="btn btn-default">Abbrechen</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Passwort geändert{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Passwort erfolgreich geändert</h3>
</div>
<div class="panel-body">
<div class="alert alert-success">
<p>Ihr Passwort wurde erfolgreich geändert.</p>
</div>
<p>
<a href="/" class="btn btn-primary">Zurück zur Startseite</a>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

266
pages/test_auth.py Normal file
View File

@@ -0,0 +1,266 @@
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from django.core.exceptions import ValidationError
class AuthenticationTest(TestCase):
"""Test login, logout, and password change functionality"""
def setUp(self):
self.client = Client()
self.test_user = User.objects.create_user(
username='testuser',
password='testpass123',
email='test@example.com'
)
self.staff_user = User.objects.create_user(
username='staffuser',
password='staffpass123',
email='staff@example.com'
)
self.staff_user.is_staff = True
self.staff_user.save()
def test_login_page_loads(self):
"""Test that login page loads correctly"""
response = self.client.get(reverse('login'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Anmelden')
self.assertContains(response, 'Benutzername:')
self.assertContains(response, 'Passwort:')
def test_login_valid_credentials(self):
"""Test successful login with valid credentials"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'testpass123'
})
self.assertEqual(response.status_code, 302) # Redirect after login
self.assertRedirects(response, '/') # Should redirect to main page
# Check user is logged in
response = self.client.get('/')
self.assertContains(response, 'testuser') # Username should appear in header
def test_login_invalid_credentials(self):
"""Test login with invalid credentials shows error"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'wrongpassword'
})
self.assertEqual(response.status_code, 200) # Stay on login page
self.assertContains(response, 'Ihr Benutzername und Passwort stimmen nicht überein')
def test_login_empty_credentials(self):
"""Test login with empty credentials"""
response = self.client.post(reverse('login'), {
'username': '',
'password': ''
})
self.assertEqual(response.status_code, 200) # Stay on login page
# Django's form validation should handle this
def test_logout_functionality(self):
"""Test logout functionality"""
# First login
self.client.login(username='testuser', password='testpass123')
# Verify user is logged in
response = self.client.get('/')
self.assertContains(response, 'testuser')
# Logout using POST
response = self.client.post(reverse('logout'))
self.assertEqual(response.status_code, 302) # Redirect after logout
self.assertRedirects(response, '/') # Should redirect to main page
# Verify user is logged out
response = self.client.get('/')
self.assertNotContains(response, 'testuser')
self.assertContains(response, 'Anmelden') # Should show login link
def test_logout_requires_post(self):
"""Test that logout requires POST method"""
# Login first
self.client.login(username='testuser', password='testpass123')
# Try GET logout (should fail with 405)
response = self.client.get(reverse('logout'))
self.assertEqual(response.status_code, 405) # Method Not Allowed
# User should still be logged in
response = self.client.get('/')
self.assertContains(response, 'testuser')
def test_password_change_page_loads(self):
"""Test that password change page loads for authenticated users"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get(reverse('password_change'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Passwort ändern')
self.assertContains(response, 'Aktuelles Passwort:')
self.assertContains(response, 'Neues Passwort:')
self.assertContains(response, 'Neues Passwort bestätigen:')
def test_password_change_requires_authentication(self):
"""Test that password change page requires authentication"""
response = self.client.get(reverse('password_change'))
self.assertEqual(response.status_code, 302) # Redirect to login
# Should redirect to login page
self.assertIn(reverse('login'), response.url)
def test_password_change_valid(self):
"""Test successful password change"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'newpass456',
'new_password2': 'newpass456'
})
self.assertEqual(response.status_code, 302) # Redirect after success
self.assertRedirects(response, '/') # Should redirect to main page
# Verify new password works
self.client.logout()
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'newpass456'
})
self.assertEqual(response.status_code, 302) # Successful login
def test_password_change_wrong_old_password(self):
"""Test password change with wrong old password"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'wrongpassword',
'new_password1': 'newpass456',
'new_password2': 'newpass456'
})
self.assertEqual(response.status_code, 200) # Stay on form page
self.assertContains(response, 'Bitte korrigieren Sie die Fehler unten')
def test_password_change_mismatched_new_passwords(self):
"""Test password change with mismatched new passwords"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'newpass456',
'new_password2': 'differentpass789'
})
self.assertEqual(response.status_code, 200) # Stay on form page
self.assertContains(response, 'Bitte korrigieren Sie die Fehler unten')
def test_password_change_same_as_old_password(self):
"""Test password change with same password as old"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'testpass123',
'new_password2': 'testpass123'
})
# Django's default validators don't prevent same password, so it should succeed
self.assertEqual(response.status_code, 302) # Redirect after success
self.assertRedirects(response, '/') # Should redirect to main page
def test_password_change_cancel_button(self):
"""Test password change cancel button"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get(reverse('password_change'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Abbrechen')
# The cancel button should link to main page
self.assertContains(response, 'href="/"')
def test_user_menu_display_for_authenticated_user(self):
"""Test that user menu displays correctly for authenticated users"""
self.client.login(username='testuser', password='testpass123')
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'testuser') # Username in menu
self.assertContains(response, 'Passwort ändern') # Password change link
self.assertContains(response, 'Abmelden') # Logout link
self.assertNotContains(response, 'Anmelden') # Should not show login link
def test_login_link_display_for_anonymous_user(self):
"""Test that login link displays for anonymous users"""
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Anmelden') # Should show login link
self.assertNotContains(response, 'testuser') # Should not show username
self.assertNotContains(response, 'Passwort ändern') # Should not show password change
self.assertNotContains(response, 'Abmelden') # Should not show logout
def test_staff_user_menu(self):
"""Test that staff users see appropriate menu"""
self.client.login(username='staffuser', password='staffpass123')
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'staffuser') # Username in menu
self.assertContains(response, 'Passwort ändern') # Password change link
self.assertContains(response, 'Abmelden') # Logout link
def test_login_redirect_to_main_page(self):
"""Test that successful login redirects to main page"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'testpass123'
}, follow=True)
self.assertEqual(response.status_code, 200)
# Should end up on main page
self.assertContains(response, 'Vorgaben Informatiksicherheit')
def test_logout_redirect_to_main_page(self):
"""Test that logout redirects to main page"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('logout'), follow=True)
self.assertEqual(response.status_code, 200)
# Should end up on main page
self.assertContains(response, 'Vorgaben Informatiksicherheit')
# Should show login link for anonymous users
self.assertContains(response, 'Anmelden')
def test_password_change_redirect_to_main_page(self):
"""Test that successful password change redirects to main page"""
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('password_change'), {
'old_password': 'testpass123',
'new_password1': 'newpass456',
'new_password2': 'newpass456'
}, follow=True)
self.assertEqual(response.status_code, 200)
# Should end up on main page
self.assertContains(response, 'Vorgaben Informatiksicherheit')
def test_csrf_token_present_in_forms(self):
"""Test that CSRF tokens are present in authentication forms"""
# Login form
response = self.client.get(reverse('login'))
self.assertContains(response, 'csrfmiddlewaretoken')
# Password change form
self.client.login(username='testuser', password='testpass123')
response = self.client.get(reverse('password_change'))
self.assertContains(response, 'csrfmiddlewaretoken')
def test_login_with_next_parameter(self):
"""Test login with next parameter for redirect"""
response = self.client.post(reverse('login'), {
'username': 'testuser',
'password': 'testpass123',
'next': '/dokumente/'
})
self.assertEqual(response.status_code, 302)
# Should redirect to the specified next page
self.assertRedirects(response, '/dokumente/')

View File

@@ -4,7 +4,6 @@ from django.utils import timezone
from datetime import date, timedelta
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Geltungsbereich, Dokumententyp, Thema
from stichworte.models import Stichwort
from unittest.mock import patch
import re
@@ -67,24 +66,24 @@ class SearchViewTest(TestCase):
"""Test POST request with valid search term"""
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für Test')
self.assertContains(response, 'Suchergebnisse')
def test_search_case_insensitive(self):
"""Test that search is case insensitive"""
# Search for lowercase
response = self.client.post('/search/', {'q': 'test'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für test')
self.assertContains(response, 'Suchergebnisse für "test"')
# Search for uppercase
response = self.client.post('/search/', {'q': 'TEST'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für TEST')
self.assertContains(response, 'Suchergebnisse für "TEST"')
# Search for mixed case
response = self.client.post('/search/', {'q': 'TeSt'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Suchresultate für TeSt')
self.assertContains(response, 'Suchergebnisse für "TeSt"')
def test_search_in_kurztext(self):
"""Test search in Kurztext content"""
@@ -114,7 +113,7 @@ class SearchViewTest(TestCase):
"""Test search with no results"""
response = self.client.post('/search/', {'q': 'NichtVorhanden'})
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Keine Resultate für "NichtVorhanden"')
self.assertContains(response, 'Keine Ergebnisse gefunden')
def test_search_expired_vorgabe_not_included(self):
"""Test that expired Vorgaben are not included in results"""
@@ -160,8 +159,8 @@ class SearchViewTest(TestCase):
"""Test that HTML tags are stripped from search input"""
response = self.client.post('/search/', {'q': '<script>alert("xss")</script>Test'})
self.assertEqual(response.status_code, 200)
# Should search for "alert('xss')Test" after HTML tag removal
self.assertContains(response, 'Suchresultate für alert(&quot;xss&quot;)Test')
# Should search for "alert("xss")Test" after HTML tag removal
self.assertContains(response, 'Suchergebnisse für "alert')
def test_search_invalid_characters_validation(self):
"""Test validation for invalid characters"""
@@ -206,7 +205,7 @@ class SearchViewTest(TestCase):
self.assertEqual(response.status_code, 200)
# The input should be preserved (escaped) in the form
# Since HTML tags are stripped, we expect "Test" to be searched
self.assertContains(response, 'Suchresultate für Test')
self.assertContains(response, 'Suchergebnisse für "Test"')
def test_search_xss_prevention_in_results(self):
"""Test that search terms are escaped in results to prevent XSS"""
@@ -218,15 +217,14 @@ class SearchViewTest(TestCase):
self.assertEqual(response.status_code, 200)
# The script tag should be escaped in the output
# Note: This depends on how the template renders the content
self.assertContains(response, 'Suchresultate für term')
self.assertContains(response, 'Suchergebnisse für "term"')
@patch('pages.views.pprint.pp')
def test_search_result_logging(self, mock_pprint):
"""Test that search results are logged for debugging"""
def test_search_result_structure(self):
"""Test that search results have expected structure"""
response = self.client.post('/search/', {'q': 'Test'})
self.assertEqual(response.status_code, 200)
# Verify that pprint.pp was called with the result
mock_pprint.assert_called_once()
# Verify the results page is rendered with correct structure
self.assertContains(response, 'Suchergebnisse für "Test"')
def test_search_multiple_documents(self):
"""Test search across multiple documents"""

View File

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