Compare commits

..

3 Commits

Author SHA1 Message Date
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
6 changed files with 271 additions and 16 deletions

View File

@@ -1,8 +1,8 @@
on:
push:
branches:
- main
- development
# branches:
# - main
# - development
pull_request:
types: [opened, synchronize, reopened]
@@ -64,4 +64,4 @@ jobs:
-Dsonar.sources=. \
-Dsonar.host.url=${SONAR_HOST_URL} \
-Dsonar.token=${SONAR_TOKEN} \
-Dsonar.python.coverage.reportPaths=coverage.xml
-Dsonar.python.coverage.reportPaths=coverage.xml

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>
</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

@@ -2182,3 +2182,164 @@ 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')

View File

@@ -4,6 +4,7 @@ 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('<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,29 @@ 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(),
})

View File

@@ -51,18 +51,19 @@
<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>
<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 %}