Comment function added

This commit is contained in:
2025-11-27 23:11:59 +01:00
parent 5535684a45
commit e5202d9b2b
9 changed files with 479 additions and 6 deletions

View File

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

View File

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

View File

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

View File

@@ -145,6 +145,20 @@
{% endif %} {% endif %}
</p> </p>
</div> </div>
<!-- Comment Button -->
{% if user.is_authenticated %}
<div class="mt-3 text-right">
<button class="btn btn-sm btn-outline-primary comment-btn"
data-vorgabe-id="{{ vorgabe.id }}"
data-vorgabe-nummer="{{ vorgabe.Vorgabennummer }}">
<span class="emoji-icon">💬</span> Kommentare
{% if vorgabe.comment_count > 0 %}
<span class="comment-count">{{ vorgabe.comment_count }}</span>
{% endif %}
</button>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -176,4 +190,180 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Comment Modal -->
<div class="modal fade" id="commentModal" tabindex="-1" role="dialog" aria-labelledby="commentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="commentModalLabel">Kommentare für <span id="modalVorgabeNummer"></span></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div id="commentsContainer">
<!-- Comments will be loaded here -->
</div>
<!-- Add Comment Form -->
<div class="mt-4">
<h6>Neuen Kommentar hinzufügen:</h6>
<textarea id="newCommentText" class="form-control" rows="3" placeholder="Ihr Kommentar..."></textarea>
<button id="addCommentBtn" class="btn btn-primary btn-sm mt-2">Kommentar hinzufügen</button>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript for Comments -->
<script>
document.addEventListener('DOMContentLoaded', function() {
let currentVorgabeId = null;
let currentVorgabeNummer = null;
// Comment button click handler
document.querySelectorAll('.comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentVorgabeId = this.dataset.vorgabeId;
currentVorgabeNummer = this.dataset.vorgabeNummer;
document.getElementById('modalVorgabeNummer').textContent = currentVorgabeNummer;
document.getElementById('newCommentText').value = '';
loadComments();
$('#commentModal').modal('show');
});
});
// Load comments function
function loadComments() {
fetch(`/dokumente/comments/${currentVorgabeId}/`)
.then(response => response.json())
.then(data => {
renderComments(data.comments);
})
.catch(error => {
console.error('Error loading comments:', error);
document.getElementById('commentsContainer').innerHTML =
'<div class="alert alert-danger">Fehler beim Laden der Kommentare</div>';
});
}
// Render comments function
function renderComments(comments) {
const container = document.getElementById('commentsContainer');
if (comments.length === 0) {
container.innerHTML = '<p class="text-muted">Noch keine Kommentare vorhanden.</p>';
return;
}
let html = '';
comments.forEach(comment => {
const canDelete = comment.is_own || {% if user.is_authenticated %}'{{ user.is_staff|yesno:"true,false" }}'{% else %}'false'{% endif %} === 'true';
html += `
<div class="comment-item border-bottom pb-2 mb-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<strong>${comment.user}</strong>
<small class="text-muted">(${comment.created_at})</small>
${comment.updated_at !== comment.created_at ? `<small class="text-muted">(bearbeitet: ${comment.updated_at})</small>` : ''}
<div class="mt-1">${comment.text.replace(/\n/g, '<br>')}</div>
</div>
${canDelete ? `
<button class="btn btn-sm btn-outline-danger ml-2 delete-comment-btn" data-comment-id="${comment.id}">
<span aria-hidden="true">&times;</span>
</button>
` : ''}
</div>
</div>
`;
});
container.innerHTML = html;
// Add delete handlers
document.querySelectorAll('.delete-comment-btn').forEach(btn => {
btn.addEventListener('click', function() {
if (confirm('Möchten Sie diesen Kommentar wirklich löschen?')) {
deleteComment(this.dataset.commentId);
}
});
});
}
// Add comment function
document.getElementById('addCommentBtn').addEventListener('click', function() {
const text = document.getElementById('newCommentText').value.trim();
if (!text) {
alert('Bitte geben Sie einen Kommentar ein.');
return;
}
fetch(`/dokumente/comments/${currentVorgabeId}/add/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ text: text })
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('newCommentText').value = '';
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error adding comment:', error);
alert('Fehler beim Hinzufügen des Kommentars');
});
});
// Delete comment function
function deleteComment(commentId) {
fetch(`/dokumente/comments/delete/${commentId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadComments();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
})
.catch(error => {
console.error('Error deleting comment:', error);
alert('Fehler beim Löschen des Kommentars');
});
}
// CSRF token helper
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});
</script>
{% endblock %} {% endblock %}

View File

@@ -8,6 +8,9 @@ urlpatterns = [
path('<str:nummer>/history/<str:check_date>/', views.standard_detail), path('<str:nummer>/history/<str:check_date>/', views.standard_detail),
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'), path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'), path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
path('<str:nummer>/json/', views.standard_json, name='standard_json') path('<str:nummer>/json/', views.standard_json, name='standard_json'),
path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),
] ]

View File

@@ -2,8 +2,10 @@ from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import JsonResponse from django.http import JsonResponse
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
import json import json
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
from datetime import date from datetime import date
@@ -45,6 +47,15 @@ def standard_detail(request, nummer,check_date=""):
referenz_items.append(r.Path()) referenz_items.append(r.Path())
vorgabe.referenzpfade = referenz_items vorgabe.referenzpfade = referenz_items
# Add comment count
if request.user.is_authenticated:
if request.user.is_staff:
vorgabe.comment_count = vorgabe.comments.count()
else:
vorgabe.comment_count = vorgabe.comments.filter(user=request.user).count()
else:
vorgabe.comment_count = 0
return render(request, 'standards/standard_detail.html', { return render(request, 'standards/standard_detail.html', {
'standard': standard, 'standard': standard,
'vorgaben': vorgaben, 'vorgaben': vorgaben,
@@ -237,3 +248,83 @@ def standard_json(request, nummer):
# Return JSON response # Return JSON response
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder) return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
@login_required
def get_vorgabe_comments(request, vorgabe_id):
"""Get comments for a specific Vorgabe"""
vorgabe = get_object_or_404(Vorgabe, id=vorgabe_id)
if request.user.is_staff:
# Staff can see all comments
comments = vorgabe.comments.all().select_related('user')
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)

View File

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

View File

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

View File

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