- Replace day-iteration algorithm with pair-based approach for fasting detection. Iterate consecutive food scrobble pairs and mark foodless days between them as fasting days. - Handle consecutive-day periodic fasting: when two food scrobbles are on consecutive days with a gap >= periodic_threshold but < full_threshold, mark the later day as a periodic fast (user's original bug fix). - Fix _dt test helper to use replace() instead of subtracting hours, producing correct timestamps (e.g., '3 days ago at 18:46' instead of '3 days + 18h46m ago'). - Fix total_days off-by-one: removed +1 from inclusive day count so a 'last_30' period correctly reports 30 days. - Add _drinks_on_day() for per-day drink detection (replaces whole- window _drinks_in_window for more accurate liquid fasting detection). - Fix Beer test fixture to use styles M2M field instead of non-existent beer_style attribute. - Add 26 test cases covering: classification, short gaps, periodic/ full/liquid fasting, multi-day gaps, vacation exemptions, custom thresholds, streaks, mixed types, edge cases. Reasoning: The original day-iteration algorithm had fundamental issues: it marked days with food as fasting (since it only checked prev/next food times), it didn't filter out normal (< periodic) gaps, and the _dt helper was misinterpreting hour parameters. The new pair-based approach is cleaner: for each consecutive food scrobble pair, we mark the foodless days between them. This naturally handles multi-day fasts and correctly avoids marking food-eating days as fasting. The consecutive-day periodic rule preserves the user's original intent of detecting overnight fasts between meals.
78 lines
1.9 KiB
Python
78 lines
1.9 KiB
Python
import pytest
|
|
from django.contrib.auth import get_user_model
|
|
from django.utils import timezone
|
|
|
|
from foods.models import Food
|
|
from profiles.models import UserProfile
|
|
from scrobbles.models import Scrobble
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
@pytest.fixture
|
|
def user(db):
|
|
u = User.objects.create_user(
|
|
username="fasting", email="fasting@example.com", password="testpass"
|
|
)
|
|
UserProfile.objects.get_or_create(user=u)
|
|
return u
|
|
|
|
|
|
@pytest.fixture
|
|
def user_with_custom_thresholds(db):
|
|
u = User.objects.create_user(
|
|
username="custom", email="custom@example.com", password="testpass"
|
|
)
|
|
profile, _ = UserProfile.objects.get_or_create(user=u)
|
|
profile.fasting_periodic_hours = 12
|
|
profile.fasting_full_hours = 20
|
|
profile.save()
|
|
return u
|
|
|
|
|
|
@pytest.fixture
|
|
def food(user):
|
|
return Food.objects.create(title="Test Food", base_run_time_seconds=1200)
|
|
|
|
|
|
@pytest.fixture
|
|
def drink(user):
|
|
from drinks.models import Drink
|
|
|
|
return Drink.objects.create(
|
|
title="Test Drink", base_run_time_seconds=120, calories=50
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def beer(user):
|
|
from drinks.models import Beer, BeerStyle
|
|
|
|
style = BeerStyle.objects.create(name="IPA")
|
|
b = Beer.objects.create(title="Test Beer", base_run_time_seconds=120)
|
|
b.styles.add(style)
|
|
return b
|
|
|
|
|
|
def make_food_scrobble(user, food, ts):
|
|
return Scrobble.objects.create(
|
|
user=user,
|
|
food=food,
|
|
media_type=Scrobble.MediaType.FOOD,
|
|
timestamp=ts,
|
|
played_to_completion=True,
|
|
)
|
|
|
|
|
|
def make_drink_scrobble(user, drink, ts, media_type=Scrobble.MediaType.DRINK):
|
|
kwargs = {"drink": drink} if media_type == Scrobble.MediaType.DRINK else {}
|
|
if media_type == Scrobble.MediaType.BEER:
|
|
kwargs = {"beer": drink}
|
|
return Scrobble.objects.create(
|
|
user=user,
|
|
media_type=media_type,
|
|
timestamp=ts,
|
|
played_to_completion=True,
|
|
**kwargs,
|
|
)
|