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:
2026-09-24 06:39:50 +03:30
parent c874c186b1
commit 701d8badcc
11 changed files with 375 additions and 27 deletions

View File

@@ -1,3 +1,5 @@
import logging
from django.core.cache import cache
from django.db import IntegrityError, transaction
from django.db.models import Count, F
@@ -43,6 +45,8 @@ from .serializers import (
QuestionReportResponseSerializer,
)
logger = logging.getLogger(__name__)
class GuestAwareContentThrottle(ScopedRateThrottle):
"""
@@ -163,6 +167,18 @@ class GradesListView(APIView):
return Response(GradesListResponseSerializer(_grades_payload()).data)
def _record_streak(user_id):
"""
فعالیتِ امروز را در استریک ثبت می‌کند. خطایش هرگز نباید ثبتِ پاسخ یا
تمام شدنِ درس را خراب کند — استریک تزئینی است، پیشرفت نیست.
"""
try:
from progress.services import record_activity
record_activity(user_id)
except Exception: # noqa: BLE001
logger.exception('streak: ثبتِ فعالیتِ کاربر %s شکست خورد', user_id)
def _node_status(lesson_ids, completed_ids, is_first_unlocked_candidate, quiz_completed=False):
# آزمونِ مبحث تمام شده → مبحث تمام است، مستقل از تک‌تکِ درس‌ها.
if quiz_completed:
@@ -437,6 +453,7 @@ class QuestionAnswerView(APIView):
'heartsRemaining': hearts_remaining if hearts_remaining is not None else 5,
'mascotMood': 'happy' if choice.is_correct else 'encourage',
}
_record_streak(user_id)
return Response(AnswerResponseSerializer(data).data)
@@ -593,6 +610,7 @@ class LessonCompleteView(APIView):
'coinsEarned': coins_earned,
'stats': stats_payload,
}
_record_streak(user_id)
return Response(LessonCompleteResponseSerializer(data).data)
@@ -904,6 +922,7 @@ class NodeQuizCompleteView(APIView):
raise
stats = UserStats.objects.filter(user_id=user_id).first()
_record_streak(user_id)
return respond(completion, stats)