Compare commits

...

54 Commits

Author SHA1 Message Date
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
3a89f6d871 Full name on comments 2025-12-01 10:55:46 +01:00
07ba717de9 Display name changed from username to full name 2025-11-28 14:41:24 +01:00
048105ef27 Comment sorting changed, Comments added to test suite.
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
2025-11-28 09:55:35 +01:00
b579f5fb42 Admin interface for comments
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 1m6s
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 10s
2025-11-28 00:13:07 +01:00
db9bd92036 Try/except-error fixed
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 36s
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-11-27 23:57:35 +01:00
7e89ffb6f1 XSS protection added to comments
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 1m1s
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-11-27 23:51:04 +01:00
dd6d0fae46 Comments migrated into database and data-loader-container. Deploying as soon as merged.
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 1m15s
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 30s
2025-11-27 23:23:51 +01:00
e5202d9b2b Comment function added 2025-11-27 23:11:59 +01:00
5535684a45 Deploy
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 33s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
2025-11-27 15:47:31 +01:00
f933b7d99a XSS prevention added (with tests) 2025-11-27 15:43:41 +01:00
fd729b3019 Merge pull request 'feature/nfs-storage' (#14) from feature/nfs-storage into development
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 14s
Reviewed-on: #14
2025-11-24 15:35:02 +00:00
e1c1eafb39 openspec updated 2025-11-24 16:32:27 +01:00
1b016c49f2 ArgoCD-Documentation added 2025-11-24 15:55:27 +01:00
4376069b11 NFS pointed to wrong place 2025-11-24 15:37:12 +01:00
c285ae81af Test with NFS
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 15:20:31 +01:00
5bfe4866a4 Deploy version 0.955
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 5s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 14s
2025-11-24 13:48:35 +01:00
f7799675d5 Typo in template fixed 2025-11-24 13:46:13 +01:00
c125427b8d ArgoCD resolved 2025-11-24 13:43:00 +01:00
a14a80f7bd Design tweaks
All checks were successful
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 15s
2025-11-24 13:38:53 +01:00
477143b3ff Merge pull request 'fix: add argocd ignore-healthcheck and ingressClassName to Ingress' (#13) from improvements/argocd-service-fix into development
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 4s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/vui-data-loader) (push) Successful in 4s
Reviewed-on: #13
2025-11-24 11:02:52 +00:00
fc404f6755 Merge pull request 'troubleshooting ingress' (#12) from improvements/frontend into development
Reviewed-on: #12
2025-11-24 10:56:18 +00:00
fe7c55eceb Merge branch 'development' into improvements/frontend 2025-11-24 10:56:10 +00:00
38ce55d8fd troubleshooting ingress 2025-11-24 10:22:09 +00:00
38 changed files with 3376 additions and 72 deletions

View File

@@ -211,32 +211,60 @@ jobs:
echo "ERROR: Found $ctype \"$cname\" image repo is \"$new_repo\" but expected \"$expected_repo\"" echo "ERROR: Found $ctype \"$cname\" image repo is \"$new_repo\" but expected \"$expected_repo\""
exit 1 exit 1
fi fi
if [ -n "${old_image:-}" ]; then
old_tag="${old_image##*:}"
else
old_tag=""
fi
registry="$(echo "$new_repo" | awk -F/ '{print $1}')" 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_image=$new_image"
echo "new_repo=$new_repo" echo "new_repo=$new_repo"
echo "new_tag=$new_tag" echo "new_tag=$new_tag"
echo "registry=$registry" echo "registry=$registry"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: Skip if tag unchanged - name: Check if image exists on registry
if: steps.img.outputs.changed != 'true' id: check_image
run: echo "${{ matrix.description }} image tag unchanged; skipping build." 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 - name: Set up Buildx
if: steps.img.outputs.changed == 'true' if: steps.check_image.outputs.exists == 'false'
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Log in to registry - name: Log in to registry
if: steps.img.outputs.changed == 'true' if: steps.check_image.outputs.exists == 'false'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ steps.img.outputs.registry }} registry: ${{ steps.img.outputs.registry }}
@@ -244,7 +272,7 @@ jobs:
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push ${{ matrix.description }} (exact tag from deployment) - 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 uses: docker/build-push-action@v6
with: with:
context: ${{ matrix.build_context }} 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 RUN mkdir /app
WORKDIR /app WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
@@ -7,22 +7,21 @@ RUN pip install --upgrade pip
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.13-slim FROM python:3.14-slim
RUN useradd -m -r appuser && \ RUN useradd -m -r appuser && \
mkdir /app && \ mkdir /app && \
chown -R appuser /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/ COPY --from=baustelle /usr/local/bin/ /usr/local/bin/
RUN rm /usr/bin/tar RUN rm /usr/bin/tar /usr/lib/x86_64-linux-gnu/libncur*
RUN rm /usr/lib/x86_64-linux-gnu/libncur*
WORKDIR /app WORKDIR /app
COPY --chown=appuser:appuser . . COPY --chown=appuser:appuser . .
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
USER appuser USER appuser
EXPOSE 8000 EXPOSE 8000
RUN rm -rf /app/Dockerfile* \ RUN rm -rvf /app/Dockerfile* \
/app/README.md \ /app/README.md \
/app/argocd \ /app/argocd \
/app/k8s \ /app/k8s \
@@ -31,7 +30,7 @@ RUN rm -rf /app/Dockerfile* \
/app/requirements.txt \ /app/requirements.txt \
/app/node_modules \ /app/node_modules \
/app/*.json \ /app/*.json \
/app/test_*.py /app/test_*.py && \
RUN python3 manage.py collectstatic python3 /app/manage.py collectstatic --noinput
CMD ["gunicorn","--bind","0.0.0.0:8000","--workers","3","VorgabenUI.wsgi:application"] CMD ["gunicorn","--bind","0.0.0.0:8000","--workers","3","VorgabenUI.wsgi:application"]

241
Documentation/ArgoCD.md Normal file
View File

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

544
Documentation/modelle.md Normal file
View File

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

View File

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

View File

@@ -15,7 +15,7 @@ Dieses Dokument bietet einen umfassenden Überblick über alle Tests im vgui-cic
## abschnitte App Tests ## abschnitte App Tests
Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-Caching und Management-Befehle abdecken. Die abschnitte App enthält 33 Tests, die Modelle, Utility-Funktionen, Diagram-Caching, Management-Befehle und Sicherheit abdecken.
### Modell-Tests ### Modell-Tests
@@ -58,6 +58,7 @@ Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-C
- **test_render_text_with_footnotes**: Verarbeitet Text, der Fußnoten enthält - **test_render_text_with_footnotes**: Verarbeitet Text, der Fußnoten enthält
- **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp - **test_render_abschnitt_without_type**: Behandelt Textabschnitte ohne AbschnittTyp
- **test_render_abschnitt_with_empty_content**: Behandelt Textabschnitte mit leerem Inhalt - **test_render_abschnitt_with_empty_content**: Behandelt Textabschnitte mit leerem Inhalt
- **test_render_textabschnitte_xss_prevention**: Überprüft, dass bösartiger HTML-Code und Skript-Tags aus gerenderten Inhalten bereinigt werden, um XSS-Angriffe zu verhindern
### Diagram-Caching-Tests ### Diagram-Caching-Tests
@@ -86,7 +87,7 @@ Die abschnitte App enthält 32 Tests, die Modelle, Utility-Funktionen, Diagram-C
## dokumente App Tests ## dokumente App Tests
Die dokumente App enthält 98 Tests und ist damit die umfassendste Test-Suite, die alle Modelle, Views, URLs und Geschäftslogik abdeckt. Die dokumente App enthält 121 Tests und ist damit die umfassendste Test-Suite, die alle Modelle, Views, URLs, Geschäftslogik und Kommentarfunktionalität mit XSS-Schutz abdeckt.
### Modell-Tests ### Modell-Tests
@@ -130,6 +131,14 @@ Die dokumente App enthält 98 Tests und ist damit die umfassendste Test-Suite, d
- **test_checklistenfrage_str**: Überprüft, dass die String-Repräsentation lange Fragen kürzt - **test_checklistenfrage_str**: Überprüft, dass die String-Repräsentation lange Fragen kürzt
- **test_checklistenfrage_related_name**: Testet die umgekehrte Beziehung von Vorgabe - **test_checklistenfrage_related_name**: Testet die umgekehrte Beziehung von Vorgabe
#### VorgabeCommentModelTest
- **test_comment_creation**: Testet die Erstellung von VorgabeComment mit Vorgabe, Benutzer und Text
- **test_comment_str**: Überprüft, dass die String-Repräsentation Benutzername und Vorgabennummer enthält
- **test_comment_related_name**: Testet die umgekehrte Beziehung von Vorgabe
- **test_comment_ordering**: Testet, dass Kommentare nach created_at absteigend sortiert sind (neueste zuerst)
- **test_comment_timestamps_auto_update**: Testet, dass sich updated_at ändert, wenn ein Kommentar geändert wird
- **test_multiple_users_can_comment**: Testet, dass mehrere Benutzer zur selben Vorgabe kommentieren können
### Text-Abschnitt-Tests ### Text-Abschnitt-Tests
#### DokumentTextAbschnitteTest #### DokumentTextAbschnitteTest
@@ -216,6 +225,40 @@ Die dokumente App enthält 98 Tests und ist damit die umfassendste Test-Suite, d
- **test_vorgabe_links**: Testet, dass Vorgaben zu korrekten Admin-Seiten verlinken - **test_vorgabe_links**: Testet, dass Vorgaben zu korrekten Admin-Seiten verlinken
- **test_back_link**: Testet, dass der Zurück-Link zur Standardübersicht existiert - **test_back_link**: Testet, dass der Zurück-Link zur Standardübersicht existiert
### Kommentar-Funktionalität Tests
#### GetVorgabeCommentsViewTest
- **test_get_comments_requires_login**: Testet, dass anonyme Benutzer keine Kommentare sehen können und weitergeleitet werden
- **test_regular_user_sees_only_own_comments**: Testet, dass normale Benutzer nur ihre eigenen Kommentare sehen
- **test_staff_user_sees_all_comments**: Testet, dass Staff-Benutzer alle Kommentare sehen können
- **test_get_comments_returns_404_for_nonexistent_vorgabe**: Testet 404-Antwort für nicht existierende Vorgabe
- **test_comments_are_html_escaped**: Testet HTML-Escaping zur Verhinderung von XSS-Angriffen (z.B. `<script>`-Tags)
- **test_line_breaks_preserved**: Testet, dass Zeilenumbrüche in `<br>`-Tags umgewandelt werden
- **test_security_headers_present**: Testet, dass Content-Security-Policy und X-Content-Type-Options Header gesetzt sind
#### AddVorgabeCommentViewTest
- **test_add_comment_requires_login**: Testet, dass anonyme Benutzer keine Kommentare hinzufügen können
- **test_add_comment_requires_post**: Testet, dass nur POST-Methode erlaubt ist (405 für GET)
- **test_add_comment_success**: Testet erfolgreiche Kommentarerstellung mit gültigen Daten
- **test_add_empty_comment_fails**: Testet, dass leere Kommentare mit 400-Fehler abgelehnt werden
- **test_add_whitespace_only_comment_fails**: Testet, dass Kommentare nur mit Leerzeichen abgelehnt werden
- **test_add_too_long_comment_fails**: Testet, dass Kommentare über 2000 Zeichen abgelehnt werden
- **test_add_comment_xss_script_tag_blocked**: Testet, dass Kommentare mit `<script>`-Tags blockiert werden
- **test_add_comment_xss_javascript_protocol_blocked**: Testet, dass `javascript:`-Protokoll blockiert wird
- **test_add_comment_xss_event_handlers_blocked**: Testet, dass Event-Handler (onload, onerror, onclick, onmouseover) blockiert werden
- **test_add_comment_invalid_json_fails**: Testet, dass ungültige JSON-Payloads abgelehnt werden
- **test_add_comment_nonexistent_vorgabe_fails**: Testet 404-Antwort für nicht existierende Vorgabe
- **test_add_comment_security_headers**: Testet, dass Sicherheits-Header in Antworten vorhanden sind
#### DeleteVorgabeCommentViewTest
- **test_delete_comment_requires_login**: Testet, dass anonyme Benutzer keine Kommentare löschen können
- **test_delete_comment_requires_post**: Testet, dass nur POST-Methode erlaubt ist (405 für GET)
- **test_user_can_delete_own_comment**: Testet, dass Benutzer ihre eigenen Kommentare löschen können
- **test_user_cannot_delete_other_users_comment**: Testet, dass Benutzer keine Kommentare anderer löschen können (403 Forbidden)
- **test_staff_can_delete_any_comment**: Testet, dass Staff-Benutzer jeden Kommentar löschen können
- **test_delete_nonexistent_comment_returns_404**: Testet 404-Antwort für nicht existierenden Kommentar
- **test_delete_comment_security_headers**: Testet, dass Sicherheits-Header in Antworten vorhanden sind
--- ---
## pages App Tests ## pages App Tests
@@ -332,9 +375,17 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
## Test-Statistiken ## Test-Statistiken
- **Gesamt-Tests**: 206 - **Gesamt-Tests**: 230
- **abschnitte**: 32 Tests - **abschnitte**: 33 Tests (einschließlich XSS-Prävention)
- **dokumente**: 116 Tests (98 in tests.py + 9 in test_json.py + 9 JSON-Tests in Haupt-tests.py) - **dokumente**: 121 Tests (einschließlich Kommentarfunktionalität mit XSS-Schutz)
- Modell-Tests: 44 Tests
- View-Tests: 7 Tests
- URL-Pattern-Tests: 4 Tests
- Sanity-Check-Tests: 16 Tests
- Management-Befehl-Tests: 2 Tests
- JSON-Export-Tests: 9 Tests
- Unvollständige-Vorgaben-Tests: 15 Tests
- Kommentar-Tests: 24 Tests (6 Modell + 18 View-Tests)
- **pages**: 4 Tests - **pages**: 4 Tests
- **referenzen**: 18 Tests - **referenzen**: 18 Tests
- **rollen**: 18 Tests - **rollen**: 18 Tests
@@ -348,6 +399,17 @@ Die stichworte App enthält 18 Tests, die Schlüsselwortmodelle und ihre Sortier
4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung 4. **Utility-Funktionen**: Textverarbeitung, Caching, Formatierung
5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung 5. **Management-Befehle**: CLI-Schnittstelle und Ausgabeverarbeitung
6. **Integration**: App-übergreifende Funktionalität und Datenfluss 6. **Integration**: App-übergreifende Funktionalität und Datenfluss
7. **Sicherheit**:
- XSS-Prävention durch HTML-Bereinigung beim Rendern von Inhalten
- XSS-Angriffsverhinderung im Kommentarsystem (Script-Tags, javascript:-Protokoll, Event-Handler)
- Eingabevalidierung und -bereinigung
- Autorisierungsprüfungen (Staff vs. normale Benutzer)
- Sicherheits-Header (Content-Security-Policy, X-Content-Type-Options)
8. **Kommentar-Funktionalität**:
- CRUD-Operationen (Create, Read, Delete)
- Benutzerberechtigungen und -besitz
- HTML-Escaping und Erhalt von Zeilenumbrüchen
- Verhinderung mehrerer XSS-Angriffsvektoren
## Ausführen der Tests ## Ausführen der Tests

View File

@@ -15,7 +15,7 @@ This document provides a comprehensive overview of all tests in the vgui-cicd Dj
## abschnitte App Tests ## abschnitte App Tests
The abschnitte app contains 32 tests covering models, utility functions, diagram caching, and management commands. The abschnitte app contains 33 tests covering models, utility functions, diagram caching, management commands, and security.
### Model Tests ### Model Tests
@@ -58,6 +58,7 @@ The abschnitte app contains 32 tests covering models, utility functions, diagram
- **test_render_text_with_footnotes**: Processes text containing footnotes - **test_render_text_with_footnotes**: Processes text containing footnotes
- **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp - **test_render_abschnitt_without_type**: Handles Textabschnitte without AbschnittTyp
- **test_render_abschnitt_with_empty_content**: Handles Textabschnitte with empty content - **test_render_abschnitt_with_empty_content**: Handles Textabschnitte with empty content
- **test_render_textabschnitte_xss_prevention**: Verifies that malicious HTML and script tags are sanitized from rendered content to prevent XSS attacks
### Diagram Caching Tests ### Diagram Caching Tests
@@ -86,7 +87,7 @@ The abschnitte app contains 32 tests covering models, utility functions, diagram
## dokumente App Tests ## dokumente App Tests
The dokumente app contains 98 tests, making it the most comprehensive test suite, covering all models, views, URLs, and business logic. The dokumente app contains 121 tests, making it the most comprehensive test suite, covering all models, views, URLs, business logic, and comment functionality with XSS protection.
### Model Tests ### Model Tests
@@ -130,6 +131,14 @@ The dokumente app contains 98 tests, making it the most comprehensive test suite
- **test_checklistenfrage_str**: Verifies string representation truncates long questions - **test_checklistenfrage_str**: Verifies string representation truncates long questions
- **test_checklistenfrage_related_name**: Tests the reverse relationship from Vorgabe - **test_checklistenfrage_related_name**: Tests the reverse relationship from Vorgabe
#### VorgabeCommentModelTest
- **test_comment_creation**: Tests VorgabeComment creation with vorgabe, user, and text
- **test_comment_str**: Verifies string representation includes username and Vorgabennummer
- **test_comment_related_name**: Tests the reverse relationship from Vorgabe
- **test_comment_ordering**: Tests comments are ordered by created_at descending (newest first)
- **test_comment_timestamps_auto_update**: Tests that updated_at changes when comment is modified
- **test_multiple_users_can_comment**: Tests multiple users can comment on same Vorgabe
### Text Abschnitt Tests ### Text Abschnitt Tests
#### DokumentTextAbschnitteTest #### DokumentTextAbschnitteTest
@@ -216,6 +225,40 @@ The dokumente app contains 98 tests, making it the most comprehensive test suite
- **test_vorgabe_links**: Tests Vorgaben link to correct admin pages - **test_vorgabe_links**: Tests Vorgaben link to correct admin pages
- **test_back_link**: Tests back link to standard list exists - **test_back_link**: Tests back link to standard list exists
### Comment Functionality Tests
#### GetVorgabeCommentsViewTest
- **test_get_comments_requires_login**: Tests anonymous users cannot view comments and are redirected
- **test_regular_user_sees_only_own_comments**: Tests regular users only see their own comments
- **test_staff_user_sees_all_comments**: Tests staff users can see all comments
- **test_get_comments_returns_404_for_nonexistent_vorgabe**: Tests 404 response for non-existent Vorgabe
- **test_comments_are_html_escaped**: Tests HTML escaping prevents XSS attacks (e.g., `<script>` tags)
- **test_line_breaks_preserved**: Tests line breaks are converted to `<br>` tags
- **test_security_headers_present**: Tests Content-Security-Policy and X-Content-Type-Options headers are set
#### AddVorgabeCommentViewTest
- **test_add_comment_requires_login**: Tests anonymous users cannot add comments
- **test_add_comment_requires_post**: Tests only POST method is allowed (405 for GET)
- **test_add_comment_success**: Tests successful comment creation with valid data
- **test_add_empty_comment_fails**: Tests empty comments are rejected with 400 error
- **test_add_whitespace_only_comment_fails**: Tests whitespace-only comments are rejected
- **test_add_too_long_comment_fails**: Tests comments exceeding 2000 characters are rejected
- **test_add_comment_xss_script_tag_blocked**: Tests comments with `<script>` tags are blocked
- **test_add_comment_xss_javascript_protocol_blocked**: Tests `javascript:` protocol is blocked
- **test_add_comment_xss_event_handlers_blocked**: Tests event handlers (onload, onerror, onclick, onmouseover) are blocked
- **test_add_comment_invalid_json_fails**: Tests invalid JSON payloads are rejected
- **test_add_comment_nonexistent_vorgabe_fails**: Tests 404 response for non-existent Vorgabe
- **test_add_comment_security_headers**: Tests security headers are present in responses
#### DeleteVorgabeCommentViewTest
- **test_delete_comment_requires_login**: Tests anonymous users cannot delete comments
- **test_delete_comment_requires_post**: Tests only POST method is allowed (405 for GET)
- **test_user_can_delete_own_comment**: Tests users can delete their own comments
- **test_user_cannot_delete_other_users_comment**: Tests users cannot delete others' comments (403 Forbidden)
- **test_staff_can_delete_any_comment**: Tests staff users can delete any comment
- **test_delete_nonexistent_comment_returns_404**: Tests 404 response for non-existent comment
- **test_delete_comment_security_headers**: Tests security headers are present in responses
--- ---
## pages App Tests ## pages App Tests
@@ -332,9 +375,17 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
## Test Statistics ## Test Statistics
- **Total Tests**: 206 - **Total Tests**: 230
- **abschnitte**: 32 tests - **abschnitte**: 33 tests (including XSS prevention)
- **dokumente**: 116 tests (98 in tests.py + 9 in test_json.py + 9 JSON tests in main tests.py) - **dokumente**: 121 tests (including comment functionality with XSS protection)
- Model tests: 44 tests
- View tests: 7 tests
- URL pattern tests: 4 tests
- Sanity check tests: 16 tests
- Management command tests: 2 tests
- JSON export tests: 9 tests
- Incomplete Vorgaben tests: 15 tests
- Comment tests: 24 tests (6 model + 18 view tests)
- **pages**: 4 tests - **pages**: 4 tests
- **referenzen**: 18 tests - **referenzen**: 18 tests
- **rollen**: 18 tests - **rollen**: 18 tests
@@ -348,6 +399,17 @@ The stichworte app contains 18 tests covering keyword models and their ordering.
4. **Utility Functions**: Text processing, caching, formatting 4. **Utility Functions**: Text processing, caching, formatting
5. **Management Commands**: CLI interface and output handling 5. **Management Commands**: CLI interface and output handling
6. **Integration**: Cross-app functionality and data flow 6. **Integration**: Cross-app functionality and data flow
7. **Security**:
- XSS prevention through HTML sanitization in content rendering
- XSS attack prevention in comment system (script tags, javascript: protocol, event handlers)
- Input validation and sanitization
- Authorization checks (staff vs. regular users)
- Security headers (Content-Security-Policy, X-Content-Type-Options)
8. **Comment Functionality**:
- CRUD operations (Create, Read, Delete)
- User permissions and ownership
- HTML escaping and line break preservation
- Multiple XSS attack vector prevention
## Running the Tests ## Running the Tests

View File

@@ -24,7 +24,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get("SECRET_KEY") SECRET_KEY = os.environ.get("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production! # 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(",") ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS","127.0.0.1").split(",")

View File

@@ -28,12 +28,6 @@ DEBUG = True
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"] ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
TEMPLATES = [
{"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
}
]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@@ -133,7 +127,7 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
#STATIC_ROOT="/home/adebaumann/VorgabenUI/staticfiles/" #STATIC_ROOT="/home/adebaumann/VorgabenUI/staticfiles/"
STATIC_ROOT="/app/staticfiles/" STATIC_ROOT="staticfiles/"
STATICFILES_DIRS= ( STATICFILES_DIRS= (
os.path.join(BASE_DIR,"static"), os.path.join(BASE_DIR,"static"),
) )

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -293,6 +293,6 @@ class VorgabeAdmin(NestedModelAdmin):
admin.site.register(Checklistenfrage) admin.site.register(Checklistenfrage)
admin.site.register(Dokumententyp) admin.site.register(Dokumententyp)
#admin.site.register(Person) admin.site.register(VorgabeComment)
#admin.site.register(Changelog) #admin.site.register(Changelog)

View File

@@ -0,0 +1,49 @@
# Generated by Django 5.2.5 on 2025-11-27 22:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0009_alter_vorgabe_options_vorgabe_order'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='VorgabenTable',
fields=[
],
options={
'verbose_name': 'Vorgabe (Tabellenansicht)',
'verbose_name_plural': 'Vorgaben (Tabellenansicht)',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('dokumente.vorgabe',),
),
migrations.AlterModelOptions(
name='person',
options={'ordering': ['name'], 'verbose_name_plural': 'Personen'},
),
migrations.CreateModel(
name='VorgabeComment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('vorgabe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='dokumente.vorgabe')),
],
options={
'verbose_name': 'Vorgabe-Kommentar',
'verbose_name_plural': 'Vorgabe-Kommentare',
'ordering': ['-created_at'],
},
),
]

View File

@@ -1,5 +1,6 @@
from django.db import models from django.db import models
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from django.contrib.auth.models import User
from abschnitte.models import Textabschnitt from abschnitte.models import Textabschnitt
from stichworte.models import Stichwort from stichworte.models import Stichwort
from referenzen.models import Referenz from referenzen.models import Referenz
@@ -53,6 +54,34 @@ class Dokument(models.Model):
def __str__(self): def __str__(self):
return f"{self.nummer} {self.name}" 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: class Meta:
verbose_name_plural="Dokumente" verbose_name_plural="Dokumente"
verbose_name="Dokument" verbose_name="Dokument"
@@ -261,3 +290,19 @@ class Changelog(models.Model):
class Meta: class Meta:
verbose_name_plural="Changelog" verbose_name_plural="Changelog"
verbose_name="Changelog-Eintrag" verbose_name="Changelog-Eintrag"
class VorgabeComment(models.Model):
vorgabe = models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name='comments')
user = models.ForeignKey(User, on_delete=models.CASCADE)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Vorgabe-Kommentar"
verbose_name_plural = "Vorgabe-Kommentare"
ordering = ['-created_at']
def __str__(self):
return f"Kommentar von {self.user.username} zu {self.vorgabe.Vorgabennummer()}"

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

@@ -105,13 +105,13 @@
{% else %} {% else %}
<div class="alert alert-success" role="alert"> <div class="alert alert-success" role="alert">
<h4 class="alert-heading"> <h4 class="alert-heading">
<i class="fas fa-check-circle"></i> Alle Vorgaben sind vollständig! <span class="emoji-icon"></span> Alle Vorgaben sind vollständig!
</h4> </h4>
<p>Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.</p> <p>Alle Vorgaben haben Referenzen, Stichworte, Text und Checklistenfragen.</p>
<hr> <hr>
<p class="mb-0"> <p class="mb-0">
<a href="{% url 'standard_list' %}" class="btn btn-primary"> <a href="{% url 'standard_list' %}" class="btn btn-primary">
<i class="fas fa-list"></i> Zurück zur Übersicht <span class="emoji-icon">📋</span> Zurück zur Übersicht
</a> </a>
</p> </p>
</div> </div>
@@ -119,7 +119,7 @@
<div class="mt-3"> <div class="mt-3">
<a href="{% url 'standard_list' %}" class="btn btn-secondary"> <a href="{% url 'standard_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Zurück zur Übersicht <span class="emoji-icon"></span> Zurück zur Übersicht
</a> </a>
</div> </div>

View File

@@ -19,13 +19,32 @@
<strong>Historische Version vom {{ standard.check_date }}</strong> <strong>Historische Version vom {{ standard.check_date }}</strong>
</div> </div>
{% endif %} {% 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 --> <!-- Einleitung -->
{% if standard.einleitung_html %} {% if standard.einleitung_html %}
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h2 class="h4 mb-0">Einleitung</h2> <h2>Einleitung</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% for typ, html in standard.einleitung_html %} {% for typ, html in standard.einleitung_html %}
@@ -43,7 +62,7 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h2 class="h4 mb-0">Geltungsbereich</h2> <h2>Geltungsbereich</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
{% for typ, html in standard.geltungsbereich_html %} {% for typ, html in standard.geltungsbereich_html %}
@@ -73,7 +92,7 @@
<a id="{{ vorgabe.Vorgabennummer }}"></a> <a id="{{ vorgabe.Vorgabennummer }}"></a>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;"> <div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
<h3 class="h5 mb-0"> <h3>
{{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }} {{ vorgabe.Vorgabennummer }} {{ vorgabe.titel }}
{% if vorgabe.long_status != "active" and standard.history == True %} {% if vorgabe.long_status != "active" and standard.history == True %}
<span class="badge badge-danger">{{ vorgabe.long_status }}</span> <span class="badge badge-danger">{{ vorgabe.long_status }}</span>
@@ -123,7 +142,7 @@
{% endif %} {% endif %}
<!-- Stichworte und Referenzen --> <!-- Stichworte und Referenzen -->
<div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6;"> <div class="mt-4 p-3" style="background-color: #f8f9fa; border-left: 3px solid #dee2e6; padding-left: 0.5en;">
<p class="mb-2"> <p class="mb-2">
<strong>Stichworte:</strong> <strong>Stichworte:</strong>
{% if vorgabe.stichworte.all %} {% if vorgabe.stichworte.all %}
@@ -145,6 +164,20 @@
{% endif %} {% endif %}
</p> </p>
</div> </div>
<!-- Comment Button -->
{% if user.is_authenticated %}
<div class="mt-3 text-right">
<button class="btn btn-sm btn-outline-primary comment-btn"
data-vorgabe-id="{{ vorgabe.id }}"
data-vorgabe-nummer="{{ vorgabe.Vorgabennummer }}">
<span class="emoji-icon">💬</span> Kommentare
{% if vorgabe.comment_count > 0 %}
<span class="comment-count">{{ vorgabe.comment_count }}</span>
{% endif %}
</button>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -176,4 +209,210 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Comment Modal -->
<div class="modal fade" id="commentModal" tabindex="-1" role="dialog" aria-labelledby="commentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="commentModalLabel">Kommentare für <span id="modalVorgabeNummer"></span></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="commentsContainer">
<!-- Comments will be loaded here -->
</div>
<!-- Add Comment Form -->
<div class="mt-4">
<h6>Neuen Kommentar hinzufügen:</h6>
<textarea id="newCommentText" class="form-control" rows="3" placeholder="Ihr Kommentar..."></textarea>
<button id="addCommentBtn" class="btn btn-primary btn-sm mt-2">Kommentar hinzufügen</button>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript for Comments -->
<script>
// Content Security Policy for comment system
document.addEventListener('DOMContentLoaded', function() {
// Prevent inline script execution in dynamically loaded content
const commentsContainer = document.getElementById('commentsContainer');
if (commentsContainer) {
// Use DOMPurify-like approach - only allow safe HTML
const allowedTags = ['br', 'small', 'div', 'span', 'button'];
const allowedAttributes = ['class', 'data-comment-id', 'aria-hidden'];
// Monitor for any script injection attempts
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
const tagName = node.tagName.toLowerCase();
if (tagName === 'script') {
console.warn('Script injection attempt blocked');
node.parentNode.removeChild(node);
}
}
});
});
});
observer.observe(commentsContainer, {
childList: true,
subtree: true
});
}
});
document.addEventListener('DOMContentLoaded', function() {
let currentVorgabeId = null;
let currentVorgabeNummer = null;
// Comment button click handler
document.querySelectorAll('.comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentVorgabeId = this.dataset.vorgabeId;
currentVorgabeNummer = this.dataset.vorgabeNummer;
document.getElementById('modalVorgabeNummer').textContent = currentVorgabeNummer;
document.getElementById('newCommentText').value = '';
loadComments();
$('#commentModal').modal('show');
});
});
// Load comments function
function loadComments() {
fetch(`/dokumente/comments/${currentVorgabeId}/`)
.then(response => response.json())
.then(data => {
renderComments(data.comments);
})
.catch(error => {
console.error('Error loading comments:', error);
document.getElementById('commentsContainer').innerHTML =
'<div class="alert alert-danger">Fehler beim Laden der Kommentare</div>';
});
}
// Render comments function
function renderComments(comments) {
const container = document.getElementById('commentsContainer');
if (comments.length === 0) {
container.innerHTML = '<p class="text-muted">Noch keine Kommentare vorhanden.</p>';
return;
}
let html = '';
comments.forEach(comment => {
const canDelete = comment.is_own || {% if user.is_authenticated %}'{{ user.is_staff|yesno:"true,false" }}'{% else %}'false'{% endif %} === 'true';
html += `
<div class="comment-item border-bottom pb-2 mb-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<strong>${comment.user}</strong>
<small class="text-muted">(${comment.created_at})</small>
${comment.updated_at !== comment.created_at ? `<small class="text-muted">(bearbeitet: ${comment.updated_at})</small>` : ''}
<div class="mt-1">${comment.text}</div>
</div>
${canDelete ? `
<button class="btn btn-sm btn-outline-danger ml-2 delete-comment-btn" data-comment-id="${comment.id}">
<span aria-hidden="true">&times;</span>
</button>
` : ''}
</div>
</div>
`;
});
container.innerHTML = html;
// Add delete handlers
document.querySelectorAll('.delete-comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
if (confirm('Möchten Sie diesen Kommentar wirklich löschen?')) {
deleteComment(this.dataset.commentId);
}
});
});
}
// Add comment function
document.getElementById('addCommentBtn').addEventListener('click', function() {
const text = document.getElementById('newCommentText').value.trim();
if (!text) {
alert('Bitte geben Sie einen Kommentar ein.');
return;
}
fetch(`/dokumente/comments/${currentVorgabeId}/add/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ text: text })
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('newCommentText').value = '';
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error adding comment:', error);
alert('Fehler beim Hinzufügen des Kommentars');
});
});
// Delete comment function
function deleteComment(commentId) {
fetch(`/dokumente/comments/delete/${commentId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error deleting comment:', error);
alert('Fehler beim Löschen des Kommentars');
});
}
// CSRF token helper
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});
</script>
{% endblock %} {% endblock %}

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 %}

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,15 @@ from . import views
urlpatterns = [ urlpatterns = [
path('', views.standard_list, name='standard_list'), path('', views.standard_list, name='standard_list'),
path('unvollstaendig/', views.incomplete_vorgaben, name='incomplete_vorgaben'), 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>/', views.standard_detail, name='standard_detail'),
path('<str:nummer>/history/<str:check_date>/', views.standard_detail), path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'), path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'), path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
path('<str:nummer>/json/', views.standard_json, name='standard_json') path('<str:nummer>/json/', views.standard_json, name='standard_json'),
path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),
] ]

View File

@@ -2,8 +2,12 @@ from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from django.utils.html import escape, mark_safe
from django.utils.safestring import SafeString
import json import json
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
from datetime import date from datetime import date
@@ -44,6 +48,15 @@ def standard_detail(request, nummer,check_date=""):
for r in vorgabe.referenzen.all(): for r in vorgabe.referenzen.all():
referenz_items.append(r.Path()) referenz_items.append(r.Path())
vorgabe.referenzpfade = referenz_items vorgabe.referenzpfade = referenz_items
# Add comment count
if request.user.is_authenticated:
if request.user.is_staff:
vorgabe.comment_count = vorgabe.comments.count()
else:
vorgabe.comment_count = vorgabe.comments.filter(user=request.user).count()
else:
vorgabe.comment_count = 0
return render(request, 'standards/standard_detail.html', { return render(request, 'standards/standard_detail.html', {
'standard': standard, 'standard': standard,
@@ -237,3 +250,173 @@ def standard_json(request, nummer):
# Return JSON response # Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder) return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
@login_required
def get_vorgabe_comments(request, vorgabe_id):
"""Get comments for a specific Vorgabe"""
vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
if request.user.is_staff:
# Staff can see all comments
comments = vorgabe.comments.all().select_related('user').order_by('created_at')
else:
# Regular users can only see their own comments
comments = vorgabe.comments.filter(user=request.user).select_related('user').order_by('created_at')
comments_data = []
for comment in comments:
# Escape HTML but preserve line breaks
escaped_text = escape(comment.text).replace('\n', '<br>')
comments_data.append({
'id': comment.id,
'text': escaped_text,
'user': escape(comment.user.first_name+" "+comment.user.last_name),
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
'is_own': comment.user == request.user
})
response = JsonResponse({'comments': comments_data})
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
@require_POST
@login_required
def add_vorgabe_comment(request, vorgabe_id):
"""Add a new comment to a Vorgabe"""
vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
try:
data = json.loads(request.body)
text = data.get('text', '').strip()
# Validate input
if not text:
return JsonResponse({'error': 'Kommentar darf nicht leer sein'}, status=400)
if len(text) > 2000: # Reasonable length limit
return JsonResponse({'error': 'Kommentar ist zu lang (max 2000 Zeichen)'}, status=400)
# Additional XSS prevention - check for dangerous patterns
dangerous_patterns = ['<script', 'javascript:', 'onload=', 'onerror=', 'onclick=', 'onmouseover=']
text_lower = text.lower()
for pattern in dangerous_patterns:
if pattern in text_lower:
return JsonResponse({'error': 'Kommentar enthält ungültige Zeichen'}, status=400)
comment = VorgabeComment.objects.create(
vorgabe=vorgabe,
user=request.user,
text=text
)
# Escape HTML but preserve line breaks
escaped_text = escape(comment.text).replace('\n', '<br>')
response = JsonResponse({
'success': True,
'comment': {
'id': comment.id,
'text': escaped_text,
'user': escape(comment.user.username),
'created_at': comment.created_at.strftime('%d.%m.%Y %H:%M'),
'updated_at': comment.updated_at.strftime('%d.%m.%Y %H:%M'),
'is_own': True
}
})
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
except json.JSONDecodeError:
response = JsonResponse({'error': 'Ungültige Daten'}, status=400)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
except Exception as e:
response = JsonResponse({'error': 'Serverfehler'}, status=500)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
@require_POST
@login_required
def delete_vorgabe_comment(request, comment_id):
"""Delete a comment (only own comments or staff can delete)"""
comment = get_object_or_404(VorgabeComment, id=comment_id)
# Check if user can delete this comment
if comment.user != request.user and not request.user.is_staff:
response = JsonResponse({'error': 'Keine Berechtigung zum Löschen dieses Kommentars'}, status=403)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
try:
comment.delete()
response = JsonResponse({'success': True})
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
except Exception as e:
response = JsonResponse({'error': 'Serverfehler'}, status=500)
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 - name: data
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: docker.io/adebaumann/vui:0.917 image: docker.io/adebaumann/vui:0.918
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000

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

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

View File

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

View File

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

View File

@@ -48,21 +48,25 @@
<div class="dropdown"> <div class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" style="text-decoration: none; color: #000; display: flex; align-items: center;"> <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 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> <span class="caret" style="margin-left: 8px;"></span>
</a> </a>
<ul class="dropdown-menu dropdown-menu-right" role="menu"> <ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a href="{% url 'password_change' %}">Passwort ändern</a></li> <li><a href="{% url 'user_comments' %}">Meine Kommentare</a></li>
<li class="divider"></li> {% if user.is_staff %}
<li> <li><a href="{% url 'all_comments' %}">Alle Kommentare</a></li>
<form method="post" action="{% url 'logout' %}" style="display: inline;"> {% endif %}
{% csrf_token %} <li><a href="{% url 'password_change' %}">Passwort ändern</a></li>
<button type="submit" style="background: none; border: none; color: inherit; padding: 3px 20px; width: 100%; text-align: left; cursor: pointer;"> <li class="divider"></li>
Abmelden <li>
</button> <form method="post" action="{% url 'logout' %}" style="display: inline;">
</form> {% csrf_token %}
</li> <button type="submit" style="background: none; border: none; color: inherit; padding: 3px 20px; width: 100%; text-align: left; cursor: pointer;">
</ul> Abmelden
</button>
</form>
</li>
</ul>
</div> </div>
</div> </div>
{% else %} {% else %}
@@ -102,6 +106,7 @@
<li><a href="/dokumente">Standards</a></li> <li><a href="/dokumente">Standards</a></li>
{% if user.is_staff %} {% if user.is_staff %}
<li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li> <li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li>
<li><a href="/autorenumgebung/">Autor</a></li>
{% endif %} {% endif %}
<li><a href="/referenzen">Referenzen</a></li> <li><a href="/referenzen">Referenzen</a></li>
<li><a href="/stichworte">Stichworte</a></li> <li><a href="/stichworte">Stichworte</a></li>
@@ -131,6 +136,9 @@
<li class="dropdown {% if 'unvollstaendig' in request.path %}current{% endif %}"> <li class="dropdown {% if 'unvollstaendig' in request.path %}current{% endif %}">
<a href="/dokumente/unvollstaendig/">Unvollständig</a> <a href="/dokumente/unvollstaendig/">Unvollständig</a>
</li> </li>
<li class="dropdown {% if 'autorenumgebung' in request.path %}current{% endif %}">
<a href="/autorenumgebung/">Autor</a>
</li>
{% endif %} {% endif %}
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}"> <li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
<a href="/referenzen">Referenzen</a> <a href="/referenzen">Referenzen</a>
@@ -211,8 +219,8 @@
</p> </p>
</div> </div>
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.953" }}</p> <p class="text-muted">Version {{ version|default:"0.968" }}</p>
</div> </div>
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.5 on 2025-11-27 22:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('referenzen', '0002_alter_referenz_table_alter_referenzerklaerung_table'),
]
operations = [
migrations.AlterModelOptions(
name='referenzerklaerung',
options={'verbose_name': 'Erklärung', 'verbose_name_plural': 'Erklärungen'},
),
]

View File

@@ -5,7 +5,7 @@ certifi==2025.8.3
charset-normalizer==3.4.3 charset-normalizer==3.4.3
curtsies==0.4.3 curtsies==0.4.3
cwcwidth==0.1.10 cwcwidth==0.1.10
Django==5.2.5 Django==5.2.8
django-admin-sortable2==2.2.8 django-admin-sortable2==2.2.8
django-js-asset==3.1.2 django-js-asset==3.1.2
django-mptt==0.17.0 django-mptt==0.17.0
@@ -32,3 +32,5 @@ six==1.17.0
sqlparse==0.5.3 sqlparse==0.5.3
urllib3==2.5.0 urllib3==2.5.0
wcwidth==0.2.13 wcwidth==0.2.13
bleach==6.1.0
coverage==7.6.1

View File

@@ -11,4 +11,93 @@
margin-bottom: 1em; margin-bottom: 1em;
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 0; padding: 0;
} }
/* Comment System Styles */
.comment-btn {
position: relative;
}
.comment-btn .comment-count {
position: absolute;
top: -8px;
right: -8px;
background-color: #dc3545;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 11px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.comment-item {
max-width: 100%;
word-wrap: break-word;
overflow-wrap: break-word;
}
.comment-item .text-muted {
font-size: 0.85em;
}
#commentModal .modal-body {
max-height: 60vh;
overflow-y: auto;
}
#commentsContainer {
min-height: 100px;
}
.delete-comment-btn {
opacity: 0.7;
transition: opacity 0.2s;
}
.delete-comment-btn:hover {
opacity: 1;
}
.delete-comment-btn {
font-size: 18px;
font-weight: bold;
line-height: 1;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.delete-comment-btn:hover {
opacity: 1;
background-color: #f8d7da;
border-color: #f5c6cb;
}
/* Icon styling for emoji replacements */
.emoji-icon {
font-size: 1.1em;
margin-right: 0.3em;
vertical-align: middle;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.comment-item .d-flex {
flex-direction: column;
}
.delete-comment-btn {
margin-left: 0 !important;
margin-top: 0.5rem;
}
}

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.5 on 2025-11-27 22:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stichworte', '0002_stichworterklaerung_order'),
]
operations = [
migrations.AlterModelOptions(
name='stichworterklaerung',
options={'verbose_name': 'Erklärung', 'verbose_name_plural': 'Erklärungen'},
),
]