Files
MathApp/progress/services.py
Zareian 9024c6b8d8 /api/user/summary: streak.activeToday and streak.lost for the app's lit / at-risk / frozen flame
activeToday: activity recorded today (Tehran date). lost: length of a streak that broke (last active day
before yesterday), so the app can show it frozen. Same rule as effective_current_streak; the weekly report
keeps its own two-field streak serializer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:27:14 +03:30

78 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
استریک: روزهای پیاپیِ فعالیت.
تا ۲۰۲۶-۰۹-۲۴ هیچ‌جای کد 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
def streak_state(streak, today=None):
"""
وضعیتِ زنجیره برای نمایش: (current, active_today, lost).
lost طولِ زنجیره‌ای است که شکسته (آخرین روزِ فعال پیش از دیروز)؛ current_streakِ دیتابیس تا فعالیتِ بعدی
همان عددِ قبلی را نگه می‌دارد (record_activity آن را به ۱ برمی‌گرداند)، پس همین عدد است.
"""
if streak is None:
return 0, False, 0
today = today or timezone.localdate()
last = streak.active_days.order_by('-date').values_list('date', flat=True).first()
if last is None:
return 0, False, 0
if last < today - timedelta(days=1):
return 0, False, streak.current_streak
return streak.current_streak, last == today, 0