feat: add ticket comments attachments and export

This commit is contained in:
hailin
2026-06-30 18:42:04 +08:00
parent c293e3f56a
commit f8298632b6
3 changed files with 154 additions and 2 deletions

View File

@@ -1,10 +1,23 @@
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.tickets.models import Ticket, TicketEvent
from apps.tickets.schemas import TicketIn, TicketOut, TicketStatusIn, TicketUpdateIn
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"])
@@ -50,6 +63,33 @@ def list_tickets(request, status: str | None = None, q: str | None = None):
return queryset[:100]
@router.get("/export")
def export_tickets(request):
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):
return Ticket.objects.get(id=ticket_id)
@@ -91,3 +131,34 @@ def update_ticket_status(request, ticket_id: int, payload: TicketStatusIn):
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):
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):
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

View File

@@ -50,3 +50,35 @@ class TicketEventOut(Schema):
to_value: dict
remark: str
created_at: datetime
class TicketCommentIn(Schema):
content: str
comment_type: str = "user"
visibility: str = "internal"
class TicketCommentOut(Schema):
id: int
content: str
comment_type: str
visibility: str
created_at: datetime
class TicketAttachmentIn(Schema):
file_name: str
file_type: str = ""
mime_type: str = ""
file_size: int = 0
storage_path: str
class TicketAttachmentOut(Schema):
id: int
file_name: str
file_type: str
mime_type: str
file_size: int
storage_path: str
created_at: datetime

View File

@@ -0,0 +1,49 @@
from apps.audit.models import OperationAuditLog
from apps.tickets.models import Ticket
def test_add_comment_to_ticket(client, db):
ticket = Ticket.objects.create(ticket_no="TK202606300010", title="A", description="A")
response = client.post(
f"/api/tickets/{ticket.id}/comments",
data={
"content": "Checked logs",
"comment_type": "internal_note",
"visibility": "internal",
},
content_type="application/json",
)
assert response.status_code == 200
assert response.json()["content"] == "Checked logs"
assert ticket.comments.count() == 1
def test_add_attachment_to_ticket(client, db):
ticket = Ticket.objects.create(ticket_no="TK202606300011", title="A", description="A")
response = client.post(
f"/api/tickets/{ticket.id}/attachments",
data={
"file_name": "error.log",
"mime_type": "text/plain",
"file_size": 123,
"storage_path": "tickets/TK202606300011/error.log",
},
content_type="application/json",
)
assert response.status_code == 200
assert response.json()["file_name"] == "error.log"
def test_export_tickets_returns_csv_and_audit_log(client, db):
Ticket.objects.create(ticket_no="TK202606300012", title="Export me", description="A")
response = client.get("/api/tickets/export")
assert response.status_code == 200
assert response["content-type"].startswith("text/csv")
assert "TK202606300012" in response.content.decode()
assert OperationAuditLog.objects.filter(module="tickets", action="export").exists()