Compare commits
14 Commits
feature/im
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
6c2c76859b
|
|||
|
44de29a9da
|
|||
|
e39a65d5b2
|
|||
|
137ed9d1a0
|
|||
|
3faa88fcea
|
|||
|
67a967da67
|
|||
|
6af0b02442
|
|||
|
f7a20648b2
|
|||
|
bae8c71028
|
|||
|
a0495fdea0
|
|||
|
c125238f12
|
|||
| f5c0d9beac | |||
|
70752a2482
|
|||
|
633ecabdb9
|
@@ -1,67 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
# branches:
|
||||
# - main
|
||||
# - development
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: SonarQube Scan
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: SonarQube Trigger
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checking out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
coverage run --source='.' manage.py test
|
||||
coverage xml
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Cache SonarQube packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.sonar/cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
|
||||
- name: Download and setup SonarScanner
|
||||
run: |
|
||||
mkdir -p $HOME/.sonar
|
||||
wget -q https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip
|
||||
unzip -q sonar-scanner-cli-5.0.1.3006-linux.zip -d $HOME/.sonar/
|
||||
echo "$HOME/.sonar/sonar-scanner-5.0.1.3006-linux/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Verify Java version
|
||||
run: java -version
|
||||
|
||||
- name: SonarQube Scan
|
||||
env:
|
||||
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }}
|
||||
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
|
||||
run: |
|
||||
sonar-scanner \
|
||||
-Dsonar.projectKey=${{ github.event.repository.name }} \
|
||||
-Dsonar.sources=. \
|
||||
-Dsonar.host.url=${SONAR_HOST_URL} \
|
||||
-Dsonar.token=${SONAR_TOKEN} \
|
||||
-Dsonar.python.coverage.reportPaths=coverage.xml
|
||||
@@ -12,7 +12,11 @@ RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM python:3.15.0a5-slim-trixie
|
||||
RUN useradd -m -r -u 99 appuser && \
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends libxslt1.1 libxml2 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
useradd -m -r -u 99 appuser && \
|
||||
mkdir /app && \
|
||||
chown -R appuser /app
|
||||
|
||||
|
||||
@@ -13,21 +13,29 @@ This directory contains the ArgoCD application manifests for deploying the Vorga
|
||||
- **Storage Class**: Uses NFS storage class for shared storage across multiple pods
|
||||
- **Namespace**: vorgabenui
|
||||
|
||||
#### `configmap.yaml`
|
||||
- **Purpose**: Django application configuration
|
||||
- **Contains**: Environment variables, application settings, version information
|
||||
- **Namespace**: vorgabenui
|
||||
- **Version**: 0.990
|
||||
|
||||
#### `deployment.yaml`
|
||||
- **Purpose**: Main application deployment configuration
|
||||
- **Contains**: Django application container, environment variables, resource limits
|
||||
- **Replicas**: Configurable replica count for high availability
|
||||
- **Version**: 0.990
|
||||
|
||||
#### `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
|
||||
- **Ingress Class**: traefik
|
||||
|
||||
#### `nfs-pv.yaml`
|
||||
- **Purpose**: PersistentVolume definition for NFS storage
|
||||
- **Server**: 192.168.17.199
|
||||
- **Path**: /mnt/user/vorgabenui
|
||||
- **Path**: /mnt/user/kubernetesdata/vorgabenui
|
||||
- **Access**: ReadWriteMany for multi-pod access
|
||||
- **Reclaim Policy**: Retain (data preserved after PVC deletion)
|
||||
|
||||
@@ -40,14 +48,21 @@ This directory contains the ArgoCD application manifests for deploying the Vorga
|
||||
#### `diagrammer.yaml`
|
||||
- **Purpose**: Deployment configuration for the diagram generation service
|
||||
- **Function**: Handles diagram creation and caching for the application
|
||||
- **Version**: 0.026
|
||||
|
||||
## NFS Storage Configuration
|
||||
#### `secret.yaml` (Template)
|
||||
- **Purpose**: Template for Django SECRET_KEY secret
|
||||
- **Contains**: Secret key configuration for cryptographic operations
|
||||
- **Namespace**: vorgabenui
|
||||
- **Generated by**: `deploy-argocd-secret.sh` script
|
||||
- **Version**: 0.026
|
||||
|
||||
### 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`
|
||||
#### `secret.yaml` (Template)
|
||||
- **Purpose**: Template for Django SECRET_KEY secret
|
||||
- **Contains**: Secret key configuration for cryptographic operations
|
||||
- **Namespace**: vorgabenui
|
||||
- **Generated by**: `deploy-argocd-secret.sh` script
|
||||
- **Version**: 0.026
|
||||
|
||||
## MicroK8s Addons Required
|
||||
|
||||
@@ -136,7 +151,7 @@ microk8s kubectl get pods -n ingress
|
||||
microk8s kubectl get svc -n ingress
|
||||
|
||||
# Test ingress connectivity
|
||||
curl -k https://your-domain.com
|
||||
curl -k https://vorgabenportal.knowyoursecurity.com
|
||||
```
|
||||
|
||||
#### Storage Issues
|
||||
@@ -159,24 +174,143 @@ 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
|
||||
sudo mkdir -p /mnt/user/kubernetesdata/vorgabenui
|
||||
sudo chmod 755 /mnt/user/kubernetesdata/vorgabenui
|
||||
|
||||
# Add to /etc/exports
|
||||
echo "/mnt/user/vorgabenui *(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
|
||||
echo "/mnt/user/kubernetesdata/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
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### ConfigMap Deployment
|
||||
|
||||
The Django application uses a ConfigMap for application configuration. The ConfigMap contains environment variables and settings for the Django application.
|
||||
|
||||
#### ConfigMap File
|
||||
- **File**: `configmap.yaml`
|
||||
- **Name**: `django-config`
|
||||
- **Namespace**: `vorgabenui`
|
||||
- **Version**: 0.990
|
||||
|
||||
#### Configuration Contents
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: django-config
|
||||
namespace: vorgabenui
|
||||
data:
|
||||
# Django Configuration
|
||||
DEBUG: "false"
|
||||
DJANGO_ALLOWED_HOSTS: "vorgabenportal.knowyoursecurity.com,localhost,127.0.0.1,*"
|
||||
DJANGO_SETTINGS_MODULE: "VorgabenUI.settings"
|
||||
|
||||
# Application Configuration
|
||||
LANGUAGE_CODE: "de-ch"
|
||||
TIME_ZONE: "UTC"
|
||||
|
||||
# Static and Media Configuration
|
||||
STATIC_URL: "/static/"
|
||||
MEDIA_URL: "/media/"
|
||||
|
||||
# Application Version
|
||||
VERSION: "0.990"
|
||||
|
||||
# Database Configuration (for future use)
|
||||
# DATABASE_ENGINE: "django.db.backends.sqlite3"
|
||||
# DATABASE_NAME: "/app/data/db.sqlite3"
|
||||
|
||||
# Security Configuration
|
||||
# CSRF_TRUSTED_ORIGINS: "https://vorgabenportal.knowyoursecurity.com"
|
||||
```
|
||||
|
||||
#### Deployment Script
|
||||
The ConfigMap is deployed using the `deploy-argocd-configmap.sh` script located in the `scripts/` directory.
|
||||
|
||||
**Script Usage**:
|
||||
```bash
|
||||
# Deploy ConfigMap
|
||||
./scripts/deploy-argocd-configmap.sh
|
||||
|
||||
# Verify ConfigMap only (no deployment)
|
||||
./scripts/deploy-argocd-configmap.sh --verify-only
|
||||
|
||||
# Dry-run (show what would be deployed)
|
||||
./scripts/deploy-argocd-configmap.sh --dry-run
|
||||
```
|
||||
|
||||
**Script Features**:
|
||||
- Validates kubectl availability
|
||||
- Checks if ConfigMap file exists
|
||||
- Creates namespace if it doesn't exist
|
||||
- Applies ConfigMap configuration
|
||||
- Verifies successful deployment
|
||||
- Provides detailed logging and error handling
|
||||
|
||||
### Secret Deployment
|
||||
|
||||
The Django application requires a secure SECRET_KEY for cryptographic signing. This is managed through a Kubernetes Secret.
|
||||
|
||||
#### Secret Configuration
|
||||
- **Secret Name**: `vorgabenui-secrets`
|
||||
- **Secret Key**: `vorgabenui_secret`
|
||||
- **Namespace**: `vorgabenui`
|
||||
|
||||
#### Secret Generation
|
||||
The secret is automatically generated using the `deploy-argocd-secret.sh` script, which creates a secure Django-style SECRET_KEY.
|
||||
|
||||
**Script Usage**:
|
||||
```bash
|
||||
# Generate and deploy new secret
|
||||
./scripts/deploy-argocd-secret.sh
|
||||
|
||||
# Verify existing secret only (no new generation)
|
||||
./scripts/deploy-argocd-secret.sh --verify-only
|
||||
|
||||
# Dry-run (show what would be done)
|
||||
./scripts/deploy-argocd-secret.sh --dry-run
|
||||
```
|
||||
|
||||
**Secret Generation Features**:
|
||||
- Generates secure 50-character SECRET_KEY using Python
|
||||
- Uses Django-style character set (letters, digits, special characters)
|
||||
- Creates or updates the secret in the vorgabenui namespace
|
||||
- Verifies secret deployment and accessibility
|
||||
- Tests secret accessibility in Django pods
|
||||
|
||||
#### Environment Variable Reference
|
||||
The deployment.yaml references the secret through environment variables:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
# Secret configuration
|
||||
- name: VORGABENUI_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: vorgabenui-secrets
|
||||
key: vorgabenui_secret
|
||||
```
|
||||
|
||||
#### Security Notes
|
||||
- The SECRET_KEY is never logged or displayed in full
|
||||
- Only the first 10 characters are shown during generation for verification
|
||||
- The secret is stored securely in Kubernetes and only accessible to authorized pods
|
||||
- Regular secret rotation is recommended for production environments
|
||||
|
||||
## 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
|
||||
4. **ConfigMap** (`configmap.yaml`) - Deploy Django configuration
|
||||
5. **Secret** (`secret.yaml`) - Generate and deploy Django SECRET_KEY
|
||||
6. **Application Deployments** (`deployment.yaml`, `diagrammer.yaml`) - Deploy application services
|
||||
7. **Ingress** (`ingress.yaml`) - Configure external access
|
||||
|
||||
## Configuration Notes
|
||||
|
||||
@@ -227,7 +361,7 @@ kubectl describe pod <pod-name> -n vorgabenui
|
||||
## Maintenance
|
||||
|
||||
### Backup Strategy
|
||||
- The NFS server should have regular backups of `/mnt/user/vorgabenui`
|
||||
- The NFS server should have regular backups of `/mnt/user/kubernetesdata/vorgabenui`
|
||||
- Consider snapshot capabilities if using enterprise NFS solutions
|
||||
|
||||
### Monitoring
|
||||
|
||||
@@ -49,7 +49,7 @@ python manage.py import-document Documentation/import\ formats/r009.txt \
|
||||
|
||||
### Dry-Run Modus
|
||||
|
||||
Der Dry-Run Modus ist besonders nützlich zum Testen:
|
||||
Der Dry-Run Modus ist zum Testen gedacht:
|
||||
|
||||
```bash
|
||||
python manage.py import-document r009.txt \
|
||||
@@ -73,7 +73,7 @@ python manage.py import-document r009.txt \
|
||||
--purge
|
||||
```
|
||||
|
||||
Nutzen Sie `--dry-run --purge` zuerst, um zu sehen, was gelöscht würde.
|
||||
Mit `--dry-run --purge` kann zuerst geprüft werden, was gelöscht würde.
|
||||
|
||||
## Dateiformat
|
||||
|
||||
@@ -257,7 +257,7 @@ Abschliessender Text nach der Liste.
|
||||
>>>Vorgabe [Thema]
|
||||
```
|
||||
|
||||
Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Übliche Themen:
|
||||
Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Die bestehenden Themen sind - wie in den bestehenden Standards - aus dem IT-Grundschutz übernommen:
|
||||
- Organisation
|
||||
- Technik
|
||||
- Informationen
|
||||
@@ -275,11 +275,7 @@ Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Übliche
|
||||
|
||||
oder inline:
|
||||
|
||||
```
|
||||
>>>Nummer: 1
|
||||
```
|
||||
|
||||
Die Nummer wird als Integer gespeichert. Sie ist eindeutig innerhalb eines Dokuments und Themas.
|
||||
Die Nummer wird als Integer gespeichert. Sie ist nicht eindeutig innerhalb eines Dokuments und Themas. Wenn mehrere Vorgaben im selben Thema mit der selben Nummer vorkommen, darf sich der Geltungszeitraum der Vorgaben nicht überschneiden (wird beim Import geprüft).
|
||||
|
||||
#### Titel (Pflicht)
|
||||
|
||||
@@ -322,14 +318,6 @@ Komma-getrennte Liste:
|
||||
Firewall, Netzwerk, Sicherheit
|
||||
```
|
||||
|
||||
oder als Block:
|
||||
|
||||
```
|
||||
>>>Stichworte
|
||||
>>>Text
|
||||
Firewall, Netzwerk, Sicherheit
|
||||
```
|
||||
|
||||
**Hinweis:** Stichworte werden automatisch in der Datenbank angelegt, falls sie noch nicht existieren.
|
||||
|
||||
#### Checkliste (Optional)
|
||||
@@ -347,7 +335,7 @@ Jede Zeile wird als separate Checklistenfrage gespeichert.
|
||||
|
||||
### 1. Dry-Run vor Import
|
||||
|
||||
Führen Sie immer zuerst einen Dry-Run durch:
|
||||
Immer zuerst einen Dry-Run durchführen:
|
||||
|
||||
```bash
|
||||
python manage.py import-document datei.txt \
|
||||
@@ -359,7 +347,7 @@ python manage.py import-document datei.txt \
|
||||
|
||||
### 2. Themen vorab erstellen
|
||||
|
||||
Stellen Sie sicher, dass alle verwendeten Themen in der Datenbank existieren:
|
||||
Sicherstellen, dass alle verwendeten Themen in der Datenbank existieren:
|
||||
|
||||
```python
|
||||
python manage.py shell
|
||||
@@ -391,11 +379,11 @@ Folgende Abschnitttypen müssen in der Datenbank existieren:
|
||||
- `code`
|
||||
- `diagramm`
|
||||
|
||||
Prüfen Sie diese in der Autorenumgebung unter "Abschnitttypen".
|
||||
Prüfen in der Autorenumgebung unter "Abschnitttypen".
|
||||
|
||||
### 5. UTF-8 Kodierung
|
||||
|
||||
Stellen Sie sicher, dass Ihre Importdatei UTF-8 kodiert ist, besonders bei Umlauten (ä, ö, ü) und Sonderzeichen.
|
||||
Sicherstellen, dass die Importdatei UTF-8 kodiert ist, besonders bei Umlauten (ä, ö, ü) und Sonderzeichen.
|
||||
|
||||
### 6. Versionierung mit Purge
|
||||
|
||||
@@ -436,37 +424,37 @@ Leerzeilen innerhalb eines Abschnitts werden beibehalten. Eine Leerzeile nach ei
|
||||
|
||||
Der angegebene Dokumententyp existiert nicht in der Datenbank.
|
||||
|
||||
**Lösung:** Erstellen Sie den Dokumententyp in der Autorenumgebung oder per Shell.
|
||||
**Lösung:** Dokumententyp aus dem IT-Grundschutz verwenden, nötigenfalls hinzufügen in der Autorenumgebung oder per Shell.
|
||||
|
||||
### "Thema not found, skipping Vorgabe"
|
||||
|
||||
Das in der Vorgabe verwendete Thema existiert nicht.
|
||||
|
||||
**Lösung:** Erstellen Sie das Thema in der Autorenumgebung oder passen Sie die Importdatei an.
|
||||
**Lösung:** Thema in der Autorenumgebung erstellen oder Importdatei anpassen.
|
||||
|
||||
### "AbschnittTyp not found"
|
||||
|
||||
Ein verwendeter Abschnitttyp existiert nicht.
|
||||
|
||||
**Lösung:**
|
||||
- Prüfen Sie die Schreibweise (Gross-/Kleinschreibung wird normalisiert)
|
||||
- Erstellen Sie den Abschnitttyp in der Autorenumgebung
|
||||
- Standardtypen: `text`, `liste geordnet`, `liste ungeordnet`
|
||||
- Schreibweise prüfen (Gross-/Kleinschreibung und "-"/" " wird normalisiert)
|
||||
- Wenn nötig Abschnitttyp in der Autorenumgebung erstellen (Achtung! Ausgabeformat muss im Code definiert werden)
|
||||
- Standardtypen: `text`, `liste geordnet`, `liste ungeordnet`, `tabelle`, `code`
|
||||
|
||||
### Vorgabe wird nicht importiert
|
||||
|
||||
Prüfen Sie:
|
||||
Prüfen:
|
||||
- Ist `>>>Nummer` gesetzt?
|
||||
- Ist `>>>Titel` gesetzt?
|
||||
- Existiert das Thema?
|
||||
|
||||
Verwenden Sie `--dry-run --verbose` für detaillierte Informationen.
|
||||
`--dry-run --verbose` für detaillierte Informationen.
|
||||
|
||||
## Weitere Informationen
|
||||
|
||||
### Beispieldateien
|
||||
|
||||
Beispieldateien finden Sie in:
|
||||
Beispieldateien:
|
||||
- `Documentation/import formats/r009.txt`
|
||||
- `Documentation/import formats/r0126.txt`
|
||||
|
||||
@@ -485,7 +473,3 @@ Oder über die Web-Oberfläche: `/dokumente/R0066/?format=json`
|
||||
- `export_json` - Exportiert Dokumente als JSON
|
||||
- `sanity_check_vorgaben` - Prüft Vorgaben auf Konflikte
|
||||
- `clear_diagram_cache` - Löscht Diagramm-Cache
|
||||
|
||||
## Kontakt
|
||||
|
||||
Bei Fragen oder Problemen wenden Sie sich an das Information Security Management BIT.
|
||||
|
||||
@@ -18,7 +18,7 @@ data:
|
||||
MEDIA_URL: "/media/"
|
||||
|
||||
# Application Version
|
||||
VERSION: "0.987"
|
||||
VERSION: "0.992"
|
||||
|
||||
# Database Configuration (for future use)
|
||||
# DATABASE_ENGINE: "django.db.backends.sqlite3"
|
||||
|
||||
@@ -18,7 +18,7 @@ spec:
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
initContainers:
|
||||
- name: loader
|
||||
image: git.baumann.gr/adebaumann/vui-data-loader:0.11
|
||||
image: git.baumann.gr/adebaumann/vui-data-loader:0.12
|
||||
securityContext:
|
||||
runAsUser: 99
|
||||
command: [ "sh","-c","if [ ! -f /data/db.sqlite3 ] || [ ! -s /data/db.sqlite3 ]; then cp preload/preload.sqlite3 /data/db.sqlite3 && echo 'Database copied from preload'; else echo 'Existing database preserved'; fi" ]
|
||||
@@ -27,7 +27,7 @@ spec:
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: web
|
||||
image: git.baumann.gr/adebaumann/vui:0.987
|
||||
image: git.baumann.gr/adebaumann/vui:0.993
|
||||
imagePullPolicy: Always
|
||||
securityContext:
|
||||
runAsUser: 99
|
||||
|
||||
@@ -15,4 +15,4 @@ spec:
|
||||
storageClassName: nfs
|
||||
nfs:
|
||||
server: 192.168.17.199
|
||||
path: /mnt/user/vorgabenui
|
||||
path: /mnt/user/kubernetesdata/vorgabenui
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
FROM alpine:3.22.1
|
||||
RUN addgroup -S appuser && \
|
||||
FROM alpine:3.22
|
||||
RUN apk upgrade --no-cache && \
|
||||
addgroup -S appuser && \
|
||||
adduser -S appuser -G appuser && \
|
||||
mkdir /preload && \
|
||||
chown -R appuser:appuser /preload
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
from django.contrib import admin
|
||||
#from nested_inline.admin import NestedStackedInline, NestedModelAdmin
|
||||
from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
|
||||
from django import forms
|
||||
from django.utils.html import format_html
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from mptt.admin import DraggableMPTTAdmin
|
||||
from adminsortable2.admin import SortableInlineAdminMixin, SortableAdminBase
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
# Register your models here.
|
||||
from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
|
||||
from .models import *
|
||||
from stichworte.models import Stichwort, Stichworterklaerung
|
||||
from referenzen.models import Referenz
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +63,6 @@ class GeltungsbereichInline(NestedStackedInline):
|
||||
extra=0
|
||||
sortable_field_name = "order"
|
||||
show_change_link=True
|
||||
classes = ['collapse']
|
||||
verbose_name_plural = "Geltungsbereich-Abschnitte"
|
||||
fieldsets = (
|
||||
(None, {
|
||||
@@ -83,7 +76,6 @@ class EinleitungInline(NestedStackedInline):
|
||||
extra = 0
|
||||
sortable_field_name = "order"
|
||||
show_change_link = True
|
||||
classes = ['collapse']
|
||||
verbose_name_plural = "Einleitungs-Abschnitte"
|
||||
fieldsets = (
|
||||
(None, {
|
||||
@@ -93,8 +85,6 @@ class EinleitungInline(NestedStackedInline):
|
||||
)
|
||||
|
||||
class VorgabeForm(forms.ModelForm):
|
||||
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Vorgabe
|
||||
fields = '__all__'
|
||||
@@ -106,7 +96,7 @@ class VorgabeForm(forms.ModelForm):
|
||||
raise forms.ValidationError('Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.')
|
||||
return thema
|
||||
|
||||
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline):
|
||||
class VorgabeInline(NestedStackedInline):
|
||||
model = Vorgabe
|
||||
form = VorgabeForm
|
||||
extra = 0
|
||||
@@ -165,7 +155,7 @@ class StichwortAdmin(NestedModelAdmin):
|
||||
count = len(vorgaben_list)
|
||||
|
||||
if count == 0:
|
||||
return format_html("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>")
|
||||
return mark_safe("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>")
|
||||
|
||||
html = "<div style='max-height: 300px; overflow-y: auto;'>"
|
||||
html += "<table style='width: 100%; border-collapse: collapse;'>"
|
||||
@@ -186,7 +176,7 @@ class StichwortAdmin(NestedModelAdmin):
|
||||
html += "</tbody></table>"
|
||||
html += f"</div><p><strong>Gesamt: {count} Vorgabe{'n' if count != 1 else ''}</strong></p>"
|
||||
|
||||
return format_html(html)
|
||||
return mark_safe(html)
|
||||
vorgaben_list.short_description = "Zugeordnete Vorgaben"
|
||||
|
||||
def get_queryset(self, request):
|
||||
@@ -204,7 +194,7 @@ class PersonAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(Dokument)
|
||||
class DokumentAdmin(SortableAdminBase, NestedModelAdmin):
|
||||
class DokumentAdmin(NestedModelAdmin):
|
||||
actions_on_top=True
|
||||
inlines = [EinleitungInline, GeltungsbereichInline, VorgabeInline]
|
||||
filter_horizontal=['autoren','pruefende']
|
||||
|
||||
BIN
dokumente/docx_template/template.docx
Normal file
BIN
dokumente/docx_template/template.docx
Normal file
Binary file not shown.
207
dokumente/docx_utils.py
Normal file
207
dokumente/docx_utils.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Utility functions for exporting Dokument instances as Word (.docx) files.
|
||||
"""
|
||||
import os
|
||||
from docx import Document
|
||||
from docx.shared import Pt
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
|
||||
TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'docx_template', 'template.docx')
|
||||
|
||||
|
||||
def build_standard_docx(dokument):
|
||||
"""
|
||||
Generate a Word Document object for the given Dokument.
|
||||
|
||||
Opens the template .docx to inherit custom styles (Hermes, Kurztext,
|
||||
Tabelle, etc.), clears the body, then writes the document structure.
|
||||
|
||||
Args:
|
||||
dokument: a Dokument instance (should be prefetched with related data)
|
||||
|
||||
Returns:
|
||||
docx.Document instance ready to be saved
|
||||
"""
|
||||
doc = Document(TEMPLATE_PATH)
|
||||
|
||||
# Clear all body content while preserving page-layout section properties
|
||||
body = doc.element.body
|
||||
for element in list(body):
|
||||
if element.tag != qn('w:sectPr'):
|
||||
body.remove(element)
|
||||
|
||||
# 1. Title
|
||||
title_para = doc.add_paragraph(style='Normal')
|
||||
run = title_para.add_run(f'Standard\nIT-Sicherheit-{dokument.name}')
|
||||
run.bold = True
|
||||
run.font.size = Pt(22)
|
||||
|
||||
# 2. Metadata table (uses the "Hermes" table style from the template)
|
||||
meta_table = doc.add_table(rows=0, cols=2, style='Hermes')
|
||||
meta_rows = [
|
||||
('Identifikation:', dokument.nummer),
|
||||
('Dokumentationsklasse:', dokument.dokumententyp.name if dokument.dokumententyp else ''),
|
||||
('Gültig ab:', dokument.gueltigkeit_von.strftime('%d.%m.%Y') if dokument.gueltigkeit_von else ''),
|
||||
('Gültig bis:', dokument.gueltigkeit_bis.strftime('%d.%m.%Y') if dokument.gueltigkeit_bis else ''),
|
||||
('Klassifizierung:', ''),
|
||||
('Verantwortliche Stelle:', 'Information Security Management BIT'),
|
||||
('Autoren:', ', '.join(a.name for a in dokument.autoren.all())),
|
||||
('Prüfende:', ', '.join(p.name for p in dokument.pruefende.all())),
|
||||
]
|
||||
for label, value in meta_rows:
|
||||
row = meta_table.add_row()
|
||||
row.cells[0].text = label
|
||||
row.cells[1].text = value
|
||||
|
||||
# 3. Einleitung
|
||||
doc.add_heading('Einleitung', level=1)
|
||||
for abschnitt in dokument.einleitung_set.order_by('order'):
|
||||
_add_abschnitt(doc, abschnitt)
|
||||
|
||||
# 4. Geltungsbereich
|
||||
doc.add_heading('Geltungsbereich', level=1)
|
||||
for abschnitt in dokument.geltungsbereich_set.order_by('order'):
|
||||
_add_abschnitt(doc, abschnitt)
|
||||
|
||||
# 5. Vorgaben
|
||||
doc.add_heading('Vorgaben', level=1)
|
||||
|
||||
vorgaben = list(
|
||||
dokument.vorgaben
|
||||
.order_by('thema__name', 'nummer')
|
||||
.select_related('thema')
|
||||
.prefetch_related(
|
||||
'vorgabekurztext_set__abschnitttyp',
|
||||
'vorgabelangtext_set__abschnitttyp',
|
||||
'checklistenfragen',
|
||||
'referenzen',
|
||||
)
|
||||
)
|
||||
|
||||
current_thema = None
|
||||
for vorgabe in vorgaben:
|
||||
thema_name = vorgabe.thema.name if vorgabe.thema else ''
|
||||
if thema_name != current_thema:
|
||||
current_thema = thema_name
|
||||
doc.add_heading(thema_name, level=2)
|
||||
|
||||
doc.add_heading(f'{vorgabe.Vorgabennummer()} \u2013 {vorgabe.titel}', level=4)
|
||||
|
||||
for kt in vorgabe.vorgabekurztext_set.order_by('order'):
|
||||
_add_abschnitt(doc, kt, default_style='Kurztext')
|
||||
|
||||
for lt in vorgabe.vorgabelangtext_set.order_by('order'):
|
||||
_add_abschnitt(doc, lt)
|
||||
|
||||
fragen = list(vorgabe.checklistenfragen.all())
|
||||
if fragen:
|
||||
doc.add_paragraph('Checklistenfragen', style='Normal')
|
||||
for frage in fragen:
|
||||
doc.add_paragraph(frage.frage, style='Normal')
|
||||
|
||||
refs = list(vorgabe.referenzen.all())
|
||||
if refs:
|
||||
doc.add_paragraph('Referenzen: ' + ', '.join(r.Path() for r in refs), style='Normal')
|
||||
|
||||
# 6. Checkliste
|
||||
doc.add_heading('Checkliste', level=1)
|
||||
|
||||
all_fragen = [
|
||||
(vorgabe.Vorgabennummer(), frage.frage)
|
||||
for vorgabe in vorgaben
|
||||
for frage in vorgabe.checklistenfragen.all()
|
||||
]
|
||||
if all_fragen:
|
||||
checklist_table = doc.add_table(rows=1, cols=3, style='Normal Table')
|
||||
header = checklist_table.rows[0].cells
|
||||
header[0].text = '#'
|
||||
header[1].text = 'Bezeichnung (WAS)'
|
||||
header[2].text = 'Richtlinieregel (WIE)'
|
||||
for vorgabe_num, frage_text in all_fragen:
|
||||
row = checklist_table.add_row()
|
||||
row.cells[0].text = vorgabe_num
|
||||
row.cells[1].text = ''
|
||||
row.cells[2].text = frage_text
|
||||
|
||||
# 7. Changelog
|
||||
changelog_entries = list(
|
||||
dokument.changelog.order_by('-datum').prefetch_related('autoren')
|
||||
)
|
||||
if changelog_entries:
|
||||
doc.add_heading('Änderungskontrolle', level=1)
|
||||
changelog_table = doc.add_table(rows=1, cols=4, style='Tabelle')
|
||||
header = changelog_table.rows[0].cells
|
||||
header[0].text = 'Wann'
|
||||
header[1].text = 'Version'
|
||||
header[2].text = 'Wer'
|
||||
header[3].text = 'Beschreibung'
|
||||
for cl in changelog_entries:
|
||||
row = changelog_table.add_row()
|
||||
row.cells[0].text = cl.datum.strftime('%d.%m.%Y') if cl.datum else ''
|
||||
row.cells[1].text = ''
|
||||
row.cells[2].text = ', '.join(a.name for a in cl.autoren.all())
|
||||
row.cells[3].text = cl.aenderung
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def _add_abschnitt(doc, abschnitt, default_style='Normal'):
|
||||
"""
|
||||
Add a single Textabschnitt to the document, respecting its section type.
|
||||
"""
|
||||
typ = abschnitt.abschnitttyp.abschnitttyp if abschnitt.abschnitttyp else 'text'
|
||||
inhalt = abschnitt.inhalt or ''
|
||||
|
||||
if not inhalt.strip():
|
||||
return
|
||||
|
||||
if typ == 'text':
|
||||
doc.add_paragraph(inhalt, style=default_style)
|
||||
|
||||
elif typ in ('liste ungeordnet', 'liste geordnet'):
|
||||
for line in inhalt.strip().split('\n'):
|
||||
line = line.strip().lstrip('- ').lstrip('* ')
|
||||
if line:
|
||||
doc.add_paragraph(line, style='Normal')
|
||||
|
||||
elif typ == 'tabelle':
|
||||
_add_markdown_table(doc, inhalt)
|
||||
|
||||
else:
|
||||
doc.add_paragraph(inhalt, style='Normal')
|
||||
|
||||
|
||||
def _add_markdown_table(doc, markdown_content):
|
||||
"""
|
||||
Parse a markdown table and add it to the document as a Word table.
|
||||
Falls back to a plain paragraph if parsing fails.
|
||||
"""
|
||||
lines = [line.strip() for line in markdown_content.strip().split('\n') if line.strip()]
|
||||
if len(lines) < 2:
|
||||
doc.add_paragraph(markdown_content, style='Normal')
|
||||
return
|
||||
|
||||
header_cells = [c.strip() for c in lines[0].split('|') if c.strip()]
|
||||
if not header_cells:
|
||||
doc.add_paragraph(markdown_content, style='Normal')
|
||||
return
|
||||
|
||||
# Skip the separator row (dashes), collect data rows
|
||||
data_lines = [
|
||||
line for line in lines[2:]
|
||||
if not all(c in '-|: ' for c in line)
|
||||
]
|
||||
|
||||
table = doc.add_table(rows=1, cols=len(header_cells), style='Table Grid')
|
||||
header_row = table.rows[0].cells
|
||||
for i, cell_text in enumerate(header_cells):
|
||||
if i < len(header_row):
|
||||
header_row[i].text = cell_text
|
||||
|
||||
for line in data_lines:
|
||||
row_cells = [c.strip() for c in line.split('|') if c.strip()]
|
||||
row = table.add_row()
|
||||
for i, cell_text in enumerate(row_cells):
|
||||
if i < len(row.cells):
|
||||
row.cells[i].text = cell_text
|
||||
55
dokumente/management/commands/export_docx.py
Normal file
55
dokumente/management/commands/export_docx.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from dokumente.models import Dokument
|
||||
from dokumente.docx_utils import build_standard_docx
|
||||
import os
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Export a Dokument (or all active Dokumente) as Word (.docx) files'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--nummer',
|
||||
type=str,
|
||||
help='Document number to export (e.g. R0129). Omit to export all active documents.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
default='.',
|
||||
help='Output directory (default: current directory)',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
output_dir = options['output']
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
if options['nummer']:
|
||||
try:
|
||||
dokumente = [Dokument.objects.prefetch_related(
|
||||
'autoren', 'pruefende', 'vorgaben__thema',
|
||||
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
|
||||
'vorgaben__vorgabekurztext_set__abschnitttyp',
|
||||
'vorgaben__vorgabelangtext_set__abschnitttyp',
|
||||
'geltungsbereich_set__abschnitttyp',
|
||||
'einleitung_set__abschnitttyp',
|
||||
'changelog__autoren',
|
||||
).get(nummer=options['nummer'])]
|
||||
except Dokument.DoesNotExist:
|
||||
raise CommandError(f"Dokument with nummer '{options['nummer']}' not found.")
|
||||
else:
|
||||
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
|
||||
'autoren', 'pruefende', 'vorgaben__thema',
|
||||
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
|
||||
'vorgaben__vorgabekurztext_set__abschnitttyp',
|
||||
'vorgaben__vorgabelangtext_set__abschnitttyp',
|
||||
'geltungsbereich_set__abschnitttyp',
|
||||
'einleitung_set__abschnitttyp',
|
||||
'changelog__autoren',
|
||||
).order_by('nummer')
|
||||
|
||||
for dokument in dokumente:
|
||||
doc = build_standard_docx(dokument)
|
||||
filename = os.path.join(output_dir, f'{dokument.nummer}.docx')
|
||||
doc.save(filename)
|
||||
self.stdout.write(self.style.SUCCESS(f'Exported {dokument.nummer} → {filename}'))
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-12 13:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dokumente', '0010_vorgabentable_alter_person_options_vorgabecomment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='einleitung',
|
||||
options={'ordering': ('order',), 'verbose_name': 'Einleitungs-Abschnitt', 'verbose_name_plural': 'Einleitung'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='geltungsbereich',
|
||||
options={'ordering': ('order',), 'verbose_name': 'Geltungsbereichs-Abschnitt', 'verbose_name_plural': 'Geltungsbereich'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='person',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Person', 'verbose_name_plural': 'Personen'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='thema',
|
||||
options={'verbose_name': 'Thema', 'verbose_name_plural': 'Themen'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dokument',
|
||||
name='aktiv',
|
||||
field=models.BooleanField(blank=True, default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-12 13:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dokumente', '0011_alter_einleitung_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='einleitung',
|
||||
options={'ordering': ['order'], 'verbose_name': 'Einleitungs-Abschnitt', 'verbose_name_plural': 'Einleitung'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='geltungsbereich',
|
||||
options={'ordering': ['order'], 'verbose_name': 'Geltungsbereichs-Abschnitt', 'verbose_name_plural': 'Geltungsbereich'},
|
||||
),
|
||||
]
|
||||
@@ -261,12 +261,14 @@ class Geltungsbereich(Textabschnitt):
|
||||
class Meta:
|
||||
verbose_name_plural="Geltungsbereich"
|
||||
verbose_name="Geltungsbereichs-Abschnitt"
|
||||
ordering = ['order']
|
||||
|
||||
class Einleitung(Textabschnitt):
|
||||
einleitung=models.ForeignKey(Dokument,on_delete=models.CASCADE)
|
||||
class Meta:
|
||||
verbose_name_plural="Einleitung"
|
||||
verbose_name="Einleitungs-Abschnitt"
|
||||
ordering = ['order']
|
||||
|
||||
class Checklistenfrage(models.Model):
|
||||
vorgabe=models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name="checklistenfragen")
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
|
||||
<h2>Checkliste</h2>
|
||||
<p>Die Checkliste wird unter anderem für Audits oder Prüfungen benutzt. Wir empfehlen allen Verantwortlichen, sie als internes Qualitätssicherungs-Hilfsmittel zu verwenden.</p>
|
||||
<ul class="list-group">
|
||||
{% for vorgabe in vorgaben %}
|
||||
{% if vorgabe.checklistenfragen.all %}
|
||||
|
||||
@@ -214,6 +214,11 @@
|
||||
download="{{ standard.nummer }}.xml">
|
||||
XML herunterladen
|
||||
</a>
|
||||
<a href="{% url 'standard_docx' standard.nummer %}"
|
||||
class="btn btn-secondary icon icon--before icon--download"
|
||||
download="{{ standard.nummer }}.docx">
|
||||
DOCX herunterladen (Prototyp)
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ urlpatterns = [
|
||||
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
|
||||
path('<str:nummer>/json/', views.standard_json, name='standard_json'),
|
||||
path('<str:nummer>/xml/', views.standard_xml, name='standard_xml'),
|
||||
path('<str:nummer>/docx/', views.standard_docx, name='standard_docx'),
|
||||
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'),
|
||||
|
||||
@@ -6,10 +6,12 @@ from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.html import escape, mark_safe
|
||||
from django.utils.safestring import SafeString
|
||||
import io
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
|
||||
from .utils import build_dokument_xml_element, prettify_xml
|
||||
from .docx_utils import build_standard_docx
|
||||
from abschnitte.utils import render_textabschnitte
|
||||
|
||||
from datetime import date
|
||||
@@ -256,6 +258,37 @@ def standard_json(request, nummer):
|
||||
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
|
||||
|
||||
|
||||
def standard_docx(request, nummer):
|
||||
"""
|
||||
Export a single Dokument as a Word (.docx) file.
|
||||
"""
|
||||
dokument = get_object_or_404(
|
||||
Dokument.objects.prefetch_related(
|
||||
'autoren', 'pruefende', 'vorgaben__thema',
|
||||
'vorgaben__referenzen', 'vorgaben__checklistenfragen',
|
||||
'vorgaben__vorgabekurztext_set__abschnitttyp',
|
||||
'vorgaben__vorgabelangtext_set__abschnitttyp',
|
||||
'geltungsbereich_set__abschnitttyp',
|
||||
'einleitung_set__abschnitttyp',
|
||||
'changelog__autoren',
|
||||
),
|
||||
nummer=nummer,
|
||||
)
|
||||
|
||||
doc = build_standard_docx(dokument)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
doc.save(buffer)
|
||||
buffer.seek(0)
|
||||
|
||||
response = HttpResponse(
|
||||
buffer.read(),
|
||||
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
)
|
||||
response['Content-Disposition'] = f'attachment; filename="{dokument.nummer}.docx"'
|
||||
return response
|
||||
|
||||
|
||||
def standard_xml(request, nummer):
|
||||
"""
|
||||
Export a single Dokument as XML
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
|
||||
from .models import *
|
||||
|
||||
@@ -14,7 +14,7 @@ class ReferenzerklaerungInline(NestedStackedInline):
|
||||
class ReferenzAdmin(NestedModelAdmin):
|
||||
list_display = ('Path', 'vorgaben_count')
|
||||
inlines=[ReferenzerklaerungInline]
|
||||
search_fields=("name_nummer","Path")
|
||||
search_fields=("name_nummer","name_text")
|
||||
readonly_fields=("vorgaben_list",)
|
||||
fieldsets = (
|
||||
(None, {
|
||||
@@ -34,7 +34,7 @@ class ReferenzAdmin(NestedModelAdmin):
|
||||
vorgaben_list = list(vorgaben) # Evaluate queryset once
|
||||
count = len(vorgaben_list)
|
||||
if count == 0:
|
||||
return format_html("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>")
|
||||
return mark_safe("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>")
|
||||
|
||||
html = "<div style='max-height: 300px; overflow-y: auto;'>"
|
||||
html += "<table style='width: 100%; border-collapse: collapse;'>"
|
||||
@@ -55,7 +55,7 @@ class ReferenzAdmin(NestedModelAdmin):
|
||||
html += "</tbody></table>"
|
||||
html += f"</div><p><strong>Gesamt: {count} Vorgabe{'n' if count != 1 else ''}</strong></p>"
|
||||
|
||||
return format_html(html)
|
||||
return mark_safe(html)
|
||||
|
||||
vorgaben_list.short_description = "Zugeordnete Vorgaben"
|
||||
|
||||
|
||||
@@ -13,15 +13,17 @@ class Referenz(MPTTModel):
|
||||
url = models.URLField(blank=True)
|
||||
|
||||
def Path(self):
|
||||
Temp = " → ".join([str(x) for x in self.get_ancestors(include_self=True)])+(" (%s)"%self.name_text if self.name_text else "")
|
||||
return Temp
|
||||
path = " → ".join([x.name_nummer for x in self.get_ancestors(include_self=True)])
|
||||
if self.name_text:
|
||||
path += " (%s)" % self.name_text
|
||||
return path
|
||||
|
||||
class MPTTMeta:
|
||||
parent_attr = 'oberreferenz' # optional, but safe
|
||||
order_insertion_by = ['name_nummer']
|
||||
|
||||
def __str__(self):
|
||||
return self.name_nummer
|
||||
return self.Path()
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural="Referenzen"
|
||||
|
||||
@@ -7,7 +7,7 @@ charset-normalizer==3.4.4
|
||||
coverage==7.13.1
|
||||
curtsies==0.4.3
|
||||
cwcwidth==0.1.12
|
||||
Django==6.0.1
|
||||
Django==6.0.3
|
||||
django-admin-sortable2==2.3
|
||||
django-js-asset==3.1.2
|
||||
django-mptt==0.18.0
|
||||
@@ -19,6 +19,7 @@ greenlet==3.3.0
|
||||
gunicorn==23.0.0
|
||||
idna==3.11
|
||||
jedi==0.19.2
|
||||
lxml==6.0.2
|
||||
jproperties==2.1.2
|
||||
Markdown==3.10
|
||||
packaging==25.0
|
||||
@@ -30,6 +31,7 @@ pyfakefs==5.9.3
|
||||
Pygments==2.19.2
|
||||
pysonar==1.2.1.3951
|
||||
python-dateutil==2.9.0.post0
|
||||
python-docx==1.2.0
|
||||
python-monkey-business==1.1.0
|
||||
pyxdg==0.28
|
||||
PyYAML==6.0.3
|
||||
|
||||
17
rollen/migrations/0003_alter_rolle_options.py
Normal file
17
rollen/migrations/0003_alter_rolle_options.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.0.1 on 2026-02-12 13:27
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rollen', '0002_alter_rolle_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='rolle',
|
||||
options={'verbose_name': 'Rolle', 'verbose_name_plural': 'Rollen'},
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user