import csv from math import ceil from django.contrib.auth import get_user_model from django.db.models import Q from django.http import HttpResponse from django.utils import timezone from ninja import Router from ninja.errors import HttpError from apps.accounts.api import require_permission from apps.audit.models import OperationAuditLog from apps.tickets.models import Ticket, TicketAttachment, TicketComment, TicketEvent from apps.tickets.schemas import ( TicketAttachmentIn, TicketAttachmentOut, TicketCommentIn, TicketCommentOut, TicketIn, TicketListOut, TicketOut, TicketStatusIn, TicketUpdateIn, ) from apps.tickets.services import transition_ticket router = Router(tags=["tickets"]) def next_ticket_no() -> str: prefix = timezone.localdate().strftime("TK%Y%m%d") count = Ticket.objects.filter(ticket_no__startswith=prefix).count() + 1 return f"{prefix}{count:04d}" def serialize_ticket_user(user): if user is None: return None return { "id": user.id, "username": user.username, "display_name": user.get_full_name() or user.username, } def serialize_ticket(ticket: Ticket) -> dict: return { "id": ticket.id, "ticket_no": ticket.ticket_no, "title": ticket.title, "description": ticket.description, "status": ticket.status, "priority": ticket.priority, "impact_scope": ticket.impact_scope, "environment": ticket.environment, "related_system": ticket.related_system, "requester_id": ticket.requester_id, "assignee_id": ticket.assignee_id, "requester": serialize_ticket_user(ticket.requester), "assignee": serialize_ticket_user(ticket.assignee), "created_at": ticket.created_at, "updated_at": ticket.updated_at, } @router.post("", response=TicketOut) def create_ticket(request, payload: TicketIn): require_permission(request, "ticket.create") assignee = None if payload.assignee_id: assignee = get_user_model().objects.filter(id=payload.assignee_id).first() ticket = Ticket.objects.create( ticket_no=next_ticket_no(), title=payload.title, description=payload.description, priority=payload.priority, impact_scope=payload.impact_scope, environment=payload.environment, related_system=payload.related_system, assignee=assignee, requester=request.user if request.user.is_authenticated else None, created_by=request.user if request.user.is_authenticated else None, ) TicketEvent.objects.create( ticket=ticket, event_type="created", to_value={"status": ticket.status}, ) return serialize_ticket(ticket) @router.get("", response=TicketListOut) def list_tickets( request, page: int = 1, page_size: int = 20, status: str | None = None, priority: str | None = None, q: str | None = None, related_system: str | None = None, assignee_id: int | None = None, view: str = "all", ): require_permission(request, "ticket.view") queryset = Ticket.objects.select_related("requester", "assignee").all() if status: queryset = queryset.filter(status=status) if priority: queryset = queryset.filter(priority=priority) if q: queryset = queryset.filter( Q(ticket_no__icontains=q) | Q(title__icontains=q) | Q(description__icontains=q) ) if related_system: queryset = queryset.filter(related_system__icontains=related_system) if assignee_id: queryset = queryset.filter(assignee_id=assignee_id) if view == "assigned_to_me": queryset = queryset.filter(assignee=request.user) if view == "created_by_me": queryset = queryset.filter(created_by=request.user) if view == "high_priority": queryset = queryset.filter(priority__in=["high", "urgent"]) page = max(page, 1) page_size = min(max(page_size, 1), 100) total = queryset.count() total_pages = ceil(total / page_size) if total else 0 start = (page - 1) * page_size end = start + page_size return { "items": [serialize_ticket(ticket) for ticket in queryset[start:end]], "page": page, "page_size": page_size, "total": total, "total_pages": total_pages, } @router.get("/export") def export_tickets(request): require_permission(request, "ticket.export") response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = 'attachment; filename="tickets.csv"' writer = csv.writer(response) writer.writerow(["ticket_no", "title", "status", "priority", "created_at"]) for ticket in Ticket.objects.all().order_by("ticket_no"): writer.writerow( [ ticket.ticket_no, ticket.title, ticket.status, ticket.priority, ticket.created_at.isoformat(), ] ) OperationAuditLog.objects.create( user=request.user if request.user.is_authenticated else None, module="tickets", action="export", target_type="ticket", result="success", request_snapshot={}, ) return response @router.get("/{ticket_id}", response=TicketOut) def get_ticket(request, ticket_id: int): require_permission(request, "ticket.view") return serialize_ticket( Ticket.objects.select_related("requester", "assignee").get(id=ticket_id) ) @router.patch("/{ticket_id}", response=TicketOut) def update_ticket(request, ticket_id: int, payload: TicketUpdateIn): require_permission(request, "ticket.edit") ticket = Ticket.objects.get(id=ticket_id) before = { "title": ticket.title, "priority": ticket.priority, "impact_scope": ticket.impact_scope, "environment": ticket.environment, "related_system": ticket.related_system, } for field, value in payload.dict(exclude_unset=True).items(): if field == "assignee_id": user = get_user_model().objects.filter(id=value).first() if value else None ticket.assignee = user else: setattr(ticket, field, value) ticket.save() TicketEvent.objects.create( ticket=ticket, event_type="updated", from_value=before, to_value=payload.dict(exclude_unset=True), operator=request.user if request.user.is_authenticated else None, ) return serialize_ticket(ticket) @router.post("/{ticket_id}/status", response=TicketOut) def update_ticket_status(request, ticket_id: int, payload: TicketStatusIn): require_permission(request, "ticket.edit") if not payload.remark.strip(): raise HttpError(400, "Transition remark is required") ticket = Ticket.objects.get(id=ticket_id) updated = transition_ticket( ticket, payload.status, payload.remark, request.user if request.user.is_authenticated else None, ) return serialize_ticket(updated) @router.post("/{ticket_id}/comments", response=TicketCommentOut) def add_ticket_comment(request, ticket_id: int, payload: TicketCommentIn): require_permission(request, "ticket.edit") ticket = Ticket.objects.get(id=ticket_id) comment = TicketComment.objects.create( ticket=ticket, content=payload.content, comment_type=payload.comment_type, visibility=payload.visibility, created_by=request.user if request.user.is_authenticated else None, ) TicketEvent.objects.create( ticket=ticket, event_type="commented", to_value={"comment_id": comment.id, "comment_type": comment.comment_type}, operator=request.user if request.user.is_authenticated else None, ) return comment @router.post("/{ticket_id}/attachments", response=TicketAttachmentOut) def add_ticket_attachment(request, ticket_id: int, payload: TicketAttachmentIn): require_permission(request, "ticket.edit") ticket = Ticket.objects.get(id=ticket_id) attachment = TicketAttachment.objects.create(ticket=ticket, **payload.dict()) TicketEvent.objects.create( ticket=ticket, event_type="attachment_added", to_value={"attachment_id": attachment.id, "file_name": attachment.file_name}, ) return attachment