diff --git a/src/dashboard/context_processors.py b/src/dashboard/context_processors.py index 2f51c0d..f5b4898 100644 --- a/src/dashboard/context_processors.py +++ b/src/dashboard/context_processors.py @@ -1,9 +1,31 @@ from django.contrib.auth import get_user_model +from django.core.cache import cache from src.accounts.models import Setting, SiteBranding from src.tickets.models import Ticket +def _get_cached_branding(): + branding = cache.get("site_branding") + if branding is not None: + return branding + obj = SiteBranding.objects.filter(is_active=True).first() + if obj: + branding = { + "branding": obj, + "brand_name": obj.brand_name, + "copyright_name": obj.copyright_name, + } + else: + branding = { + "branding": None, + "brand_name": "Project Management", + "copyright_name": "Project Management", + } + cache.set("site_branding", branding, timeout=300) + return branding + + def site_stats(request): User = get_user_model() @@ -25,13 +47,6 @@ def site_stats(request): ctx["navigation_style"] = user_ui_settings.get("navigation_style", "sidebar") ctx["color_theme"] = user_ui_settings.get("color_theme", "blue") - branding = SiteBranding.objects.filter(is_active=True).first() - if branding: - ctx["branding"] = branding - ctx["brand_name"] = branding.brand_name - ctx["copyright_name"] = branding.copyright_name - else: - ctx["branding"] = None - ctx["brand_name"] = "Project Management" - ctx["copyright_name"] = "Project Management" + branding = _get_cached_branding() + ctx.update(branding) return ctx diff --git a/src/dashboard/page_views/other_views.py b/src/dashboard/page_views/other_views.py index f4c0404..73f0001 100644 --- a/src/dashboard/page_views/other_views.py +++ b/src/dashboard/page_views/other_views.py @@ -4,6 +4,7 @@ from datetime import date, timedelta from django.contrib import messages from django.contrib.auth import get_user_model +from django.db.models import Count from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from django.views.decorators.http import require_http_methods @@ -48,41 +49,55 @@ def leaderboard(request): top_n = int(request.GET.get("top", 10)) since = timezone.now() - timedelta(days=days) - users = User.objects.filter(is_active=True) + user_ids = list(User.objects.filter(is_active=True).values_list("id", flat=True)) + + tickets_created_map = dict( + Ticket.objects.filter(created_by_id__in=user_ids, created_at__gte=since) + .values("created_by_id") + .annotate(count=Count("id")) + .values_list("created_by_id", "count") + ) + status_changes_map = dict( + TicketHistory.objects.filter(user_id__in=user_ids, created_at__gte=since) + .values("user_id") + .annotate(count=Count("id")) + .values_list("user_id", "count") + ) + comments_map = dict( + TicketComment.objects.filter(user_id__in=user_ids, created_at__gte=since) + .values("user_id") + .annotate(count=Count("id")) + .values_list("user_id", "count") + ) + active_days_map = dict( + TicketHistory.objects.filter(user_id__in=user_ids, created_at__gte=since) + .values("user_id") + .annotate(count=Count("created_at", distinct=True)) + .values_list("user_id", "count") + ) + + users = User.objects.filter(id__in=user_ids).only( + "id", "first_name", "last_name", "username" + ) leaderboard_data = [] for user in users: - tickets_created = Ticket.objects.filter( - created_by=user, created_at__gte=since - ).count() - status_changes = TicketHistory.objects.filter( - user=user, created_at__gte=since - ).count() - comments_made = TicketComment.objects.filter( - user=user, created_at__gte=since - ).count() - active_days = ( - TicketHistory.objects.filter(user=user, created_at__gte=since) - .dates("created_at", "day") - .count() - ) + created = tickets_created_map.get(user.id, 0) + changes = status_changes_map.get(user.id, 0) + comments = comments_map.get(user.id, 0) + days_active = active_days_map.get(user.id, 0) - score = ( - (status_changes * 5) - + (tickets_created * 2) - + (comments_made * 2) - + (active_days * 1) - ) + score = (changes * 5) + (created * 2) + (comments * 2) + (days_active * 1) - if score > 0 or tickets_created > 0 or comments_made > 0: + if score > 0 or created > 0 or comments > 0: leaderboard_data.append( { "user": user, "score": score, - "tickets_created": tickets_created, - "status_changes": status_changes, - "comments_made": comments_made, - "active_days": active_days, + "tickets_created": created, + "status_changes": changes, + "comments_made": comments, + "active_days": days_active, } ) @@ -139,47 +154,75 @@ def user_contributions(request): available_years.append(target_year) available_years.sort(reverse=True) - users = User.objects.filter(is_active=True)[:10] + user_ids = list( + User.objects.filter(is_active=True).values_list("id", flat=True)[:10] + ) + + tickets_created_map = dict( + Ticket.objects.filter(created_by_id__in=user_ids, created_at__year=target_year) + .values("created_by_id") + .annotate(count=Count("id")) + .values_list("created_by_id", "count") + ) + status_changes_map = dict( + TicketHistory.objects.filter(user_id__in=user_ids, created_at__year=target_year) + .values("user_id") + .annotate(count=Count("id")) + .values_list("user_id", "count") + ) + comments_map = dict( + TicketComment.objects.filter(user_id__in=user_ids, created_at__year=target_year) + .values("user_id") + .annotate(count=Count("id")) + .values_list("user_id", "count") + ) + active_days_map = dict( + TicketHistory.objects.filter(user_id__in=user_ids, created_at__year=target_year) + .values("user_id") + .annotate(count=Count("created_at", distinct=True)) + .values_list("user_id", "count") + ) + + all_history = TicketHistory.objects.filter( + user_id__in=user_ids, created_at__year=target_year + ).values("user_id", "created_at") + + history_by_user = {} + for h in all_history: + uid = h["user_id"] + if uid not in history_by_user: + history_by_user[uid] = [] + history_by_user[uid].append(h["created_at"]) + + users = User.objects.filter(id__in=user_ids).only( + "id", "first_name", "last_name", "username" + ) contributions = [] for user in users: - tickets_created = Ticket.objects.filter( - created_by=user, created_at__year=target_year - ).count() - status_changes = TicketHistory.objects.filter( - user=user, created_at__year=target_year - ).count() - comments_made = TicketComment.objects.filter( - user=user, created_at__year=target_year - ).count() - active_days = ( - TicketHistory.objects.filter(user=user, created_at__year=target_year) - .dates("created_at", "day") - .count() - ) + uid = user.id + created = tickets_created_map.get(uid, 0) + changes = status_changes_map.get(uid, 0) + comments = comments_map.get(uid, 0) + days_active = active_days_map.get(uid, 0) - # Build heatmap data heatmap_values = {} current = since.date() - # end is already defined above while current <= end: heatmap_values[current.isoformat()] = 0 current += timedelta(days=1) - for h in TicketHistory.objects.filter(user=user, created_at__year=target_year): - day = h.created_at.date().isoformat() + for dt in history_by_user.get(uid, []): + day = dt.date().isoformat() if day in heatmap_values: heatmap_values[day] += 1 max_val = max(heatmap_values.values()) if heatmap_values else 1 - # Group into weeks (list of lists of day objects) weeks = [] week = [] sorted_days = sorted(heatmap_values.keys()) - for i, day_str in enumerate(sorted_days): - if i > 0 and sorted_days[i - 1] < day_str: - pass + for day_str in sorted_days: day_date = date.fromisoformat(day_str) count = heatmap_values[day_str] if count == 0: @@ -200,7 +243,7 @@ def user_contributions(request): "day": day_date.weekday(), } ) - if day_date.weekday() == 6: # Sunday + if day_date.weekday() == 6: weeks.append(week) week = [] if week: @@ -209,10 +252,10 @@ def user_contributions(request): contributions.append( { "user": user, - "tickets_created": tickets_created, - "status_changes": status_changes, - "comments_made": comments_made, - "active_days": active_days, + "tickets_created": created, + "status_changes": changes, + "comments_made": comments, + "active_days": days_active, "weeks": weeks, "max_val": max_val, } diff --git a/src/epics/migrations/0002_epic_epics_project_eeb4de_idx.py b/src/epics/migrations/0002_epic_epics_project_eeb4de_idx.py index 61b7e71..ebdb0f4 100644 --- a/src/epics/migrations/0002_epic_epics_project_eeb4de_idx.py +++ b/src/epics/migrations/0002_epic_epics_project_eeb4de_idx.py @@ -4,15 +4,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('epics', '0001_initial'), - ('projects', '0002_projectmember_project_mem_user_id_6b0bb0_idx'), + ("epics", "0001_initial"), + ("projects", "0002_projectmember_project_mem_user_id_6b0bb0_idx"), ] operations = [ migrations.AddIndex( - model_name='epic', - index=models.Index(fields=['project', 'sort_order'], name='epics_project_eeb4de_idx'), + model_name="epic", + index=models.Index( + fields=["project", "sort_order"], name="epics_project_eeb4de_idx" + ), ), ] diff --git a/src/epics/views.py b/src/epics/views.py index 712abc6..000304b 100644 --- a/src/epics/views.py +++ b/src/epics/views.py @@ -9,7 +9,7 @@ class EpicListCreateView(generics.ListCreateAPIView): permission_classes = (permissions.IsAuthenticated,) def get_queryset(self): - qs = Epic.objects.all() + qs = Epic.objects.all().select_related("project") project_id = self.request.query_params.get("project") if project_id: qs = qs.filter(project_id=project_id) @@ -17,6 +17,6 @@ class EpicListCreateView(generics.ListCreateAPIView): class EpicDetailView(generics.RetrieveUpdateDestroyAPIView): - queryset = Epic.objects.all() + queryset = Epic.objects.all().select_related("project") serializer_class = EpicSerializer permission_classes = (permissions.IsAuthenticated,)