11 KiB
Food Planner Quantity Standardization Plan Problem Statement The application has inconsistent handling of food quantities throughout the codebase:
Current Issue: MealFood.quantity is being used sometimes as a multiplier of serving_size and sometimes as grams directly Impact: Confusing calculations in nutrition functions and unclear user interface expectations Goal: Standardize so MealFood.quantity always represents grams of the food item
Core Data Model Definition Standard to Adopt Food.serving_size = base serving size in grams (e.g., 100) Food.[nutrients] = nutritional values per serving_size grams MealFood.quantity = actual grams to use (e.g., 150g) TrackedMealFood.quantity = actual grams to use (e.g., 200g)
Calculation: multiplier = quantity / serving_size
Implementation Plan Phase 1: Audit & Document (Non-Breaking) Task 1.1: Add documentation header to app/database.py python""" QUANTITY CONVENTION: All quantity fields in this application represent GRAMS.
- Food.serving_size: base serving size in grams (e.g., 100.0)
- Food nutrition values: per serving_size grams
- MealFood.quantity: grams of this food in the meal (e.g., 150.0)
- TrackedMealFood.quantity: grams of this food as tracked (e.g., 200.0)
To calculate nutrition: multiplier = quantity / serving_size """ Task 1.2: Audit all locations where quantity is read/written
app/database.py - calculation functions app/api/routes/meals.py - meal food operations app/api/routes/tracker.py - tracked meal operations app/api/routes/plans.py - detailed view Templates using quantity values
Phase 2: Fix Core Calculation Functions Task 2.1: Fix calculate_meal_nutrition() in app/database.py Current behavior: Assumes quantity is already a multiplier New behavior: Calculate multiplier from grams pythondef calculate_meal_nutrition(meal, db: Session): """ Calculate total nutrition for a meal. MealFood.quantity is in GRAMS. """ totals = { 'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0, 'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0 }
for meal_food in meal.meal_foods:
food = meal_food.food
grams = meal_food.quantity
# Convert grams to multiplier based on serving size
try:
serving_size = float(food.serving_size)
multiplier = grams / serving_size if serving_size > 0 else 0
except (ValueError, TypeError):
multiplier = 0
totals['calories'] += food.calories * multiplier
totals['protein'] += food.protein * multiplier
totals['carbs'] += food.carbs * multiplier
totals['fat'] += food.fat * multiplier
totals['fiber'] += (food.fiber or 0) * multiplier
totals['sugar'] += (food.sugar or 0) * multiplier
totals['sodium'] += (food.sodium or 0) * multiplier
totals['calcium'] += (food.calcium or 0) * multiplier
# Calculate percentages (unchanged)
total_cals = totals['calories']
if total_cals > 0:
totals['protein_pct'] = round((totals['protein'] * 4 / total_cals) * 100, 1)
totals['carbs_pct'] = round((totals['carbs'] * 4 / total_cals) * 100, 1)
totals['fat_pct'] = round((totals['fat'] * 9 / total_cals) * 100, 1)
totals['net_carbs'] = totals['carbs'] - totals['fiber']
else:
totals['protein_pct'] = 0
totals['carbs_pct'] = 0
totals['fat_pct'] = 0
totals['net_carbs'] = 0
return totals
Task 2.2: Fix calculate_tracked_meal_nutrition() in app/database.py Apply the same pattern to handle TrackedMealFood.quantity as grams. Task 2.3: Remove or fix convert_grams_to_quantity() function This function appears to be unused but creates confusion. Either:
Remove it entirely, OR Rename to calculate_multiplier_from_grams() and update documentation
Phase 3: Fix API Routes Task 3.1: Fix app/api/routes/meals.py Location: POST /meals/{meal_id}/add_food python@router.post("/meals/{meal_id}/add_food") async def add_food_to_meal( meal_id: int, food_id: int = Form(...), grams: float = Form(...), # Changed from 'quantity' to be explicit db: Session = Depends(get_db) ): try: # Store grams directly - no conversion needed meal_food = MealFood( meal_id=meal_id, food_id=food_id, quantity=grams # This is grams ) db.add(meal_food) db.commit() return {"status": "success"} except Exception as e: db.rollback() return {"status": "error", "message": str(e)} Location: POST /meals/update_food_quantity python@router.post("/meals/update_food_quantity") async def update_meal_food_quantity( meal_food_id: int = Form(...), grams: float = Form(...), # Changed from 'quantity' db: Session = Depends(get_db) ): try: meal_food = db.query(MealFood).filter(MealFood.id == meal_food_id).first() if not meal_food: return {"status": "error", "message": "meal food not found"}
meal_food.quantity = grams # Store grams directly
db.commit()
return {"status": "success"}
except Exception as e:
db.rollback()
return {"status": "error", "message": str(e)}
Task 3.2: Fix app/api/routes/tracker.py Review all tracked meal operations to ensure they handle grams correctly. Task 3.3: Fix app/api/routes/plans.py detailed view The detailed view calculates nutrition per food item. Update to show grams clearly: pythonfor mf in tm.meal.meal_foods: try: serving_size_value = float(mf.food.serving_size) num_servings = mf.quantity / serving_size_value if serving_size_value != 0 else 0 except (ValueError, TypeError): num_servings = 0
foods.append({
'name': mf.food.name,
'total_grams': mf.quantity, # Explicitly show it's grams
'num_servings': round(num_servings, 2),
'serving_size': mf.food.serving_size,
'serving_unit': mf.food.serving_unit,
# Don't recalculate nutrition here - it's done in calculate_meal_nutrition
})
Phase 4: Fix CSV Import Functions Task 4.1: Fix app/api/routes/meals.py - POST /meals/upload Currently processes ingredient pairs as (food_name, grams). Ensure it stores grams directly: pythonfor i in range(1, len(row), 2): if i+1 >= len(row) or not row[i].strip(): continue
food_name = row[i].strip()
grams = float(row[i+1].strip()) # This is grams
# ... find food ...
ingredients.append((food.id, grams)) # Store grams directly
Later when creating MealFood:
for food_id, grams in ingredients: meal_food = MealFood( meal_id=existing.id, food_id=food_id, quantity=grams # Store grams directly )
Phase 5: Update Templates & UI Task 5.1: Update templates/detailed.html Ensure the food breakdown clearly shows grams: html
Phase 6: Add Tests Task 6.1: Create test file tests/test_quantity_consistency.py pythondef test_meal_nutrition_uses_grams_correctly(db_session): """Verify that MealFood.quantity as grams calculates nutrition correctly""" # Create a food: 100 cal per 100g food = Food( name="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()
# Create a meal with 200g of this food
meal = Meal(name="Test Meal", meal_type="breakfast")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=200.0 # 200 grams
)
db_session.add(meal_food)
db_session.commit()
# Calculate nutrition
nutrition = calculate_meal_nutrition(meal, db_session)
# Should be 2x the base values (200g / 100g = 2x multiplier)
assert nutrition['calories'] == 200.0
assert nutrition['protein'] == 20.0
assert nutrition['carbs'] == 40.0
assert nutrition['fat'] == 10.0
def test_fractional_servings(db_session): """Test that fractional grams work correctly""" food = Food( name="Test Food", serving_size=100.0, serving_unit="g", calories=100.0 ) db_session.add(food) db_session.commit()
meal = Meal(name="Test Meal")
db_session.add(meal)
db_session.commit()
# Add 50g (half serving)
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=50.0
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
assert nutrition['calories'] == 50.0
Task 6.2: Run existing tests to verify no regressions
Phase 7: Data Migration (if needed) Task 7.1: Determine if existing data needs migration Check if current database has MealFood entries where quantity is already being stored as multipliers instead of grams. If so, create a data migration script. Task 7.2: Create Alembic migration (documentation only) python"""clarify quantity fields represent grams
Revision ID: xxxxx Revises: 2295851db11e Create Date: 2025-10-01 """
def upgrade() -> None: # No schema changes needed # This migration documents that all quantity fields = grams # If data migration is needed, add conversion logic here pass
def downgrade() -> None: pass
Testing Checklist
Add 100g of a food with 100 cal/100g → should show 100 cal Add 200g of a food with 100 cal/100g → should show 200 cal Add 50g of a food with 100 cal/100g → should show 50 cal Import meals from CSV with gram values → should calculate correctly View detailed page for template → should show grams and correct totals View detailed page for tracked day → should show grams and correct totals Edit meal food quantity → should accept and store grams
Rollout Plan
Deploy to staging: Test all functionality manually Run automated tests: Verify calculations Check existing data: Ensure no corruption Deploy to production: Monitor for errors Document changes: Update any user documentation
Risk Assessment Low Risk:
Adding documentation Fixing calculation functions (if current behavior is already treating quantity as grams)
Medium Risk:
Changing API parameter names from quantity to grams Updating templates
High Risk:
If existing database has mixed data (some quantities are multipliers, some are grams) Need to audit actual database content before proceeding
Notes
The CSV import already seems to expect grams based on the code The main issue appears to be in calculate_meal_nutrition() if it's not properly converting grams to multipliers Consider adding database constraints or validation to ensure quantity > 0 and reasonable ranges