feat: added permissions and allowed transitions logic

This commit is contained in:
2025-05-31 21:32:55 +02:00
parent aabf84ecc0
commit f5d2550000
4 changed files with 117 additions and 17 deletions

View File

@@ -25,19 +25,104 @@ class TicketForm(forms.ModelForm):
}) })
} }
def __init__(self, *args, **kwargs):
# User und ticket aus kwargs holen (werden von der View übergeben)
self.user = kwargs.pop('user', None)
self.ticket = kwargs.pop('ticket', None)
super().__init__(*args, **kwargs)
# Status-Choices basierend auf aktueller Situation einschränken
if self.ticket and self.user:
self._limit_status_choices()
self._set_field_permissions()
def _set_field_permissions(self):
"""Setzt welche Felder bearbeitet werden dürfen"""
is_creator = self.user == self.ticket.created_by
is_tutor = self.user == self.ticket.assigned_to
is_superuser = self.user.is_superuser
if is_tutor and not is_superuser:
# Tutor darf nur Status und Answer ändern
readonly_fields = ['title', 'description', 'course', 'priority']
for field_name in readonly_fields:
if field_name in self.fields:
self.fields[field_name].disabled = True
elif is_creator and not is_superuser:
# Ersteller darf gar nichts ändern
for field_name in self.fields:
self.fields[field_name].disabled = True
def _limit_status_choices(self):
"""Beschränkt die verfügbaren Status-Optionen basierend auf Rolle und aktuellem Status"""
current_status = self.ticket.status
is_creator = self.user == self.ticket.created_by
is_tutor = self.user == self.ticket.assigned_to
# Alle möglichen Status
all_choices = list(self.fields['status'].choices)
allowed_choices = []
if is_tutor:
if current_status == 'new':
allowed_choices = ['new', 'in_progress']
elif current_status == 'in_progress':
allowed_choices = ['in_progress', 'resolved', 'new']
elif current_status == 'resolved':
allowed_choices = ['resolved', 'closed']
elif current_status == 'closed':
allowed_choices = ['closed']
else:
# Nicht-Tutoren sehen nur aktuellen Status
allowed_choices = [current_status]
# Filtern der erlaubten Choices
self.fields['status'].choices = [
choice for choice in all_choices
if choice[0] in allowed_choices
]
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
status = cleaned_data.get('status') status = cleaned_data.get('status')
answer = cleaned_data.get('answer') answer = cleaned_data.get('answer')
# Wenn Status auf "gelöst" gesetzt wird, muss eine Antwort vorhanden sein # Wenn Status auf "resolved" gesetzt wird, muss eine Antwort vorhanden sein
if status == 'resolved' and not answer: if status == 'resolved' and not answer:
raise ValidationError({ raise ValidationError({
'answer': 'Eine Antwort ist erforderlich, wenn der Status auf "Gelöst" gesetzt wird.' 'answer': 'Eine Antwort ist erforderlich, wenn der Status auf "Gelöst" gesetzt wird.'
}) })
# Zusätzliche Validierung: Status-Übergang erlaubt?
if self.ticket and status:
old_status = self.ticket.status
if not self._is_status_transition_allowed(old_status, status):
raise ValidationError({
'status': f'Übergang von "{self.ticket.get_status_display()}" zu "{dict(self.fields["status"].choices)[status]}" ist nicht erlaubt.'
})
return cleaned_data return cleaned_data
def _is_status_transition_allowed(self, old_status, new_status):
"""Prüft ob ein Status-Übergang erlaubt ist"""
if old_status == new_status:
return True
is_tutor = self.user == self.ticket.assigned_to
# Erlaubte Übergänge für Tutoren
allowed_transitions = {
'new': ['in_progress'],
'in_progress': ['resolved', 'new'],
'resolved': ['closed'],
'closed': [] # Keine Änderung von closed
}
if is_tutor and old_status in allowed_transitions:
return new_status in allowed_transitions[old_status]
return False
def save(self, commit=True): def save(self, commit=True):
ticket = super().save(commit=False) ticket = super().save(commit=False)

View File

