feat: status transition and allowed fields consolidated
This commit is contained in:
@@ -15,6 +15,19 @@ class CommentForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class TicketForm(forms.ModelForm):
|
class TicketForm(forms.ModelForm):
|
||||||
|
# Zentrale Definition der Status-Übergänge
|
||||||
|
STATUS_TRANSITIONS = {
|
||||||
|
'new': ['in_progress'],
|
||||||
|
'in_progress': ['resolved', 'closed'],
|
||||||
|
'resolved': ['closed'],
|
||||||
|
'closed': [], # Keine weiteren Übergänge
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zentrale Definition welche Felder wann required sind
|
||||||
|
REQUIRED_FIELDS_BY_STATUS = {
|
||||||
|
'resolved': ['answer'], # Answer required when resolving
|
||||||
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = ["title", "description", "status", "priority", "course", "answer", "material"]
|
fields = ["title", "description", "status", "priority", "course", "answer", "material"]
|
||||||
@@ -26,25 +39,30 @@ class TicketForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# User und ticket aus kwargs holen (werden von der View übergeben)
|
|
||||||
self.user = kwargs.pop('user', None)
|
self.user = kwargs.pop('user', None)
|
||||||
self.ticket = kwargs.pop('ticket', None)
|
self.ticket = kwargs.pop('ticket', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Status-Choices basierend auf aktueller Situation einschränken
|
|
||||||
if self.ticket and self.user:
|
if self.ticket and self.user:
|
||||||
self._limit_status_choices()
|
self._configure_form_based_on_permissions()
|
||||||
self._set_field_permissions()
|
|
||||||
|
|
||||||
def _set_field_permissions(self):
|
def _configure_form_based_on_permissions(self):
|
||||||
"""Setzt welche Felder bearbeitet werden dürfen"""
|
"""Konfiguriert das Formular basierend auf Benutzerrechten"""
|
||||||
is_creator = self.user == self.ticket.created_by
|
is_creator = self.user == self.ticket.created_by
|
||||||
is_tutor = self.user == self.ticket.assigned_to
|
is_tutor = self.user == self.ticket.assigned_to
|
||||||
is_superuser = self.user.is_superuser
|
is_superuser = self.user.is_superuser
|
||||||
|
|
||||||
|
# Status-Choices einschränken
|
||||||
|
self._limit_status_choices(is_tutor, is_creator)
|
||||||
|
|
||||||
|
# Feld-Berechtigungen setzen
|
||||||
|
self._set_field_permissions(is_tutor, is_creator, is_superuser)
|
||||||
|
|
||||||
|
def _set_field_permissions(self, is_tutor, is_creator, is_superuser):
|
||||||
|
"""Setzt welche Felder bearbeitet werden dürfen"""
|
||||||
if is_tutor and not is_superuser:
|
if is_tutor and not is_superuser:
|
||||||
# Tutor darf nur Status und Answer ändern
|
# Tutor darf nur Status und Answer ändern
|
||||||
readonly_fields = ['title', 'description', 'course', 'priority']
|
readonly_fields = ['title', 'description']
|
||||||
for field_name in readonly_fields:
|
for field_name in readonly_fields:
|
||||||
if field_name in self.fields:
|
if field_name in self.fields:
|
||||||
self.fields[field_name].disabled = True
|
self.fields[field_name].disabled = True
|
||||||
@@ -53,80 +71,68 @@ class TicketForm(forms.ModelForm):
|
|||||||
for field_name in self.fields:
|
for field_name in self.fields:
|
||||||
self.fields[field_name].disabled = True
|
self.fields[field_name].disabled = True
|
||||||
|
|
||||||
def _limit_status_choices(self):
|
def _limit_status_choices(self, is_tutor, is_creator):
|
||||||
"""Beschränkt die verfügbaren Status-Optionen basierend auf Rolle und aktuellem Status"""
|
"""Beschränkt verfügbare Status-Optionen basierend auf der zentralen Logik"""
|
||||||
current_status = self.ticket.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 is_tutor:
|
||||||
if current_status == 'new':
|
allowed_statuses = self._get_allowed_transitions(current_status)
|
||||||
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:
|
else:
|
||||||
# Nicht-Tutoren sehen nur aktuellen Status
|
# Nicht-Tutoren sehen nur aktuellen Status
|
||||||
allowed_choices = [current_status]
|
allowed_statuses = [current_status]
|
||||||
|
|
||||||
# Filtern der erlaubten Choices
|
# Status-Choices filtern
|
||||||
|
all_choices = list(self.fields['status'].choices)
|
||||||
self.fields['status'].choices = [
|
self.fields['status'].choices = [
|
||||||
choice for choice in all_choices
|
choice for choice in all_choices
|
||||||
if choice[0] in allowed_choices
|
if choice[0] in allowed_statuses
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _get_allowed_transitions(self, from_status):
|
||||||
|
"""Gibt erlaubte Status-Übergänge zurück"""
|
||||||
|
return [from_status] + self.STATUS_TRANSITIONS.get(from_status, [])
|
||||||
|
|
||||||
|
def _is_transition_allowed(self, from_status, to_status):
|
||||||
|
"""Prüft ob ein Status-Übergang erlaubt ist"""
|
||||||
|
if from_status == to_status:
|
||||||
|
return True
|
||||||
|
return to_status in self.STATUS_TRANSITIONS.get(from_status, [])
|
||||||
|
|
||||||
|
def _get_required_fields_for_status(self, status):
|
||||||
|
"""Gibt zurück welche Felder für einen Status required sind"""
|
||||||
|
return self.REQUIRED_FIELDS_BY_STATUS.get(status, [])
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# Wenn Status auf "resolved" gesetzt wird, muss eine Antwort vorhanden sein
|
if not self.ticket or not status:
|
||||||
if status == 'resolved' and not answer:
|
return cleaned_data
|
||||||
raise ValidationError({
|
|
||||||
'answer': 'Eine Antwort ist erforderlich, wenn der Status auf "Gelöst" gesetzt wird.'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Zusätzliche Validierung: Status-Übergang erlaubt?
|
# Prüfe Status-Übergang
|
||||||
if self.ticket and status:
|
|
||||||
old_status = self.ticket.status
|
old_status = self.ticket.status
|
||||||
if not self._is_status_transition_allowed(old_status, status):
|
is_tutor = self.user == self.ticket.assigned_to
|
||||||
|
|
||||||
|
if is_tutor and not self._is_transition_allowed(old_status, status):
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'status': f'Übergang von "{self.ticket.get_status_display()}" zu "{dict(self.fields["status"].choices)[status]}" ist nicht erlaubt.'
|
'status': f'Übergang von "{self.ticket.get_status_display()}" zu "{dict(self.fields["status"].choices)[status]}" ist nicht erlaubt.'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Prüfe required fields für neuen Status
|
||||||
|
required_fields = self._get_required_fields_for_status(status)
|
||||||
|
for field_name in required_fields:
|
||||||
|
if not cleaned_data.get(field_name):
|
||||||
|
field_label = self.fields[field_name].label
|
||||||
|
raise ValidationError({
|
||||||
|
field_name: f'{field_label} ist erforderlich, wenn der Status auf "{dict(self.fields["status"].choices)[status]}" gesetzt wird.'
|
||||||
|
})
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
# Automatische Tutor-Zuweisung basierend auf dem ausgewählten Kurs
|
# Automatische Tutor-Zuweisung
|
||||||
if ticket.course and ticket.course.tutor:
|
if ticket.course and ticket.course.tutor:
|
||||||
ticket.assigned_to = ticket.course.tutor
|
ticket.assigned_to = ticket.course.tutor
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ class Ticket(models.Model):
|
|||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("new", "Neu"),
|
("new", "Neu"),
|
||||||
("in_progress", "In Bearbeitung"),
|
("in_progress", "In Bearbeitung"),
|
||||||
("resolved", "Gelöst"),
|
("resolved", "Lösungsvorschlag"),
|
||||||
("pending_close", "Wartend - Schließen"),
|
|
||||||
("closed", "Geschlossen"),
|
("closed", "Geschlossen"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
<label class="block text-sm font-medium mb-1">Kurs:</label>
|
<label class="block text-sm font-medium mb-1">Kurs:</label>
|
||||||
<select name="course"
|
<select name="course"
|
||||||
id="id_course"
|
id="id_course"
|
||||||
{% if not view.can_edit %}disabled{% endif %}
|
{% if not view.can_edit or form.course.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.course.field.disabled %}bg-gray-100{% endif %}">
|
||||||
{% for course in form.course.field.queryset %}
|
{% for course in form.course.field.queryset %}
|
||||||
<option value="{{ course.pk }}"
|
<option value="{{ course.pk }}"
|
||||||
{% if course.pk == ticket.course.pk %}selected{% endif %}>{{ course }}</option>
|
{% if course.pk == ticket.course.pk %}selected{% endif %}>{{ course }}</option>
|
||||||
@@ -52,8 +52,8 @@
|
|||||||
<label class="block text-sm font-medium mb-1">Material:</label>
|
<label class="block text-sm font-medium mb-1">Material:</label>
|
||||||
<select name="material"
|
<select name="material"
|
||||||
id="id_material"
|
id="id_material"
|
||||||
{% if not view.can_edit %}disabled{% endif %}
|
{% if not view.can_edit or form.material.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.material.field.disabled %}bg-gray-100{% endif %}">
|
||||||
{% for value, display in form.fields.material.choices %}
|
{% for value, display in form.fields.material.choices %}
|
||||||
<option value="{{ value }}"
|
<option value="{{ value }}"
|
||||||
{% if form.material.value == value %}selected{% endif %}>{{ display }}</option>
|
{% if form.material.value == value %}selected{% endif %}>{{ display }}</option>
|
||||||
@@ -65,19 +65,20 @@
|
|||||||
<div>
|
<div>
|
||||||
<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"
|
||||||
{% if not view.can_edit %}disabled{% endif %}
|
{% if not view.can_edit or form.status.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.status.field.disabled %}bg-gray-100{% endif %}">
|
||||||
{% for value, label in form.status.field.choices %}
|
{% for value, label in form.status.field.choices %}
|
||||||
<option value="{{ value }}"
|
<option value="{{ value }}"
|
||||||
{% if value == ticket.status %}selected{% endif %}>{{ label }}</option>
|
{% if value == ticket.status %}selected{% endif %}>{{ label }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
{% if form.status.errors %}<div class="text-red-600 text-sm mt-1">{{ form.status.errors }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1">Priorität:</label>
|
<label class="block text-sm font-medium mb-1">Priorität:</label>
|
||||||
<select name="priority"
|
<select name="priority"
|
||||||
{% if not view.can_edit %}disabled{% endif %}
|
{% if not view.can_edit or form.priority.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.priority.field.disabled %}bg-gray-100{% endif %}">
|
||||||
<option value="low" {% if ticket.priority == 'low' %}selected{% endif %}>Niedrig</option>
|
<option value="low" {% if ticket.priority == 'low' %}selected{% endif %}>Niedrig</option>
|
||||||
<option value="medium"
|
<option value="medium"
|
||||||
{% if ticket.priority == 'medium' %}selected{% endif %}>Normal</option>
|
{% if ticket.priority == 'medium' %}selected{% endif %}>Normal</option>
|
||||||
@@ -123,7 +124,7 @@
|
|||||||
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>
|
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 %}<div class="text-red-600 text-sm mt-1">{{ form.answer.errors }}</div>{% endif %}
|
{% if form.answer.errors %}<div class="text-red-600 text-sm mt-1">{{ form.answer.errors }}</div>{% endif %}
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
{% if ticket.status != 'resolved' %}Eine Antwort ist erforderlich beim Setzen des Status auf "Gelöst"{% endif %}
|
{% if ticket.status != 'resolved' or ticket.status != 'closed' %}Eine Antwort ist erforderlich beim Setzen des Status auf "Gelöst"{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -233,7 +234,7 @@
|
|||||||
const answerLabel = answerField.previousElementSibling;
|
const answerLabel = answerField.previousElementSibling;
|
||||||
|
|
||||||
function toggleAnswerField() {
|
function toggleAnswerField() {
|
||||||
if (statusSelect.value === 'resolved') {
|
if (statusSelect.value === 'resolved' || statusSelect.value === 'closed') {
|
||||||
answerField.disabled = false;
|
answerField.disabled = false;
|
||||||
answerField.classList.remove('bg-gray-100', 'cursor-not-allowed');
|
answerField.classList.remove('bg-gray-100', 'cursor-not-allowed');
|
||||||
answerField.classList.add('bg-white');
|
answerField.classList.add('bg-white');
|
||||||
|
|||||||
Reference in New Issue
Block a user