import csv 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 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, 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}" @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, 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 ticket @router.get("", response=list[TicketOut]) def list_tickets(request, status: str | None = None, q: str | None = None): require_permission(request, "ticket.view") queryset = Ticket.objects.all() if status: queryset = queryset.filter(status=status) if q: queryset = queryset.filter(Q(ticket_no__icontains=q) | Q(title__icontains=q)) return queryset[:100] @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 Ticket.objects.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 ticket @router.post("/{ticket_id}/status", response=TicketOut) def update_ticket_status(request, ticket_id: int, payload: TicketStatusIn): require_permission(request, "ticket.edit") ticket = Ticket.objects.get(id=ticket_id) return transition_ticket( ticket, payload.status, payload.remark, request.user if request.user.is_authenticated else None, ) @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