Files
ai-ticket-platform/backend/apps/tickets/api.py
2026-07-01 15:10:21 +08:00

252 lines
8.2 KiB
Python

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