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)