Compare commits
3 Commits
61a7ca6403
...
349ccabd74
| Author | SHA1 | Date | |
|---|---|---|---|
| 349ccabd74 | |||
| 272415d219 | |||
| 8af0bbdb37 |
@@ -1,24 +1,32 @@
|
|||||||
asgiref==3.8.1
|
asgiref==3.8.1
|
||||||
|
astroid==3.3.10
|
||||||
black==25.1.0
|
black==25.1.0
|
||||||
chardet==5.2.0
|
chardet==5.2.0
|
||||||
click==8.1.8
|
click==8.1.8
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
cssbeautifier==1.15.4
|
cssbeautifier==1.15.4
|
||||||
|
dill==0.4.0
|
||||||
Django==5.2
|
Django==5.2
|
||||||
djlint==1.36.4
|
djlint==1.36.4
|
||||||
EditorConfig==0.17.0
|
EditorConfig==0.17.0
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
|
isort==6.0.1
|
||||||
jsbeautifier==1.15.4
|
jsbeautifier==1.15.4
|
||||||
json5==0.12.0
|
json5==0.12.0
|
||||||
|
mccabe==0.7.0
|
||||||
mypy_extensions==1.1.0
|
mypy_extensions==1.1.0
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
pathspec==0.12.1
|
pathspec==0.12.1
|
||||||
pillow==11.2.1
|
pillow==11.2.1
|
||||||
platformdirs==4.3.7
|
platformdirs==4.3.7
|
||||||
|
pylint==3.3.7
|
||||||
|
pylint-django==2.6.1
|
||||||
|
pylint-plugin-utils==0.8.2
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
regex==2024.11.6
|
regex==2024.11.6
|
||||||
reportlab==4.4.1
|
reportlab==4.4.1
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
|
tomlkit==0.13.3
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Ticket, Course, FAQ
|
from .models import FAQ, Course, Ticket
|
||||||
|
|
||||||
admin.site.register(Ticket)
|
admin.site.register(Ticket)
|
||||||
admin.site.register(Course)
|
admin.site.register(Course)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import Comment, Ticket
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from .models import Comment, Ticket
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(forms.ModelForm):
|
class CommentForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -17,45 +18,55 @@ class CommentForm(forms.ModelForm):
|
|||||||
class TicketForm(forms.ModelForm):
|
class TicketForm(forms.ModelForm):
|
||||||
# Zentrale Definition der Status-Übergänge
|
# Zentrale Definition der Status-Übergänge
|
||||||
STATUS_TRANSITIONS = {
|
STATUS_TRANSITIONS = {
|
||||||
'tutor': {
|
"tutor": {
|
||||||
'new': ['in_progress'],
|
"new": ["in_progress"],
|
||||||
'in_progress': ['resolved', 'new'],
|
"in_progress": ["resolved", "new"],
|
||||||
'resolved': ['closed'],
|
"resolved": ["closed"],
|
||||||
'closed': [],
|
"closed": [],
|
||||||
},
|
},
|
||||||
'creator': {
|
"creator": {
|
||||||
'new': [],
|
"new": [],
|
||||||
'in_progress': [],
|
"in_progress": [],
|
||||||
'resolved': ['closed', 'new'],
|
"resolved": ["closed", "new"],
|
||||||
'closed': [],
|
"closed": [],
|
||||||
},
|
},
|
||||||
'superuser': {
|
"superuser": {
|
||||||
# Superuser können alle Übergänge machen
|
# Superuser können alle Übergänge machen
|
||||||
'new': ['in_progress', 'resolved', 'closed'],
|
"new": ["in_progress", "resolved", "closed"],
|
||||||
'in_progress': ['new', 'resolved', 'closed'],
|
"in_progress": ["new", "resolved", "closed"],
|
||||||
'resolved': ['new', 'in_progress', 'closed'],
|
"resolved": ["new", "in_progress", "closed"],
|
||||||
'closed': ['new', 'in_progress', 'resolved'],
|
"closed": ["new", "in_progress", "resolved"],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Zentrale Definition welche Felder wann required sind
|
# Zentrale Definition welche Felder wann required sind
|
||||||
REQUIRED_FIELDS_BY_STATUS = {
|
REQUIRED_FIELDS_BY_STATUS = {
|
||||||
'resolved': ['answer'], # Answer required when resolving
|
"resolved": ["answer"], # Answer required when resolving
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = ["title", "description", "status", "mistake", "course", "answer", "material"]
|
fields = [
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"status",
|
||||||
|
"mistake",
|
||||||
|
"course",
|
||||||
|
"answer",
|
||||||
|
"material",
|
||||||
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'answer': forms.Textarea(attrs={
|
"answer": forms.Textarea(
|
||||||
'rows': 4,
|
attrs={
|
||||||
'placeholder': 'Beschreibe die Lösung des Problems...'
|
"rows": 4,
|
||||||
})
|
"placeholder": "Beschreibe die Lösung des Problems...",
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
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)
|
||||||
|
|
||||||
if self.ticket and self.user:
|
if self.ticket and self.user:
|
||||||
@@ -79,17 +90,17 @@ class TicketForm(forms.ModelForm):
|
|||||||
if is_superuser:
|
if is_superuser:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.ticket.status == 'resolved' and is_creator:
|
if self.ticket.status == "resolved" and is_creator:
|
||||||
for field_name in self.fields:
|
for field_name in self.fields:
|
||||||
if field_name == "answer":
|
if field_name == "answer":
|
||||||
self.fields[field_name].disabled = True
|
self.fields[field_name].disabled = True
|
||||||
elif is_tutor:
|
elif is_tutor:
|
||||||
# Tutor darf ändern:
|
# Tutor darf ändern:
|
||||||
readonly_fields = ['title', 'description', 'material']
|
readonly_fields = ["title", "description", "material"]
|
||||||
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
|
||||||
elif is_creator and self.ticket.status != 'resolved':
|
elif is_creator and self.ticket.status != "resolved":
|
||||||
for field_name in self.fields:
|
for field_name in self.fields:
|
||||||
self.fields[field_name].disabled = True
|
self.fields[field_name].disabled = True
|
||||||
|
|
||||||
@@ -99,11 +110,11 @@ class TicketForm(forms.ModelForm):
|
|||||||
|
|
||||||
# Superuser bekommen alle Status-Optionen
|
# Superuser bekommen alle Status-Optionen
|
||||||
if is_superuser:
|
if is_superuser:
|
||||||
role = 'superuser'
|
role = "superuser"
|
||||||
elif is_tutor:
|
elif is_tutor:
|
||||||
role = 'tutor'
|
role = "tutor"
|
||||||
elif is_creator:
|
elif is_creator:
|
||||||
role = 'creator'
|
role = "creator"
|
||||||
else:
|
else:
|
||||||
role = None
|
role = None
|
||||||
|
|
||||||
@@ -114,10 +125,9 @@ class TicketForm(forms.ModelForm):
|
|||||||
allowed_statuses = [current_status]
|
allowed_statuses = [current_status]
|
||||||
|
|
||||||
# Status-Choices filtern
|
# Status-Choices filtern
|
||||||
all_choices = list(self.fields['status'].choices)
|
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_statuses
|
||||||
if choice[0] in allowed_statuses
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_allowed_transitions(self, from_status, role):
|
def _get_allowed_transitions(self, from_status, role):
|
||||||
@@ -137,7 +147,7 @@ class TicketForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
status = cleaned_data.get('status')
|
status = cleaned_data.get("status")
|
||||||
|
|
||||||
if not self.ticket or not status:
|
if not self.ticket or not status:
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
@@ -150,27 +160,31 @@ class TicketForm(forms.ModelForm):
|
|||||||
|
|
||||||
# Superuser dürfen alle Übergänge
|
# Superuser dürfen alle Übergänge
|
||||||
if is_superuser:
|
if is_superuser:
|
||||||
role = 'superuser'
|
role = "superuser"
|
||||||
elif is_tutor:
|
elif is_tutor:
|
||||||
role = 'tutor'
|
role = "tutor"
|
||||||
elif is_creator:
|
elif is_creator:
|
||||||
role = 'creator'
|
role = "creator"
|
||||||
else:
|
else:
|
||||||
role = None
|
role = None
|
||||||
|
|
||||||
if role and not self._is_transition_allowed(old_status, status, role):
|
if role and not self._is_transition_allowed(old_status, status, role):
|
||||||
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
|
# Prüfe required fields für neuen Status
|
||||||
required_fields = self._get_required_fields_for_status(status)
|
required_fields = self._get_required_fields_for_status(status)
|
||||||
for field_name in required_fields:
|
for field_name in required_fields:
|
||||||
if not cleaned_data.get(field_name):
|
if not cleaned_data.get(field_name):
|
||||||
field_label = self.fields[field_name].label
|
field_label = self.fields[field_name].label
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
|
{
|
||||||
field_name: f'{field_label} ist erforderlich, wenn der Status auf "{dict(self.fields["status"].choices)[status]}" gesetzt wird.'
|
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
|
||||||
|
|
||||||
@@ -184,6 +198,7 @@ class TicketForm(forms.ModelForm):
|
|||||||
# Setze answered_at wenn eine Antwort gegeben wird
|
# Setze answered_at wenn eine Antwort gegeben wird
|
||||||
if ticket.answer and not ticket.answered_at:
|
if ticket.answer and not ticket.answered_at:
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
ticket.answered_at = timezone.now()
|
ticket.answered_at = timezone.now()
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from django.db import models
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class Course(models.Model):
|
class Course(models.Model):
|
||||||
"""Kurs-Model für Backend-Verwaltung"""
|
"""Kurs-Model für Backend-Verwaltung"""
|
||||||
|
|
||||||
name = models.CharField(max_length=200, verbose_name="Kurs-Name")
|
name = models.CharField(max_length=200, verbose_name="Kurs-Name")
|
||||||
code = models.CharField(max_length=50, unique=True, verbose_name="Kurs-Code") # z.B. "PROG-101"
|
code = models.CharField(
|
||||||
|
max_length=50, unique=True, verbose_name="Kurs-Code"
|
||||||
|
) # z.B. "PROG-101"
|
||||||
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||||
|
|
||||||
tutor = models.ForeignKey(
|
tutor = models.ForeignKey(
|
||||||
@@ -14,14 +17,14 @@ class Course(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="Tutor",
|
verbose_name="Tutor",
|
||||||
related_name="courses_as_tutor"
|
related_name="courses_as_tutor",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_active = models.BooleanField(default=True, verbose_name="Aktiv")
|
is_active = models.BooleanField(default=True, verbose_name="Aktiv")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ["name"]
|
||||||
verbose_name = "Kurs"
|
verbose_name = "Kurs"
|
||||||
verbose_name_plural = "Kurse"
|
verbose_name_plural = "Kurse"
|
||||||
|
|
||||||
@@ -64,37 +67,40 @@ class Ticket(models.Model):
|
|||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="new")
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="new")
|
||||||
mistake = models.CharField(max_length=20, choices=MISTAKE_CHOICES, default="medium")
|
mistake = models.CharField(max_length=20, choices=MISTAKE_CHOICES, default="medium")
|
||||||
material = models.CharField(max_length=20, choices=MATERIAL_CHOICES, default="script")
|
material = models.CharField(
|
||||||
|
max_length=20, choices=MATERIAL_CHOICES, default="script"
|
||||||
|
)
|
||||||
|
|
||||||
answer = models.TextField(
|
answer = models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="Antwort/Lösung",
|
verbose_name="Antwort/Lösung",
|
||||||
help_text="Beschreibung der Lösung (erforderlich bei Status 'Gelöst')"
|
help_text="Beschreibung der Lösung (erforderlich bei Status 'Gelöst')",
|
||||||
)
|
)
|
||||||
answered_at = models.DateTimeField(
|
answered_at = models.DateTimeField(
|
||||||
blank=True,
|
blank=True, null=True, verbose_name="Beantwortet am"
|
||||||
null=True,
|
|
||||||
verbose_name="Beantwortet am"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
course = models.ForeignKey(
|
course = models.ForeignKey(
|
||||||
Course,
|
Course, on_delete=models.CASCADE, verbose_name="Kurs", related_name="tickets"
|
||||||
on_delete=models.CASCADE,
|
|
||||||
verbose_name="Kurs",
|
|
||||||
related_name="tickets"
|
|
||||||
)
|
)
|
||||||
created_by = models.ForeignKey(
|
created_by = models.ForeignKey(
|
||||||
User, related_name="tickets_created", on_delete=models.CASCADE
|
User, related_name="tickets_created", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
assigned_to = models.ForeignKey(
|
assigned_to = models.ForeignKey(
|
||||||
User, related_name="tickets_assigned", null=True, blank=True, on_delete=models.CASCADE,
|
User,
|
||||||
|
related_name="tickets_assigned",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"[{self.get_mistake_display()}] {self.title} ({self.get_status_display()})"
|
return (
|
||||||
|
f"[{self.get_mistake_display()}] {self.title} ({self.get_status_display()})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Comment(models.Model):
|
class Comment(models.Model):
|
||||||
@@ -126,13 +132,14 @@ class TicketHistory(models.Model):
|
|||||||
|
|
||||||
class FAQ(models.Model):
|
class FAQ(models.Model):
|
||||||
"""Einfaches FAQ Model"""
|
"""Einfaches FAQ Model"""
|
||||||
|
|
||||||
question = models.CharField(max_length=300, verbose_name="Frage")
|
question = models.CharField(max_length=300, verbose_name="Frage")
|
||||||
answer = models.TextField(verbose_name="Antwort")
|
answer = models.TextField(verbose_name="Antwort")
|
||||||
order = models.IntegerField(default=0, verbose_name="Reihenfolge")
|
order = models.IntegerField(default=0, verbose_name="Reihenfolge")
|
||||||
is_active = models.BooleanField(default=True, verbose_name="Aktiv")
|
is_active = models.BooleanField(default=True, verbose_name="Aktiv")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['order', 'question']
|
ordering = ["order", "question"]
|
||||||
verbose_name = "FAQ"
|
verbose_name = "FAQ"
|
||||||
verbose_name_plural = "FAQs"
|
verbose_name_plural = "FAQs"
|
||||||
|
|
||||||
|
|||||||
33
ticketsystem/static/js/faq.js
Normal file
33
ticketsystem/static/js/faq.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
function toggleFAQ(index) {
|
||||||
|
const content = document.getElementById(`content-${index}`);
|
||||||
|
const icon = document.getElementById(`icon-${index}`);
|
||||||
|
|
||||||
|
if (content.classList.contains('hidden')) {
|
||||||
|
content.classList.remove('hidden');
|
||||||
|
icon.classList.add('rotate-180');
|
||||||
|
} else {
|
||||||
|
content.classList.add('hidden');
|
||||||
|
icon.classList.remove('rotate-180');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAllFAQs() {
|
||||||
|
const button = document.getElementById('toggle-all-btn');
|
||||||
|
const allContents = document.querySelectorAll('[id^="content-"]');
|
||||||
|
const allIcons = document.querySelectorAll('[id^="icon-"]');
|
||||||
|
|
||||||
|
// Prüfen ob alle eingeklappt sind
|
||||||
|
const allCollapsed = Array.from(allContents).every(content => content.classList.contains('hidden'));
|
||||||
|
|
||||||
|
if (allCollapsed) {
|
||||||
|
// Alle ausklappen
|
||||||
|
allContents.forEach(content => content.classList.remove('hidden'));
|
||||||
|
allIcons.forEach(icon => icon.classList.add('rotate-180'));
|
||||||
|
button.innerHTML = '📁 Alle einklappen';
|
||||||
|
} else {
|
||||||
|
// Alle einklappen
|
||||||
|
allContents.forEach(content => content.classList.add('hidden'));
|
||||||
|
allIcons.forEach(icon => icon.classList.remove('rotate-180'));
|
||||||
|
button.innerHTML = '📂 Alle ausklappen';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,46 @@
|
|||||||
{% extends "ticketsystem/base.html" %}
|
{% extends "ticketsystem/base.html" %}
|
||||||
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-6xl mx-auto px-4 py-6">
|
<div class="max-w-6xl mx-auto px-4 py-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h1 class="text-3xl font-bold">❓ Häufig gestellte Fragen</h1>
|
<h1 class="text-3xl font-bold">❓ Häufig gestellte Fragen</h1>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="toggle-all-btn"
|
||||||
|
onclick="toggleAllFAQs()"
|
||||||
|
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded">
|
||||||
|
📂 Alle ausklappen
|
||||||
|
</button>
|
||||||
<a href="{% url 'faq-pdf-download' %}"
|
<a href="{% url 'faq-pdf-download' %}"
|
||||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">📄 PDF Download</a>
|
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">📄 PDF Download</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- FAQ Liste -->
|
<!-- FAQ Liste -->
|
||||||
{% for faq in faqs %}
|
{% for faq in faqs %}
|
||||||
<div class="bg-white rounded-lg shadow p-6 mb-4">
|
<div class="bg-white rounded-lg shadow mb-4 overflow-hidden">
|
||||||
<h3 class="font-bold text-lg mb-2">{{ forloop.counter }}. {{ faq.question }}</h3>
|
<!-- FAQ Header - klickbar -->
|
||||||
<div class="text-gray-700">{{ faq.answer|linebreaks }}</div>
|
<button class="w-full p-6 text-left hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition-colors duration-200"
|
||||||
|
onclick="toggleFAQ({{ forloop.counter0 }})">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 class="font-bold text-lg pr-4">{{ forloop.counter }}. {{ faq.question }}</h3>
|
||||||
|
<svg id="icon-{{ forloop.counter0 }}"
|
||||||
|
class="w-5 h-5 text-gray-500 transform transition-transform duration-200"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- FAQ Content - versteckt standardmäßig -->
|
||||||
|
<div id="content-{{ forloop.counter0 }}"
|
||||||
|
class="hidden px-6 pb-6">
|
||||||
|
<div class="text-gray-700 border-t pt-4">{{ faq.answer|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="bg-white rounded-lg shadow p-8 text-center">
|
<div class="bg-white rounded-lg shadow p-8 text-center">
|
||||||
@@ -21,4 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- FAQ JavaScript -->
|
||||||
|
<script src="{% static 'js/faq.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import (
|
from .views import (AssignedTicketListView, HomeView, TicketCreateView,
|
||||||
TicketListView,
|
TicketDetailUpdateView, TicketListView, TicketUpdateView,
|
||||||
TicketCreateView,
|
faq_list, faq_pdf_download)
|
||||||
TicketUpdateView,
|
|
||||||
HomeView,
|
|
||||||
AssignedTicketListView,
|
|
||||||
TicketDetailUpdateView,
|
|
||||||
faq_list,
|
|
||||||
faq_pdf_download
|
|
||||||
)
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# /ticketsystem/
|
# /ticketsystem/
|
||||||
@@ -22,6 +15,6 @@ urlpatterns = [
|
|||||||
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"),
|
||||||
path("meine-tickets/", AssignedTicketListView.as_view(), name="assigned-tickets"),
|
path("meine-tickets/", AssignedTicketListView.as_view(), name="assigned-tickets"),
|
||||||
path('faq/', faq_list, name='faq-list'),
|
path("faq/", faq_list, name="faq-list"),
|
||||||
path('faq/download/', faq_pdf_download, name='faq-pdf-download'),
|
path("faq/download/", faq_pdf_download, name="faq-pdf-download"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
from django.views.generic import ListView, TemplateView
|
|
||||||
from django.views.generic.edit import CreateView, UpdateView
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.views.generic.detail import DetailView
|
|
||||||
from django.views.generic.edit import FormMixin
|
|
||||||
from .forms import CommentForm, TicketForm
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import redirect, render
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.generic import ListView, TemplateView
|
||||||
|
from django.views.generic.edit import CreateView, UpdateView
|
||||||
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
||||||
from reportlab.lib.pagesizes import A4
|
from reportlab.lib.pagesizes import A4
|
||||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||||
from reportlab.lib.units import cm
|
from reportlab.lib.units import cm
|
||||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
|
||||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
|
||||||
|
|
||||||
from .models import Ticket, TicketHistory, FAQ, Course
|
from .forms import CommentForm, TicketForm
|
||||||
|
from .models import FAQ, Course, Ticket, TicketHistory
|
||||||
|
|
||||||
|
|
||||||
class HomeView(TemplateView):
|
class HomeView(TemplateView):
|
||||||
@@ -77,6 +75,12 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
template_name = "ticketsystem/detail.html"
|
template_name = "ticketsystem/detail.html"
|
||||||
comment_form_class = CommentForm
|
comment_form_class = CommentForm
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.can_edit = False
|
||||||
|
self.is_creator = False
|
||||||
|
self.is_tutor = False
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse("detail", kwargs={"pk": self.object.pk})
|
return reverse("detail", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
@@ -92,9 +96,9 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
# Bearbeitungsrechte abhängig vom Status
|
# Bearbeitungsrechte abhängig vom Status
|
||||||
if is_superuser:
|
if is_superuser:
|
||||||
self.can_edit = True
|
self.can_edit = True
|
||||||
elif self.ticket.status == 'resolved' and is_creator:
|
elif self.ticket.status == "resolved" and is_creator:
|
||||||
self.can_edit = True
|
self.can_edit = True
|
||||||
elif self.ticket.status == 'closed':
|
elif self.ticket.status == "closed":
|
||||||
self.can_edit = False
|
self.can_edit = False
|
||||||
else:
|
else:
|
||||||
self.can_edit = is_assigned_tutor
|
self.can_edit = is_assigned_tutor
|
||||||
@@ -108,16 +112,16 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
"""Übergibt zusätzliche kwargs ans Form"""
|
"""Übergibt zusätzliche kwargs ans Form"""
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs['user'] = self.request.user
|
kwargs["user"] = self.request.user
|
||||||
kwargs['ticket'] = self.object
|
kwargs["ticket"] = self.object
|
||||||
return kwargs
|
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
|
# Füge Berechtigungs-Infos zum Context hinzu
|
||||||
context['is_creator'] = self.is_creator
|
context["is_creator"] = self.is_creator
|
||||||
context['is_tutor'] = self.is_tutor
|
context["is_tutor"] = self.is_tutor
|
||||||
context['can_edit'] = self.can_edit
|
context["can_edit"] = self.can_edit
|
||||||
|
|
||||||
# Kommentarformular hinzufügen
|
# Kommentarformular hinzufügen
|
||||||
if "comment_form" not in context:
|
if "comment_form" not in context:
|
||||||
@@ -134,7 +138,15 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
response = super().form_valid(form) # Speichert das Ticket
|
response = super().form_valid(form) # Speichert das Ticket
|
||||||
|
|
||||||
# History tracking für geänderte Felder
|
# History tracking für geänderte Felder
|
||||||
tracked_fields = ["title", "description", "status", "mistake", "course", "answer", "material"]
|
tracked_fields = [
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"status",
|
||||||
|
"mistake",
|
||||||
|
"course",
|
||||||
|
"answer",
|
||||||
|
"material",
|
||||||
|
]
|
||||||
for field in tracked_fields:
|
for field in tracked_fields:
|
||||||
if field in form.changed_data:
|
if field in form.changed_data:
|
||||||
old_value = getattr(original, field)
|
old_value = getattr(original, field)
|
||||||
@@ -152,11 +164,15 @@ class TicketDetailUpdateView(UpdateView):
|
|||||||
new_value = str(new_value)
|
new_value = str(new_value)
|
||||||
elif field == "answer":
|
elif field == "answer":
|
||||||
if old_value:
|
if old_value:
|
||||||
old_value = old_value[:50] + "..." if len(old_value) > 50 else old_value
|
old_value = (
|
||||||
|
old_value[:50] + "..." if len(old_value) > 50 else old_value
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
old_value = "Keine Antwort"
|
old_value = "Keine Antwort"
|
||||||
if new_value:
|
if new_value:
|
||||||
new_value = new_value[:50] + "..." if len(new_value) > 50 else new_value
|
new_value = (
|
||||||
|
new_value[:50] + "..." if len(new_value) > 50 else new_value
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
new_value = "Keine Antwort"
|
new_value = "Keine Antwort"
|
||||||
|
|
||||||
@@ -286,41 +302,54 @@ class TicketUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
def faq_list(request):
|
def faq_list(request):
|
||||||
"""Zeigt alle aktiven FAQs an"""
|
"""Zeigt alle aktiven FAQs an"""
|
||||||
faqs = FAQ.objects.filter(is_active=True)
|
faqs = FAQ.objects.filter(is_active=True)
|
||||||
return render(request, 'ticketsystem/faq.html', {'faqs': faqs})
|
return render(request, "ticketsystem/faq.html", {"faqs": faqs})
|
||||||
|
|
||||||
|
|
||||||
def faq_pdf_download(request):
|
def faq_pdf_download(request):
|
||||||
"""Generiert PDF mit allen FAQs"""
|
"""Generiert PDF mit allen FAQs"""
|
||||||
# Response Setup
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
current_date = timezone.now()
|
||||||
response['Content-Disposition'] = 'attachment; filename="FAQ_Ticketsystem.pdf"'
|
date_string = current_date.strftime("%Y-%m-%d")
|
||||||
|
date_display = current_date.strftime("%d.%m.%Y")
|
||||||
|
|
||||||
|
# Response Setup mit Datum im Dateinamen
|
||||||
|
response = HttpResponse(content_type="application/pdf")
|
||||||
|
response["Content-Disposition"] = (
|
||||||
|
f'attachment; filename="FAQ_Ticketsystem_{date_string}.pdf"'
|
||||||
|
)
|
||||||
|
|
||||||
# PDF erstellen
|
# PDF erstellen
|
||||||
doc = SimpleDocTemplate(response, pagesize=A4, topMargin=2 * cm, bottomMargin=2 * cm)
|
doc = SimpleDocTemplate(
|
||||||
|
response, pagesize=A4, topMargin=2 * cm, bottomMargin=2 * cm
|
||||||
|
)
|
||||||
|
|
||||||
# Styles
|
# Styles
|
||||||
styles = getSampleStyleSheet()
|
styles = getSampleStyleSheet()
|
||||||
title_style = ParagraphStyle(
|
title_style = ParagraphStyle(
|
||||||
'CustomTitle',
|
"CustomTitle",
|
||||||
parent=styles['Heading1'],
|
parent=styles["Heading1"],
|
||||||
fontSize=24,
|
fontSize=24,
|
||||||
textColor='#1a1a1a',
|
textColor="#1a1a1a",
|
||||||
|
spaceAfter=20,
|
||||||
|
alignment=TA_CENTER,
|
||||||
|
)
|
||||||
|
subtitle_style = ParagraphStyle(
|
||||||
|
"Subtitle",
|
||||||
|
parent=styles["Normal"],
|
||||||
|
fontSize=12,
|
||||||
|
textColor="#666666",
|
||||||
spaceAfter=30,
|
spaceAfter=30,
|
||||||
alignment=TA_CENTER
|
alignment=TA_CENTER,
|
||||||
)
|
)
|
||||||
question_style = ParagraphStyle(
|
question_style = ParagraphStyle(
|
||||||
'Question',
|
"Question",
|
||||||
parent=styles['Heading2'],
|
parent=styles["Heading2"],
|
||||||
fontSize=14,
|
fontSize=14,
|
||||||
textColor='#2563eb',
|
textColor="#2563eb",
|
||||||
spaceAfter=10
|
spaceAfter=10,
|
||||||
)
|
)
|
||||||
answer_style = ParagraphStyle(
|
answer_style = ParagraphStyle(
|
||||||
'Answer',
|
"Answer", parent=styles["Normal"], fontSize=11, spaceAfter=20, alignment=TA_LEFT
|
||||||
parent=styles['Normal'],
|
|
||||||
fontSize=11,
|
|
||||||
spaceAfter=20,
|
|
||||||
alignment=TA_LEFT
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Content
|
# Content
|
||||||
@@ -328,21 +357,27 @@ def faq_pdf_download(request):
|
|||||||
|
|
||||||
# Titel
|
# Titel
|
||||||
elements.append(Paragraph("Häufig gestellte Fragen (FAQ)", title_style))
|
elements.append(Paragraph("Häufig gestellte Fragen (FAQ)", title_style))
|
||||||
|
|
||||||
|
elements.append(Paragraph(f"Stand: {date_display}", subtitle_style))
|
||||||
|
|
||||||
elements.append(Spacer(1, 20))
|
elements.append(Spacer(1, 20))
|
||||||
|
|
||||||
# FAQs holen
|
# FAQs holen
|
||||||
faqs = FAQ.objects.filter(is_active=True)
|
faqs = FAQ.objects.filter(is_active=True)
|
||||||
|
|
||||||
|
if faqs.exists():
|
||||||
# FAQs hinzufügen
|
# FAQs hinzufügen
|
||||||
for i, faq in enumerate(faqs, 1):
|
for i, faq in enumerate(faqs, 1):
|
||||||
# Frage
|
# Frage
|
||||||
elements.append(Paragraph(f"{i}. {faq.question}", question_style))
|
elements.append(Paragraph(f"{i}. {faq.question}", question_style))
|
||||||
|
|
||||||
# Antwort
|
# Antwort
|
||||||
elements.append(Paragraph(faq.answer.replace('\n', '<br/>'), answer_style))
|
elements.append(Paragraph(faq.answer.replace("\n", "<br/>"), answer_style))
|
||||||
|
|
||||||
# Abstand zwischen FAQs
|
# Abstand zwischen FAQs
|
||||||
elements.append(Spacer(1, 10))
|
elements.append(Spacer(1, 10))
|
||||||
|
else:
|
||||||
|
elements.append(Paragraph("Derzeit sind keine FAQs verfügbar.", answer_style))
|
||||||
|
|
||||||
# PDF generieren
|
# PDF generieren
|
||||||
doc.build(elements)
|
doc.build(elements)
|
||||||
|
|||||||
Reference in New Issue
Block a user