From 1bdb254e943a260f2993e22e516cd3ddf15aa739 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 29 Jun 2026 07:19:35 +0545 Subject: [PATCH] feat(src/dashboard/board_views): add atomic transaction and select_for_update to move_ticket view Ensure consistent status updates and prevent race conditions when moving tickets --- src/dashboard/board_views/actions.py | 60 +++++++++++++++++----------- src/dashboard/board_views/board.py | 21 +++------- src/dashboard/board_views/export.py | 2 +- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/dashboard/board_views/actions.py b/src/dashboard/board_views/actions.py index df9fbc2..dbe0ad1 100644 --- a/src/dashboard/board_views/actions.py +++ b/src/dashboard/board_views/actions.py @@ -2,6 +2,7 @@ import json from django.contrib import messages from django.contrib.auth import get_user_model +from django.db import transaction from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect from django.views.decorators.http import require_http_methods @@ -48,6 +49,11 @@ def board_create_ticket(request, project_id): due_date=due_date, created_by=request.user, ) + ticket = ( + Ticket.objects.select_related("project", "created_by") + .prefetch_related("assignees") + .get(pk=ticket.pk) + ) for uid in assignee_ids: TicketUser.objects.get_or_create(ticket=ticket, user_id=uid) @@ -89,34 +95,41 @@ def board_create_ticket(request, project_id): @require_http_methods(["POST"]) def move_ticket(request, project_id, ticket_id): - ticket = get_object_or_404(Ticket, pk=ticket_id, project_id=project_id) - data = json.loads(request.body) - status_id = data.get("status_id") - if not status_id: - return JsonResponse( - {"success": False, "error": "status_id required"}, status=400 + with transaction.atomic(): + ticket = get_object_or_404( + Ticket.objects.select_for_update() + .select_related("project", "created_by") + .prefetch_related("assignees"), + pk=ticket_id, + project_id=project_id, ) + data = json.loads(request.body) + status_id = data.get("status_id") + if not status_id: + return JsonResponse( + {"success": False, "error": "status_id required"}, status=400 + ) - status = get_object_or_404(TicketStatus, pk=status_id, project_id=project_id) + status = get_object_or_404(TicketStatus, pk=status_id, project_id=project_id) - if not ticket.project.user_can_move_to_status(request.user, status): - return JsonResponse( - { - "success": False, - "error": "You don't have permission to move tickets to this status.", - }, - status=403, + if not ticket.project.user_can_move_to_status(request.user, status): + return JsonResponse( + { + "success": False, + "error": "You don't have permission to move tickets to this status.", + }, + status=403, + ) + + ticket.status = status + ticket.save(update_fields=["status", "updated_at"]) + + TicketHistory.objects.create( + ticket=ticket, + user=request.user, + status=status, ) - ticket.status = status - ticket.save(update_fields=["status", "updated_at"]) - - TicketHistory.objects.create( - ticket=ticket, - user=request.user, - status=status, - ) - notify_users = list(ticket.assignees.all()) if ticket.created_by and ticket.created_by not in notify_users: notify_users.append(ticket.created_by) @@ -128,7 +141,6 @@ def move_ticket(request, project_id, ticket_id): message=f"Ticket moved to {status.name} by {request.user.get_full_name() or request.user.username}", data={"ticket_id": ticket.id, "ticket_uuid": ticket.uuid}, ) - # Send email notification send_notification_email( user=u, notification_type="status_changed", diff --git a/src/dashboard/board_views/board.py b/src/dashboard/board_views/board.py index 94acf35..39ed51a 100644 --- a/src/dashboard/board_views/board.py +++ b/src/dashboard/board_views/board.py @@ -47,9 +47,7 @@ def project_board(request, project_id=None): pk=project_id, ) project_members = selected_project.project_members.select_related("user").all() - statuses = TicketStatus.objects.filter(project=selected_project).order_by( - "sort_order" - ) + statuses = list(selected_project.ticket_statuses.order_by("sort_order")) tickets_qs = ( Ticket.objects.filter(project=selected_project) @@ -132,25 +130,18 @@ def project_board(request, project_id=None): first_status_name = None statuses_json = "[]" if selected_project: - first_status = ( - TicketStatus.objects.filter(project=selected_project) - .order_by("sort_order") - .first() - ) + first_status = statuses[0] if statuses else None if first_status: first_status_id = first_status.id first_status_name = first_status.name statuses_json = json.dumps( - [ - {"id": s.id, "name": s.name, "color": s.color} - for s in TicketStatus.objects.filter(project=selected_project).order_by( - "sort_order" - ) - ] + [{"id": s.id, "name": s.name, "color": s.color} for s in statuses] ) permissions = ( - BoardRolePermission.objects.filter(project=selected_project).order_by("role") + BoardRolePermission.objects.filter(project=selected_project) + .prefetch_related("move_to_statuses") + .order_by("role") if selected_project else [] ) diff --git a/src/dashboard/board_views/export.py b/src/dashboard/board_views/export.py index 3636bba..239246c 100644 --- a/src/dashboard/board_views/export.py +++ b/src/dashboard/board_views/export.py @@ -25,7 +25,7 @@ def export_board_tickets(request, project_id): tickets = ( Ticket.objects.filter(project=project) - .select_related("status", "priority", "epic", "created_by") + .select_related("status", "priority", "epic", "created_by", "project") .prefetch_related("assignees") .order_by("-created_at") )