This commit is contained in:
2025-10-02 00:54:34 +02:00
commit 84b34c9615
200 changed files with 42048 additions and 0 deletions

View File

View File

@@ -0,0 +1,30 @@
from django.contrib import admin
from .models import ServiceTicket
from trznice.admin import custom_admin_site
class ServiceTicketAdmin(admin.ModelAdmin):
list_display = ("id", "title", "status", "user", "created_at", "is_deleted")
list_filter = ("status", "is_deleted")
search_fields = ("title", "description", "user__username", "user__email")
ordering = ("-created_at",)
readonly_fields = ['created_at']
base_fields = ['title', 'category', 'description', 'user', 'status', 'created_at']
def get_fields(self, request, obj=None):
fields = self.base_fields.copy()
if request.user.role == "admin":
fields += ['is_deleted', 'deleted_at']
return fields
def get_queryset(self, request):
# Use the all_objects manager to show even soft-deleted entries
if request.user.role == "admin":
qs = self.model.all_objects.all()
else:
qs = self.model.objects.all()
return qs
custom_admin_site.register(ServiceTicket, ServiceTicketAdmin)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ServicedeskConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'servicedesk'

View File

@@ -0,0 +1,11 @@
import django_filters
from .models import ServiceTicket
class ServiceTicketFilter(django_filters.FilterSet):
user = django_filters.NumberFilter(field_name="user__id")
status = django_filters.ChoiceFilter(choices=ServiceTicket.STATUS_CHOICES)
category = django_filters.ChoiceFilter(choices=ServiceTicket.CATEGORY_CHOICES)
class Meta:
model = ServiceTicket
fields = ["user", "status", "category"]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.2.4 on 2025-08-07 15:13
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ServiceTicket',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('title', models.CharField(max_length=255, verbose_name='Název')),
('description', models.TextField(blank=True, null=True, verbose_name='Popis problému')),
('status', models.CharField(blank=True, choices=[('new', 'Nový'), ('in_progress', 'Řeší se'), ('resolved', 'Vyřešeno'), ('closed', 'Uzavřeno')], default='new', max_length=20, verbose_name='Stav')),
('category', models.CharField(blank=True, choices=[('tech', 'Technická chyba'), ('reservation', 'Chyba při rezervaci'), ('payment', 'Problém s platbou'), ('account', 'Problém s účtem'), ('content', 'Nesrovnalost v obsahu'), ('suggestion', 'Návrh na zlepšení'), ('other', 'Jiný')], default='tech', max_length=20, verbose_name='Kategorie')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Datum')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to=settings.AUTH_USER_MODEL, verbose_name='Zadavatel')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,32 @@
from django.db import models
from django.conf import settings
from trznice.models import SoftDeleteModel
class ServiceTicket(SoftDeleteModel):
STATUS_CHOICES = [
("new", "Nový"),
("in_progress", "Řeší se"),
("resolved", "Vyřešeno"),
("closed", "Uzavřeno"),
]
CATEGORY_CHOICES = [
("tech", "Technická chyba"),
("reservation", "Chyba při rezervaci"),
("payment", "Problém s platbou"),
("account", "Problém s účtem"),
("content", "Nesrovnalost v obsahu"),
("suggestion", "Návrh na zlepšení"),
("other", "Jiný"),
]
title = models.CharField(max_length=255, verbose_name="Název")
description = models.TextField(verbose_name="Popis problému", null=True, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="Zadavatel", related_name="tickets", null=False, blank=False)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="new", verbose_name="Stav", blank=True)
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default="tech", verbose_name="Kategorie", blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Datum", editable=False)
def __str__(self):
return f"{self.title} ({self.get_status_display()})"

View File

