From e5202d9b2bf3cbd0703987857083e8741acee720 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Thu, 27 Nov 2025 23:11:59 +0100 Subject: [PATCH] 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'}, + ), + ]