feat(src): cache branding lookup and improve leaderboard performance
Prevent DB hits by caching site branding and refactor leaderboard query using annotate
This commit is contained in:
4 files changed
+129
-70
No files matched your search
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
+2
-2
@@ -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,)
|
||||
Reference in new issue
Block a user