@@ -0,0 +1,47 @@
from rest_framework import serializers
from .models import ServiceTicket
from account.models import CustomUser
class ServiceTicketSerializer(serializers.ModelSerializer):
class Meta:
model = ServiceTicket
fields = [
"id", "title", "description", "user",
"status", "category", "created_at"
]
read_only_fields = ["id", "created_at"]
extra_kwargs = {
"title": {"help_text": "Stručný název požadavku", "required": True},
"description": {"help_text": "Detailní popis problému", "required": False},
"user": {"help_text": "ID uživatele, který požadavek zadává", "required": True},
"status": {"help_text": "Stav požadavku (new / in_progress / resolved / closed)", "required": False},
"category": {"help_text": "Kategorie požadavku (tech / reservation / payment / account / content / suggestion / other)", "required": True},
}
def validate(self, data):
user = data.get("user", None)
# if user is None:
# raise serializers.ValidationError("Product is a required field.")
# # Check if user exists in DB
# if not CustomUser.objects.filter(pk=user.pk if hasattr(user, 'pk') else user).exists():
# raise serializers.ValidationError("Neplatné ID Užívatele.")
# Example validation: status must be one of the defined choices
if "status" in data and data["status"] not in dict(ServiceTicket.STATUS_CHOICES):
raise serializers.ValidationError({"status": "Neplatný stav požadavku."})
if "category" in data and data["category"] not in dict(ServiceTicket.CATEGORY_CHOICES):
raise serializers.ValidationError({"category": "Neplatná kategorie požadavku."})
title = data.get("title", "").strip()
if not title:
raise serializers.ValidationError("Název požadavku nemůže být prázdný.")
if len(title) > 255:
raise serializers.ValidationError("Název požadavku nemůže být delší než 255 znaků.")
data["title"] = title # Optional: overwrite with trimmed version
return data

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ServiceTicketViewSet
router = DefaultRouter()
router.register(r'', ServiceTicketViewSet, basename='tickets')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,84 @@
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema
from django.contrib.auth import get_user_model
from .models import ServiceTicket
from .serializers import ServiceTicketSerializer
from .filters import ServiceTicketFilter
from account.email import send_email_with_context
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import PermissionDenied
# from account.permissions import RoleAllowed
@extend_schema(
tags=["ServiceTicket"],
description="Správa uživatelských požadavků vytvoření, úprava a výpis. Filtrování podle stavu, urgence, uživatele atd."
)
class ServiceTicketViewSet(viewsets.ModelViewSet):
# queryset = ServiceTicket.objects.select_related("user").all().order_by("-created_at")
queryset = ServiceTicket.objects.all().order_by("-created_at")
serializer_class = ServiceTicketSerializer
filter_backends = [DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter]
filterset_class = ServiceTicketFilter
ordering_fields = ["created_at"]
search_fields = ["title", "description", "user__username"]
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
if user.role in ["admin", "cityClerk"]: # Adjust as needed for staff roles
# return ServiceTicket.objects.select_related("user").all().order_by("-created_at")
return ServiceTicket.objects.all().order_by("-created_at")
else:
# return ServiceTicket.objects.select_related("user").filter(user=user).order_by("-created_at")
return ServiceTicket.objects.filter(user=user).order_by("-created_at")
def get_object(self):
obj = super().get_object()
if self.request.user.role not in ["admin", "cityClerk"] and obj.user != self.request.user:
raise PermissionDenied("Nemáte oprávnění pracovat s tímto požadavkem.")
return obj
def perform_create(self, serializer):
user_request = serializer.save(user=self.request.user)
# Map categories to roles responsible for handling them
category_role_map = {
"tech": "admin",
"reservation": "cityClerk",
"payment": "admin",
"account": "admin",
"content": "admin",
"suggestion": "admin",
"other": "admin"
}
role = category_role_map.get(user_request.category)
if not role:
return # Or log: unknown category, no notification sent
User = get_user_model()
recipients = User.objects.filter(role=role, email__isnull=False).exclude(email="").values_list("email", flat=True)
if not recipients:
recipients = User.objects.filter(role='admin', email__isnull=False).exclude(email="").values_list("email", flat=True)
if not recipients:
return
subject = "Nový uživatelský požadavek"
message = f"""
Nový požadavek byl vytvořen:
Název: {user_request.title}
Kategorie: {user_request.get_category_display()}
Popis: {user_request.description or ""}
Vytvořeno: {user_request.created_at.strftime('%d.%m.%Y %H:%M')}
Zadal: {user_request.user.get_full_name()} ({user_request.user.email})
Spravujte požadavky v systému.
"""
send_email_with_context(list(recipients), subject, message)