Files
MathApp/progress/models.py
Zareian a275eac7fd fix heart refill and grant unlimited hearts to active subscribers
next_refill_at was set on UserHearts but never read or recalculated,
so a heart lost to zero never came back. UserHearts.apply_regen() now
catches up missed 24h intervals on read/deduct, and lose_heart() starts
the timer only when it isn't already running.

Any user with an active (non-expired) subscription now skips heart
deduction entirely, regardless of which plan they're on — good enough
for now until per-plan perks are needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 13:26:49 +03:30

95 lines
3.4 KiB
Python

from datetime import timedelta
from django.db import models
from django.utils import timezone
HEART_REFILL_INTERVAL = timedelta(hours=24)
class UserStats(models.Model):
user_id = models.IntegerField(unique=True, db_index=True) # ارجاع سبک به accounts.User
xp = models.PositiveIntegerField(default=0)
coins = models.PositiveIntegerField(default=0)
progress_percent = models.FloatField(default=0)
def __str__(self):
return f"stats-user{self.user_id}"
class LeaderboardEntry(models.Model):
PERIOD_CHOICES = [('weekly', 'هفتگی'), ('all_time', 'کل زمان')]
user_id = models.IntegerField(db_index=True)
period = models.CharField(max_length=10, choices=PERIOD_CHOICES)
xp_in_period = models.PositiveIntegerField(default=0)
rank = models.PositiveIntegerField()
class Meta:
ordering = ['rank']
def __str__(self):
return f"lb-{self.period}-user{self.user_id}-rank{self.rank}"
class StreakRecord(models.Model):
user_id = models.IntegerField(unique=True, db_index=True)
current_streak = models.PositiveIntegerField(default=0)
longest_streak = models.PositiveIntegerField(default=0)
def __str__(self):
return f"streak-user{self.user_id}"
class ActiveDay(models.Model):
streak = models.ForeignKey(StreakRecord, on_delete=models.CASCADE, related_name='active_days')
date = models.DateField()
class Meta:
ordering = ['date']
def __str__(self):
return str(self.date)
class UserHearts(models.Model):
user_id = models.IntegerField(unique=True, db_index=True)
current_hearts = models.PositiveSmallIntegerField(default=5)
max_hearts = models.PositiveSmallIntegerField(default=5)
next_refill_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f"hearts-user{self.user_id}"
def apply_regen(self):
"""اگه از next_refill_at گذشته باشه، هارت(های) گذشته رو پر می‌کنه (چندتا هم
اگه کاربر چند بازه‌ی HEART_REFILL_INTERVAL غایب بوده باشه). True یعنی
state عوض شده و باید save بشه."""
if self.current_hearts >= self.max_hearts:
if self.next_refill_at is not None:
self.next_refill_at = None
return True
return False
now = timezone.now()
if self.next_refill_at is None:
self.next_refill_at = now + HEART_REFILL_INTERVAL
return True
if now < self.next_refill_at:
return False
elapsed_intervals = 1 + (now - self.next_refill_at) // HEART_REFILL_INTERVAL
gained = min(elapsed_intervals, self.max_hearts - self.current_hearts)
self.current_hearts += gained
if self.current_hearts >= self.max_hearts:
self.next_refill_at = None
else:
self.next_refill_at += elapsed_intervals * HEART_REFILL_INTERVAL
return True
def lose_heart(self):
"""یک هارت کم می‌کنه؛ اگه تایمرِ refill از قبل در حال شمارش نیست، از الان شروعش می‌کنه."""
if self.current_hearts > 0:
self.current_hearts -= 1
if self.next_refill_at is None and self.current_hearts < self.max_hearts:
self.next_refill_at = timezone.now() + HEART_REFILL_INTERVAL