From e5202d9b2bf3cbd0703987857083e8741acee720 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 27 Nov 2025 23:11:59 +0100 Subject: [PATCH 1/7] Comment function added --- ...ble_alter_person_options_vorgabecomment.py | 49 +++++ dokumente/models.py | 17 ++ .../standards/incomplete_vorgaben.html | 6 +- .../templates/standards/standard_detail.html | 190 ++++++++++++++++++ dokumente/urls.py | 5 +- dokumente/views.py | 93 ++++++++- .../0003_alter_referenzerklaerung_options.py | 17 ++ static/custom/css/admin_extras.css | 91 ++++++++- .../0003_alter_stichworterklaerung_options.py | 17 ++ 9 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 dokumente/migrations/0010_vorgabentable_alter_person_options_vorgabecomment.py create mode 100644 referenzen/migrations/0003_alter_referenzerklaerung_options.py create mode 100644 stichworte/migrations/0003_alter_stichworterklaerung_options.py diff --git a/dokumente/migrations/0010_vorgabentable_alter_person_options_vorgabecomment.py b/dokumente/migrations/0010_vorgabentable_alter_person_options_vorgabecomment.py new file mode 100644 index 0000000..b1937e4 --- /dev/null +++ b/dokumente/migrations/0010_vorgabentable_alter_person_options_vorgabecomment.py @@ -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'], + }, + ), + ] diff --git a/dokumente/models.py b/dokumente/models.py index 8c5f716..df90bff 100644 --- a/dokumente/models.py +++ b/dokumente/models.py @@ -1,5 +1,6 @@ from django.db import models from mptt.models import MPTTModel, TreeForeignKey +from django.contrib.auth.models import User from abschnitte.models import Textabschnitt from stichworte.models import Stichwort from referenzen.models import Referenz @@ -261,3 +262,19 @@ class Changelog(models.Model): class Meta: verbose_name_plural="Changelog" 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()}" diff --git a/dokumente/templates/standards/incomplete_vorgaben.html b/dokumente/templates/standards/incomplete_vorgaben.html index e6c9f15..b8425f9 100644 --- a/dokumente/templates/standards/incomplete_vorgaben.html +++ b/dokumente/templates/standards/incomplete_vorgaben.html @@ -105,13 +105,13 @@ {% else %} @@ -119,7 +119,7 @@
- Zurück zur Übersicht + Zurück zur Übersicht
diff --git a/dokumente/templates/standards/standard_detail.html b/dokumente/templates/standards/standard_detail.html index f847591..033f96f 100644 --- a/dokumente/templates/standards/standard_detail.html +++ b/dokumente/templates/standards/standard_detail.html @@ -145,6 +145,20 @@ {% endif %}