@@ -25,15 +25,15 @@
<input type="text" <input type="text"
name="title" name="title"
value="{{ ticket.title }}" value="{{ ticket.title }}"
{% if not view.can_edit %}disabled{% endif %} {% if not view.can_edit or form.title.field.disabled %}disabled{% endif %}
class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 {% if not view.can_edit %}bg-gray-100{% endif %}"> class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 {% if not view.can_edit or form.title.field.disabled %}bg-gray-100{% endif %}">
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1">Beschreibung:</label> <label class="block text-sm font-medium mb-1">Beschreibung:</label>
<textarea name="description" <textarea name="description"
rows="4" rows="4"
{% if not view.can_edit %}disabled{% endif %} {% if not view.can_edit or form.description.field.disabled %}disabled{% endif %}
class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 {% if not view.can_edit %}bg-gray-100{% endif %}">{{ ticket.description }}</textarea> class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 {% if not view.can_edit or form.description.field.disabled %}bg-gray-100{% endif %}">{{ ticket.description }}</textarea>
</div> </div>
<div> <div>
<label class="block text-sm font-medium mb-1">Kurs:</label> <label class="block text-sm font-medium mb-1">Kurs:</label>
@@ -74,7 +74,7 @@
<!-- Zugewiesen an (Nur Anzeige) --> <!-- Zugewiesen an (Nur Anzeige) -->
<div> <div>
<label class="block text-sm font-medium mb-1">Zugewiesen an:</label> <label class="block text-sm font-medium mb-1">Zugewiesen an:</label>
<div id="tutor_display" class="w-full p-2 border border-gray-300 bg-gray-300 rounded shadow-sm"> <div id="tutor_display" class="w-full p-2 border border-gray-300 bg-gray-100 rounded shadow-sm">
<span id="tutor_text">{% if ticket.assigned_to %}{{ ticket.assigned_to.username }}{% else %}Niemand zugewiesen{% endif %}</span> <span id="tutor_text">{% if ticket.assigned_to %}{{ ticket.assigned_to.username }}{% else %}Niemand zugewiesen{% endif %}</span>
</div> </div>
<p class="text-xs text-gray-500 mt-1">Wird automatisch basierend auf dem ausgewählten Kurs zugewiesen</p> <p class="text-xs text-gray-500 mt-1">Wird automatisch basierend auf dem ausgewählten Kurs zugewiesen</p>
@@ -104,7 +104,7 @@
rows="4" rows="4"
{% if not view.can_edit or ticket.status != 'resolved' %}disabled{% endif %} {% if not view.can_edit or ticket.status != 'resolved' %}disabled{% endif %}
placeholder="Beschreibe die Lösung des Problems..." placeholder="Beschreibe die Lösung des Problems..."
class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder:text-black {% if not view.can_edit or ticket.status != 'resolved' %}bg-gray-300 cursor-not-allowed{% endif %}">{{ ticket.answer|default:'' }}</textarea> class="w-full p-2 border border-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 placeholder:text-black {% if not view.can_edit or ticket.status != 'resolved' %}bg-gray-100 cursor-not-allowed{% endif %}">{{ ticket.answer|default:'' }}</textarea>
{% if form.answer.errors %} {% if form.answer.errors %}
<div class="text-red-600 text-sm mt-1">{{ form.answer.errors }}</div> <div class="text-red-600 text-sm mt-1">{{ form.answer.errors }}</div>
{% endif %} {% endif %}

View File

@@ -64,7 +64,7 @@
<label class="block text-sm font-medium mb-1">Status:</label> <label class="block text-sm font-medium mb-1">Status:</label>
<select name="status" <select name="status"
disabled disabled
class="w-full p-2 border border-gray-300 bg-gray-300 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> class="w-full p-2 border border-gray-300 bg-gray-100 rounded shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="new" <option value="new"
{% if form.status.value == 'new' or not form.status.value %}selected{% endif %}> {% if form.status.value == 'new' or not form.status.value %}selected{% endif %}>
Neu Neu
@@ -91,7 +91,7 @@
<!-- Zugewiesen an --> <!-- Zugewiesen an -->
<div> <div>
<label class="block text-sm font-medium mb-1">Zugewiesen an:</label> <label class="block text-sm font-medium mb-1">Zugewiesen an:</label>
<div id="tutor_display" class="w-full p-2 border border-gray-300 bg-gray-300 rounded shadow-sm"> <div id="tutor_display" class="w-full p-2 border border-gray-300 bg-gray-100 rounded shadow-sm">
<span id="tutor_text">Bitte Kurs auswählen</span> <span id="tutor_text">Bitte Kurs auswählen</span>
</div> </div>
<p class="text-xs text-gray-500 mt-1">Wird automatisch basierend auf dem ausgewählten Kurs zugewiesen</p> <p class="text-xs text-gray-500 mt-1">Wird automatisch basierend auf dem ausgewählten Kurs zugewiesen</p>

View File

@@ -79,19 +79,34 @@ class TicketDetailUpdateView(UpdateView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.ticket = self.get_object() self.ticket = self.get_object()
user = request.user user = request.user
# Prüfen, ob User bearbeiten darf
self.can_edit = (user == self.ticket.assigned_to) or user.is_superuser # Erweiterte Berechtigungslogik
is_creator = user == self.ticket.created_by
is_assigned_tutor = user == self.ticket.assigned_to
is_superuser = user.is_superuser
self.can_edit = is_assigned_tutor or is_superuser
# Zusätzliche Flags für Template
self.is_creator = is_creator
self.is_tutor = is_assigned_tutor
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_form(self, form_class=None): def get_form_kwargs(self):
form = super().get_form(form_class) """Übergibt zusätzliche kwargs ans Form"""
if not self.can_edit: kwargs = super().get_form_kwargs()
for field in form.fields: kwargs['user'] = self.request.user
form.fields[field].disabled = True # Felder lesbar, aber nicht änderbar kwargs['ticket'] = self.object
return form return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Füge Berechtigungs-Infos zum Context hinzu
context['is_creator'] = self.is_creator
context['is_tutor'] = self.is_tutor
context['can_edit'] = self.can_edit
# Kommentarformular hinzufügen # Kommentarformular hinzufügen
if "comment_form" not in context: if "comment_form" not in context:
context["comment_form"] = self.comment_form_class() context["comment_form"] = self.comment_form_class()