mirror of
https://github.com/sstent/foodplanner.git
synced 2026-01-25 11:11:36 +00:00
320 lines
11 KiB
Markdown
320 lines
11 KiB
Markdown
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<li>
|
|
{{ food.total_grams }}g of {{ food.name }}
|
|
({{ food.num_servings|round(2) }} servings of {{ food.serving_size }}{{ food.serving_unit }})
|
|
</li>
|
|
Task 5.2: Update meal editing forms
|
|
Ensure all forms ask for "grams" not "quantity" to avoid confusion.
|
|
|
|
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 |