From e3551a822cd3ffc2d6500289f07089f462de6933 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 29 Jun 2026 07:19:53 +0545 Subject: [PATCH] refactor(src/dashboard): enhance DB query efficiency and pagination stability Prevent N+1 queries, streamline aggregation, and fix role deletion count --- src/dashboard/page_views/external_views.py | 3 +- .../page_views/notification_views.py | 22 +++---- src/dashboard/page_views/other_views.py | 5 +- src/dashboard/page_views/project_views.py | 10 +-- src/dashboard/page_views/ticket_views.py | 63 ++++++++++--------- src/dashboard/page_views/user_views.py | 1 + 6 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/dashboard/page_views/external_views.py b/src/dashboard/page_views/external_views.py index 2a1fc78..95d91c9 100644 --- a/src/dashboard/page_views/external_views.py +++ b/src/dashboard/page_views/external_views.py @@ -110,7 +110,8 @@ def external_access_view(request, token): if authenticated and access.is_active: tickets = ( Ticket.objects.filter(project=access.project) - .select_related("status", "priority") + .select_related("status", "priority", "created_by", "epic") + .prefetch_related("assignees") .order_by("-created_at") ) return render( diff --git a/src/dashboard/page_views/notification_views.py b/src/dashboard/page_views/notification_views.py index d9ad234..68fe7ac 100644 --- a/src/dashboard/page_views/notification_views.py +++ b/src/dashboard/page_views/notification_views.py @@ -62,17 +62,17 @@ def notifications_list(request): except ValueError: per_page = 20 + from django.db.models import Count + paginator = Paginator(qs.select_related("user"), per_page) page_number = request.GET.get("page", 1) page_obj = paginator.get_page(page_number) - all_count = Notification.objects.filter(user=request.user).count() - unread_count = Notification.objects.filter( - user=request.user, read_at__isnull=True - ).count() - read_count = Notification.objects.filter( - user=request.user, read_at__isnull=False - ).count() + counts = Notification.objects.filter(user=request.user).aggregate( + all_count=Count("id"), + unread_count=Count("id", filter=Q(read_at__isnull=True)), + read_count=Count("id", filter=Q(read_at__isnull=False)), + ) return render( request, @@ -81,9 +81,9 @@ def notifications_list(request): "notifications": page_obj, "page_obj": page_obj, "filter": filter_param, - "all_count": all_count, - "unread_count": unread_count, - "read_count": read_count, + "all_count": counts["all_count"], + "unread_count": counts["unread_count"], + "read_count": counts["read_count"], "search_query": search_query, "type_filter": type_filter, "per_page": per_page, @@ -93,7 +93,7 @@ def notifications_list(request): @require_http_methods(["GET"]) def notifications_dropdown(request): - notifications = Notification.objects.filter( + notifications = Notification.objects.select_related("user").filter( user=request.user, read_at__isnull=True )[:10] return render( diff --git a/src/dashboard/page_views/other_views.py b/src/dashboard/page_views/other_views.py index 3b33059..739fc69 100644 --- a/src/dashboard/page_views/other_views.py +++ b/src/dashboard/page_views/other_views.py @@ -369,10 +369,11 @@ def role_delete(request, role_id): if role.is_system: messages.error(request, "System roles cannot be deleted.") return redirect("roles-list") - if role.users.exists(): + user_count = role.users.count() + if user_count: messages.error( request, - f"Cannot delete role '{role.name}' — it is assigned to {role.users.count()} user(s).", + f"Cannot delete role '{role.name}' — it is assigned to {user_count} user(s).", ) return redirect("roles-list") BoardRolePermission.objects.filter(role=role.slug).delete() diff --git a/src/dashboard/page_views/project_views.py b/src/dashboard/page_views/project_views.py index 7fa817c..e01155d 100644 --- a/src/dashboard/page_views/project_views.py +++ b/src/dashboard/page_views/project_views.py @@ -187,7 +187,7 @@ def project_detail(request, project_id): # ─ Tickets with pagination ─ tickets_qs = ( Ticket.objects.filter(project=project) - .select_related("status", "priority") + .select_related("status", "priority", "created_by", "epic", "project") .prefetch_related("assignees") .order_by("-created_at") ) @@ -292,9 +292,9 @@ def project_detail(request, project_id): "today": today, "external_access": external_access, "is_admin": request.user.is_admin, - "permissions": BoardRolePermission.objects.filter(project=project).order_by( - "role" - ), + "permissions": BoardRolePermission.objects.filter(project=project) + .prefetch_related("move_to_statuses") + .order_by("role"), "role_choices": Role.objects.all(), "admin_roles": ADMIN_ROLES, }, @@ -396,7 +396,7 @@ def project_edit(request, project_id): tickets_qs = ( Ticket.objects.filter(project=project) - .select_related("status", "priority") + .select_related("status", "priority", "created_by", "epic", "project") .prefetch_related("assignees") .order_by("-created_at") ) diff --git a/src/dashboard/page_views/ticket_views.py b/src/dashboard/page_views/ticket_views.py index f285e94..ab7abf6 100644 --- a/src/dashboard/page_views/ticket_views.py +++ b/src/dashboard/page_views/ticket_views.py @@ -4,6 +4,7 @@ from datetime import date import openpyxl from django.contrib import messages from django.contrib.auth import get_user_model +from django.db import transaction from django.db.models import Count, Prefetch from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render @@ -774,39 +775,45 @@ def tickets_bulk_status(request): {"success": False, "error": "Missing ticket_ids or status_id"}, status=400 ) - status = get_object_or_404(TicketStatus, pk=status_id) - tickets = Ticket.objects.filter(pk__in=ticket_ids).select_related("status") - - if not request.user.is_admin: - user_project_ids = request.user.projects.values_list("id", flat=True) - tickets = tickets.filter(project_id__in=user_project_ids) - - count = 0 - for ticket in tickets: - old_name = ticket.status.name - ticket.status = status - ticket.save(update_fields=["status", "updated_at"]) - - TicketHistory.objects.create( - ticket=ticket, - user=request.user, - status=status, + with transaction.atomic(): + status = get_object_or_404(TicketStatus, pk=status_id) + tickets = ( + Ticket.objects.select_for_update() + .filter(pk__in=ticket_ids) + .select_related("status", "created_by") + .prefetch_related("assignees") ) - notify_users = list(ticket.assignees.all()) - if ticket.created_by and ticket.created_by not in notify_users: - notify_users.append(ticket.created_by) + if not request.user.is_admin: + user_project_ids = request.user.projects.values_list("id", flat=True) + tickets = tickets.filter(project_id__in=user_project_ids) - for u in notify_users: - Notification.objects.create( - user=u, - type="status_changed", - title=f"Ticket {ticket.uuid} status changed", - message=f"Status changed from {old_name} to {status.name} by {request.user.get_full_name() or request.user.username}", - data={"ticket_id": ticket.id, "ticket_uuid": ticket.uuid}, + count = 0 + for ticket in tickets: + old_name = ticket.status.name + ticket.status = status + ticket.save(update_fields=["status", "updated_at"]) + + TicketHistory.objects.create( + ticket=ticket, + user=request.user, + status=status, ) - count += 1 + notify_users = list(ticket.assignees.all()) + if ticket.created_by and ticket.created_by not in notify_users: + notify_users.append(ticket.created_by) + + for u in notify_users: + Notification.objects.create( + user=u, + type="status_changed", + title=f"Ticket {ticket.uuid} status changed", + message=f"Status changed from {old_name} to {status.name} by {request.user.get_full_name() or request.user.username}", + data={"ticket_id": ticket.id, "ticket_uuid": ticket.uuid}, + ) + + count += 1 return JsonResponse({"success": True, "count": count}) diff --git a/src/dashboard/page_views/user_views.py b/src/dashboard/page_views/user_views.py index f067ee0..7915c0a 100644 --- a/src/dashboard/page_views/user_views.py +++ b/src/dashboard/page_views/user_views.py @@ -46,6 +46,7 @@ def users_list(request): return redirect("dashboard") qs = ( User.objects.filter(is_active=True) + .select_related("role") .annotate( project_count=Count("projects", distinct=True), ticket_count=Count("created_tickets", distinct=True),