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
This commit is contained in:
3 files changed
+43
-40
No files matched your search
@@ -2,6 +2,7 @@ import json
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
@@ -48,6 +49,11 @@ def board_create_ticket(request, project_id):
|
|||||||
due_date=due_date,
|
due_date=due_date,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
|
ticket = (
|
||||||
|
Ticket.objects.select_related("project", "created_by")
|
||||||
|
.prefetch_related("assignees")
|
||||||
|
.get(pk=ticket.pk)
|
||||||
|
)
|
||||||
|
|
||||||
for uid in assignee_ids:
|
for uid in assignee_ids:
|
||||||
TicketUser.objects.get_or_create(ticket=ticket, user_id=uid)
|
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"])
|
@require_http_methods(["POST"])
|
||||||
def move_ticket(request, project_id, ticket_id):
|
def move_ticket(request, project_id, ticket_id):
|
||||||
ticket = get_object_or_404(Ticket, pk=ticket_id, project_id=project_id)
|
with transaction.atomic():
|
||||||
data = json.loads(request.body)
|
ticket = get_object_or_404(
|
||||||
status_id = data.get("status_id")
|
Ticket.objects.select_for_update()
|
||||||
if not status_id:
|
.select_related("project", "created_by")
|
||||||
return JsonResponse(
|
.prefetch_related("assignees"),
|
||||||
{"success": False, "error": "status_id required"}, status=400
|
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):
|
if not ticket.project.user_can_move_to_status(request.user, status):
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "You don't have permission to move tickets to this status.",
|
"error": "You don't have permission to move tickets to this status.",
|
||||||
},
|
},
|
||||||
status=403,
|
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())
|
notify_users = list(ticket.assignees.all())
|
||||||
if ticket.created_by and ticket.created_by not in notify_users:
|
if ticket.created_by and ticket.created_by not in notify_users:
|
||||||
notify_users.append(ticket.created_by)
|
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}",
|
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},
|
data={"ticket_id": ticket.id, "ticket_uuid": ticket.uuid},
|
||||||
)
|
)
|
||||||
# Send email notification
|
|
||||||
send_notification_email(
|
send_notification_email(
|
||||||
user=u,
|
user=u,
|
||||||
notification_type="status_changed",
|
notification_type="status_changed",
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ def project_board(request, project_id=None):
|
|||||||
pk=project_id,
|
pk=project_id,
|
||||||
)
|
)
|
||||||
project_members = selected_project.project_members.select_related("user").all()
|
project_members = selected_project.project_members.select_related("user").all()
|
||||||
statuses = TicketStatus.objects.filter(project=selected_project).order_by(
|
statuses = list(selected_project.ticket_statuses.order_by("sort_order"))
|
||||||
"sort_order"
|
|
||||||
)
|
|
||||||
|
|
||||||
tickets_qs = (
|
tickets_qs = (
|
||||||
Ticket.objects.filter(project=selected_project)
|
Ticket.objects.filter(project=selected_project)
|
||||||
@@ -132,25 +130,18 @@ def project_board(request, project_id=None):
|
|||||||
first_status_name = None
|
first_status_name = None
|
||||||
statuses_json = "[]"
|
statuses_json = "[]"
|
||||||
if selected_project:
|
if selected_project:
|
||||||
first_status = (
|
first_status = statuses[0] if statuses else None
|
||||||
TicketStatus.objects.filter(project=selected_project)
|
|
||||||
.order_by("sort_order")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if first_status:
|
if first_status:
|
||||||
first_status_id = first_status.id
|
first_status_id = first_status.id
|
||||||
first_status_name = first_status.name
|
first_status_name = first_status.name
|
||||||
statuses_json = json.dumps(
|
statuses_json = json.dumps(
|
||||||
[
|
[{"id": s.id, "name": s.name, "color": s.color} for s in statuses]
|
||||||
{"id": s.id, "name": s.name, "color": s.color}
|
|
||||||
for s in TicketStatus.objects.filter(project=selected_project).order_by(
|
|
||||||
"sort_order"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
permissions = (
|
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
|
if selected_project
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def export_board_tickets(request, project_id):
|
|||||||
|
|
||||||
tickets = (
|
tickets = (
|
||||||
Ticket.objects.filter(project=project)
|
Ticket.objects.filter(project=project)
|
||||||
.select_related("status", "priority", "epic", "created_by")
|
.select_related("status", "priority", "epic", "created_by", "project")
|
||||||
.prefetch_related("assignees")
|
.prefetch_related("assignees")
|
||||||
.order_by("-created_at")
|
.order_by("-created_at")
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user