Compare commits

...

26 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
16 changed files with 1013 additions and 53 deletions

View File

@@ -211,28 +211,17 @@ jobs:
echo "ERROR: Found $ctype \"$cname\" image repo is \"$new_repo\" but expected \"$expected_repo\""
exit 1
fi
if [ -n "${old_image:-}" ]; then
old_tag="${old_image##*:}"
else
old_tag=""
fi
registry="$(echo "$new_repo" | awk -F/ '{print $1}')"
{
echo "changed=$([ "$old_tag" != "$new_tag" ] && echo true || echo false)"
echo "new_image=$new_image"
echo "new_repo=$new_repo"
echo "new_tag=$new_tag"
echo "registry=$registry"
} >> "$GITHUB_OUTPUT"
- name: Skip if tag unchanged
if: steps.img.outputs.changed != 'true'
run: echo "${{ matrix.description }} image tag unchanged; skipping build."
- name: Check if image exists on registry
if: steps.img.outputs.changed == 'true'
id: check_image
shell: bash
run: |
@@ -267,15 +256,15 @@ jobs:
fi
- name: Skip if image already exists
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'true'
if: steps.check_image.outputs.exists == 'true'
run: echo "${{ matrix.description }} image ${{ steps.img.outputs.new_image }} already exists on registry; skipping build."
- name: Set up Buildx
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'false'
if: steps.check_image.outputs.exists == 'false'
uses: docker/setup-buildx-action@v3
- name: Log in to registry
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'false'
if: steps.check_image.outputs.exists == 'false'
uses: docker/login-action@v3
with:
registry: ${{ steps.img.outputs.registry }}
@@ -283,7 +272,7 @@ jobs:
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push ${{ matrix.description }} (exact tag from deployment)
if: steps.img.outputs.changed == 'true' && steps.check_image.outputs.exists == 'false'
if: steps.check_image.outputs.exists == 'false'
uses: docker/build-push-action@v6
with:
context: ${{ matrix.build_context }}

View File

@@ -0,0 +1,67 @@
on:
push:
# branches:
# - main
# - development
pull_request:
types: [opened, synchronize, reopened]
name: SonarQube Scan
jobs:
sonarqube:
name: SonarQube Trigger
runs-on: ubuntu-latest
steps:
- name: Checking out
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests with coverage
run: |
coverage run --source='.' manage.py test
coverage xml
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache SonarQube packages
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Download and setup SonarScanner
run: |
mkdir -p $HOME/.sonar
wget -q https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip
unzip -q sonar-scanner-cli-5.0.1.3006-linux.zip -d $HOME/.sonar/
echo "$HOME/.sonar/sonar-scanner-5.0.1.3006-linux/bin" >> $GITHUB_PATH
- name: Verify Java version
run: java -version
- name: SonarQube Scan
env:
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }}
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
run: |
sonar-scanner \
-Dsonar.projectKey=${{ github.event.repository.name }} \
-Dsonar.sources=. \
-Dsonar.host.url=${SONAR_HOST_URL} \
-Dsonar.token=${SONAR_TOKEN} \
-Dsonar.python.coverage.reportPaths=coverage.xml

View File

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

View File

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

View File

@@ -127,7 +127,7 @@ USE_TZ = True
STATIC_URL = '/static/'
#STATIC_ROOT="/home/adebaumann/VorgabenUI/staticfiles/"
STATIC_ROOT="/app/staticfiles/"
STATIC_ROOT="staticfiles/"
STATICFILES_DIRS= (
os.path.join(BASE_DIR,"static"),
)

View File

@@ -25,7 +25,7 @@ spec:
mountPath: /data
containers:
- name: web
image: git.baumann.gr/adebaumann/vui:0.961
image: git.baumann.gr/adebaumann/vui:0.968
imagePullPolicy: Always
ports:
- containerPort: 8000

View File

