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:
59
progress/services.py
Normal file
59
progress/services.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
استریک: روزهای پیاپیِ فعالیت.
|
||||
|
||||
تا ۲۰۲۶-۰۹-۲۴ هیچجای کد 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
|
||||
Reference in New Issue
Block a user