You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

260 lines
11 KiB

import logging
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiParameter
from apps.tickets.models import Ticket, TicketMessage, TicketStatus
from apps.tickets.permissions import IsTicketOwnerOrStaff
from apps.tickets.serializers import (
TicketListSerializer,
TicketDetailSerializer,
TicketCreateSerializer,
TicketMessageSerializer,
TicketMessageCreateSerializer,
TicketRateSerializer,
)
logger = logging.getLogger(__name__)
def get_ticket_by_id_or_code(pk_or_code, queryset=None):
if queryset is None:
queryset = Ticket.objects.all()
if str(pk_or_code).isdigit():
return get_object_or_404(queryset, pk=int(pk_or_code))
return get_object_or_404(queryset, tracking_code__iexact=str(pk_or_code).strip())
class TicketListCreateView(GenericAPIView):
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
serializer_class = TicketListSerializer
queryset = Ticket.objects.all()
def get_serializer_class(self):
if self.request.method == 'POST':
return TicketCreateSerializer
return TicketListSerializer
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False) or not self.request.user.is_authenticated:
return Ticket.objects.none()
user = self.request.user
if user.is_staff or getattr(user, 'is_super_admin', False) or getattr(user, 'is_regional_admin', False):
qs = Ticket.objects.all()
else:
# User's own tickets or institution tickets
institution_ids = user.institution_memberships.values_list('institution_id', flat=True)
qs = Ticket.objects.filter(Q(user=user) | Q(institution_id__in=institution_ids)).distinct()
return qs
@extend_schema(
summary="List user and institution tickets",
description="Retrieves a list of official inquiry tickets submitted by the user or their institution.",
parameters=[
OpenApiParameter('status', str, description="Filter by status (open, in_progress, answered, closed)"),
OpenApiParameter('category', str, description="Filter by category (cultural_affairs, religious_inquiry, etc.)"),
OpenApiParameter('priority', str, description="Filter by priority (urgent, high, medium, low)"),
OpenApiParameter('search', str, description="Search in subject or tracking code"),
],
responses={200: TicketListSerializer(many=True)},
tags=["Ticketing & Official Inquiries"],
)
def get(self, request, *args, **kwargs):
qs = self.get_queryset()
status_param = request.query_params.get('status')
if status_param:
qs = qs.filter(status=status_param)
category = request.query_params.get('category')
if category:
qs = qs.filter(category=category)
priority = request.query_params.get('priority')
if priority:
qs = qs.filter(priority=priority)
search = request.query_params.get('search')
if search:
qs = qs.filter(Q(subject__icontains=search) | Q(tracking_code__icontains=search))
serializer = TicketListSerializer(qs, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Submit an official ticket inquiry",
description="Creates a new official inquiry ticket with initial message content and optional file attachment.",
request=TicketCreateSerializer,
responses={
201: TicketDetailSerializer,
400: OpenApiResponse(description="Validation error"),
},
tags=["Ticketing & Official Inquiries"],
)
def post(self, request, *args, **kwargs):
serializer = TicketCreateSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
ticket = serializer.save()
response_serializer = TicketDetailSerializer(ticket)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class TicketDetailView(GenericAPIView):
permission_classes = [IsAuthenticated, IsTicketOwnerOrStaff]
serializer_class = TicketDetailSerializer
queryset = Ticket.objects.all()
@extend_schema(
summary="Retrieve ticket thread and responses",
description="Returns full ticket details including all dialogue messages and attachments.",
responses={
200: TicketDetailSerializer,
403: OpenApiResponse(description="Permission denied"),
404: OpenApiResponse(description="Ticket not found"),
},
tags=["Ticketing & Official Inquiries"],
)
def get(self, request, pk_or_code, *args, **kwargs):
ticket = get_ticket_by_id_or_code(pk_or_code, self.get_queryset())
self.check_object_permissions(request, ticket)
serializer = self.get_serializer(ticket)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Update ticket status or priority (Staff/Owner)",
description="Updates ticket priority, category, or assigned expert.",
request=TicketListSerializer,
responses={200: TicketDetailSerializer},
tags=["Ticketing & Official Inquiries"],
)
def patch(self, request, pk_or_code, *args, **kwargs):
ticket = get_ticket_by_id_or_code(pk_or_code, self.get_queryset())
self.check_object_permissions(request, ticket)
if 'priority' in request.data:
ticket.priority = request.data['priority']
if 'category' in request.data:
ticket.category = request.data['category']
if 'status' in request.data and request.user.is_staff:
ticket.status = request.data['status']
if 'assigned_expert' in request.data and request.user.is_staff:
ticket.assigned_expert_id = request.data['assigned_expert']
ticket.save()
serializer = self.get_serializer(ticket)
return Response(serializer.data, status=status.HTTP_200_OK)
class TicketReplyView(GenericAPIView):
permission_classes = [IsAuthenticated, IsTicketOwnerOrStaff]
parser_classes = [MultiPartParser, FormParser, JSONParser]
serializer_class = TicketMessageCreateSerializer
queryset = Ticket.objects.all()
@extend_schema(
summary="Reply to ticket inquiry",
description="Appends a new message or official response to an existing ticket thread.",
request=TicketMessageCreateSerializer,
responses={
201: TicketMessageSerializer,
400: OpenApiResponse(description="Invalid reply"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Ticketing & Official Inquiries"],
)
def post(self, request, pk_or_code, *args, **kwargs):
ticket = get_ticket_by_id_or_code(pk_or_code, self.get_queryset())
self.check_object_permissions(request, ticket)
if ticket.status == TicketStatus.CLOSED:
return Response({'error': _("Cannot reply to a closed ticket.")}, status=status.HTTP_400_BAD_REQUEST)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
is_staff_user = request.user.is_staff or getattr(request.user, 'is_super_admin', False)
msg = TicketMessage.objects.create(
ticket=ticket,
sender=request.user,
content=serializer.validated_data['content'],
attachment=serializer.validated_data.get('attachment'),
is_official_response=is_staff_user,
department_label=serializer.validated_data.get('department_label', 'Astan Quds Razavi Official Support' if is_staff_user else ''),
)
# Update ticket status automatically
if is_staff_user:
ticket.status = TicketStatus.ANSWERED
if not ticket.assigned_expert:
ticket.assigned_expert = request.user
else:
ticket.status = TicketStatus.IN_PROGRESS
ticket.save()
response_serializer = TicketMessageSerializer(msg)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
class TicketCloseView(GenericAPIView):
permission_classes = [IsAuthenticated, IsTicketOwnerOrStaff]
serializer_class = TicketDetailSerializer
queryset = Ticket.objects.all()
@extend_schema(
summary="Close ticket inquiry",
description="Marks a ticket as closed and records the resolution timestamp.",
responses={200: TicketDetailSerializer},
tags=["Ticketing & Official Inquiries"],
)
def post(self, request, pk_or_code, *args, **kwargs):
ticket = get_ticket_by_id_or_code(pk_or_code, self.get_queryset())
self.check_object_permissions(request, ticket)
ticket.status = TicketStatus.CLOSED
ticket.closed_at = timezone.now()
ticket.save()
serializer = self.get_serializer(ticket)
return Response(serializer.data, status=status.HTTP_200_OK)
class TicketRateView(GenericAPIView):
permission_classes = [IsAuthenticated, IsTicketOwnerOrStaff]
serializer_class = TicketRateSerializer
queryset = Ticket.objects.all()
@extend_schema(
summary="Submit satisfaction rating for ticket",
description="Records 1 to 5 star rating and optional user feedback upon inquiry completion.",
request=TicketRateSerializer,
responses={
200: TicketDetailSerializer,
400: OpenApiResponse(description="Validation error"),
},
tags=["Ticketing & Official Inquiries"],
)
def post(self, request, pk_or_code, *args, **kwargs):
ticket = get_ticket_by_id_or_code(pk_or_code, self.get_queryset())
self.check_object_permissions(request, ticket)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
ticket.satisfaction_rating = serializer.validated_data['rating']
ticket.feedback_comment = serializer.validated_data.get('feedback', '')
ticket.save()
response_serializer = TicketDetailSerializer(ticket)
return Response(response_serializer.data, status=status.HTTP_200_OK)