Files
MathApp/progress/services.py
Zareian 701d8badcc 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>
2026-09-24 06:39:50 +03:30

60 lines
2.4 KiB
Python

"""
استریک: روزهای پیاپیِ فعالیت.
تا ۲۰۲۶-۰۹-۲۴ هیچ‌جای کد StreakRecord یا ActiveDay نمی‌نوشت — فقط
seed_test_data. یعنی «روز پیاپی» روی صفحه‌ی خانه برای هر کاربرِ واقعی برای
همیشه صفر بود و فقط کاربرهای دمو عدد داشتند.
«فعالیت» یعنی پاسخ به سوال یا تمام کردنِ درس/جزیره. هر روز (به وقتِ تهران،
TIME_ZONE) یک بار شمرده می‌شود؛ صدا زدنِ مکرر در یک روز بی‌اثر است.
"""
from datetime import timedelta
from django.db import transaction
from django.utils import timezone
from .models import ActiveDay, StreakRecord
def record_activity(user_id, today=None):
"""امروز را روزِ فعال ثبت می‌کند و استریک را جلو می‌برد. idempotent در یک روز."""
today = today or timezone.localdate()
with transaction.atomic():
StreakRecord.objects.get_or_create(user_id=user_id)
streak = StreakRecord.objects.select_for_update().get(user_id=user_id)
last = (
ActiveDay.objects.filter(streak=streak)
.order_by('-date').values_list('date', flat=True).first()
)
if last == today:
return streak
if last == today - timedelta(days=1):
streak.current_streak += 1
else:
streak.current_streak = 1
streak.longest_streak = max(streak.longest_streak, streak.current_streak)
streak.save(update_fields=['current_streak', 'longest_streak'])
ActiveDay.objects.get_or_create(streak=streak, date=today)
return streak
def effective_current_streak(streak, today=None):
"""
استریکی که الان باید نشان داده شود.
current_streak فقط در لحظه‌ی فعالیت به‌روز می‌شود؛ کاربری که دو روز
نیامده هنوز عددِ قبلی را در دیتابیس دارد. اگر آخرین روزِ فعال نه امروز
است نه دیروز، زنجیره شکسته و عددِ واقعی صفر است.
"""
if streak is None:
return 0
today = today or timezone.localdate()
last = (
streak.active_days.order_by('-date').values_list('date', flat=True).first()
)
if last is None or last < today - timedelta(days=1):
return 0
return streak.current_streak