@@ -54,6 +54,34 @@ class Dokument(models.Model):
def __str__(self):
return f"{self.nummer} {self.name}"
@property
def dates(self):
"""
Returns an array of unique, chronologically sorted dates representing
state-change dates from all Vorgaben in this document.
These are dates where Vorgaben become active (gueltigkeit_von) or change state
(the day after gueltigkeit_bis). The very last date in the list is excluded
as it has no relevance (nothing changes after it).
"""
dates_set = set()
# Get all vorgaben for this document
for vorgabe in self.vorgaben.all():
# Add gueltigkeit_von (when vorgabe becomes active)
if vorgabe.gueltigkeit_von:
dates_set.add(vorgabe.gueltigkeit_von)
# Add the day after gueltigkeit_bis (when vorgabe expires/changes state)
# Only if gueltigkeit_bis is defined (not None)
if vorgabe.gueltigkeit_bis:
dates_set.add(vorgabe.gueltigkeit_bis + datetime.timedelta(days=1))
# Return sorted unique dates from oldest to newest, excluding the last date
# (but only if there are multiple dates; single dates are kept)
sorted_dates = sorted(list(dates_set))
return sorted_dates[:-1] if len(sorted_dates) > 1 else sorted_dates
class Meta:
verbose_name_plural="Dokumente"
verbose_name="Dokument"

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

