feat: replaced TicketDetailView with TicketDetailUpdateView
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{% extends "ticketsystem/base.html" %}
|
{% extends "ticketsystem/base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div style="max-width: 600px; margin: 1rem auto;">
|
<div style="max-width: 700px; margin: 1rem auto;">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div style="padding: 1rem; border-radius: 5px; margin-bottom: 1rem;
|
<div style="padding: 1rem; border-radius: 5px; margin-bottom: 1rem;
|
||||||
background-color: {% if message.tags == 'error' %}#f8d7da
|
background-color: {% if message.tags == 'error' %}#f8d7da
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ticket-container {
|
.ticket-container {
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
@@ -24,24 +25,48 @@
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticket-container h1 {
|
.form-group {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
font-size: 1.8rem;
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticket-attribute {
|
.form-group input, .form-group select, .form-group textarea {
|
||||||
margin-bottom: 1rem;
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticket-attribute strong {
|
.form-group textarea {
|
||||||
display: inline-block;
|
min-height: 100px;
|
||||||
width: 150px;
|
resize: vertical;
|
||||||
color: #555;
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #0056b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticket-meta {
|
.ticket-meta {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
@@ -54,57 +79,162 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-meta {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-text {
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-form {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-form h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
.comment-form textarea {
|
.comment-form textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem;
|
padding: 0.75rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-form button {
|
.comment-form button {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
background: #007bff;
|
background: #28a745;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-form button:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-entry {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-left: 4px solid #6c757d;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-user {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-field {
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-change {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-old {
|
||||||
|
color: #dc3545;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-new {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-date {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-container h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px dashed #dee2e6;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Ticket Bearbeitung -->
|
||||||
<div class="ticket-container">
|
<div class="ticket-container">
|
||||||
<h1>🎫 Ticket #{{ ticket.id }} – {{ ticket.title }}</h1>
|
<h1>🎫 Ticket #{{ ticket.id }} – {{ ticket.title }}</h1>
|
||||||
|
|
||||||
<p style="text-align: right;">
|
<form method="post" style="margin-top: 1rem;">
|
||||||
<a href="{% url 'modify' ticket.pk %}" style="text-decoration: none; font-weight: bold;">✏️ Dieses Ticket
|
{% csrf_token %}
|
||||||
bearbeiten</a>
|
{{ form.non_field_errors }}
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="ticket-attribute">
|
<div class="form-group">
|
||||||
<strong>Status:</strong> {{ ticket.get_status_display }}
|
<label for="id_title">Titel:</label>
|
||||||
|
{{ form.title }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ticket-attribute">
|
<div class="form-group">
|
||||||
<strong>Priorität:</strong> {{ ticket.get_priority_display }}
|
<label for="id_description">Beschreibung:</label>
|
||||||
|
{{ form.description }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ticket-attribute">
|
<div class="form-group">
|
||||||
<strong>Beschreibung:</strong><br>
|
<label for="id_status">Status:</label>
|
||||||
<div style="margin-top: 0.5rem;">{{ ticket.description }}</div>
|
{{ form.status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ticket-attribute">
|
<div class="form-group">
|
||||||
<strong>Erstellt von:</strong> {{ ticket.created_by.username }}
|
<label for="id_priority">Priorität:</label>
|
||||||
|
{{ form.priority }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ticket-attribute">
|
<div class="form-group">
|
||||||
<strong>Zugewiesen an:</strong>
|
<label for="id_assigned_to">Zugewiesen an:</label>
|
||||||
{% if ticket.assigned_to %}
|
{{ form.assigned_to }}
|
||||||
{{ ticket.assigned_to.username }}
|
</div>
|
||||||
|
|
||||||
|
{% if view.can_edit %}
|
||||||
|
<button type="submit" class="btn">💾 Änderungen speichern</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<em>Niemand zugewiesen</em>
|
<p style="color: #999; margin-top: 1rem;">Du darfst dieses Ticket nicht bearbeiten.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<div class="ticket-meta">
|
<div class="ticket-meta">
|
||||||
🕒 Erstellt am: {{ ticket.created_at|date:"d.m.Y H:i" }}<br>
|
🕒 Erstellt am: {{ ticket.created_at|date:"d.m.Y H:i" }}<br>
|
||||||
@@ -112,43 +242,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ticket-container" style="margin-top: 2rem;">
|
<!-- Kommentare Sektion -->
|
||||||
|
<div class="section-container">
|
||||||
<h2>💬 Kommentare</h2>
|
<h2>💬 Kommentare</h2>
|
||||||
|
|
||||||
{% if ticket.comments.exists %}
|
{% if ticket.comments.exists %}
|
||||||
{% for comment in ticket.comments.all %}
|
{% for comment in ticket.comments.all %}
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
<p><strong>{{ comment.author.username }}</strong> am {{ comment.created_at|date:"d.m.Y H:i" }}</p>
|
<div class="comment-meta">
|
||||||
<p>{{ comment.text }}</p>
|
<strong>{{ comment.author.username }}</strong> am {{ comment.created_at|date:"d.m.Y H:i" }}
|
||||||
|
</div>
|
||||||
|
<div class="comment-text">{{ comment.text }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Keine Kommentare vorhanden.</p>
|
<div class="no-content">
|
||||||
|
Keine Kommentare vorhanden.
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
<div class="comment-form">
|
||||||
<h3>📝 Neuen Kommentar schreiben</h3>
|
<h3>📝 Neuen Kommentar schreiben</h3>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p }}
|
{{ comment_form.as_p }}
|
||||||
<button type="submit">Absenden</button>
|
<button type="submit" name="comment_submit">Kommentar absenden</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div style="margin-top: 3rem;">
|
</div>
|
||||||
|
|
||||||
|
<!-- Bearbeitungshistorie Sektion -->
|
||||||
|
<div class="section-container">
|
||||||
<h2>🕓 Bearbeitungshistorie</h2>
|
<h2>🕓 Bearbeitungshistorie</h2>
|
||||||
|
|
||||||
{% if ticket.history.exists %}
|
{% if ticket.history.exists %}
|
||||||
<ul style="list-style: none; padding: 0;">
|
|
||||||
{% for entry in ticket.history.all %}
|
{% for entry in ticket.history.all %}
|
||||||
<li style="margin-bottom: 1rem; background-color: #f9f9f9; border-left: 4px solid #ccc; padding: 0.5rem 1rem;">
|
<div class="history-entry">
|
||||||
<strong>{{ entry.changed_by.username }}</strong>
|
<div class="history-user">{{ entry.changed_by.username }}</div>
|
||||||
hat <code>{{ entry.field }}</code> geändert:<br>
|
<div class="history-change">
|
||||||
<em>{{ entry.old_value }}</em> → <strong>{{ entry.new_value }}</strong><br>
|
hat <span class="history-field">{{ entry.field }}</span> geändert:
|
||||||
<small>am {{ entry.changed_at|date:"d.m.Y H:i" }}</small>
|
</div>
|
||||||
</li>
|
<div class="history-change">
|
||||||
|
<span class="history-old">{{ entry.old_value }}</span> → <span class="history-new">{{ entry.new_value }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="history-date">am {{ entry.changed_at|date:"d.m.Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><em>Keine Änderungen bisher.</em></p>
|
<div class="no-content">
|
||||||
|
Keine Änderungen bisher.
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -6,8 +6,8 @@ from .views import (
|
|||||||
TicketCreateView,
|
TicketCreateView,
|
||||||
TicketUpdateView,
|
TicketUpdateView,
|
||||||
HomeView,
|
HomeView,
|
||||||
TicketDetailView,
|
AssignedTicketListView,
|
||||||
AssignedTicketListView
|
TicketDetailUpdateView
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -16,7 +16,7 @@ urlpatterns = [
|
|||||||
# /ticketsystem/tickets
|
# /ticketsystem/tickets
|
||||||
path("tickets", TicketListView.as_view(), name="ticket-list"),
|
path("tickets", TicketListView.as_view(), name="ticket-list"),
|
||||||
# /ticketsystem/detail/
|
# /ticketsystem/detail/
|
||||||
path("<int:pk>/", TicketDetailView.as_view(), name="detail"),
|
path('<int:pk>/', TicketDetailUpdateView.as_view(), name='detail'),
|
||||||
# /ticketsystem/new/
|
# /ticketsystem/new/
|
||||||
path("new/", TicketCreateView.as_view(), name="create"),
|
path("new/", TicketCreateView.as_view(), name="create"),
|
||||||
path("<int:pk>/modify/", TicketUpdateView.as_view(), name="modify"),
|
path("<int:pk>/modify/", TicketUpdateView.as_view(), name="modify"),
|
||||||
|
|||||||
@@ -46,31 +46,96 @@ class TicketListView(ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class TicketDetailView(FormMixin, DetailView):
|
class TicketDetailUpdateView(UpdateView):
|
||||||
model = Ticket
|
model = Ticket
|
||||||
|
fields = ["title", "description", "status", "priority", "assigned_to"]
|
||||||
template_name = "ticketsystem/detail.html"
|
template_name = "ticketsystem/detail.html"
|
||||||
context_object_name = "ticket"
|
comment_form_class = CommentForm
|
||||||
form_class = CommentForm
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("detail", kwargs={"pk": self.object.pk})
|
return reverse('detail-update', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.ticket = self.get_object()
|
||||||
form = self.get_form()
|
user = request.user
|
||||||
if form.is_valid():
|
# Prüfen, ob User bearbeiten darf
|
||||||
comment = form.save(commit=False)
|
self.can_edit = (user == self.ticket.assigned_to) or user.is_superuser
|
||||||
comment.ticket = self.object
|
return super().dispatch(request, *args, **kwargs)
|
||||||
comment.author = request.user
|
|
||||||
comment.save()
|
def get_form(self, form_class=None):
|
||||||
return super().form_valid(form)
|
form = super().get_form(form_class)
|
||||||
return self.form_invalid(form)
|
if not self.can_edit:
|
||||||
|
for field in form.fields:
|
||||||
|
form.fields[field].disabled = True # Felder lesbar, aber nicht änderbar
|
||||||
|
return form
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["form"] = self.get_form()
|
# Kommentarformular hinzufügen
|
||||||
|
if 'comment_form' not in context:
|
||||||
|
context['comment_form'] = self.comment_form_class()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
ticket = form.instance
|
||||||
|
original = Ticket.objects.get(pk=ticket.pk)
|
||||||
|
response = super().form_valid(form) # Speichert das Ticket
|
||||||
|
|
||||||
|
# History tracking für geänderte Felder
|
||||||
|
tracked_fields = ["title", "description", "status", "priority", "assigned_to"]
|
||||||
|
for field in tracked_fields:
|
||||||
|
if field in form.changed_data:
|
||||||
|
old_value = getattr(original, field)
|
||||||
|
new_value = form.cleaned_data.get(field)
|
||||||
|
|
||||||
|
# Für ForeignKey Felder den Display-Namen verwenden
|
||||||
|
if field == "assigned_to":
|
||||||
|
old_value = old_value.username if old_value else "Niemand"
|
||||||
|
new_value = new_value.username if new_value else "Niemand"
|
||||||
|
elif field == "status":
|
||||||
|
old_value = original.get_status_display()
|
||||||
|
new_value = ticket.get_status_display()
|
||||||
|
elif field == "priority":
|
||||||
|
old_value = original.get_priority_display()
|
||||||
|
new_value = ticket.get_priority_display()
|
||||||
|
|
||||||
|
TicketHistory.objects.create(
|
||||||
|
ticket=ticket,
|
||||||
|
changed_by=self.request.user,
|
||||||
|
field=field,
|
||||||
|
old_value=str(old_value),
|
||||||
|
new_value=str(new_value),
|
||||||
|
)
|
||||||
|
|
||||||
|
if form.changed_data:
|
||||||
|
messages.success(self.request,
|
||||||
|
f"Ticket erfolgreich aktualisiert. Geänderte Felder: {', '.join(form.changed_data)}")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object() # Wichtig: object setzen für beide Fälle
|
||||||
|
|
||||||
|
if 'comment_submit' in request.POST:
|
||||||
|
# Kommentar absenden
|
||||||
|
comment_form = self.comment_form_class(request.POST)
|
||||||
|
if comment_form.is_valid():
|
||||||
|
comment = comment_form.save(commit=False)
|
||||||
|
comment.ticket = self.object
|
||||||
|
comment.author = request.user
|
||||||
|
comment.save()
|
||||||
|
messages.success(request, "Kommentar hinzugefügt.")
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
else:
|
||||||
|
# Kommentarformular fehlerhaft: Seite neu laden mit Fehlern
|
||||||
|
context = self.get_context_data(comment_form=comment_form)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
else:
|
||||||
|
# Ticket bearbeiten (UpdateView standard)
|
||||||
|
if not self.can_edit:
|
||||||
|
messages.error(request, "⛔ Du darfst dieses Ticket nicht bearbeiten.")
|
||||||
|
return redirect(self.get_success_url())
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
class AssignedTicketListView(LoginRequiredMixin, ListView):
|
class AssignedTicketListView(LoginRequiredMixin, ListView):
|
||||||
model = Ticket
|
model = Ticket
|
||||||
|
|||||||
Reference in New Issue
Block a user