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
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)
|