Compare commits

..

46 Commits

Author SHA1 Message Date
51969141e7 Typo in Dockerfile fixed
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 1m5s
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
2026-01-07 14:49:26 +01:00
b7f50ce30f XML export added
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 1m5s
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) Failing after 8s
2026-01-07 14:43:59 +01:00
d3d0298ad1 scripts added and Dockerfile adjusted to remove them from production 2026-01-07 14:31:54 +01:00
29c1ad1dcf Tests for document import added
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 4m14s
2025-12-09 15:27:49 +01:00
4504e8a2a5 Database updated with collaborators
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 53s
2025-12-08 16:26:25 +01:00
502dd85efb Document import management command updated to work with changed structure 2025-12-08 16:26:03 +01:00
9e6e2b5a03 Workflow: Changed description (old vs new was obsolete)
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
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 16:01:07 +01:00
c492b7bda6 New Preload-DB-image
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 11s
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 15:59:13 +01:00
0d7e63d3a2 Changed "Historische Version..." to "Zukünftige Version..." when appropriate
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 16s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 15:34:14 +01:00
0866e604bc Documentation change regarding diagram cache
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-08 15:01:14 +01:00
753c00bc45 Validation for 'Thema' on Vorgabe added
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 14s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 54s
2025-12-08 14:41:02 +01:00
74d2f15d6a Package versions lifted for urllib and django due to trivy scan
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 39s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 59s
2025-12-08 14:23:37 +01:00
615908e569 Password miminum length: 12
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 20s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 56s
2025-12-08 12:34:09 +01:00
55d467ee58 Deployment 963
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 16s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 56s
2025-12-04 14:28:40 +01:00
5a1df7345d UI element added
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-04 14:26:35 +01:00
e3c5f6a9d7 Last date removed from array - tests adapted 2025-12-04 14:16:18 +01:00
a26290fc92 Date array calculation for documents added
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 57s
2025-12-04 14:08:18 +01:00
08d94a9269 Link to Autorenumgebung shortened, version bump
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 5s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-04 13:37:30 +01:00
6f8f273344 XSS prevention added (with tests)
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 15s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 55s
2025-12-04 13:26:12 +01:00
f96226170b Add staff-only all comments page and bump versions
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 15s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 47s
- Add new "alle-kommentare" (all comments) view for staff users only
  - Allows staff to view and manage all user comments across the system
  - Grouped by document with user information displayed
  - Staff can delete any comment via the dedicated delete button
  - Restricts access via user_passes_test decorator

- Create all_comments.html template
  - Based on user_comments template with added username field
  - Shows comment author, creation time, and edit time
  - Provides delete functionality for comment management

- Update navigation menu
  - Add "Alle Kommentare" link in user dropdown menu
  - Link only visible to staff members

- Add URL route for alle-kommentare page
  - Path: /dokumente/alle-kommentare/
  - URL name: all_comments

- Bump application versions
  - Update footer version from 0.965 to 0.966
  - Update K8s deployment version from 0.917 to 0.918
  - ArgoCD deployment already at 0.966