+ + + {% if user.is_authenticated %} +
+ +
+ {% endif %} @@ -176,4 +190,180 @@ + + + + + + + {% endblock %} diff --git a/dokumente/urls.py b/dokumente/urls.py index a2db977..23e4fe1 100644 --- a/dokumente/urls.py +++ b/dokumente/urls.py @@ -8,6 +8,9 @@ urlpatterns = [ path('/history//', views.standard_detail), path('/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'), path('/checkliste/', views.standard_checkliste, name='standard_checkliste'), - path('/json/', views.standard_json, name='standard_json') + path('/json/', views.standard_json, name='standard_json'), + path('comments//', views.get_vorgabe_comments, name='get_vorgabe_comments'), + path('comments//add/', views.add_vorgabe_comment, name='add_vorgabe_comment'), + path('comments/delete//', views.delete_vorgabe_comment, name='delete_vorgabe_comment'), ] diff --git a/dokumente/views.py b/dokumente/views.py index 0c6df0a..053973f 100644 --- a/dokumente/views.py +++ b/dokumente/views.py @@ -2,8 +2,10 @@ from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required, user_passes_test from django.http import JsonResponse from django.core.serializers.json import DjangoJSONEncoder +from django.views.decorators.http import require_POST +from django.views.decorators.csrf import csrf_exempt 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 datetime import date @@ -44,6 +46,15 @@ def standard_detail(request, nummer,check_date=""): for r in vorgabe.referenzen.all(): referenz_items.append(r.Path()) 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', { 'standard': standard, @@ -237,3 +248,83 @@ def standard_json(request, nummer): # Return JSON response 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') + else: + # Regular users can only see their own comments + comments = vorgabe.comments.filter(user=request.user).select_related('user') + + comments_data = [] + for comment in comments: + comments_data.append({ + 'id': comment.id, + 'text': comment.text, + 'user': 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': comment.user == request.user + }) + + return JsonResponse({'comments': comments_data}) + + +@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() + + if not text: + return JsonResponse({'error': 'Kommentar darf nicht leer sein'}, status=400) + + comment = VorgabeComment.objects.create( + vorgabe=vorgabe, + user=request.user, + text=text + ) + + return JsonResponse({ + 'success': True, + 'comment': { + 'id': comment.id, + 'text': comment.text, + 'user': 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 + } + }) + + except json.JSONDecodeError: + return JsonResponse({'error': 'Ungültige Daten'}, status=400) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@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: + return JsonResponse({'error': 'Keine Berechtigung zum Löschen dieses Kommentars'}, status=403) + + try: + comment.delete() + return JsonResponse({'success': True}) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) diff --git a/referenzen/migrations/0003_alter_referenzerklaerung_options.py b/referenzen/migrations/0003_alter_referenzerklaerung_options.py new file mode 100644 index 0000000..e214b19 --- /dev/null +++ b/referenzen/migrations/0003_alter_referenzerklaerung_options.py @@ -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'}, + ), + ] diff --git a/static/custom/css/admin_extras.css b/static/custom/css/admin_extras.css index 2e15826..29450c5 100644 --- a/static/custom/css/admin_extras.css +++ b/static/custom/css/admin_extras.css @@ -11,4 +11,93 @@ margin-bottom: 1em; border: 1px solid #ccc; padding: 0; - } \ No newline at end of file + } + +/* 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; + } +} \ No newline at end of file diff --git a/stichworte/migrations/0003_alter_stichworterklaerung_options.py b/stichworte/migrations/0003_alter_stichworterklaerung_options.py new file mode 100644 index 0000000..948e9f4 --- /dev/null +++ b/stichworte/migrations/0003_alter_stichworterklaerung_options.py @@ -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'}, + ), + ] From dd6d0fae46c85566be878fa4a4665a47e577d24f Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 27 Nov 2025 23:23:51 +0100 Subject: [PATCH 2/7] Comments migrated into database and data-loader-container. Deploying as soon as merged. --- argocd/deployment.yaml | 4 ++-- data-loader/preload.sqlite3 | Bin 921600 -> 921600 bytes data/db.sqlite3 | Bin 921600 -> 921600 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml index ab2534c..deb99fd 100644 --- a/argocd/deployment.yaml +++ b/argocd/deployment.yaml @@ -18,14 +18,14 @@ spec: fsGroupChangePolicy: "OnRootMismatch" initContainers: - 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" ] volumeMounts: - name: data mountPath: /data containers: - name: web - image: git.baumann.gr/adebaumann/vui:0.957-xss + image: git.baumann.gr/adebaumann/vui:0.958-comments imagePullPolicy: Always ports: - containerPort: 8000 diff --git a/data-loader/preload.sqlite3 b/data-loader/preload.sqlite3 index 58a6294adc36770293115110731a235821b1d0f9..3900958aec61298020ef07dc7803f4b321aaf4b2 100644 GIT binary patch delta 5736 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 S=VWvGJYO}h{Ri^$qyG;qguIym delta 1544 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 delta 4927 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 Date: Thu, 27 Nov 2025 23:51:04 +0100 Subject: [PATCH 3/7] XSS protection added to comments --- argocd/deployment.yaml | 2 +- .../templates/standards/standard_detail.html | 32 +++++++++- dokumente/views.py | 60 +++++++++++++++---- pages/templates/base.html | 2 +- 4 files changed, 82 insertions(+), 14 deletions(-) diff --git a/argocd/deployment.yaml b/argocd/deployment.yaml index deb99fd..61ed7eb 100644 --- a/argocd/deployment.yaml +++ b/argocd/deployment.yaml @@ -25,7 +25,7 @@ spec: mountPath: /data containers: - name: web - image: git.baumann.gr/adebaumann/vui:0.958-comments + image: git.baumann.gr/adebaumann/vui:0.958-comments-XSS imagePullPolicy: Always ports: - containerPort: 8000 diff --git a/dokumente/templates/standards/standard_detail.html b/dokumente/templates/standards/standard_detail.html index 033f96f..6f43da3 100644 --- a/dokumente/templates/standards/standard_detail.html +++ b/dokumente/templates/standards/standard_detail.html @@ -219,6 +219,36 @@ comment" + ) + + self.client.login(username='regularuser', password='testpass123') + + url = reverse('get_vorgabe_comments', kwargs={'vorgabe_id': self.vorgabe.id}) + response = self.client.get(url) + + import json + data = json.loads(response.content) + + # Find the comment with script tag + script_comment = [c for c in data['comments'] if 'script' in c['text'].lower()][0] + + # Should be escaped + self.assertIn('<script>', script_comment['text']) + self.assertNotIn(' comment"}', + content_type='application/json' + ) + + self.assertEqual(response.status_code, 400) + + import json + data = json.loads(response.content) + + self.assertIn('error', data) + self.assertIn('ungültige', data['error'].lower()) + + # No comment should be created + self.assertEqual(VorgabeComment.objects.count(), 0) + + def test_add_comment_xss_javascript_protocol_blocked(self): + """Test that comments with javascript: protocol are blocked""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id}) + response = self.client.post(url, + data='{"text": "Click here"}', + content_type='application/json' + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual(VorgabeComment.objects.count(), 0) + + def test_add_comment_xss_event_handlers_blocked(self): + """Test that comments with event handlers are blocked""" + dangerous_inputs = [ + 'Test onload=alert(1) comment', + 'Test onerror=alert(1) comment', + 'Test onclick=alert(1) comment', + 'Test onmouseover=alert(1) comment' + ] + + self.client.login(username='testuser', password='testpass123') + url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id}) + + for dangerous_input in dangerous_inputs: + response = self.client.post(url, + data=f'{{"text": "{dangerous_input}"}}', + content_type='application/json' + ) + + self.assertEqual(response.status_code, 400) + + # No comments should be created + self.assertEqual(VorgabeComment.objects.count(), 0) + + def test_add_comment_invalid_json_fails(self): + """Test that invalid JSON is rejected""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id}) + response = self.client.post(url, + data='invalid json', + content_type='application/json' + ) + + self.assertEqual(response.status_code, 400) + + import json + data = json.loads(response.content) + + self.assertIn('error', data) + self.assertIn('Ungültige', data['error']) + + def test_add_comment_nonexistent_vorgabe_fails(self): + """Test that adding comment to non-existent Vorgabe returns 404""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': 99999}) + response = self.client.post(url, + data='{"text": "Test comment"}', + content_type='application/json' + ) + + self.assertEqual(response.status_code, 404) + + def test_add_comment_security_headers(self): + """Test that security headers are present in response""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('add_vorgabe_comment', kwargs={'vorgabe_id': self.vorgabe.id}) + response = self.client.post(url, + data='{"text": "Test comment"}', + content_type='application/json' + ) + + self.assertIn('Content-Security-Policy', response) + self.assertIn('X-Content-Type-Options', response) + self.assertEqual(response['X-Content-Type-Options'], 'nosniff') + + +class DeleteVorgabeCommentViewTest(TestCase): + """Test cases for delete_vorgabe_comment view""" + + def setUp(self): + """Set up test data""" + self.client = Client() + + self.user = User.objects.create_user( + username='testuser', + password='testpass123' + ) + + self.other_user = User.objects.create_user( + username='otheruser', + password='testpass123' + ) + + self.staff_user = User.objects.create_user( + username='staffuser', + password='testpass123' + ) + self.staff_user.is_staff = True + self.staff_user.save() + + self.dokumententyp = Dokumententyp.objects.create( + name="Test Typ", + verantwortliche_ve="Test VE" + ) + + self.thema = Thema.objects.create(name="Test Thema") + + self.dokument = Dokument.objects.create( + nummer="COMM-001", + dokumententyp=self.dokumententyp, + name="Comment Test", + aktiv=True + ) + + self.vorgabe = Vorgabe.objects.create( + order=1, + nummer=1, + dokument=self.dokument, + thema=self.thema, + titel="Test Vorgabe", + gueltigkeit_von=date.today() + ) + + self.comment = VorgabeComment.objects.create( + vorgabe=self.vorgabe, + user=self.user, + text="Test comment to delete" + ) + + def test_delete_comment_requires_login(self): + """Test that anonymous users cannot delete comments""" + url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id}) + response = self.client.post(url) + + # Should redirect to login + self.assertEqual(response.status_code, 302) + + # Comment should still exist + self.assertTrue(VorgabeComment.objects.filter(id=self.comment.id).exists()) + + def test_delete_comment_requires_post(self): + """Test that only POST method is allowed""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id}) + response = self.client.get(url) + + # Should return method not allowed + self.assertEqual(response.status_code, 405) + + def test_user_can_delete_own_comment(self): + """Test that users can delete their own comments""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id}) + response = self.client.post(url) + + self.assertEqual(response.status_code, 200) + + import json + data = json.loads(response.content) + + self.assertTrue(data['success']) + + # Comment should be deleted + self.assertFalse(VorgabeComment.objects.filter(id=self.comment.id).exists()) + + def test_user_cannot_delete_other_users_comment(self): + """Test that users cannot delete other users' comments""" + self.client.login(username='otheruser', password='testpass123') + + url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id}) + response = self.client.post(url) + + self.assertEqual(response.status_code, 403) + + import json + data = json.loads(response.content) + + self.assertIn('error', data) + self.assertIn('Berechtigung', data['error']) + + # Comment should still exist + self.assertTrue(VorgabeComment.objects.filter(id=self.comment.id).exists()) + + def test_staff_can_delete_any_comment(self): + """Test that staff users can delete any comment""" + self.client.login(username='staffuser', password='testpass123') + + url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id}) + response = self.client.post(url) + + self.assertEqual(response.status_code, 200) + + import json + data = json.loads(response.content) + + self.assertTrue(data['success']) + + # Comment should be deleted + self.assertFalse(VorgabeComment.objects.filter(id=self.comment.id).exists()) + + def test_delete_nonexistent_comment_returns_404(self): + """Test that deleting non-existent comment returns 404""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('delete_vorgabe_comment', kwargs={'comment_id': 99999}) + response = self.client.post(url) + + self.assertEqual(response.status_code, 404) + + def test_delete_comment_security_headers(self): + """Test that security headers are present in response""" + self.client.login(username='testuser', password='testpass123') + + url = reverse('delete_vorgabe_comment', kwargs={'comment_id': self.comment.id}) + response = self.client.post(url) + + self.assertIn('Content-Security-Policy', response) + self.assertIn('X-Content-Type-Options', response) + self.assertEqual(response['X-Content-Type-Options'], 'nosniff') diff --git a/dokumente/views.py b/dokumente/views.py index a413894..0b535dd 100644 --- a/dokumente/views.py +++ b/dokumente/views.py @@ -259,10 +259,10 @@ def get_vorgabe_comments(request, vorgabe_id): if request.user.is_staff: # Staff can see all comments - comments = vorgabe.comments.all().select_related('user') + 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') + comments = vorgabe.comments.filter(user=request.user).select_related('user').order_by('created_at') comments_data = [] for comment in comments: diff --git a/pages/templates/base.html b/pages/templates/base.html index 27b84a4..fd19f62 100644 --- a/pages/templates/base.html +++ b/pages/templates/base.html @@ -215,7 +215,7 @@

-

Version {{ version|default:"0.959" }}

+

Version {{ version|default:"0.960" }}

From 3a89f6d871d0fab058515faecd3b2cdd953734a7 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Mon, 1 Dec 2025 10:55:46 +0100 Subject: [PATCH 7/7] Full name on comments --- dokumente/templates/standards/standard_detail.html | 2 +- dokumente/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dokumente/templates/standards/standard_detail.html b/dokumente/templates/standards/standard_detail.html index 6f43da3..38c2d54 100644 --- a/dokumente/templates/standards/standard_detail.html +++ b/dokumente/templates/standards/standard_detail.html @@ -297,7 +297,7 @@ document.addEventListener('DOMContentLoaded', function() {
- ${comment.user} + ${comment.user} (${comment.created_at}) ${comment.updated_at !== comment.created_at ? `(bearbeitet: ${comment.updated_at})` : ''}
${comment.text}
diff --git a/dokumente/views.py b/dokumente/views.py index 0b535dd..53e7b49 100644 --- a/dokumente/views.py +++ b/dokumente/views.py @@ -271,7 +271,7 @@ def get_vorgabe_comments(request, vorgabe_id): comments_data.append({ 'id': comment.id, 'text': escaped_text, - 'user': escape(comment.user.username), + '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