mirror of
https://github.com/sstent/foodplanner.git
synced 2026-03-14 01:05:23 +00:00
feat(phase): Complete Phase 2: Logic & Calculation Updates
This commit is contained in:
@@ -746,23 +746,25 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
|
||||
# Store grams directly
|
||||
quantity = grams
|
||||
|
||||
# Create a new Meal for this single food entry
|
||||
# This allows it to be treated like any other meal in the tracker view
|
||||
new_meal = Meal(name=food_item.name, meal_type="single_food", meal_time=meal_time)
|
||||
db.add(new_meal)
|
||||
db.flush() # Flush to get the new meal ID
|
||||
|
||||
# Link the food to the new meal
|
||||
meal_food = MealFood(meal_id=new_meal.id, food_id=food_id, quantity=grams)
|
||||
db.add(meal_food)
|
||||
|
||||
# Create tracked meal entry
|
||||
# Create tracked meal entry without a parent Meal template
|
||||
tracked_meal = TrackedMeal(
|
||||
tracked_day_id=tracked_day.id,
|
||||
meal_id=new_meal.id,
|
||||
meal_time=meal_time
|
||||
meal_id=None,
|
||||
meal_time=meal_time,
|
||||
name=food_item.name
|
||||
)
|
||||
db.add(tracked_meal)
|
||||
db.flush() # Flush to get the tracked_meal ID
|
||||
|
||||
# Link the food directly to the tracked meal via TrackedMealFood
|
||||
new_entry = TrackedMealFood(
|
||||
tracked_meal_id=tracked_meal.id,
|
||||
food_id=food_id,
|
||||
quantity=grams,
|
||||
is_override=False,
|
||||
is_deleted=False
|
||||
)
|
||||
db.add(new_entry)
|
||||
|
||||
# Mark day as modified
|
||||
tracked_day.is_modified = True
|
||||
|
||||
@@ -428,9 +428,11 @@ def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
|
||||
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
|
||||
}
|
||||
|
||||
# 1. Get base foods from the meal
|
||||
# 1. Get base foods from the meal (if it exists)
|
||||
# access via relationship, assume eager loading or lazy loading
|
||||
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
|
||||
base_foods = {}
|
||||
if tracked_meal.meal:
|
||||
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
|
||||
|
||||
# 2. Get tracked foods (overrides, deletions, additions)
|
||||
tracked_foods = tracked_meal.tracked_foods
|
||||
|
||||
@@ -98,20 +98,9 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
|
||||
query_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)()
|
||||
|
||||
try:
|
||||
# Find the created Meal
|
||||
created_meal = query_session.query(Meal).order_by(Meal.id.desc()).first()
|
||||
assert created_meal is not None
|
||||
assert created_meal.name == "Test Food"
|
||||
assert created_meal.meal_type == "single_food"
|
||||
|
||||
# Find the MealFood
|
||||
meal_food = query_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first()
|
||||
assert meal_food is not None
|
||||
assert meal_food.food_id == food.id
|
||||
|
||||
# This assertion fails because the backend used data.get("grams", 1.0), so quantity=1.0 instead of 50.0
|
||||
# After the fix changing to data.get("quantity", 1.0), it will pass
|
||||
assert meal_food.quantity == 50.0, f"Expected quantity 50.0, but got {meal_food.quantity}"
|
||||
# Verify NO new Meal was created
|
||||
meals = query_session.query(Meal).all()
|
||||
assert len(meals) == 0
|
||||
|
||||
# Also verify TrackedDay and TrackedMeal were created
|
||||
tracked_day = query_session.query(TrackedDay).filter(
|
||||
@@ -123,8 +112,16 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
|
||||
|
||||
tracked_meal = query_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
|
||||
assert tracked_meal is not None
|
||||
assert tracked_meal.meal_id == created_meal.id
|
||||
assert tracked_meal.meal_id is None
|
||||
assert tracked_meal.name == "Test Food"
|
||||
assert tracked_meal.meal_time == "Snack 1"
|
||||
|
||||
# Find the TrackedMealFood
|
||||
from app.database import TrackedMealFood
|
||||
tmf = query_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||
assert tmf is not None
|
||||
assert tmf.food_id == food.id
|
||||
assert tmf.quantity == 50.0
|
||||
|
||||
finally:
|
||||
query_session.close()
|
||||
|
||||
@@ -140,10 +140,13 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
# Verify the tracked meal food quantity
|
||||
tracked_meal = session.query(Meal).filter(Meal.name == sample_food_100g.name).first()
|
||||
tracked_meal = session.query(TrackedMeal).filter(TrackedMeal.name == sample_food_100g.name).first()
|
||||
assert tracked_meal is not None
|
||||
meal_food = session.query(MealFood).filter(MealFood.meal_id == tracked_meal.id).first()
|
||||
assert meal_food.quantity == grams
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
tmf = session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||
assert tmf is not None
|
||||
assert tmf.quantity == grams
|
||||
|
||||
def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g, sample_food_50g):
|
||||
"""Test updating tracked meal foods with grams input"""
|
||||
|
||||
113
tests/test_tracked_meal_refactor.py
Normal file
113
tests/test_tracked_meal_refactor.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import pytest
|
||||
from app.database import Food, TrackedMeal, TrackedMealFood, calculate_tracked_meal_nutrition
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def test_calculate_tracked_meal_nutrition_no_meal_template(db_session: Session):
|
||||
"""Test nutrition calculation for a tracked meal with no parent meal template (meal_id=None)"""
|
||||
# Create a food
|
||||
food = Food(
|
||||
name="Test Food",
|
||||
serving_size=100.0,
|
||||
serving_unit="g",
|
||||
calories=100.0,
|
||||
protein=10.0,
|
||||
carbs=20.0,
|
||||
fat=5.0,
|
||||
fiber=5.0,
|
||||
sugar=10.0,
|
||||
sodium=100.0,
|
||||
calcium=50.0
|
||||
)
|
||||
db_session.add(food)
|
||||
db_session.commit()
|
||||
db_session.refresh(food)
|
||||
|
||||
# Create a tracked meal without a template
|
||||
tracked_meal = TrackedMeal(
|
||||
meal_id=None,
|
||||
meal_time="Snack",
|
||||
name="Single Food Log"
|
||||
)
|
||||
db_session.add(tracked_meal)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_meal)
|
||||
|
||||
# Add a tracked food entry to it
|
||||
tracked_food = TrackedMealFood(
|
||||
tracked_meal_id=tracked_meal.id,
|
||||
food_id=food.id,
|
||||
quantity=200.0, # 2 servings
|
||||
is_override=False,
|
||||
is_deleted=False
|
||||
)
|
||||
db_session.add(tracked_food)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_food)
|
||||
|
||||
# Calculate nutrition
|
||||
nutrition = calculate_tracked_meal_nutrition(tracked_meal, db_session)
|
||||
|
||||
# Assertions
|
||||
assert nutrition['calories'] == 200.0
|
||||
assert nutrition['protein'] == 20.0
|
||||
assert nutrition['carbs'] == 40.0
|
||||
assert nutrition['fat'] == 10.0
|
||||
assert nutrition['fiber'] == 10.0
|
||||
assert nutrition['sugar'] == 20.0
|
||||
assert nutrition['sodium'] == 200.0
|
||||
assert nutrition['calcium'] == 100.0
|
||||
assert nutrition['net_carbs'] == 30.0
|
||||
assert nutrition['protein_pct'] == 40.0 # (20 * 4) / 200 = 80 / 200 = 40%
|
||||
assert nutrition['carbs_pct'] == 80.0 # (40 * 4) / 200 = 160 / 200 = 80%
|
||||
assert nutrition['fat_pct'] == 45.0 # (10 * 9) / 200 = 90 / 200 = 45%
|
||||
|
||||
def test_tracker_add_food_api_no_new_meal(client, db_session: Session):
|
||||
"""Test /tracker/add_food endpoint to ensure it doesn't create redundant Meal templates"""
|
||||
# Create a food
|
||||
food = Food(
|
||||
name="API Test Food",
|
||||
serving_size=100.0,
|
||||
serving_unit="g",
|
||||
calories=100.0,
|
||||
protein=10.0,
|
||||
carbs=20.0,
|
||||
fat=5.0
|
||||
)
|
||||
db_session.add(food)
|
||||
db_session.commit()
|
||||
db_session.refresh(food)
|
||||
|
||||
from app.database import Meal
|
||||
initial_meal_count = db_session.query(Meal).count()
|
||||
|
||||
# Call the API
|
||||
response = client.post("/tracker/add_food", json={
|
||||
"person": "Sarah",
|
||||
"date": "2025-02-24",
|
||||
"food_id": food.id,
|
||||
"quantity": 150.0,
|
||||
"meal_time": "Snack"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
# Verify NO new Meal was created
|
||||
assert db_session.query(Meal).count() == initial_meal_count
|
||||
|
||||
# Verify TrackedMeal exists with meal_id=None and correct name
|
||||
from app.database import TrackedMeal, TrackedDay
|
||||
tracked_day = db_session.query(TrackedDay).filter(TrackedDay.date == "2025-02-24").first()
|
||||
assert tracked_day is not None
|
||||
|
||||
tracked_meal = db_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
|
||||
assert tracked_meal is not None
|
||||
assert tracked_meal.meal_id is None
|
||||
assert tracked_meal.name == "API Test Food"
|
||||
|
||||
# Verify TrackedMealFood exists
|
||||
from app.database import TrackedMealFood
|
||||
tmf = db_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||
assert tmf is not None
|
||||
assert tmf.food_id == food.id
|
||||
assert tmf.quantity == 150.0
|
||||
@@ -384,12 +384,13 @@ class TestTrackerAddFood:
|
||||
assert len(tracked_meals) == 1
|
||||
|
||||
tracked_meal = tracked_meals[0]
|
||||
assert tracked_meal.meal.name == sample_food.name # The meal name should be the food name
|
||||
assert tracked_meal.name == sample_food.name # The meal name should be the food name
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
# Verify the food is in the tracked meal's foods
|
||||
assert len(tracked_meal.meal.meal_foods) == 1
|
||||
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.meal.meal_foods[0].quantity == 100.0
|
||||
assert len(tracked_meal.tracked_foods) == 1
|
||||
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.tracked_foods[0].quantity == 100.0
|
||||
|
||||
|
||||
def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session):
|
||||
@@ -418,11 +419,12 @@ class TestTrackerAddFood:
|
||||
assert len(tracked_meals) == 1
|
||||
|
||||
tracked_meal = tracked_meals[0]
|
||||
assert tracked_meal.meal.name == sample_food.name
|
||||
assert tracked_meal.name == sample_food.name
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
assert len(tracked_meal.meal.meal_foods) == 1
|
||||
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.meal.meal_foods[0].quantity == 150.0
|
||||
assert len(tracked_meal.tracked_foods) == 1
|
||||
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.tracked_foods[0].quantity == 150.0
|
||||
|
||||
def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session):
|
||||
"""
|
||||
@@ -464,12 +466,13 @@ class TestTrackerAddFood:
|
||||
assert len(tracked_meals) == 1
|
||||
|
||||
tracked_meal = tracked_meals[0]
|
||||
assert tracked_meal.meal.name == food.name
|
||||
assert tracked_meal.name == food.name
|
||||
assert tracked_meal.meal_id is None
|
||||
|
||||
# Verify the food is in the tracked meal's foods and quantity is in servings
|
||||
assert len(tracked_meal.meal.meal_foods) == 1
|
||||
assert tracked_meal.meal.meal_foods[0].food_id == food.id
|
||||
assert tracked_meal.meal.meal_foods[0].quantity == grams_to_add
|
||||
# Verify the food is in the tracked meal's foods
|
||||
assert len(tracked_meal.tracked_foods) == 1
|
||||
assert tracked_meal.tracked_foods[0].food_id == food.id
|
||||
assert tracked_meal.tracked_foods[0].quantity == grams_to_add
|
||||
|
||||
# Verify nutrition calculation
|
||||
day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session)
|
||||
|
||||
Reference in New Issue
Block a user