""" امتیازهای جدولِ رتبه‌بندی، مستقیم از فعالیتِ واقعی. تا ۲۰۲۶-۰۹-۲۴ `GET /api/leaderboard` فقط جدولِ LeaderboardEntry را می‌خواند که تنها با اجرای دستیِ `rebuild_leaderboard` پر می‌شد؛ XPی که کاربرِ تازه می‌گرفت هیچ‌وقت در جدول دیده نمی‌شد. حالا view همین تابع را صدا می‌زند و نتیجه را کوتاه‌مدت کش می‌کند، و دستورِ rebuild هم از همین تابع می‌خواند تا دو منبعِ عدد از هم جدا نیفتند. - `all_time`: همان `UserStats.xp` که در `/api/user/summary` هم دیده می‌شود. - `weekly`: XPی درس‌ها و جزیره‌هایی که در `days` روزِ اخیر تمام شده‌اند — همان دو جایی که `UserStats.xp` زیاد می‌شود. `UserAnswer.xp_earned` عمداً شمرده نمی‌شود چون به `UserStats.xp` اضافه نمی‌شود. """ from collections import defaultdict from django.db.models import Sum from django.utils import timezone PERIODS = ('weekly', 'all_time') def compute_scores(period, days=7, limit=50, include_zero=False): """[(user_id, xp), ...] مرتب: XPی بیشتر جلوتر، در تساوی user_id کوچک‌تر. پیش‌فرض فقط XPی مثبت؛ include_zero برای دستورِ rebuild است.""" if period == 'all_time': from progress.models import UserStats qs = UserStats.objects.all() if include_zero else UserStats.objects.filter(xp__gt=0) rows = list( qs.order_by('-xp', 'user_id') .values_list('user_id', 'xp')[:limit] ) return [(uid, xp) for uid, xp in rows] if period != 'weekly': return [] from content.models import UserLessonCompletion, UserNodeCompletion since = timezone.now() - timezone.timedelta(days=days) totals = defaultdict(int) for model in (UserLessonCompletion, UserNodeCompletion): for row in ( model.objects.filter(created_at__gte=since) .values('user_id').annotate(total=Sum('xp_earned')) ): totals[row['user_id']] += row['total'] or 0 scores = [(uid, xp) for uid, xp in totals.items() if include_zero or xp > 0] scores.sort(key=lambda p: (-p[1], p[0])) return scores[:limit]