@@ -19,6 +19,25 @@
<strong>Historische Version vom {{ standard.check_date }}</strong>
</div>
{% endif %}
<!-- History Dates Dropdown -->
{% if standard.dates %}
<div class="mb-3">
<div class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" style="text-decoration: none;">
📅 Historische Versionen
</a>
<ul class="dropdown-menu" role="menu">
<li><a href="/dokumente/{{ standard.nummer }}/">Aktuelle Version</a></li>
<li class="divider"></li>
{% for date in standard.dates %}
<li><a href="/dokumente/{{ standard.nummer }}/history/{{ date|date:'Y-m-d' }}/">{{ date|date:'d.m.Y' }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- Einleitung -->
{% if standard.einleitung_html %}
<div class="row mb-4">

View File

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

View File

@@ -1620,19 +1620,25 @@ class GetVorgabeCommentsViewTest(TestCase):
# Create users
self.regular_user = User.objects.create_user(
username='regularuser',
password='testpass123'
password='testpass123',
first_name='Regular',
last_name='User'
)
self.staff_user = User.objects.create_user(
username='staffuser',
password='testpass123'
password='testpass123',
first_name='Staff',
last_name='User'
)
self.staff_user.is_staff = True
self.staff_user.save()
self.other_user = User.objects.create_user(
username='otheruser',
password='testpass123'
password='testpass123',
first_name='Other',
last_name='User'
)
# Create test data
@@ -1697,7 +1703,7 @@ class GetVorgabeCommentsViewTest(TestCase):
# Should only see their own comment
self.assertEqual(len(data['comments']), 1)
self.assertEqual(data['comments'][0]['text'], 'Kommentar von Regular User')
self.assertEqual(data['comments'][0]['user'], 'regularuser')
self.assertEqual(data['comments'][0]['user'], 'Regular User')
self.assertTrue(data['comments'][0]['is_own'])
def test_staff_user_sees_all_comments(self):
@@ -1715,8 +1721,8 @@ class GetVorgabeCommentsViewTest(TestCase):
# Should see all comments
self.assertEqual(len(data['comments']), 2)
usernames = [c['user'] for c in data['comments']]
self.assertIn('regularuser', usernames)
self.assertIn('otheruser', usernames)
self.assertIn('Regular User', usernames)
self.assertIn('Other User', usernames)
def test_get_comments_returns_404_for_nonexistent_vorgabe(self):
"""Test that requesting comments for non-existent Vorgabe returns 404"""
@@ -1786,6 +1792,279 @@ class GetVorgabeCommentsViewTest(TestCase):
self.assertEqual(response['X-Content-Type-Options'], 'nosniff')
class DokumentDatesPropertyTest(TestCase):
"""Test cases for Dokument.dates property"""
def setUp(self):
"""Set up test data for dates property tests"""
self.dokumententyp = Dokumententyp.objects.create(
name="Standard IT-Sicherheit",
verantwortliche_ve="SR-SUR-SEC"
)
self.dokument = Dokument.objects.create(
nummer="R0066",
dokumententyp=self.dokumententyp,
name="IT Security Standard",
aktiv=True
)
self.thema = Thema.objects.create(name="Organisation")
def test_dates_property_no_vorgaben(self):
"""Test dates property returns empty list when dokument has no vorgaben"""
dates = self.dokument.dates
self.assertEqual(dates, [])
def test_dates_property_single_vorgabe_with_only_gueltigkeit_von(self):
"""Test dates property with single vorgabe with only gueltigkeit_von"""
vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date(2025, 1, 1)
)
dates = self.dokument.dates
self.assertEqual(len(dates), 1)
self.assertEqual(dates[0], date(2025, 1, 1))
def test_dates_property_single_vorgabe_with_both_dates(self):
"""Test dates property with single vorgabe with both gueltigkeit_von and gueltigkeit_bis"""
vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date(2025, 1, 1),
gueltigkeit_bis=date(2026, 1, 1)
)
dates = self.dokument.dates
# gueltigkeit_bis would add 2026-01-02, but that's the last date so it's excluded
self.assertEqual(len(dates), 1)
self.assertEqual(dates[0], date(2025, 1, 1))
def test_dates_property_multiple_vorgaben_different_dates(self):
"""Test dates property with multiple vorgaben with different dates"""
vorgabe1 = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe 1",
gueltigkeit_von=date(2025, 1, 1),
gueltigkeit_bis=date(2025, 6, 30)
)
vorgabe2 = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe 2",
gueltigkeit_von=date(2025, 7, 1),
gueltigkeit_bis=date(2026, 1, 1)
)
dates = self.dokument.dates
# Dates: 2025-01-01, 2025-07-01, 2026-01-02 (but last one excluded)
self.assertEqual(len(dates), 2)
self.assertIn(date(2025, 1, 1), dates) # Start of vorgabe1
self.assertIn(date(2025, 7, 1), dates) # End of vorgabe1 + 1 day = Start of vorgabe2 (deduplicated)
def test_dates_property_ensures_uniqueness(self):
"""Test dates property returns unique dates only"""
# Create two vorgaben with overlapping dates
vorgabe1 = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe A",
gueltigkeit_von=date(2025, 1, 1),
gueltigkeit_bis=date(2026, 1, 1)
)
vorgabe2 = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe B",
gueltigkeit_von=date(2025, 1, 1), # Same start date
gueltigkeit_bis=date(2026, 1, 1) # Same end date
)
dates = self.dokument.dates
# Both vorgaben have same dates, and the last date (2026-01-02) is excluded
self.assertEqual(len(dates), 1)
self.assertEqual(dates[0], date(2025, 1, 1))
def test_dates_property_sorted_chronologically(self):
"""Test dates property returns dates sorted from oldest to newest"""
# Create vorgaben in non-chronological order
vorgabe1 = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe 1",
gueltigkeit_von=date(2026, 1, 1)
)
vorgabe2 = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe 2",
gueltigkeit_von=date(2024, 1, 1)
)
vorgabe3 = Vorgabe.objects.create(
order=3,
nummer=3,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe 3",
gueltigkeit_von=date(2025, 1, 1)
)
dates = self.dokument.dates
# Dates are [2024-01-01, 2025-01-01, 2026-01-01] but the last one is excluded
self.assertEqual(len(dates), 2)
self.assertEqual(dates[0], date(2024, 1, 1))
self.assertEqual(dates[1], date(2025, 1, 1))
def test_dates_property_ignores_none_dates(self):
"""Test dates property ignores None date values"""
vorgabe1 = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe 1",
gueltigkeit_von=date(2025, 1, 1)
# No gueltigkeit_bis (None)
)
vorgabe2 = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe 2",
gueltigkeit_von=date(2026, 1, 1),
gueltigkeit_bis=None # Explicitly None
)
dates = self.dokument.dates
# Dates are 2025-01-01 and 2026-01-01, but the last date (2026-01-01) is excluded
self.assertEqual(len(dates), 1)
self.assertEqual(dates[0], date(2025, 1, 1))
def test_dates_property_complex_scenario(self):
"""Test dates property with complex real-world scenario
Vorgabe A: 2025-01-01 to 2025-12-31
Vorgabe B: 2025-06-01 to 2026-01-01 (overlaps with A)
Vorgabe C: 2026-02-01 to None (no end date)
Expected dates: [2025-01-01, 2025-06-01, 2025-12-31, 2026-01-01, 2026-02-01]
The middle date (2026-01-01) should NOT be excluded even though B overlaps with A
"""
vorgabe_a = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe A",
gueltigkeit_von=date(2025, 1, 1),
gueltigkeit_bis=date(2025, 12, 31)
)
vorgabe_b = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe B",
gueltigkeit_von=date(2025, 6, 1),
gueltigkeit_bis=date(2026, 1, 1)
)
vorgabe_c = Vorgabe.objects.create(
order=3,
nummer=3,
dokument=self.dokument,
thema=self.thema,
titel="Vorgabe C",
gueltigkeit_von=date(2026, 2, 1)
# No gueltigkeit_bis
)
dates = self.dokument.dates
# All dates: 2025-01-01, 2025-06-01, 2026-01-01, 2026-01-02, 2026-02-01
# Last date (2026-02-01) is excluded
expected = [
date(2025, 1, 1), # Start of A
date(2025, 6, 1), # Start of B
date(2026, 1, 1), # End of A + 1 day
date(2026, 1, 2) # End of B + 1 day
]
self.assertEqual(dates, expected)
def test_dates_property_returns_list(self):
"""Test dates property returns a list (not a set or tuple)"""
vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date(2025, 1, 1)
)
dates = self.dokument.dates
self.assertIsInstance(dates, list)
def test_dates_property_does_not_persist_to_database(self):
"""Test dates property is calculated on-the-fly, not stored"""
vorgabe = Vorgabe.objects.create(
order=1,
nummer=1,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe",
gueltigkeit_von=date(2025, 1, 1),
gueltigkeit_bis=date(2025, 12, 31)
)
# Get dates before adding new vorgabe
dates_before = self.dokument.dates
self.assertEqual(len(dates_before), 1) # 2025-01-01 (2026-01-02 is last, so excluded)
# Add new vorgabe
vorgabe2 = Vorgabe.objects.create(
order=2,
nummer=2,
dokument=self.dokument,
thema=self.thema,
titel="Test Vorgabe 2",
gueltigkeit_von=date(2026, 2, 1)
)
# Get dates after - new dates are 2025-01-01, 2026-01-02, 2026-02-01
# Last date (2026-02-01) is excluded, so we get [2025-01-01, 2026-01-01]
dates_after = self.dokument.dates
self.assertEqual(len(dates_after), 2)
self.assertEqual(dates_after[0], date(2025, 1, 1))
self.assertEqual(dates_after[1], date(2026, 1, 1))
class AddVorgabeCommentViewTest(TestCase):
"""Test cases for add_vorgabe_comment view"""
@@ -2041,12 +2320,16 @@ class DeleteVorgabeCommentViewTest(TestCase):
self.other_user = User.objects.create_user(
username='otheruser',
password='testpass123'
password='testpass123',
first_name='Other',
last_name='User'
)
self.staff_user = User.objects.create_user(
username='staffuser',
password='testpass123'
password='testpass123',
first_name='Staff',
last_name='User'
)
self.staff_user.is_staff = True
self.staff_user.save()
@@ -2172,3 +2455,384 @@ class DeleteVorgabeCommentViewTest(TestCase):
self.assertIn('Content-Security-Policy', response)
self.assertIn('X-Content-Type-Options', response)
self.assertEqual(response['X-Content-Type-Options'], 'nosniff')
class UserCommentsViewTest(TestCase):
"""Test the user comments view that displays all comments grouped by document"""
def setUp(self):
"""Set up test data"""
# Create users
self.user1 = User.objects.create_user(username='user1', password='pass123')
self.user2 = User.objects.create_user(username='user2', password='pass123')
# Create documents
self.doc_type = Dokumententyp.objects.create(name='Test Type', verantwortliche_ve='test')
self.doc1 = Dokument.objects.create(nummer='DOC-001', name='Document 1', dokumententyp=self.doc_type, aktiv=True)
self.doc2 = Dokument.objects.create(nummer='DOC-002', name='Document 2', dokumententyp=self.doc_type, aktiv=True)
# Create themes
self.theme1 = Thema.objects.create(name='Theme 1')
self.theme2 = Thema.objects.create(name='Theme 2')
# Create vorgaben
from datetime import date
self.vorgabe1 = Vorgabe.objects.create(
nummer=1,
order=1,
dokument=self.doc1,
thema=self.theme1,
titel='Vorgabe 1',
gueltigkeit_von=date.today()
)
self.vorgabe2 = Vorgabe.objects.create(
nummer=2,
order=2,
dokument=self.doc1,
thema=self.theme1,
titel='Vorgabe 2',
gueltigkeit_von=date.today()
)
self.vorgabe3 = Vorgabe.objects.create(
nummer=1,
order=1,
dokument=self.doc2,
thema=self.theme2,
titel='Vorgabe 3',
gueltigkeit_von=date.today()
)
# Create comments for user1
self.comment1 = VorgabeComment.objects.create(
vorgabe=self.vorgabe1,
user=self.user1,
text='User1 comment on vorgabe1'
)
self.comment2 = VorgabeComment.objects.create(
vorgabe=self.vorgabe2,
user=self.user1,
text='User1 comment on vorgabe2'
)
self.comment3 = VorgabeComment.objects.create(
vorgabe=self.vorgabe3,
user=self.user1,
text='User1 comment on vorgabe3'
)
# Create comment for user2
self.comment4 = VorgabeComment.objects.create(
vorgabe=self.vorgabe1,
user=self.user2,
text='User2 comment on vorgabe1'
)
def test_user_comments_requires_login(self):
"""Test that user comments view requires authentication"""
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 302)
self.assertIn('/login/', response.url)
def test_user_comments_shows_only_own_comments(self):
"""Test that user only sees their own comments"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'User1 comment on vorgabe1')
self.assertContains(response, 'User1 comment on vorgabe2')
self.assertContains(response, 'User1 comment on vorgabe3')
self.assertNotContains(response, 'User2 comment on vorgabe1')
def test_user_comments_grouped_by_document(self):
"""Test that comments are properly grouped by document"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
# Check that document titles appear
self.assertContains(response, 'DOC-001 Document 1')
self.assertContains(response, 'DOC-002 Document 2')
# Check context
self.assertIn('comments_by_document', response.context)
self.assertEqual(len(response.context['comments_by_document']), 2)
def test_user_comments_count_display(self):
"""Test that total comment count is displayed"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['total_comments'], 3)
self.assertContains(response, '3 Kommentare')
def test_user_comments_empty_view(self):
"""Test the view when user has no comments"""
# Create a new user with no comments
user3 = User.objects.create_user(username='user3', password='pass123')
self.client.login(username='user3', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['total_comments'], 0)
self.assertContains(response, 'Sie haben noch keine Kommentare')
def test_user_comments_comment_text_preserved(self):
"""Test that comment text is correctly displayed"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
# Check that comment text appears in response
self.assertContains(response, 'User1 comment on vorgabe1')
def test_user_comments_vorgabe_number_link(self):
"""Test that vorgabe numbers are linked correctly"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
# Check that vorgabe numbers appear (format is DOC-001.T.1)
self.assertContains(response, 'DOC-001.T.1')
self.assertContains(response, 'DOC-001.T.2')
self.assertContains(response, 'DOC-002.T.1')
def test_user_comments_ordered_by_creation_date(self):
"""Test that comments are ordered by creation date (newest first)"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
# The queryset orders by vorgabe document, then by -created_at
# Check that all three comments are in the response
self.assertContains(response, 'User1 comment on vorgabe1')
self.assertContains(response, 'User1 comment on vorgabe2')
self.assertContains(response, 'User1 comment on vorgabe3')
def test_user_comments_template_used(self):
"""Test that correct template is used"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('user_comments'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'standards/user_comments.html')
class AllCommentsViewTest(TestCase):
"""Test the all comments view that displays all comments from all users (staff only)"""
def setUp(self):
"""Set up test data"""
# Create users
self.user1 = User.objects.create_user(username='user1', password='pass123', first_name='Max', last_name='Mustermann')
self.user2 = User.objects.create_user(username='user2', password='pass123', first_name='Anna', last_name='Mueller')
self.staff_user = User.objects.create_user(username='staff', password='pass123', is_staff=True, first_name='Admin', last_name='User')
# Create documents
self.doc_type = Dokumententyp.objects.create(name='Test Type', verantwortliche_ve='test')
self.doc1 = Dokument.objects.create(nummer='DOC-001', name='Document 1', dokumententyp=self.doc_type, aktiv=True)
self.doc2 = Dokument.objects.create(nummer='DOC-002', name='Document 2', dokumententyp=self.doc_type, aktiv=True)
# Create themes
self.theme1 = Thema.objects.create(name='Theme 1')
self.theme2 = Thema.objects.create(name='Theme 2')
# Create vorgaben
self.vorgabe1 = Vorgabe.objects.create(
nummer=1,
order=1,
dokument=self.doc1,
thema=self.theme1,
titel='Vorgabe 1',
gueltigkeit_von=date.today()
)
self.vorgabe2 = Vorgabe.objects.create(
nummer=2,
order=2,
dokument=self.doc1,
thema=self.theme1,
titel='Vorgabe 2',
gueltigkeit_von=date.today()
)
self.vorgabe3 = Vorgabe.objects.create(
nummer=1,
order=1,
dokument=self.doc2,
thema=self.theme2,
titel='Vorgabe 3',
gueltigkeit_von=date.today()
)
# Create comments from different users
self.comment1 = VorgabeComment.objects.create(
vorgabe=self.vorgabe1,
user=self.user1,
text='User1 comment on vorgabe1'
)
self.comment2 = VorgabeComment.objects.create(
vorgabe=self.vorgabe2,
user=self.user1,
text='User1 comment on vorgabe2'
)
self.comment3 = VorgabeComment.objects.create(
vorgabe=self.vorgabe3,
user=self.user2,
text='User2 comment on vorgabe3'
)
self.comment4 = VorgabeComment.objects.create(
vorgabe=self.vorgabe1,
user=self.user2,
text='User2 comment on vorgabe1'
)
def test_all_comments_requires_login(self):
"""Test that all comments view requires authentication"""
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 302)
self.assertIn('/login/', response.url)
def test_all_comments_staff_only(self):
"""Test that non-staff users cannot access all comments view"""
self.client.login(username='user1', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 302)
self.assertIn('/login/', response.url)
def test_all_comments_staff_can_access(self):
"""Test that staff users can access all comments view"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
def test_all_comments_shows_all_comments(self):
"""Test that staff sees all comments from all users"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'User1 comment on vorgabe1')
self.assertContains(response, 'User1 comment on vorgabe2')
self.assertContains(response, 'User2 comment on vorgabe3')
self.assertContains(response, 'User2 comment on vorgabe1')
def test_all_comments_shows_usernames(self):
"""Test that all comments display the username of the author"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
# Check that user names appear in the response
self.assertContains(response, 'Max Mustermann')
self.assertContains(response, 'Anna Mueller')
def test_all_comments_grouped_by_document(self):
"""Test that comments are properly grouped by document"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
# Check that document titles appear
self.assertContains(response, 'DOC-001 Document 1')
self.assertContains(response, 'DOC-002 Document 2')
# Check context
self.assertIn('comments_by_document', response.context)
self.assertEqual(len(response.context['comments_by_document']), 2)
def test_all_comments_count_display(self):
"""Test that total comment count is displayed"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['total_comments'], 4)
self.assertContains(response, '4 Kommentare')
def test_all_comments_empty_view(self):
"""Test the view when there are no comments"""
# Delete all comments
VorgabeComment.objects.all().delete()
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['total_comments'], 0)
self.assertContains(response, 'Es gibt noch keine Kommentare')
def test_all_comments_template_used(self):
"""Test that correct template is used"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'standards/all_comments.html')
def test_all_comments_has_delete_buttons(self):
"""Test that delete buttons are present for each comment"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
# Check for delete button form elements - look for the delete form action URLs
self.assertContains(response, '/dokumente/comments/delete/', count=4)
# Also check for the delete button text
self.assertContains(response, 'Löschen', count=4)
def test_all_comments_vorgabe_number_link(self):
"""Test that vorgabe numbers are linked correctly"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
# Check that vorgabe numbers appear
self.assertContains(response, 'DOC-001.T.1')
self.assertContains(response, 'DOC-001.T.2')
self.assertContains(response, 'DOC-002.T.1')
def test_all_comments_ordered_by_document_and_date(self):
"""Test that comments are ordered by document then by creation date"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
# Check context has properly grouped comments
comments_by_doc = response.context['comments_by_document']
# Verify all documents are present
doc_numbers = [doc.nummer for doc in comments_by_doc.keys()]
self.assertIn('DOC-001', doc_numbers)
self.assertIn('DOC-002', doc_numbers)
def test_all_comments_displays_timestamps(self):
"""Test that comment timestamps are displayed"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
self.assertEqual(response.status_code, 200)
# Check that timestamp patterns appear (date formatting)
self.assertContains(response, 'Erstellt:')
def test_all_comments_regular_user_redirect(self):
"""Test that regular users are redirected to login"""
# Create and login as regular user
regular_user = User.objects.create_user(username='regular', password='pass123')
self.client.login(username='regular', password='pass123')
response = self.client.get(reverse('all_comments'))
# Should redirect to login since user is not staff
self.assertEqual(response.status_code, 302)
self.assertIn('/login/', response.url)
def test_all_comments_no_own_comments_filter(self):
"""Test that staff sees comments from ALL users, not just their own"""
self.client.login(username='staff', password='pass123')
response = self.client.get(reverse('all_comments'))
# Verify all comments are visible, not filtered by user
self.assertContains(response, 'User1 comment on vorgabe1')
self.assertContains(response, 'User2 comment on vorgabe1')
# Both users' comments on the same vorgabe should be visible
self.assertEqual(response.context['total_comments'], 4)

View File

@@ -4,6 +4,8 @@ from . import views
urlpatterns = [
path('', views.standard_list, name='standard_list'),
path('unvollstaendig/', views.incomplete_vorgaben, name='incomplete_vorgaben'),
path('meine-kommentare/', views.user_comments, name='user_comments'),
path('alle-kommentare/', views.all_comments, name='all_comments'),
path('<str:nummer>/', views.standard_detail, name='standard_detail'),
path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),

View File

@@ -366,3 +366,57 @@ def delete_vorgabe_comment(request, comment_id):
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
@login_required
def user_comments(request):
"""
Display all comments made by the logged-in user, grouped by document.
"""
# Get all comments by the current user
user_comments = VorgabeComment.objects.filter(
user=request.user
).select_related('vorgabe', 'vorgabe__dokument').order_by(
'vorgabe__dokument__nummer', '-created_at'
)
# Group comments by document
comments_by_document = {}
for comment in user_comments:
dokument = comment.vorgabe.dokument
if dokument not in comments_by_document:
comments_by_document[dokument] = []
comments_by_document[dokument].append(comment)
return render(request, 'standards/user_comments.html', {
'comments_by_document': comments_by_document,
'total_comments': user_comments.count(),
})
@login_required
@user_passes_test(is_staff_user)
def all_comments(request):
"""
Display all comments from all users, grouped by document.
Staff only.
"""
# Get all comments
all_comments_qs = VorgabeComment.objects.select_related(
'vorgabe', 'vorgabe__dokument', 'user'
).order_by(
'vorgabe__dokument__nummer', '-created_at'
)
# Group comments by document
comments_by_document = {}
for comment in all_comments_qs:
dokument = comment.vorgabe.dokument
if dokument not in comments_by_document:
comments_by_document[dokument] = []
comments_by_document[dokument].append(comment)
return render(request, 'standards/all_comments.html', {
'comments_by_document': comments_by_document,
'total_comments': all_comments_qs.count(),
})

View File

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

View File

@@ -51,18 +51,22 @@
<span class="hidden-xs" style="margin-left: 0;">{{ user.first_name }} {{ user.last_name }}</span>
<span class="caret" style="margin-left: 8px;"></span>
</a>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a href="{% url 'password_change' %}">Passwort ändern</a></li>
<li class="divider"></li>
<li>
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" style="background: none; border: none; color: inherit; padding: 3px 20px; width: 100%; text-align: left; cursor: pointer;">
Abmelden
</button>
</form>
</li>
</ul>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a href="{% url 'user_comments' %}">Meine Kommentare</a></li>
{% if user.is_staff %}
<li><a href="{% url 'all_comments' %}">Alle Kommentare</a></li>
{% endif %}
<li><a href="{% url 'password_change' %}">Passwort ändern</a></li>
<li class="divider"></li>
<li>
<form method="post" action="{% url 'logout' %}" style="display: inline;">
{% csrf_token %}
<button type="submit" style="background: none; border: none; color: inherit; padding: 3px 20px; width: 100%; text-align: left; cursor: pointer;">
Abmelden
</button>
</form>
</li>
</ul>
</div>
</div>
{% else %}
@@ -102,7 +106,7 @@
<li><a href="/dokumente">Standards</a></li>
{% if user.is_staff %}
<li><a href="/dokumente/unvollstaendig/">Unvollständig</a></li>
<li><a href="/autorenumgebung/">Autorenumgebung</a></li>
<li><a href="/autorenumgebung/">Autor</a></li>
{% endif %}
<li><a href="/referenzen">Referenzen</a></li>
<li><a href="/stichworte">Stichworte</a></li>
@@ -133,7 +137,7 @@
<a href="/dokumente/unvollstaendig/">Unvollständig</a>
</li>
<li class="dropdown {% if 'autorenumgebung' in request.path %}current{% endif %}">
<a href="/autorenumgebung/">Autorenumgebung</a>
<a href="/autorenumgebung/">Autor</a>
</li>
{% endif %}
<li class="dropdown {% if 'referenzen' in request.path %}current{% endif %}">
@@ -215,8 +219,8 @@
</p>
</div>
<div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.961" }}</p>
</div>
<p class="text-muted">Version {{ version|default:"0.968" }}</p>
</div>
</div>
</div>
</footer>

View File

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