feat: add category-driven ticket workflows
This commit is contained in:
@@ -10,7 +10,7 @@ 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.models import Ticket, TicketAttachment, TicketCategory, TicketComment, TicketEvent
|
||||
from apps.tickets.schemas import (
|
||||
TicketAttachmentIn,
|
||||
TicketAttachmentOut,
|
||||
@@ -23,7 +23,7 @@ from apps.tickets.schemas import (
|
||||
TicketStatusIn,
|
||||
TicketUpdateIn,
|
||||
)
|
||||
from apps.tickets.services import transition_ticket
|
||||
from apps.tickets.services import TicketStatusError, transition_requires_remark, transition_ticket
|
||||
|
||||
router = Router(tags=["tickets"])
|
||||
|
||||
@@ -44,14 +44,35 @@ def serialize_ticket_user(user):
|
||||
}
|
||||
|
||||
|
||||
def serialize_ticket_category(category):
|
||||
if category is None:
|
||||
return None
|
||||
return {
|
||||
"id": category.id,
|
||||
"code": category.code,
|
||||
"name": category.name,
|
||||
"description": category.description,
|
||||
}
|
||||
|
||||
|
||||
def serialize_ticket(ticket: Ticket) -> dict:
|
||||
return {
|
||||
"id": ticket.id,
|
||||
"ticket_no": ticket.ticket_no,
|
||||
"title": ticket.title,
|
||||
"description": ticket.description,
|
||||
"category": serialize_ticket_category(ticket.category),
|
||||
"category_code": ticket.category.code if ticket.category_id else None,
|
||||
"status": ticket.status,
|
||||
"priority": ticket.priority,
|
||||
"problem_performance": ticket.problem_performance,
|
||||
"problem_object": ticket.problem_object,
|
||||
"submitter_name": ticket.submitter_name,
|
||||
"submitter_org": ticket.submitter_org,
|
||||
"ai_solution": ticket.ai_solution,
|
||||
"resolution": ticket.resolution_summary,
|
||||
"is_resolved": ticket.is_resolved,
|
||||
"replied_to_customer": ticket.replied_to_customer,
|
||||
"impact_scope": ticket.impact_scope,
|
||||
"environment": ticket.environment,
|
||||
"related_system": ticket.related_system,
|
||||
@@ -82,11 +103,21 @@ def create_ticket(request, payload: TicketIn):
|
||||
assignee = None
|
||||
if payload.assignee_id:
|
||||
assignee = get_user_model().objects.filter(id=payload.assignee_id).first()
|
||||
category = None
|
||||
if payload.category_code:
|
||||
category = TicketCategory.objects.filter(code=payload.category_code, enabled=True).first()
|
||||
if category is None:
|
||||
raise HttpError(400, "Unknown ticket category")
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no=next_ticket_no(),
|
||||
title=payload.title,
|
||||
description=payload.description,
|
||||
category=category,
|
||||
priority=payload.priority,
|
||||
problem_performance=payload.problem_performance,
|
||||
problem_object=payload.problem_object,
|
||||
submitter_name=payload.submitter_name,
|
||||
submitter_org=payload.submitter_org,
|
||||
impact_scope=payload.impact_scope,
|
||||
environment=payload.environment,
|
||||
related_system=payload.related_system,
|
||||
@@ -97,7 +128,7 @@ def create_ticket(request, payload: TicketIn):
|
||||
TicketEvent.objects.create(
|
||||
ticket=ticket,
|
||||
event_type="created",
|
||||
to_value={"status": ticket.status},
|
||||
to_value={"status": ticket.status, "category_code": payload.category_code},
|
||||
operator=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
return serialize_ticket(ticket)
|
||||
@@ -110,22 +141,29 @@ def list_tickets(
|
||||
page_size: int = 20,
|
||||
status: str | None = None,
|
||||
priority: str | None = None,
|
||||
category_code: 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()
|
||||
queryset = Ticket.objects.select_related("requester", "assignee", "category").all()
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
if priority:
|
||||
queryset = queryset.filter(priority=priority)
|
||||
if category_code:
|
||||
queryset = queryset.filter(category__code=category_code)
|
||||
if q:
|
||||
queryset = queryset.filter(
|
||||
Q(ticket_no__icontains=q)
|
||||
| Q(title__icontains=q)
|
||||
| Q(description__icontains=q)
|
||||
| Q(problem_performance__icontains=q)
|
||||
| Q(problem_object__icontains=q)
|
||||
| Q(submitter_name__icontains=q)
|
||||
| Q(submitter_org__icontains=q)
|
||||
)
|
||||
if related_system:
|
||||
queryset = queryset.filter(related_system__icontains=related_system)
|
||||
@@ -185,7 +223,7 @@ def export_tickets(request):
|
||||
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)
|
||||
Ticket.objects.select_related("requester", "assignee", "category").get(id=ticket_id)
|
||||
)
|
||||
|
||||
|
||||
@@ -202,7 +240,15 @@ def update_ticket(request, ticket_id: int, payload: TicketUpdateIn):
|
||||
ticket = Ticket.objects.get(id=ticket_id)
|
||||
before = {
|
||||
"title": ticket.title,
|
||||
"category_code": ticket.category.code if ticket.category_id else None,
|
||||
"priority": ticket.priority,
|
||||
"problem_performance": ticket.problem_performance,
|
||||
"problem_object": ticket.problem_object,
|
||||
"submitter_name": ticket.submitter_name,
|
||||
"submitter_org": ticket.submitter_org,
|
||||
"resolution": ticket.resolution_summary,
|
||||
"is_resolved": ticket.is_resolved,
|
||||
"replied_to_customer": ticket.replied_to_customer,
|
||||
"impact_scope": ticket.impact_scope,
|
||||
"environment": ticket.environment,
|
||||
"related_system": ticket.related_system,
|
||||
@@ -211,6 +257,15 @@ def update_ticket(request, ticket_id: int, payload: TicketUpdateIn):
|
||||
if field == "assignee_id":
|
||||
user = get_user_model().objects.filter(id=value).first() if value else None
|
||||
ticket.assignee = user
|
||||
elif field == "category_code":
|
||||
category = (
|
||||
TicketCategory.objects.filter(code=value, enabled=True).first() if value else None
|
||||
)
|
||||
if value and category is None:
|
||||
raise HttpError(400, "Unknown ticket category")
|
||||
ticket.category = category
|
||||
elif field == "resolution":
|
||||
ticket.resolution_summary = value or ""
|
||||
else:
|
||||
setattr(ticket, field, value)
|
||||
ticket.save()
|
||||
@@ -227,15 +282,29 @@ def update_ticket(request, ticket_id: int, payload: TicketUpdateIn):
|
||||
@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,
|
||||
ticket = Ticket.objects.select_related("category").get(id=ticket_id)
|
||||
category_code = ticket.category.code if ticket.category_id else None
|
||||
has_resolution = bool(payload.resolution.strip())
|
||||
if transition_requires_remark(
|
||||
ticket.status,
|
||||
payload.status,
|
||||
payload.remark,
|
||||
request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
category_code,
|
||||
) and not payload.remark.strip():
|
||||
raise HttpError(400, "Transition remark is required")
|
||||
if payload.status == "resolved" and not (payload.remark.strip() or has_resolution):
|
||||
raise HttpError(400, "Resolution or transition remark is required")
|
||||
try:
|
||||
updated = transition_ticket(
|
||||
ticket,
|
||||
payload.status,
|
||||
payload.remark,
|
||||
request.user if request.user.is_authenticated else None,
|
||||
resolution=payload.resolution.strip(),
|
||||
is_resolved=payload.is_resolved,
|
||||
replied_to_customer=payload.replied_to_customer,
|
||||
)
|
||||
except TicketStatusError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
return serialize_ticket(updated)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user