All existing tests pass (148 tests total)
2025-12-04 13:17:35 +01:00
0783033c70 Titles on comment page; Deployment 965
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 16s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 47s
2025-12-04 08:41:53 +01:00
35fbfdccec Deployment 963
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 41s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 43s
2025-12-04 01:35:01 +01:00
1196d3cdd2 Merge pull request 'feature/comment-page' (#16) from feature/comment-page into development
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 45s
Reviewed-on: #16
2025-12-04 00:29:41 +00:00
df67948efc All user comments on one page implemented, including tests
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 49s
SonarQube Scan / SonarQube Trigger (pull_request) Successful in 45s
2025-12-03 13:26:19 +01:00
a78f53f58e All user comments on one page implemented, including tests 2025-12-03 13:23:11 +01:00
2c39db104e Simplified container building workflow - now only checks for presence of container tag on repo
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 15m51s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 6s
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-12-02 19:08:56 +01:00
ad17b394a3 Dockerfile updated, deploy 0.963
Some checks failed
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
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 5s
2025-12-02 18:54:39 +01:00
745ce4fabc Python and Django update
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Failing after 53s
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 7s
SonarQube Scan / SonarQube Trigger (push) Successful in 2m0s
2025-12-02 16:49:00 +01:00
b6fbe750a2 Python and Django update
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Failing after 1m30s
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 7s
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2025-12-02 16:45:31 +01:00
89d3eec5fb Python and Django update
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 8s
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
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2025-12-02 16:44:21 +01:00
cd4783efc4 Python and Django update
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Failing after 54s
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 7s
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2025-12-02 16:42:31 +01:00
9efef2c5e2 Fixed an age-old unmatched bracket...
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 2m15s
2025-12-02 13:13:25 +01:00
09010a117e Tests adjusted to reflect changes in comments (Full name instead of user name)
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 1m9s
2025-12-02 13:11:07 +01:00
8ea0937ea4 Coverage added
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 2m13s
2025-12-02 13:02:27 +01:00
5330493c85 Dockerfile changed to check SonarQube
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 9s
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
SonarQube Scan / SonarQube Trigger (push) Successful in 57s
2025-12-02 11:41:37 +01:00
9e6e9e9830 Typo in secrets
All checks were successful
SonarQube Scan / SonarQube Trigger (push) Successful in 1m0s
2025-12-02 11:35:15 +01:00
f311050412 Typo in secrets
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 11s
2025-12-02 11:33:51 +01:00
492b3c5a20 Java mismatch in workflow
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 1m13s
2025-12-02 11:22:43 +01:00
a81b6eb9d5 Java mismatch in workflow
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 1m17s
2025-12-02 11:17:52 +01:00
f6be6d6a02 SonarQube integration - fit the first
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 28s
2025-12-02 11:10:59 +01:00
c8d3ef4631 Deployment 961
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 8s
2025-12-01 14:40:28 +01:00
46912cff8c Merge feature/comments 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 8s
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:35:41 +01:00
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
28 changed files with 3558 additions and 71 deletions

View File

@@ -60,7 +60,7 @@ jobs:
chmod +x /usr/local/bin/yq
yq --version
- name: Read ${{ matrix.description }} image from deployment (old vs new)
- name: Read ${{ matrix.description }} image from deployment
id: img
shell: bash
run: |
@@ -211,32 +211,60 @@ jobs:
echo "ERROR: Found $ctype \"$cname\" image repo is \"$new_repo\" but expected \"$expected_repo\""
exit 1
fi
if [ -n "${old_image:-}" ]; then
old_tag="${old_image##*:}"
else
old_tag=""
fi
registry="$(echo "$new_repo" | awk -F/ '{print $1}')"
{
echo "changed=$([ "$old_tag" != "$new_tag" ] && echo true || echo false)"
echo "new_image=$new_image"
echo "new_repo=$new_repo"
echo "new_tag=$new_tag"
echo "registry=$registry"
} >> "$GITHUB_OUTPUT"
- name: Skip if tag unchanged
if: steps.img.outputs.changed != 'true'
run: echo "${{ matrix.description }} image tag unchanged; skipping build."
- name: Check if image exists on registry
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.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'
if: 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.check_image.outputs.exists == 'false'
uses: docker/login-action@v3
with:
registry: ${{ steps.img.outputs.registry }}
@@ -244,7 +272,7 @@ jobs:
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push ${{ matrix.description }} (exact tag from deployment)
if: steps.img.outputs.changed == 'true'
if: steps.check_image.outputs.exists == 'false'
uses: docker/build-push-action@v6
with:
context: ${{ matrix.build_context }}

View File

@@ -0,0 +1,67 @@
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

View File

@@ -1,4 +1,4 @@
FROM python:3.13-slim AS baustelle
FROM python:3.14 AS baustelle
RUN mkdir /app
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
@@ -7,22 +7,21 @@ RUN pip install --upgrade pip
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.13-slim
FROM python:3.14-slim
RUN useradd -m -r appuser && \
mkdir /app && \
chown -R appuser /app
COPY --from=baustelle /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/
COPY --from=baustelle /usr/local/lib/python3.14/site-packages/ /usr/local/lib/python3.14/site-packages/
COPY --from=baustelle /usr/local/bin/ /usr/local/bin/
RUN rm /usr/bin/tar
RUN rm /usr/lib/x86_64-linux-gnu/libncur*
RUN rm /usr/bin/tar /usr/lib/x86_64-linux-gnu/libncur*
WORKDIR /app
COPY --chown=appuser:appuser . .
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
USER appuser
EXPOSE 8000
RUN rm -rf /app/Dockerfile* \
RUN rm -rvf /app/Dockerfile* \
/app/README.md \
/app/argocd \
/app/k8s \
@@ -31,7 +30,8 @@ RUN rm -rf /app/Dockerfile* \
/app/requirements.txt \
/app/node_modules \
/app/*.json \
/app/test_*.py
RUN python3 manage.py collectstatic
/app/scripts \
/app/test_*.py && \
python3 /app/manage.py collectstatic --noinput
CMD ["gunicorn","--bind","0.0.0.0:8000","--workers","3","VorgabenUI.wsgi:application"]

View File

@@ -540,5 +540,5 @@ digraph {
Bei Fragen oder Problemen mit Diagrammen:
1. Code auf https://kroki.io/ testen
2. Syntax-Dokumentation des jeweiligen Diagrammtyps konsultieren
3. Diagramm-Cache leeren: `python manage.py clear_diagram_cache`
3. (Nur mit Shell-Zugriff auf Kubernetes-Pod möglich): Diagramm-Cache leeren: `python manage.py clear_diagram_cache`
4. Bei technischen Problemen: Information Security Management BIT kontaktieren

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

@@ -24,7 +24,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(os.environ.get("DEBUG", default=0)
DEBUG = bool(os.environ.get("DEBUG", default=0))
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS","127.0.0.1").split(",")

View File

@@ -127,7 +127,7 @@ USE_TZ = True
STATIC_URL = '/static/'
#STATIC_ROOT="/home/adebaumann/VorgabenUI/staticfiles/"
STATIC_ROOT="/app/staticfiles/"
STATIC_ROOT="staticfiles/"
STATICFILES_DIRS= (
os.path.join(BASE_DIR,"static"),
)
@@ -151,6 +151,24 @@ LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = 'login'
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {
"min_length": 12,
},
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
#LOGGING = {
# "version": 1,
# "handlers" :{

View File

@@ -18,14 +18,14 @@ spec:
fsGroupChangePolicy: "OnRootMismatch"
initContainers:
- name: loader
image: git.baumann.gr/adebaumann/vui-data-loader:0.10
image: git.baumann.gr/adebaumann/vui-data-loader:0.11
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.960
image: git.baumann.gr/adebaumann/vui:0.974
imagePullPolicy: Always
ports:
- containerPort: 8000

Binary file not shown.

Binary file not shown.

View File

@@ -94,9 +94,17 @@ class EinleitungInline(NestedStackedInline):
class VorgabeForm(forms.ModelForm):
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
class Meta:
model = Vorgabe
fields = '__all__'
def clean_thema(self):
"""Validate that thema is provided."""
thema = self.cleaned_data.get('thema')
if not thema:
raise forms.ValidationError('Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.')
return thema
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline):
model = Vorgabe

View File

@@ -0,0 +1,144 @@
from django.core.management.base import BaseCommand
import xml.etree.ElementTree as ET
from datetime import datetime
from dokumente.models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage
class Command(BaseCommand):
help = 'Export all dokumente as XML'
def add_arguments(self, parser):
parser.add_argument(
'--output',
type=str,
help='Output file path (default: stdout)',
)
def handle(self, *args, **options):
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
).order_by('nummer')
root = ET.Element('Vorgabendokumente')
for dokument in dokumente:
doc_element = ET.SubElement(root, 'Vorgabendokument')
ET.SubElement(doc_element, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else ""
ET.SubElement(doc_element, 'Nummer').text = dokument.nummer
ET.SubElement(doc_element, 'Name').text = dokument.name
autoren_element = ET.SubElement(doc_element, 'Autoren')
for autor in dokument.autoren.all():
ET.SubElement(autoren_element, 'Autor').text = autor.name
pruefende_element = ET.SubElement(doc_element, 'Pruefende')
for pruefender in dokument.pruefende.all():
ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name
gueltigkeit_element = ET.SubElement(doc_element, 'Gueltigkeit')
ET.SubElement(gueltigkeit_element, 'Von').text = dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else ""
ET.SubElement(gueltigkeit_element, 'Bis').text = dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
ET.SubElement(doc_element, 'SignaturCSO').text = dokument.signatur_cso
geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order')
if geltungsbereich_sections:
geltungsbereich_element = ET.SubElement(doc_element, 'Geltungsbereich')
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
for gb in geltungsbereich_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text")
section.text = gb.inhalt
einleitung_sections = dokument.einleitung_set.all().order_by('order')
if einleitung_sections:
einleitung_element = ET.SubElement(doc_element, 'Einleitung')
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
for ei in einleitung_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text")
section.text = ei.inhalt
ET.SubElement(doc_element, 'Ziel').text = ""
ET.SubElement(doc_element, 'Grundlagen').text = ""
changelog_element = ET.SubElement(doc_element, 'Changelog')
for cl in dokument.changelog.all().order_by('-datum'):
entry = ET.SubElement(changelog_element, 'Eintrag')
ET.SubElement(entry, 'Datum').text = cl.datum.strftime("%Y-%m-%d")
autoren = ET.SubElement(entry, 'Autoren')
for autor in cl.autoren.all():
ET.SubElement(autoren, 'Autor').text = autor.name
ET.SubElement(entry, 'Aenderung').text = cl.aenderung
anhaenge_element = ET.SubElement(doc_element, 'Anhaenge')
ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge
ET.SubElement(doc_element, 'Verantwortlich').text = "Information Security Management BIT"
ET.SubElement(doc_element, 'Klassifizierung').text = ""
glossar_element = ET.SubElement(doc_element, 'Glossar')
vorgaben_element = ET.SubElement(doc_element, 'Vorgaben')
for vorgabe in dokument.vorgaben.all().order_by('order'):
vorgabe_el = ET.SubElement(vorgaben_element, 'Vorgabe')
ET.SubElement(vorgabe_el, 'Nummer').text = str(vorgabe.nummer)
ET.SubElement(vorgabe_el, 'Titel').text = vorgabe.titel
ET.SubElement(vorgabe_el, 'Thema').text = vorgabe.thema.name if vorgabe.thema else ""
kurztext_sections = vorgabe.vorgabekurztext_set.all().order_by('order')
if kurztext_sections:
kurztext_element = ET.SubElement(vorgabe_el, 'Kurztext')
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
for kt in kurztext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text")
teil.text = kt.inhalt
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
if langtext_sections:
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
for lt in langtext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text")
teil.text = lt.inhalt
referenz_element = ET.SubElement(vorgabe_el, 'Referenzen')
for ref in vorgabe.referenzen.all():
ref_text = f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer
ET.SubElement(referenz_element, 'Referenz').text = ref_text
vorgabe_gueltigkeit = ET.SubElement(vorgabe_el, 'Gueltigkeit')
ET.SubElement(vorgabe_gueltigkeit, 'Von').text = vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else ""
ET.SubElement(vorgabe_gueltigkeit, 'Bis').text = vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
checklistenfragen_element = ET.SubElement(vorgabe_el, 'Checklistenfragen')
for cf in vorgabe.checklistenfragen.all():
ET.SubElement(checklistenfragen_element, 'Frage').text = cf.frage
stichworte_element = ET.SubElement(vorgabe_el, 'Stichworte')
for stw in vorgabe.stichworte.all():
ET.SubElement(stichworte_element, 'Stichwort').text = stw.stichwort
xml_str = ET.tostring(root, encoding='unicode', method='xml')
xml_output = self._prettify_xml(xml_str)
if options['output']:
with open(options['output'], 'w', encoding='utf-8') as f:
f.write(xml_output)
self.stdout.write(self.style.SUCCESS(f'XML exported to {options["output"]}'))
else:
self.stdout.write(xml_output)
def _prettify_xml(self, xml_string):
import xml.dom.minidom
dom = xml.dom.minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')

View File

@@ -71,6 +71,7 @@ class Command(BaseCommand):
"name": name,
"gueltigkeit_von": options["gueltigkeit_von"],
"gueltigkeit_bis": options["gueltigkeit_bis"],
"aktiv":False,
},
)
if created:
@@ -319,6 +320,7 @@ class Command(BaseCommand):
thema=thema,
titel=v["titel"],
gueltigkeit_von=timezone.now().date(),
order=0,
)
# Stichworte

View File

@@ -54,6 +54,34 @@ class Dokument(models.Model):
def __str__(self):
return f"{self.nummer} {self.name}"
@property
def dates(self):
"""
Returns an array of unique, chronologically sorted dates representing
state-change dates from all Vorgaben in this document.
These are dates where Vorgaben become active (gueltigkeit_von) or change state
(the day after gueltigkeit_bis). The very last date in the list is excluded
as it has no relevance (nothing changes after it).
"""
dates_set = set()
# Get all vorgaben for this document
for vorgabe in self.vorgaben.all():
# Add gueltigkeit_von (when vorgabe becomes active)
if vorgabe.gueltigkeit_von:
dates_set.add(vorgabe.gueltigkeit_von)
# Add the day after gueltigkeit_bis (when vorgabe expires/changes state)
# Only if gueltigkeit_bis is defined (not None)
if vorgabe.gueltigkeit_bis:
dates_set.add(vorgabe.gueltigkeit_bis + datetime.timedelta(days=1))
# Return sorted unique dates from oldest to newest, excluding the last date
# (but only if there are multiple dates; single dates are kept)
sorted_dates = sorted(list(dates_set))
return sorted_dates[:-1] if len(sorted_dates) > 1 else sorted_dates
class Meta:
verbose_name_plural="Dokumente"
verbose_name="Dokument"
@@ -141,6 +169,12 @@ class Vorgabe(models.Model):
"""
from django.core.exceptions import ValidationError
# Check that thema is provided
if not self.thema_id:
raise ValidationError({
'thema': 'Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.'
})
# Check for conflicts with existing Vorgaben
conflicts = self.find_conflicts()
if conflicts:

View File

@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block content %}
<h1>Alle Kommentare</h1>
{% if total_comments == 0 %}
<div class="alert alert-info">
<p>Es gibt noch keine Kommentare zu Vorgaben.</p>
<p><a href="{% url 'standard_list' %}">Zu den Standards</a></p>
</div>
{% else %}
<p class="text-muted">Insgesamt {{ total_comments }} Kommentar{{ total_comments|pluralize:"e" }}</p>
{% for dokument, comments in comments_by_document.items %}
<div class="panel panel-default" style="margin-top: 2rem;">
<div class="panel-heading">
<h2 style="margin: 0;">
<a href="{% url 'standard_detail' nummer=dokument.nummer %}">
{{ dokument.nummer }} {{ dokument.name }}
</a>
</h2>
<p style="margin: 0; color: #666; font-size: 0.9rem;">
{{ comments|length }} Kommentar{{ comments|length|pluralize:"e" }}
</p>
</div>
<div class="panel-body">
<div class="list-group">
{% for comment in comments %}
<div class="list-group-item" style="border-left: 3px solid #007bff; padding: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div style="flex: 1;">
<h4 style="margin: 0 0 0.5rem 0;">
<a href="{% url 'standard_detail' nummer=comment.vorgabe.dokument.nummer %}#{{ comment.vorgabe.Vorgabennummer }}">
{{ comment.vorgabe.Vorgabennummer }}
</a> {{ comment.vorgabe.titel }}
</h4>
<p style="margin: 0 0 0.75rem 0; color: #666; font-size: 0.9rem;">
<strong>Benutzer:</strong> {{ comment.user.first_name }} {{ comment.user.last_name }}<br>
<strong>Erstellt:</strong> {{ comment.created_at|date:"d.m.Y H:i" }}
{% if comment.updated_at != comment.created_at %}
<br>
<strong>Bearbeitet:</strong> {{ comment.updated_at|date:"d.m.Y H:i" }}
{% endif %}
</p>
</div>
<form method="POST" action="{% url 'delete_vorgabe_comment' comment.id %}"
style="display: inline; margin-left: 1rem;"
onsubmit="return confirm('Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?');">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-danger">Löschen</button>
</form>
</div>
<div style="background: #f8f9fa; padding: 0.75rem; border-radius: 4px; margin-top: 0.5rem; white-space: pre-wrap; word-wrap: break-word;">
{{ comment.text }}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
{% endif %}
<div style="margin-top: 2rem; padding-top: 2rem; border-top: 1px solid #ddd;">
<a href="{% url 'standard_list' %}" class="btn btn-default">Zu den Standards</a>
</div>
{% endblock %}

View File

@@ -16,9 +16,32 @@
{% if standard.history == True %}
<div class="alert alert-warning" role="alert">
{% if standard.is_future %}
<strong>Zukünftige Version vom {{ standard.check_date }}</strong>
{% else %}
<strong>Historische Version vom {{ standard.check_date }}</strong>
{% endif %}
</div>
{% endif %}
<!-- History Dates Dropdown -->
{% if standard.dates %}
<div class="mb-3">
<div class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" style="text-decoration: none;">
📅 Historische Versionen
</a>
<ul class="dropdown-menu" role="menu">
<li><a href="/dokumente/{{ standard.nummer }}/">Aktuelle Version</a></li>
<li class="divider"></li>
{% for date in standard.dates %}
<li><a href="/dokumente/{{ standard.nummer }}/history/{{ date|date:'Y-m-d' }}/">{{ date|date:'d.m.Y' }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- Einleitung -->
{% if standard.einleitung_html %}
<div class="row mb-4">
@@ -170,23 +193,28 @@
<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>
<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">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>
<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>
<a href="{% url 'standard_xml' standard.nummer %}"
class="btn btn-secondary icon icon--before icon--download"
download="{{ standard.nummer }}.xml">
XML herunterladen
</a>
</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block content %}
<h1>Meine Kommentare</h1>
{% if total_comments == 0 %}
<div class="alert alert-info">
<p>Sie haben noch keine Kommentare zu Vorgaben hinterlassen.</p>
<p><a href="{% url 'standard_list' %}">Zu den Standards</a></p>
</div>
{% else %}
<p class="text-muted">Insgesamt {{ total_comments }} Kommentar{{ total_comments|pluralize:"e" }}</p>
{% for dokument, comments in comments_by_document.items %}
<div class="panel panel-default" style="margin-top: 2rem;">
<div class="panel-heading">
<h2 style="margin: 0;">
<a href="{% url 'standard_detail' nummer=dokument.nummer %}">
{{ dokument.nummer }} {{ dokument.name }}
</a>
</h2>
<p style="margin: 0; color: #666; font-size: 0.9rem;">
{{ comments|length }} Kommentar{{ comments|length|pluralize:"e" }}
</p>
</div>
<div class="panel-body">
<div class="list-group">
{% for comment in comments %}
<div class="list-group-item" style="border-left: 3px solid #007bff; padding: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div style="flex: 1;">
<h4 style="margin: 0 0 0.5rem 0;">
<a href="{% url 'standard_detail' nummer=comment.vorgabe.dokument.nummer %}#{{ comment.vorgabe.Vorgabennummer }}">
{{ comment.vorgabe.Vorgabennummer }}
</a> {{ comment.vorgabe.titel }}
</h4>
<p style="margin: 0 0 0.75rem 0; color: #666; font-size: 0.9rem;">
<strong>Erstellt:</strong> {{ comment.created_at|date:"d.m.Y H:i" }}
{% if comment.updated_at != comment.created_at %}
<br>
<strong>Bearbeitet:</strong> {{ comment.updated_at|date:"d.m.Y H:i" }}
{% endif %}
</p>
</div>
<form method="POST" action="{% url 'delete_vorgabe_comment' comment.id %}"
style="display: inline; margin-left: 1rem;"
onsubmit="return confirm('Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?');">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-danger">Löschen</button>
</form>
</div>
<div style="background: #f8f9fa; padding: 0.75rem; border-radius: 4px; margin-top: 0.5rem; white-space: pre-wrap; word-wrap: break-word;">
{{ comment.text }}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
{% endif %}
<div style="margin-top: 2rem; padding-top: 2rem; border-top: 1px solid #ddd;">
<a href="{% url 'standard_list' %}" class="btn btn-default">Zu den Standards</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,960 @@
"""
Tests for the import-document management command.
This test suite covers:
- Basic import functionality
- Dry-run mode
- Purge functionality
- Error handling (missing file, dokumententyp, thema, abschnitttyp)
- Context switching (einleitung → geltungsbereich → vorgabe)
- Header normalization
- Vorgaben with Kurztext, Langtext, Stichworte, Checklistenfragen
- Edge cases and malformed input
"""
import os
import tempfile
from io import StringIO
from pathlib import Path
from django.test import TestCase
from django.core.management import call_command
from django.core.management.base import CommandError
from dokumente.models import (
Dokumententyp,
Dokument,
Thema,
Vorgabe,
VorgabeKurztext,
VorgabeLangtext,
Geltungsbereich,
Einleitung,
Checklistenfrage,
)
from abschnitte.models import AbschnittTyp
from stichworte.models import Stichwort
class ImportDocumentCommandTestCase(TestCase):
"""Test cases for the import-document management command"""
def setUp(self):
"""Set up test fixtures"""
# Create required Dokumententyp
self.dokumententyp = Dokumententyp.objects.create(
name="IT-Sicherheit",
verantwortliche_ve="TEST-VE"
)
# Create required AbschnittTyp instances
self.text_typ = AbschnittTyp.objects.create(abschnitttyp="text")
self.liste_geordnet_typ = AbschnittTyp.objects.create(
abschnitttyp="liste geordnet"
)
self.liste_ungeordnet_typ = AbschnittTyp.objects.create(
abschnitttyp="liste ungeordnet"
)
# Create test Themen
self.thema_organisation = Thema.objects.create(
name="Organisation",
erklaerung="Organisatorische Anforderungen"
)
self.thema_technik = Thema.objects.create(
name="Technik",
erklaerung="Technische Anforderungen"
)
# Additional Themen for r009.txt example
self.thema_informationen = Thema.objects.create(
name="Informationen",
erklaerung="Informationssicherheit"
)
self.thema_systeme = Thema.objects.create(
name="Systeme",
erklaerung="Systemanforderungen"
)
self.thema_anwendungen = Thema.objects.create(
name="Anwendungen",
erklaerung="Anwendungsanforderungen"
)
self.thema_zonen = Thema.objects.create(
name="Zonen",
erklaerung="Zonenanforderungen"
)
def create_test_file(self, content):
"""Helper to create a temporary test file with given content"""
fd, path = tempfile.mkstemp(suffix=".txt", text=True)
with os.fdopen(fd, 'w', encoding='utf-8') as f:
f.write(content)
return path
def test_basic_import_creates_document(self):
"""Test that basic import creates a document"""
test_content = """>>>Einleitung
>>>text
This is the introduction.
>>>geltungsbereich
>>>text
This is the scope.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Test Requirement
>>>Kurztext
>>>Text
Short description.
>>>Langtext
>>>Text
Long description.
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-001',
'--name', 'Test Document',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
# Check document was created
dokument = Dokument.objects.get(nummer='TEST-001')
self.assertEqual(dokument.name, 'Test Document')
self.assertEqual(dokument.dokumententyp, self.dokumententyp)
# Check output message
output = out.getvalue()
self.assertIn('Created Document TEST-001', output)
self.assertIn('Imported document TEST-001', output)
finally:
os.unlink(test_file)
def test_import_creates_einleitung(self):
"""Test that Einleitung sections are created"""
test_content = """>>>Einleitung
>>>text
This is the introduction text.
>>>geltungsbereich
>>>text
Scope text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-002',
'--name', 'Test Document 2',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-002')
einleitung = Einleitung.objects.filter(einleitung=dokument)
self.assertEqual(einleitung.count(), 1)
self.assertEqual(einleitung.first().inhalt, 'This is the introduction text.')
self.assertEqual(einleitung.first().abschnitttyp, self.text_typ)
finally:
os.unlink(test_file)
def test_import_creates_geltungsbereich(self):
"""Test that Geltungsbereich sections are created"""
test_content = """>>>geltungsbereich
>>>text
This standard applies to all servers.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-003',
'--name', 'Test Document 3',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-003')
geltungsbereich = Geltungsbereich.objects.filter(geltungsbereich=dokument)
self.assertEqual(geltungsbereich.count(), 1)
self.assertEqual(
geltungsbereich.first().inhalt,
'This standard applies to all servers.'
)
self.assertEqual(geltungsbereich.first().abschnitttyp, self.text_typ)
finally:
os.unlink(test_file)
def test_import_creates_vorgabe_with_all_fields(self):
"""Test creating a Vorgabe with all fields"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Complete Requirement
>>>Kurztext
>>>Text
Short text here.
>>>Langtext
>>>Text
Long text here.
>>>Stichworte
Testing, Management, Security
>>>Checkliste
Is the requirement met?
Has documentation been provided?
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-004',
'--name', 'Test Document 4',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-004')
vorgabe = Vorgabe.objects.get(dokument=dokument, nummer=1)
# Check basic fields
self.assertEqual(vorgabe.titel, 'Complete Requirement')
self.assertEqual(vorgabe.thema, self.thema_organisation)
# Check Kurztext
kurztext = VorgabeKurztext.objects.filter(abschnitt=vorgabe)
self.assertEqual(kurztext.count(), 1)
self.assertEqual(kurztext.first().inhalt, 'Short text here.')
# Check Langtext
langtext = VorgabeLangtext.objects.filter(abschnitt=vorgabe)
self.assertEqual(langtext.count(), 1)
self.assertEqual(langtext.first().inhalt, 'Long text here.')
# Check Stichworte
stichworte = vorgabe.stichworte.all()
self.assertEqual(stichworte.count(), 3)
stichwort_names = {s.stichwort for s in stichworte}
self.assertEqual(stichwort_names, {'Testing', 'Management', 'Security'})
# Check Checklistenfragen
fragen = Checklistenfrage.objects.filter(vorgabe=vorgabe)
self.assertEqual(fragen.count(), 2)
frage_texts = {f.frage for f in fragen}
self.assertEqual(frage_texts, {
'Is the requirement met?',
'Has documentation been provided?'
})
finally:
os.unlink(test_file)
def test_import_multiple_vorgaben(self):
"""Test importing multiple Vorgaben"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
First Requirement
>>>Kurztext
>>>Text
First requirement text.
>>>Vorgabe Technik
>>>Nummer 2
>>>Titel
Second Requirement
>>>Kurztext
>>>Text
Second requirement text.
>>>Vorgabe Organisation
>>>Nummer 3
>>>Titel
Third Requirement
>>>Kurztext
>>>Text
Third requirement text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-005',
'--name', 'Test Document 5',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-005')
vorgaben = Vorgabe.objects.filter(dokument=dokument).order_by('nummer')
self.assertEqual(vorgaben.count(), 3)
self.assertEqual(vorgaben[0].nummer, 1)
self.assertEqual(vorgaben[0].thema, self.thema_organisation)
self.assertEqual(vorgaben[1].nummer, 2)
self.assertEqual(vorgaben[1].thema, self.thema_technik)
self.assertEqual(vorgaben[2].nummer, 3)
self.assertEqual(vorgaben[2].thema, self.thema_organisation)
finally:
os.unlink(test_file)
def test_dry_run_creates_no_data(self):
"""Test that dry-run mode creates no database records"""
test_content = """>>>Einleitung
>>>text
Introduction text.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Test Requirement
>>>Kurztext
>>>Text
Short text.
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-DRY',
'--name', 'Dry Run Test',
'--dokumententyp', 'IT-Sicherheit',
'--dry-run',
stdout=out
)
# Document is created (for counting purposes) but not saved
output = out.getvalue()
self.assertIn('Dry run: no database changes will be made', output)
self.assertIn('Dry run complete', output)
# Check that Einleitung and Vorgabe were NOT created
dokument = Dokument.objects.get(nummer='TEST-DRY')
self.assertEqual(Einleitung.objects.filter(einleitung=dokument).count(), 0)
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 0)
finally:
os.unlink(test_file)
def test_dry_run_verbose_shows_details(self):
"""Test that dry-run with verbose shows detailed output"""
test_content = """>>>Einleitung
>>>text
Introduction.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Test
>>>Kurztext
>>>Text
Short.
>>>Langtext
>>>Text
Long.
>>>Stichworte
Keyword1, Keyword2
>>>Checkliste
Question 1?
Question 2?
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-VERBOSE',
'--name', 'Verbose Test',
'--dokumententyp', 'IT-Sicherheit',
'--dry-run',
'--verbose',
stdout=out
)
output = out.getvalue()
self.assertIn('[DRY RUN] Einleitung Abschnitt', output)
self.assertIn('[DRY RUN] Would create Vorgabe 1', output)
self.assertIn('Stichworte: Keyword1, Keyword2', output)
self.assertIn('Checkliste: Question 1?', output)
self.assertIn('Checkliste: Question 2?', output)
self.assertIn('Kurztext', output)
self.assertIn('Langtext', output)
finally:
os.unlink(test_file)
def test_purge_deletes_existing_content(self):
"""Test that --purge deletes existing content before import"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
New Requirement
>>>Kurztext
>>>Text
New text.
"""
test_file = self.create_test_file(test_content)
try:
# First import
call_command(
'import-document',
test_file,
'--nummer', 'TEST-PURGE',
'--name', 'Purge Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-PURGE')
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 1)
# Second import with different content and --purge
test_content_2 = """>>>Vorgabe Technik
>>>Nummer 2
>>>Titel
Replacement Requirement
>>>Kurztext
>>>Text
Replacement text.
"""
test_file_2 = self.create_test_file(test_content_2)
try:
out = StringIO()
call_command(
'import-document',
test_file_2,
'--nummer', 'TEST-PURGE',
'--name', 'Purge Test',
'--dokumententyp', 'IT-Sicherheit',
'--purge',
stdout=out
)
# Old Vorgabe should be deleted, only new one exists
vorgaben = Vorgabe.objects.filter(dokument=dokument)
self.assertEqual(vorgaben.count(), 1)
self.assertEqual(vorgaben.first().nummer, 2)
self.assertEqual(vorgaben.first().thema, self.thema_technik)
output = out.getvalue()
self.assertIn('Purged', output)
finally:
os.unlink(test_file_2)
finally:
os.unlink(test_file)
def test_purge_dry_run_shows_what_would_be_deleted(self):
"""Test that --purge with --dry-run shows deletion counts"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Original
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
# First import to create data
call_command(
'import-document',
test_file,
'--nummer', 'TEST-PURGE-DRY',
'--name', 'Purge Dry Test',
'--dokumententyp', 'IT-Sicherheit'
)
# Dry run with purge
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-PURGE-DRY',
'--name', 'Purge Dry Test',
'--dokumententyp', 'IT-Sicherheit',
'--purge',
'--dry-run',
stdout=out
)
output = out.getvalue()
self.assertIn('[DRY RUN] Would purge:', output)
self.assertIn('1 Vorgaben', output)
finally:
os.unlink(test_file)
def test_header_normalization(self):
"""Test that headers with hyphens are normalized correctly"""
test_content = """>>>geltungsbereich
>>>Liste-ungeordnet
Item 1
Item 2
Item 3
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-NORM',
'--name', 'Normalization Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-NORM')
geltungsbereich = Geltungsbereich.objects.get(geltungsbereich=dokument)
# Should have normalized "Liste-ungeordnet" to "liste ungeordnet"
self.assertEqual(geltungsbereich.abschnitttyp, self.liste_ungeordnet_typ)
finally:
os.unlink(test_file)
def test_missing_file_raises_error(self):
"""Test that missing file raises CommandError"""
with self.assertRaises(CommandError) as cm:
call_command(
'import-document',
'/nonexistent/file.txt',
'--nummer', 'TEST-ERR',
'--name', 'Error Test',
'--dokumententyp', 'IT-Sicherheit'
)
self.assertIn('does not exist', str(cm.exception))
def test_missing_dokumententyp_raises_error(self):
"""Test that missing Dokumententyp raises CommandError"""
test_content = """>>>geltungsbereich
>>>text
Text.
"""
test_file = self.create_test_file(test_content)
try:
with self.assertRaises(CommandError) as cm:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-ERR',
'--name', 'Error Test',
'--dokumententyp', 'NonExistentType'
)
self.assertIn('does not exist', str(cm.exception))
finally:
os.unlink(test_file)
def test_missing_thema_skips_vorgabe(self):
"""Test that missing Thema causes Vorgabe to be skipped with warning"""
test_content = """>>>Vorgabe NonExistentThema
>>>Nummer 1
>>>Titel
Test
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-SKIP',
'--name', 'Skip Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='TEST-SKIP')
# Vorgabe should NOT be created
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 0)
output = out.getvalue()
self.assertIn('not found, skipping Vorgabe', output)
finally:
os.unlink(test_file)
def test_missing_abschnitttyp_defaults_to_text(self):
"""Test that missing AbschnittTyp defaults to 'text' with warning"""
# Delete all but text type
AbschnittTyp.objects.exclude(abschnitttyp='text').delete()
test_content = """>>>geltungsbereich
>>>liste geordnet
Item 1
"""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-DEFAULT',
'--name', 'Default Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='TEST-DEFAULT')
geltungsbereich = Geltungsbereich.objects.get(geltungsbereich=dokument)
# Should default to 'text' type
self.assertEqual(geltungsbereich.abschnitttyp.abschnitttyp, 'text')
output = out.getvalue()
self.assertIn("not found; defaulting to 'text'", output)
finally:
os.unlink(test_file)
def test_inline_titel(self):
"""Test that inline title (on same line as header) is parsed"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Inline Title Here
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-INLINE',
'--name', 'Inline Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-INLINE')
vorgabe = Vorgabe.objects.get(dokument=dokument)
self.assertEqual(vorgabe.titel, 'Inline Title Here')
finally:
os.unlink(test_file)
def test_inline_stichworte(self):
"""Test that inline Stichworte (on same line as header) are parsed"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Test
>>>Stichworte Security, Testing, Compliance
>>>Kurztext
>>>Text
Text.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-INLINE-STW',
'--name', 'Inline Stichwort Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-INLINE-STW')
vorgabe = Vorgabe.objects.get(dokument=dokument)
stichworte = {s.stichwort for s in vorgabe.stichworte.all()}
self.assertEqual(stichworte, {'Security', 'Testing', 'Compliance'})
finally:
os.unlink(test_file)
def test_gueltigkeit_dates(self):
"""Test that validity dates are set correctly"""
test_content = """>>>geltungsbereich
>>>text
Scope.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-DATES',
'--name', 'Date Test',
'--dokumententyp', 'IT-Sicherheit',
'--gueltigkeit_von', '2024-01-01',
'--gueltigkeit_bis', '2024-12-31'
)
dokument = Dokument.objects.get(nummer='TEST-DATES')
self.assertEqual(str(dokument.gueltigkeit_von), '2024-01-01')
self.assertEqual(str(dokument.gueltigkeit_bis), '2024-12-31')
finally:
os.unlink(test_file)
def test_existing_document_updates(self):
"""Test that importing to existing document number shows warning"""
test_content = """>>>geltungsbereich
>>>text
First version.
"""
test_file = self.create_test_file(test_content)
try:
# First import
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-EXISTS',
'--name', 'Existing Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
output1 = out.getvalue()
self.assertIn('Created Document TEST-EXISTS', output1)
# Second import with same number
out2 = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-EXISTS',
'--name', 'Existing Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out2
)
output2 = out2.getvalue()
self.assertIn('already exists', output2)
finally:
os.unlink(test_file)
def test_multiple_kurztext_sections(self):
"""Test Vorgabe with multiple Kurztext sections"""
test_content = """>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Multiple Sections
>>>Kurztext
>>>Text
First kurztext section.
>>>Liste ungeordnet
Item A
Item B
>>>Langtext
>>>Text
Langtext.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-MULTI',
'--name', 'Multi Section Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-MULTI')
vorgabe = Vorgabe.objects.get(dokument=dokument)
kurztext_sections = VorgabeKurztext.objects.filter(abschnitt=vorgabe).order_by('id')
self.assertEqual(kurztext_sections.count(), 2)
self.assertEqual(kurztext_sections[0].abschnitttyp.abschnitttyp, 'text')
self.assertEqual(kurztext_sections[1].abschnitttyp.abschnitttyp, 'liste ungeordnet')
finally:
os.unlink(test_file)
def test_empty_file(self):
"""Test importing an empty file"""
test_content = ""
test_file = self.create_test_file(test_content)
try:
out = StringIO()
call_command(
'import-document',
test_file,
'--nummer', 'TEST-EMPTY',
'--name', 'Empty Test',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='TEST-EMPTY')
# Document created but no content
self.assertEqual(Einleitung.objects.filter(einleitung=dokument).count(), 0)
self.assertEqual(Geltungsbereich.objects.filter(geltungsbereich=dokument).count(), 0)
self.assertEqual(Vorgabe.objects.filter(dokument=dokument).count(), 0)
output = out.getvalue()
self.assertIn('with 0 Vorgaben', output)
finally:
os.unlink(test_file)
def test_unicode_content(self):
"""Test that Unicode characters (German umlauts, etc.) are handled correctly"""
test_content = """>>>Einleitung
>>>text
Übersicht über die Sicherheitsanforderungen für IT-Systeme.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel
Überprüfung der Systemkonfiguration
>>>Kurztext
>>>Text
Die Konfiguration muss regelmäßig überprüft werden.
>>>Stichworte
Überprüfung, Sicherheit, Qualität
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-UNICODE',
'--name', 'Unicode Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-UNICODE')
# Check Einleitung
einleitung = Einleitung.objects.get(einleitung=dokument)
self.assertIn('Übersicht', einleitung.inhalt)
# Check Vorgabe
vorgabe = Vorgabe.objects.get(dokument=dokument)
self.assertEqual(vorgabe.titel, 'Überprüfung der Systemkonfiguration')
# Check Kurztext
kurztext = VorgabeKurztext.objects.get(abschnitt=vorgabe)
self.assertIn('regelmäßig', kurztext.inhalt)
# Check Stichworte
stichworte = {s.stichwort for s in vorgabe.stichworte.all()}
self.assertIn('Überprüfung', stichworte)
finally:
os.unlink(test_file)
def test_context_switching(self):
"""Test that context switches correctly between sections"""
test_content = """>>>Einleitung
>>>text
Intro text 1.
>>>text
Intro text 2.
>>>geltungsbereich
>>>text
Scope text 1.
>>>text
Scope text 2.
>>>Vorgabe Organisation
>>>Nummer 1
>>>Titel Test
>>>Kurztext
>>>text
Kurztext 1.
>>>text
Kurztext 2.
>>>Langtext
>>>text
Langtext 1.
"""
test_file = self.create_test_file(test_content)
try:
call_command(
'import-document',
test_file,
'--nummer', 'TEST-CONTEXT',
'--name', 'Context Test',
'--dokumententyp', 'IT-Sicherheit'
)
dokument = Dokument.objects.get(nummer='TEST-CONTEXT')
# Check Einleitung has 2 sections
einleitung = Einleitung.objects.filter(einleitung=dokument)
self.assertEqual(einleitung.count(), 2)
# Check Geltungsbereich has 2 sections
geltungsbereich = Geltungsbereich.objects.filter(geltungsbereich=dokument)
self.assertEqual(geltungsbereich.count(), 2)
# Check Vorgabe has correct Kurztext and Langtext counts
vorgabe = Vorgabe.objects.get(dokument=dokument)
kurztext = VorgabeKurztext.objects.filter(abschnitt=vorgabe)
langtext = VorgabeLangtext.objects.filter(abschnitt=vorgabe)
self.assertEqual(kurztext.count(), 2)
self.assertEqual(langtext.count(), 1)
finally:
os.unlink(test_file)
def test_real_world_example(self):
"""Test importing the real r009.txt example document"""
# Use the actual example file
example_file = Path(__file__).parent.parent / 'Documentation' / 'import formats' / 'r009.txt'
if not example_file.exists():
self.skipTest("r009.txt example file not found")
out = StringIO()
call_command(
'import-document',
str(example_file),
'--nummer', 'R009',
'--name', 'IT-Sicherheit Serversysteme',
'--dokumententyp', 'IT-Sicherheit',
stdout=out
)
dokument = Dokument.objects.get(nummer='R009')
# Check that Einleitung was created
self.assertGreater(Einleitung.objects.filter(einleitung=dokument).count(), 0)
# Check that Geltungsbereich was created
self.assertGreater(Geltungsbereich.objects.filter(geltungsbereich=dokument).count(), 0)
# Check that multiple Vorgaben were created (r009.txt has 23 Vorgaben)
vorgaben = Vorgabe.objects.filter(dokument=dokument)
self.assertGreaterEqual(vorgaben.count(), 20)
# Verify output message
output = out.getvalue()
self.assertIn('Imported document R009', output)

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,14 @@ from . import views
urlpatterns = [
path('', views.standard_list, name='standard_list'),
path('unvollstaendig/', views.incomplete_vorgaben, name='incomplete_vorgaben'),
path('meine-kommentare/', views.user_comments, name='user_comments'),
path('alle-kommentare/', views.all_comments, name='all_comments'),
path('<str:nummer>/', views.standard_detail, name='standard_detail'),
path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
path('<str:nummer>/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('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),

View File

@@ -1,12 +1,13 @@
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse
from django.http import JsonResponse, HttpResponse
from django.core.serializers.json import DjangoJSONEncoder
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 json
import xml.etree.ElementTree as ET
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from abschnitte.utils import render_textabschnitte
@@ -29,9 +30,11 @@ def standard_detail(request, nummer,check_date=""):
if check_date:
check_date = calendar.parseDT(check_date)[0].date()
standard.history = True
standard.is_future = check_date > date.today()
else:
check_date = date.today()
standard.history = False
standard.is_future = False
standard.check_date=check_date
vorgaben = list(standard.vorgaben.order_by("thema","nummer").select_related("thema","dokument")) # convert queryset to list so we can attach attributes
@@ -252,6 +255,138 @@ def standard_json(request, nummer):
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
def standard_xml(request, nummer):
"""
Export a single Dokument as XML
"""
# Get the document with all related data
dokument = get_object_or_404(
Dokument.objects.prefetch_related(
'autoren', 'pruefende', 'vorgaben__thema',
'vorgaben__referenzen', 'vorgaben__stichworte',
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
'einleitung_set', 'changelog__autoren'
),
nummer=nummer
)
root = ET.Element('Vorgabendokument')
ET.SubElement(root, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else ""
ET.SubElement(root, 'Nummer').text = dokument.nummer
ET.SubElement(root, 'Name').text = dokument.name
autoren_element = ET.SubElement(root, 'Autoren')
for autor in dokument.autoren.all():
ET.SubElement(autoren_element, 'Autor').text = autor.name
pruefende_element = ET.SubElement(root, 'Pruefende')
for pruefender in dokument.pruefende.all():
ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name
gueltigkeit_element = ET.SubElement(root, 'Gueltigkeit')
ET.SubElement(gueltigkeit_element, 'Von').text = dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else ""
ET.SubElement(gueltigkeit_element, 'Bis').text = dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
ET.SubElement(root, 'SignaturCSO').text = dokument.signatur_cso
geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order')
if geltungsbereich_sections:
geltungsbereich_element = ET.SubElement(root, 'Geltungsbereich')
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
for gb in geltungsbereich_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text")
section.text = gb.inhalt
einleitung_sections = dokument.einleitung_set.all().order_by('order')
if einleitung_sections:
einleitung_element = ET.SubElement(root, 'Einleitung')
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
for ei in einleitung_sections:
section = ET.SubElement(abschnitt_element, 'Teil')
section.set('typ', ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text")
section.text = ei.inhalt
ET.SubElement(root, 'Ziel').text = ""
ET.SubElement(root, 'Grundlagen').text = ""
changelog_element = ET.SubElement(root, 'Changelog')
for cl in dokument.changelog.all().order_by('-datum'):
entry = ET.SubElement(changelog_element, 'Eintrag')
ET.SubElement(entry, 'Datum').text = cl.datum.strftime("%Y-%m-%d")
autoren = ET.SubElement(entry, 'Autoren')
for autor in cl.autoren.all():
ET.SubElement(autoren, 'Autor').text = autor.name
ET.SubElement(entry, 'Aenderung').text = cl.aenderung
anhaenge_element = ET.SubElement(root, 'Anhaenge')
ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge
ET.SubElement(root, 'Verantwortlich').text = "Information Security Management BIT"
ET.SubElement(root, 'Klassifizierung').text = ""
glossar_element = ET.SubElement(root, 'Glossar')
vorgaben_element = ET.SubElement(root, 'Vorgaben')
for vorgabe in dokument.vorgaben.all().order_by('order'):
vorgabe_el = ET.SubElement(vorgaben_element, 'Vorgabe')
ET.SubElement(vorgabe_el, 'Nummer').text = str(vorgabe.nummer)
ET.SubElement(vorgabe_el, 'Titel').text = vorgabe.titel
ET.SubElement(vorgabe_el, 'Thema').text = vorgabe.thema.name if vorgabe.thema else ""
kurztext_sections = vorgabe.vorgabekurztext_set.all().order_by('order')
if kurztext_sections:
kurztext_element = ET.SubElement(vorgabe_el, 'Kurztext')
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
for kt in kurztext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text")
teil.text = kt.inhalt
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
if langtext_sections:
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
for lt in langtext_sections:
teil = ET.SubElement(abschnitt, 'Teil')
teil.set('typ', lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text")
teil.text = lt.inhalt
referenz_element = ET.SubElement(vorgabe_el, 'Referenzen')
for ref in vorgabe.referenzen.all():
ref_text = f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer
ET.SubElement(referenz_element, 'Referenz').text = ref_text
vorgabe_gueltigkeit = ET.SubElement(vorgabe_el, 'Gueltigkeit')
ET.SubElement(vorgabe_gueltigkeit, 'Von').text = vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else ""
ET.SubElement(vorgabe_gueltigkeit, 'Bis').text = vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
checklistenfragen_element = ET.SubElement(vorgabe_el, 'Checklistenfragen')
for cf in vorgabe.checklistenfragen.all():
ET.SubElement(checklistenfragen_element, 'Frage').text = cf.frage
stichworte_element = ET.SubElement(vorgabe_el, 'Stichworte')
for stw in vorgabe.stichworte.all():
ET.SubElement(stichworte_element, 'Stichwort').text = stw.stichwort
xml_str = ET.tostring(root, encoding='unicode', method='xml')
xml_output = _prettify_xml(xml_str)
response = HttpResponse(xml_output, content_type='application/xml; charset=utf-8')
response['Content-Disposition'] = f'attachment; filename="{dokument.nummer}.xml"'
return response
def _prettify_xml(xml_string):
import xml.dom.minidom
dom = xml.dom.minidom.parseString(xml_string)
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')
@login_required
def get_vorgabe_comments(request, vorgabe_id):
"""Get comments for a specific Vorgabe"""
@@ -366,3 +501,57 @@ def delete_vorgabe_comment(request, comment_id):
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
@login_required
def user_comments(request):
"""
Display all comments made by the logged-in user, grouped by document.
"""
# Get all comments by the current user
user_comments = VorgabeComment.objects.filter(
user=request.user
).select_related('vorgabe', 'vorgabe__dokument').order_by(
'vorgabe__dokument__nummer', '-created_at'
)
# Group comments by document
comments_by_document = {}
for comment in user_comments:
dokument = comment.vorgabe.dokument
if dokument not in comments_by_document:
comments_by_document[dokument] = []
comments_by_document[dokument].append(comment)
return render(request, 'standards/user_comments.html', {
'comments_by_document': comments_by_document,
'total_comments': user_comments.count(),
})
@login_required
@user_passes_test(is_staff_user)
def all_comments(request):
"""
Display all comments from all users, grouped by document.
Staff only.
"""
# Get all comments
all_comments_qs = VorgabeComment.objects.select_related(
'vorgabe', 'vorgabe__dokument', 'user'
).order_by(
'vorgabe__dokument__nummer', '-created_at'
)
# Group comments by document
comments_by_document = {}
for comment in all_comments_qs:
dokument = comment.vorgabe.dokument
if dokument not in comments_by_document:
comments_by_document[dokument] = []
comments_by_document[dokument].append(comment)
return render(request, 'standards/all_comments.html', {
'comments_by_document': comments_by_document,
'total_comments': all_comments_qs.count(),
})

View File

@@ -24,8 +24,8 @@ spec:
- name: data
mountPath: /data
containers:
- name: web
image: docker.io/adebaumann/vui:0.917
- name: web
image: docker.io/adebaumann/vui:0.918
imagePullPolicy: Always
ports:
- containerPort: 8000

View File

@@ -48,21 +48,25 @@
<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.username }}</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>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a href="{% url 'user_comments' %}">Meine Kommentare</a></li>
{% if user.is_staff %}
<li><a href="{% url 'all_comments' %}">Alle Kommentare</a></li>
{% endif %}
<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 %}
@@ -102,7 +106,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>
<li><a href="/autorenumgebung/">Autor</a></li>
{% endif %}
<li><a href="/referenzen">Referenzen</a></li>
<li><a href="/stichworte">Stichworte</a></li>
@@ -133,7 +137,7 @@
<a href="/dokumente/unvollstaendig/">Unvollständig</a>
</li>
<li class="dropdown {% if 'autorenumgebung' in request.path %}current{% endif %}">
<a href="/autorenumgebung/">Autorenumgebung</a>
<a href="/autorenumgebung/">Autor</a>
</li>
{% endif %}
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
@@ -215,8 +219,8 @@
</p>
</div>
<div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.960" }}</p>
</div>
<p class="text-muted">Version {{ version|default:"0.973" }}</p>
</div>
</div>
</div>
</footer>

View File

@@ -5,7 +5,7 @@ certifi==2025.8.3
charset-normalizer==3.4.3
curtsies==0.4.3
cwcwidth==0.1.10
Django==5.2.5
Django==5.2.9
django-admin-sortable2==2.2.8
django-js-asset==3.1.2
django-mptt==0.17.0
@@ -30,6 +30,7 @@ pyxdg==0.28
requests==2.32.5
six==1.17.0
sqlparse==0.5.3
urllib3==2.5.0
urllib3==2.6.0
wcwidth==0.2.13
bleach==6.1.0
coverage==7.6.1

45
scripts/deploy_secret.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Generate and deploy Django secret key to Kubernetes
NAMESPACE="vorgabenui"
SECRET_NAME="django-secret"
SECRET_FILE="argocd/secret.yaml"
# Check if secret file exists
if [ ! -f "$SECRET_FILE" ]; then
echo "Error: $SECRET_FILE not found"
exit 1
fi
# Generate random secret key
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")
# Create temporary secret file with generated key
TEMP_SECRET_FILE=$(mktemp)
cat "$SECRET_FILE" | sed "s/CHANGE_ME_TO_RANDOM_STRING/$SECRET_KEY/g" > "$TEMP_SECRET_FILE"
# Check if secret already exists
if kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" &>/dev/null; then
echo "Secret $SECRET_NAME already exists in namespace $NAMESPACE"
read -p "Do you want to replace it? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted"
rm "$TEMP_SECRET_FILE"
exit 0
fi
kubectl apply -f "$TEMP_SECRET_FILE"
echo "Secret updated successfully"
else
kubectl apply -f "$TEMP_SECRET_FILE"
echo "Secret created successfully"
fi
# Clean up
rm "$TEMP_SECRET_FILE"
echo ""
echo "Secret deployed:"
echo " Name: $SECRET_NAME"
echo " Namespace: $NAMESPACE"
echo " Key: secret-key"

45
scripts/full_deploy.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Full deployment script - bumps both container versions by 0.001 and copies database
DEPLOYMENT_FILE="argocd/deployment.yaml"
DB_SOURCE="data/db.sqlite3"
DB_DEST="data-loader/preload.sqlite3"
# Check if deployment file exists
if [ ! -f "$DEPLOYMENT_FILE" ]; then
echo "Error: $DEPLOYMENT_FILE not found"
exit 1
fi
# Check if source database exists
if [ ! -f "$DB_SOURCE" ]; then
echo "Error: $DB_SOURCE not found"
exit 1
fi
# Extract current version of data-loader
LOADER_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vgui-data-loader:[0-9]" "$DEPLOYMENT_FILE" | sed -E 's/.*:([0-9.]+)/\1/')
# Extract current version of main container
MAIN_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vgui:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
if [ -z "$LOADER_VERSION" ] || [ -z "$MAIN_VERSION" ]; then
echo "Error: Could not find current versions"
exit 1
fi
# Calculate new versions (add 0.001), preserve leading zero
NEW_LOADER_VERSION=$(echo "$LOADER_VERSION + 0.001" | bc | sed 's/^\./0./')
NEW_MAIN_VERSION=$(echo "$MAIN_VERSION + 0.001" | bc | sed 's/^\./0./')
# Update the deployment file
sed -i "s|image: git.baumann.gr/adebaumann/labhelper-data-loader:$LOADER_VERSION|image: git.baumann.gr/adebaumann/labhelper-data-loader:$NEW_LOADER_VERSION|" "$DEPLOYMENT_FILE"
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE"
# Copy database
cp "$DB_SOURCE" "$DB_DEST"
echo "Full deployment prepared:"
echo " Data loader: $LOADER_VERSION -> $NEW_LOADER_VERSION"
echo " Main container: $MAIN_VERSION -> $NEW_MAIN_VERSION"
echo " Database copied to $DB_DEST"

27
scripts/partial_deploy.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Partial deployment script - bumps main container version by 0.001
DEPLOYMENT_FILE="argocd/deployment.yaml"
# Check if file exists
if [ ! -f "$DEPLOYMENT_FILE" ]; then
echo "Error: $DEPLOYMENT_FILE not found"
exit 1
fi
# Extract current version of main container (labhelper, not labhelper-data-loader)
CURRENT_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vui:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
if [ -z "$CURRENT_VERSION" ]; then
echo "Error: Could not find current version"
exit 1
fi
# Calculate new version (add 0.001), preserve leading zero
NEW_VERSION=$(echo "$CURRENT_VERSION + 0.001" | bc | sed 's/^\./0./')
# Update the deployment file (only the main container, not the data-loader)
sed -i "s|image: git.baumann.gr/adebaumann/vui:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/vui:$NEW_VERSION|" "$DEPLOYMENT_FILE"
echo "Partial deployment prepared:"
echo " Main container: $CURRENT_VERSION -> $NEW_VERSION"

0
test Normal file
View File