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.contrib.auth import get_user_model
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from ninja import Router
|
from ninja import Router
|
||||||
|
|
||||||
from apps.tickets.models import Ticket, TicketEvent
|
from apps.audit.models import OperationAuditLog
|
||||||
from apps.tickets.schemas import TicketIn, TicketOut, TicketStatusIn, TicketUpdateIn
|
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
|
from apps.tickets.services import transition_ticket
|
||||||
|
|
||||||
router = Router(tags=["tickets"])
|
router = Router(tags=["tickets"])
|
||||||
@@ -50,6 +63,33 @@ def list_tickets(request, status: str | None = None, q: str | None = None):
|
|||||||
return queryset[:100]
|
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)
|
@router.get("/{ticket_id}", response=TicketOut)
|
||||||
def get_ticket(request, ticket_id: int):
|
def get_ticket(request, ticket_id: int):
|
||||||
return Ticket.objects.get(id=ticket_id)
|
return Ticket.objects.get(id=ticket_id)
|
||||||
@@ -91,3 +131,34 @@ def update_ticket_status(request, ticket_id: int, payload: TicketStatusIn):
|
|||||||
payload.remark,
|
payload.remark,
|
||||||
request.user if request.user.is_authenticated else None,
|
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
|
to_value: dict
|
||||||
remark: str
|
remark: str
|
||||||
created_at: datetime
|
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