feat: add ticket comments attachments and export
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
49
backend/apps/tickets/tests/test_ticket_export.py
Normal file
49
backend/apps/tickets/tests/test_ticket_export.py
Normal 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()
|
||||
Reference in New Issue
Block a user