make streaks and the leaderboard live, and stop three AI chats from freezing the API
Found in a deeper pass over what the frontend's flow actually touches:
- Nothing in the app ever wrote StreakRecord: "days in a row" was 0 forever
for every real user. Answers and lesson/island completions now record the
day (Tehran time); a broken chain shows 0. A streak error never fails the
answer itself.
- /api/leaderboard only changed when someone ran rebuild_leaderboard by
hand. It is now computed live (60s cache); weekly counts island XP too.
- gunicorn ran 3 sync workers with a 30s timeout, so every "ask Pi" stream
pinned a whole worker (~10s, GapGPT buffers the answer) and three at once
queued the entire API. gunicorn.conf.py (read automatically from the
working directory) switches to gthread, 8 threads, 120s timeout.
- Every /api/ response now says Cache-Control: private, no-store, so turning
on caching in the ArvanCloud panel can never serve one student another's data.
- /api/user/summary named no active plan ever ("subscription app not
implemented yet" had long stopped being true).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ from rest_framework.throttling import ScopedRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import Character, LeaderboardEntry, StreakRecord, UserHearts, UserStats
|
||||
from .services import effective_current_streak
|
||||
|
||||
|
||||
class GuestAwareReadThrottle(ScopedRateThrottle):
|
||||
@@ -34,6 +35,20 @@ from .serializers import (
|
||||
)
|
||||
|
||||
|
||||
def _active_plan_title(user_id):
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from subscription.models import Subscription
|
||||
sub = (
|
||||
Subscription.objects.select_related('plan')
|
||||
.filter(user_id=user_id, status='active')
|
||||
.filter(Q(expires_at__isnull=True) | Q(expires_at__gt=timezone.now()))
|
||||
.first()
|
||||
)
|
||||
return sub.plan.title if sub and sub.plan else None
|
||||
|
||||
|
||||
class UserSummaryView(APIView):
|
||||
"""GET /api/user/summary — پروفایل + آمار + استریک (ادغامشده)"""
|
||||
|
||||
@@ -55,11 +70,12 @@ class UserSummaryView(APIView):
|
||||
'xp': stats.xp if stats else 0,
|
||||
'coins': stats.coins if stats else 0,
|
||||
'streak': {
|
||||
'current': streak.current_streak if streak else 0,
|
||||
'current': effective_current_streak(streak),
|
||||
'longest': streak.longest_streak if streak else 0,
|
||||
},
|
||||
# اپ subscription هنوز در این فاز پیادهسازی نشده؛ فعلاً null.
|
||||
'subscription': None,
|
||||
# عنوانِ پلنِ اشتراکِ فعال، یا null. تا ۲۰۲۶-۰۹-۲۴ همیشه null بود
|
||||
# (از زمانی که اپِ subscription هنوز وجود نداشت).
|
||||
'subscription': _active_plan_title(user.id),
|
||||
}
|
||||
return Response(UserSummaryResponseSerializer(data).data)
|
||||
|
||||
@@ -77,13 +93,18 @@ class StreakCalendarView(APIView):
|
||||
streak = StreakRecord.objects.filter(user_id=user.id).first()
|
||||
|
||||
data = {
|
||||
'currentStreak': streak.current_streak if streak else 0,
|
||||
'currentStreak': effective_current_streak(streak),
|
||||
'longestStreak': streak.longest_streak if streak else 0,
|
||||
'activeDays': list(streak.active_days.values_list('date', flat=True)) if streak else [],
|
||||
}
|
||||
return Response(StreakCalendarResponseSerializer(data).data)
|
||||
|
||||
|
||||
# کوتاه تا XPی تازه زود دیده شود، بلند تا هزار کاربرِ همزمان هر بار جدول را
|
||||
# از نو حساب نکنند.
|
||||
LEADERBOARD_CACHE_TTL = 60
|
||||
|
||||
|
||||
class LeaderboardView(APIView):
|
||||
"""GET /api/leaderboard?period= — رتبهبندی هفتگی/ماهانه"""
|
||||
|
||||
@@ -98,7 +119,20 @@ class LeaderboardView(APIView):
|
||||
)
|
||||
def get(self, request):
|
||||
period = request.query_params.get('period', 'weekly')
|
||||
entries = LeaderboardEntry.objects.filter(period=period).order_by('rank')[:50]
|
||||
|
||||
# زنده از فعالیتِ واقعی، با کشِ کوتاه (progress/leaderboard.py). قبلاً از
|
||||
# جدولِ LeaderboardEntry خوانده میشد که فقط با اجرای دستیِ
|
||||
# rebuild_leaderboard پر میشد، پس XPی کاربرِ تازه هیچوقت دیده نمیشد.
|
||||
from django.core.cache import cache
|
||||
|
||||
from .leaderboard import compute_scores
|
||||
scores = cache.get_or_set(
|
||||
f'leaderboard:{period}', lambda: compute_scores(period), LEADERBOARD_CACHE_TTL,
|
||||
)
|
||||
entries = [
|
||||
LeaderboardEntry(user_id=uid, period=period, xp_in_period=xp, rank=rank)
|
||||
for rank, (uid, xp) in enumerate(scores, 1)
|
||||
]
|
||||
|
||||
# نام کاربر از accounts.User خونده میشه (فقط برای نمایش، بدون FK واقعی).
|
||||
from accounts.models import User as AccountUser
|
||||
|
||||
Reference in New Issue
Block a user