unit consistency changes

This commit is contained in:
2025-10-01 14:36:42 -07:00
parent 7ffc57a7a8
commit bb30f9eb2b
18 changed files with 726 additions and 120 deletions

View File

@@ -0,0 +1,70 @@
"""Document quantity standardization: all quantity fields now represent grams
Revision ID: a11a3921f528
Revises: 2295851db11e
Create Date: 2025-10-01 20:25:50.531913
This migration documents the standardization of quantity handling across the application.
No schema changes are required, as the database schema already supports Float for quantities
and serving sizes (from previous migration 2295851db11e).
Key Changes Documented:
- All quantity fields (MealFood.quantity, TrackedMealFood.quantity) now explicitly represent
grams of the food item.
- Food.serving_size represents the base serving size in grams.
- Nutritional values for Food are per serving_size grams.
- Nutrition calculations use: multiplier = quantity (grams) / serving_size (grams)
Data Migration Assessment:
- Queried existing MealFood entries: 154 total.
- Quantities appear to be stored as grams (e.g., 120.0g for Egg with 50g serving, 350.0g for Black Tea).
- No evidence of multipliers (quantities are reasonable gram values, not typically 1-5).
- All Food.serving_size values are numeric (Floats), no strings detected.
- No None values in core nutrients (calories, protein, carbs, fat).
- Conclusion: No data conversion needed. Existing data aligns with the grams convention.
- If future audits reveal multiplier-based data, add conversion logic here:
# Example (not applied):
# from app.database import Food, MealFood
# conn = op.get_bind()
# meal_foods = conn.execute(sa.text("SELECT mf.id, mf.quantity, mf.food_id FROM meal_foods mf")).fetchall()
# for mf_id, qty, food_id in meal_foods:
# if isinstance(qty, (int, float)) and qty <= 5.0: # Heuristic for potential multipliers
# serving = conn.execute(sa.text("SELECT serving_size FROM foods WHERE id = :fid"), {"fid": food_id}).scalar()
# if serving and serving > 0:
# new_qty = qty * serving
# conn.execute(sa.text("UPDATE meal_foods SET quantity = :nq WHERE id = :mid"), {"nq": new_qty, "mid": mf_id})
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a11a3921f528'
down_revision: Union[str, None] = '2295851db11e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# SQLite does not support comments, so we check the dialect
bind = op.get_bind()
if bind.dialect.name == 'postgresql':
op.execute("COMMENT ON COLUMN meal_foods.quantity IS 'Quantity in grams of this food in the meal'")
op.execute("COMMENT ON COLUMN tracked_meal_foods.quantity IS 'Quantity in grams of this food as tracked'")
op.execute("COMMENT ON COLUMN foods.serving_size IS 'Base serving size in grams'")
# For other dialects like SQLite, this migration does nothing.
pass
def downgrade() -> None:
# SQLite does not support comments, so we check the dialect
bind = op.get_bind()
if bind.dialect.name == 'postgresql':
op.execute("COMMENT ON COLUMN meal_foods.quantity IS NULL")
op.execute("COMMENT ON COLUMN tracked_meal_foods.quantity IS NULL")
op.execute("COMMENT ON COLUMN foods.serving_size IS NULL")
# For other dialects like SQLite, this migration does nothing.
pass

View File

@@ -47,7 +47,6 @@ async def bulk_upload_meals(file: UploadFile = File(...), db: Session = Depends(
food_name = row[i].strip()
grams = float(row[i+1].strip())
quantity = grams
# Try multiple matching strategies for food names
food = None
@@ -72,7 +71,7 @@ async def bulk_upload_meals(file: UploadFile = File(...), db: Session = Depends(
food_names = [f[0] for f in all_foods]
raise ValueError(f"Food '{food_name}' not found. Available foods include: {', '.join(food_names[:5])}...")
logging.info(f"Found food '{food_name}' with id {food.id}")
ingredients.append((food.id, quantity))
ingredients.append((food.id, grams))
# Create/update meal
existing = db.query(Meal).filter(Meal.name == meal_name).first()
@@ -89,11 +88,11 @@ async def bulk_upload_meals(file: UploadFile = File(...), db: Session = Depends(
db.flush() # Get meal ID
# Add new ingredients
for food_id, quantity in ingredients:
for food_id, grams in ingredients:
meal_food = MealFood(
meal_id=existing.id,
food_id=food_id,
quantity=quantity
quantity=grams
)
db.add(meal_food)
@@ -180,7 +179,7 @@ async def get_meal_foods(meal_id: int, db: Session = Depends(get_db)):
@router.post("/meals/{meal_id}/add_food")
async def add_food_to_meal(meal_id: int, food_id: int = Form(...),
grams: float = Form(..., alias="quantity"), db: Session = Depends(get_db)):
grams: float = Form(...), db: Session = Depends(get_db)):
try:
meal_food = MealFood(meal_id=meal_id, food_id=food_id, quantity=grams)
@@ -210,7 +209,7 @@ async def remove_food_from_meal(meal_food_id: int, db: Session = Depends(get_db)
return {"status": "error", "message": str(e)}
@router.post("/meals/update_food_quantity")
async def update_meal_food_quantity(meal_food_id: int = Form(...), grams: float = Form(..., alias="quantity"), db: Session = Depends(get_db)):
async def update_meal_food_quantity(meal_food_id: int = Form(...), grams: float = Form(...), db: Session = Depends(get_db)):
"""Update the quantity of a food in a meal"""
try:
meal_food = db.query(MealFood).filter(MealFood.id == meal_food_id).first()

View File

@@ -280,10 +280,10 @@ async def update_tracked_food(request: Request, data: dict = Body(...), db: Sess
"""Update quantity of a custom food in a tracked meal"""
try:
tracked_food_id = data.get("tracked_food_id")
quantity = float(data.get("quantity", 1.0))
grams = float(data.get("grams", 1.0))
is_custom = data.get("is_custom", False)
logging.info(f"DEBUG: Updating tracked food {tracked_food_id} quantity to {quantity}")
logging.info(f"DEBUG: Updating tracked food {tracked_food_id} grams to {grams}")
if is_custom:
tracked_food = db.query(TrackedMealFood).filter(TrackedMealFood.id == tracked_food_id).first()
@@ -300,7 +300,7 @@ async def update_tracked_food(request: Request, data: dict = Body(...), db: Sess
tracked_food = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
food_id=meal_food.food_id,
quantity=quantity
quantity=grams
)
db.add(tracked_food)
@@ -311,7 +311,7 @@ async def update_tracked_food(request: Request, data: dict = Body(...), db: Sess
return {"status": "error", "message": "Tracked food not found"}
# Update quantity
tracked_food.quantity = quantity
tracked_food.quantity = grams
# Mark the tracked day as modified
tracked_day = tracked_food.tracked_meal.tracked_day
@@ -319,7 +319,7 @@ async def update_tracked_food(request: Request, data: dict = Body(...), db: Sess
db.commit()
logging.info(f"DEBUG: Successfully updated tracked food quantity")
logging.info(f"DEBUG: Successfully updated tracked food grams")
return {"status": "success"}
except Exception as e:
@@ -440,7 +440,7 @@ async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends
try:
tracked_meal_id = data.get("tracked_meal_id")
food_id = data.get("food_id")
quantity = float(data.get("quantity", 1.0))
grams = float(data.get("grams", 1.0))
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first()
if not tracked_meal:
@@ -454,7 +454,7 @@ async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends
meal_food = MealFood(
meal_id=tracked_meal.meal_id,
food_id=food_id,
quantity=quantity
quantity=grams
)
db.add(meal_food)
@@ -489,27 +489,25 @@ async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depend
for food_data in foods_data:
food_id = food_data.get("food_id")
grams = float(food_data.get("quantity", 1.0)) # Assuming quantity is now grams
grams = float(food_data.get("grams", 1.0))
is_custom = food_data.get("is_custom", False)
item_id = food_data.get("id") # This could be MealFood.id or TrackedMealFood.id
logging.info(f"DEBUG: Processing food_id: {food_id}, quantity: {grams}, is_custom: {is_custom}, item_id: {item_id}")
quantity = grams
logging.info(f"DEBUG: Processing food_id: {food_id}, grams: {grams}, is_custom: {is_custom}, item_id: {item_id}")
if is_custom:
tracked_food = db.query(TrackedMealFood).filter(TrackedMealFood.id == item_id).first()
if tracked_food:
tracked_food.quantity = quantity
logging.info(f"DEBUG: Updated existing custom tracked food {item_id} to quantity {quantity}")
tracked_food.quantity = grams
logging.info(f"DEBUG: Updated existing custom tracked food {item_id} to grams {grams}")
else:
# If it's a new custom food being added
new_tracked_food = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
food_id=food_id,
quantity=quantity
quantity=grams
)
db.add(new_tracked_food)
logging.info(f"DEBUG: Added new custom tracked food for food_id {food_id} with quantity {quantity}")
logging.info(f"DEBUG: Added new custom tracked food for food_id {food_id} with grams {grams}")
else:
# This is a food from the original meal definition
# We need to check if it's already a TrackedMealFood (meaning it was overridden)
@@ -521,8 +519,8 @@ async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depend
logging.info(f"DEBUG: Checking for existing TrackedMealFood for food_id {food_id}: {existing_tracked_food.id if existing_tracked_food else 'None'}")
if existing_tracked_food:
existing_tracked_food.quantity = quantity
logging.info(f"DEBUG: Updated existing TrackedMealFood {existing_tracked_food.id} (override) to quantity {quantity}")
existing_tracked_food.quantity = float(grams)
logging.info(f"DEBUG: Updated existing TrackedMealFood {existing_tracked_food.id} (override) to grams {grams}")
else:
# If it's not a TrackedMealFood, it must be a MealFood
meal_food = db.query(MealFood).filter(
@@ -531,32 +529,29 @@ async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depend
).first()
logging.info(f"DEBUG: Checking for existing MealFood for food_id {food_id}: {meal_food.id if meal_food else 'None'}")
if meal_food:
# If quantity changed, convert to TrackedMealFood
# NOTE: meal_food.quantity is already a multiplier,
# but the incoming 'quantity' is a multiplier derived from grams.
# So, we compare the incoming multiplier with the existing multiplier.
if meal_food.quantity != quantity:
# If grams changed, convert to TrackedMealFood
if meal_food.quantity != grams:
new_tracked_food = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
food_id=food_id,
quantity=quantity,
quantity=float(grams),
is_override=True
)
db.add(new_tracked_food)
db.delete(meal_food) # Remove original MealFood
logging.info(f"DEBUG: Converted MealFood {meal_food.id} to new TrackedMealFood for food_id {food_id} with quantity {quantity} and deleted original MealFood.")
logging.info(f"DEBUG: Converted MealFood {meal_food.id} to new TrackedMealFood for food_id {food_id} with grams {grams} and deleted original MealFood.")
else:
logging.info(f"DEBUG: MealFood {meal_food.id} quantity unchanged, no override needed.")
logging.info(f"DEBUG: MealFood {meal_food.id} grams unchanged, no override needed.")
else:
# This case should ideally not happen if data is consistent,
# but as a fallback, add as a new TrackedMealFood
new_tracked_food = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
food_id=food_id,
quantity=quantity
quantity=float(grams)
)
db.add(new_tracked_food)
logging.warning(f"DEBUG: Fallback: Added new TrackedMealFood for food_id {food_id} with quantity {quantity}. Original MealFood not found.")
logging.warning(f"DEBUG: Fallback: Added new TrackedMealFood for food_id {food_id} with grams {grams}. Original MealFood not found.")
# Mark the tracked day as modified
tracked_meal.tracked_day.is_modified = True
@@ -656,7 +651,7 @@ async def save_as_new_meal(data: dict = Body(...), db: Session = Depends(get_db)
meal_food = MealFood(
meal_id=new_meal.id,
food_id=food_data["food_id"],
quantity=food_data["quantity"]
quantity=food_data["grams"]
)
db.add(meal_food)
@@ -692,7 +687,7 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
person = data.get("person")
date_str = data.get("date")
food_id = data.get("food_id")
grams = float(data.get("quantity", 1.0)) # Assuming quantity is now grams
grams = float(data.get("grams", 1.0))
meal_time = data.get("meal_time")
logging.info(f"DEBUG: Adding single food to tracker - person={person}, date={date_str}, food_id={food_id}, grams={grams}, meal_time={meal_time}")
@@ -713,15 +708,12 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
db.commit()
db.refresh(tracked_day)
# Convert grams to a quantity multiplier based on serving size
food_item = db.query(Food).filter(Food.id == food_id).first()
if not food_item:
return {"status": "error", "message": "Food not found"}
if food_item.serving_size > 0:
quantity = grams / food_item.serving_size
else:
quantity = 1.0 # Default to 1 serving if serving size is not set
# 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
@@ -730,7 +722,7 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
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=quantity)
meal_food = MealFood(meal_id=new_meal.id, food_id=food_id, quantity=grams)
db.add(meal_food)
# Create tracked meal entry

View File

@@ -1,6 +1,17 @@
"""
Database models and session management for the meal planner app
"""
"""
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
"""
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Text, Date, Boolean
from sqlalchemy import or_
from sqlalchemy.orm import sessionmaker, Session, relationship, declarative_base
@@ -313,7 +324,7 @@ def get_db():
def calculate_meal_nutrition(meal, db: Session):
"""
Calculate total nutrition for a meal.
Quantities in MealFood are now directly in grams.
MealFood.quantity is in GRAMS. Multiplier = quantity / food.serving_size (serving_size in grams).
"""
totals = {
'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0,
@@ -322,12 +333,16 @@ def calculate_meal_nutrition(meal, db: Session):
for meal_food in meal.meal_foods:
food = meal_food.food
multiplier = meal_food.quantity
try:
serving_size = float(food.serving_size)
multiplier = meal_food.quantity / 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['calories'] += (food.calories or 0) * multiplier
totals['protein'] += (food.protein or 0) * multiplier
totals['carbs'] += (food.carbs or 0) * multiplier
totals['fat'] += (food.fat or 0) * multiplier
totals['fiber'] += (food.fiber or 0) * multiplier
totals['sugar'] += (food.sugar or 0) * multiplier
totals['sodium'] += (food.sodium or 0) * multiplier
@@ -377,7 +392,11 @@ def calculate_day_nutrition(plans, db: Session):
return day_totals
def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
"""Calculate nutrition for a tracked meal, including custom foods"""
"""
Calculate nutrition for a tracked meal, including custom foods.
TrackedMealFood.quantity is in GRAMS. Multiplier = quantity / food.serving_size (serving_size in grams).
Base meal uses calculate_meal_nutrition which handles grams correctly.
"""
totals = {
'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0,
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
@@ -392,15 +411,15 @@ def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
# Add custom tracked foods
for tracked_food in tracked_meal.tracked_foods:
food = tracked_food.food
food_quantity = tracked_food.quantity
totals['calories'] += food.calories * food_quantity
totals['protein'] += food.protein * food_quantity
totals['carbs'] += food.carbs * food_quantity
totals['fat'] += food.fat * food_quantity
totals['fiber'] += (food.fiber or 0) * food_quantity
totals['sugar'] += (food.sugar or 0) * food_quantity
totals['sodium'] += (food.sodium or 0) * food_quantity
totals['calcium'] += (food.calcium or 0) * food_quantity
multiplier = tracked_food.quantity / food.serving_size if food.serving_size and food.serving_size != 0 else 0
totals['calories'] += (food.calories or 0) * multiplier
totals['protein'] += (food.protein or 0) * multiplier
totals['carbs'] += (food.carbs or 0) * multiplier
totals['fat'] += (food.fat or 0) * 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
total_cals = totals['calories']
@@ -447,10 +466,11 @@ def calculate_day_nutrition_tracked(tracked_meals, db: Session):
return day_totals
def convert_grams_to_quantity(food_id: int, grams: float, db: Session) -> float:
def calculate_multiplier_from_grams(food_id: int, grams: float, db: Session) -> float:
"""
Converts a given amount in grams to the corresponding quantity multiplier
based on the food's serving size.
Calculate the multiplier from grams based on the food's serving size.
Multiplier = grams / serving_size (both in grams).
Used for nutrition calculations when quantity is provided in grams.
"""
food = db.query(Food).filter(Food.id == food_id).first()
if not food:

View File

@@ -276,4 +276,8 @@ async def root(request: Request):
@app.get("/test")
async def test_route():
logging.info("DEBUG: Test route called")
# Add a test route to check template inheritance
@app.get("/test_template", response_class=HTMLResponse)
async def test_template(request: Request):
return templates.TemplateResponse("test_template.html", {"request": request, "person": "Sarah"})
return {"status": "success", "message": "Test route is working"}

320
standardization.md Normal file
View File

@@ -0,0 +1,320 @@
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

View File

@@ -166,7 +166,7 @@
</td>
<td>
<div class="serving-info">
{{ "%.1f"|format(meal_food.num_servings) }} × {{ meal_food.serving_size }}{{ meal_food.serving_unit }} ({{ "%.0f"|format(meal_food.total_grams) }}g)
{{ "%.0f"|format(meal_food.total_grams) }}g ({{ "%.2f"|format(meal_food.num_servings) }} servings of {{ meal_food.serving_size }}{{ meal_food.serving_unit }})
</div>
</td>
<td class="nutrient-value">{{ "%.0f"|format(meal_food.calories) }}</td>

View File

@@ -158,7 +158,7 @@
</td>
<td>
<div class="serving-info">
{{ "%.1f"|format(meal_food.num_servings) }} × {{ meal_food.serving_size }}{{ meal_food.serving_unit }} ({{ "%.0f"|format(meal_food.total_grams) }}g)
{{ "%.0f"|format(meal_food.total_grams) }}g ({{ "%.2f"|format(meal_food.num_servings) }} servings of {{ meal_food.serving_size }}{{ meal_food.serving_unit }})
</div>
</td>
<td class="nutrient-value">{{ "%.0f"|format(meal_food.calories) }}</td>

View File

@@ -26,7 +26,7 @@
{% if meal.meal_foods %}
<ul class="list-unstyled">
{% for meal_food in meal.meal_foods %}
<li>{{ "%.2f" | format(meal_food.quantity) }}g × {{ meal_food.food.name }}</li>
<li>{{ meal_food.quantity }}g of {{ meal_food.food.name }} ({{ (meal_food.quantity / meal_food.food.serving_size)|round(2) }} servings of {{ meal_food.food.serving_size }}{{ meal_food.food.serving_unit }})</li>
{% endfor %}
</ul>
{% else %}

View File

@@ -23,8 +23,8 @@
</select>
</div>
<div class="mb-3">
<label class="form-label">Quantity</label>
<input type="number" step="0.01" class="form-control" name="quantity" value="1" required>
<label class="form-label">Quantity (g)</label>
<input type="number" step="0.01" class="form-control" name="quantity" value="100" placeholder="Enter weight in grams" required>
</div>
<button type="button" class="btn btn-success" onclick="addFoodToTrackedMeal()">Add Food</button>
</form>

View File

@@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<h1>Test Template</h1>
<p>This is a test template to check if inheritance works.</p>
{% endblock %}

View File

@@ -10,7 +10,7 @@ def sample_chart_data(db_session):
# Create sample food
food = Food(
name="Sample Food",
serving_size="100g",
serving_size=100.0,
serving_unit="g",
calories=100.0,
protein=10.0,
@@ -35,7 +35,7 @@ def sample_chart_data(db_session):
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=1.0
quantity=100.0
)
db_session.add(meal_food)
db_session.commit()

View File

@@ -25,11 +25,9 @@ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_
def session_fixture():
Base.metadata.create_all(bind=test_engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=test_engine)
yield db
db.close()
Base.metadata.drop_all(bind=test_engine)
@pytest.fixture(name="client")
def client_fixture(session):
@@ -50,7 +48,7 @@ def test_detailed_page_default_date(client, session):
# Create mock data for today
food = Food(
name="Apple",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=52,
protein=0.3,
@@ -71,7 +69,7 @@ def test_detailed_page_default_date(client, session):
session.commit()
session.refresh(meal)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=100.0)
session.add(meal_food)
session.commit()
@@ -84,7 +82,7 @@ def test_detailed_page_default_date(client, session):
response = client.get("/detailed?person=Sarah")
assert response.status_code == 200
# Check for the unescaped version or the page title
assert "Detailed Plan for" in response.text
assert "Detailed Plan for Sarah" in response.text
assert test_date.strftime('%B %d, %Y') in response.text
assert "Fruit Snack" in response.text
@@ -93,7 +91,7 @@ def test_detailed_page_with_plan_date(client, session):
# Create mock data
food = Food(
name="Apple",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=52,
protein=0.3,
@@ -114,7 +112,7 @@ def test_detailed_page_with_plan_date(client, session):
session.commit()
session.refresh(meal)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=100.0)
session.add(meal_food)
session.commit()
@@ -126,7 +124,7 @@ def test_detailed_page_with_plan_date(client, session):
response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}")
assert response.status_code == 200
# Check for the page content without assuming apostrophe encoding
assert "Detailed Plan for" in response.text
assert "Detailed Plan for Sarah" in response.text
assert "Fruit Snack" in response.text
@@ -134,7 +132,7 @@ def test_detailed_page_with_template_id(client, session):
# Create mock data
food = Food(
name="Banana",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=89,
protein=1.1,
@@ -155,7 +153,7 @@ def test_detailed_page_with_template_id(client, session):
session.commit()
session.refresh(meal)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=100.0)
session.add(meal_food)
session.commit()
@@ -177,7 +175,7 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
# Create mock data for a tracked day
food1 = Food(
name="Chicken Breast",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=165, protein=31, carbs=0, fat=3.6,
fiber=0, sugar=0, sodium=74, calcium=11,
@@ -185,7 +183,7 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
)
food2 = Food(
name="Broccoli",
serving_size="100",
serving_size=100.0,
serving_unit="g",
calories=55, protein=3.7, carbs=11.2, fat=0.6,
fiber=5.1, sugar=2.2, sodium=33, calcium=47,
@@ -201,8 +199,8 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
session.commit()
session.refresh(meal)
meal_food1 = MealFood(meal_id=meal.id, food_id=food1.id, quantity=1.5) # 150g chicken
meal_food2 = MealFood(meal_id=meal.id, food_id=food2.id, quantity=2.0) # 200g broccoli
meal_food1 = MealFood(meal_id=meal.id, food_id=food1.id, quantity=150.0) # 150g chicken
meal_food2 = MealFood(meal_id=meal.id, food_id=food2.id, quantity=200.0) # 200g broccoli
session.add_all([meal_food1, meal_food2])
session.commit()
@@ -230,8 +228,8 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
assert "Chicken and Broccoli" in response.text
assert "Chicken Breast" in response.text
assert "Broccoli" in response.text
assert "1.5 × 100g" in response.text # Check quantity and unit for chicken
assert "2.0 × 100g" in response.text # Check quantity and unit for broccoli
assert "150.0g of Chicken Breast (1.5 servings of 100.0g)" in response.text
assert "200.0g of Broccoli (2.0 servings of 100.0g)" in response.text
assert "248" in response.text # Check calories for chicken (1.5 * 165 = 247.5, rounded to 248)
assert "110" in response.text # Check calories for broccoli (2.0 * 55 = 110)
@@ -240,7 +238,7 @@ def test_detailed_page_with_invalid_plan_date(client):
response = client.get(f"/detailed?person=Sarah&plan_date={invalid_date.isoformat()}")
assert response.status_code == 200
# Check for content that indicates empty plan
assert "Detailed Plan for" in response.text
assert "Detailed Plan for Sarah" in response.text
assert "No meals planned for this day." in response.text

View File

@@ -107,8 +107,8 @@ def test_edit_tracked_meal_with_override_flow(client: TestClient, session: Testi
# Prepare update data: update food1 quantity (should create a TrackedMealFood and delete original MealFood)
updated_foods_data = [
{"id": original_meal_food1.id, "food_id": food1.id, "quantity": 175.0, "is_custom": False}, # Original MealFood, but quantity changed
{"id": None, "food_id": food2.id, "quantity": 100.0, "is_custom": False} # Unchanged original MealFood
{"id": original_meal_food1.id, "food_id": food1.id, "grams": 175.0, "is_custom": False}, # Original MealFood, but quantity changed
{"id": None, "food_id": food2.id, "grams": 100.0, "is_custom": False} # Unchanged original MealFood
]
response_update = client.post(
@@ -169,8 +169,8 @@ def test_update_tracked_meal_foods_endpoint(client: TestClient, session: Testing
# Prepare update data
updated_foods = [
{"id": tracked_meal_food1.id, "food_id": food1.id, "quantity": 200.0, "is_custom": True},
{"id": None, "food_id": food2.id, "quantity": 50.0, "is_custom": False} # This represents original meal food
{"id": tracked_meal_food1.id, "food_id": food1.id, "grams": 200.0, "is_custom": True},
{"id": None, "food_id": food2.id, "grams": 50.0, "is_custom": False} # This represents original meal food
]
response = client.post(
@@ -210,7 +210,7 @@ def test_add_food_to_tracked_meal_endpoint(client: TestClient, session: TestingS
json={
"tracked_meal_id": tracked_meal.id,
"food_id": food3.id,
"quantity": 200
"grams": 200
}
)
assert response.status_code == 200

View File

@@ -1,8 +1,11 @@
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from app.database import calculate_multiplier_from_grams
from sqlalchemy.orm import sessionmaker
from app.database import Base, get_db, Food, Meal, MealFood
from app.database import TrackedMealFood
from app.database import TrackedDay, TrackedMeal
from main import app
# Setup a test database
@@ -47,19 +50,19 @@ def sample_food_50g(session):
def test_convert_grams_to_quantity_100g_food(session, sample_food_100g):
"""Test convert_grams_to_quantity for a 100g serving size food"""
grams = 150.0
quantity = session.convert_grams_to_quantity(sample_food_100g.id, grams, session)
quantity = calculate_multiplier_from_grams(sample_food_100g.id, grams, session)
assert quantity == 1.5
def test_convert_grams_to_quantity_50g_food(session, sample_food_50g):
"""Test convert_grams_to_quantity for a 50g serving size food"""
grams = 125.0
quantity = session.convert_grams_to_quantity(sample_food_50g.id, grams, session)
quantity = calculate_multiplier_from_grams(sample_food_50g.id, grams, session)
assert quantity == 2.5
def test_convert_grams_to_quantity_invalid_food_id(session):
"""Test convert_grams_to_quantity with an invalid food ID"""
with pytest.raises(ValueError, match="Food with ID 999 not found."):
session.convert_grams_to_quantity(999, 100.0, session)
calculate_multiplier_from_grams(999, 100.0, session)
def test_convert_grams_to_quantity_zero_serving_size(session):
"""Test convert_grams_to_quantity with zero serving size"""
@@ -68,7 +71,7 @@ def test_convert_grams_to_quantity_zero_serving_size(session):
session.commit()
session.refresh(food)
with pytest.raises(ValueError, match="Serving size for food ID .* cannot be zero."):
session.convert_grams_to_quantity(food.id, 100.0, session)
calculate_multiplier_from_grams(food.id, 100.0, session)
def test_add_food_to_meal_grams_input(client, session, sample_food_100g):
"""Test adding food to a meal with grams input"""
@@ -79,14 +82,14 @@ def test_add_food_to_meal_grams_input(client, session, sample_food_100g):
response = client.post(
f"/meals/{meal.id}/add_food",
data={"food_id": sample_food_100g.id, "quantity": 250.0} # 250 grams
data={"food_id": sample_food_100g.id, "grams": 250.0} # 250 grams
)
assert response.status_code == 200
assert response.json()["status"] == "success"
meal_food = session.query(MealFood).filter(MealFood.meal_id == meal.id).first()
assert meal_food.food_id == sample_food_100g.id
assert meal_food.quantity == 2.5 # 250g / 100g serving = 2.5 multiplier
assert meal_food.quantity == 250.0
def test_update_meal_food_quantity_grams_input(client, session, sample_food_50g):
"""Test updating meal food quantity with grams input"""
@@ -97,8 +100,7 @@ def test_update_meal_food_quantity_grams_input(client, session, sample_food_50g)
# Add initial food with 100g (2.0 multiplier for 50g serving)
initial_grams = 100.0
initial_quantity = session.convert_grams_to_quantity(sample_food_50g.id, initial_grams, session)
meal_food = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=initial_quantity)
meal_food = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=initial_grams)
session.add(meal_food)
session.commit()
session.refresh(meal_food)
@@ -106,14 +108,13 @@ def test_update_meal_food_quantity_grams_input(client, session, sample_food_50g)
updated_grams = 150.0
response = client.post(
"/meals/update_food_quantity",
data={"meal_food_id": meal_food.id, "quantity": updated_grams}
data={"meal_food_id": meal_food.id, "grams": updated_grams}
)
assert response.status_code == 200
assert response.json()["status"] == "success"
session.refresh(meal_food)
expected_quantity = session.convert_grams_to_quantity(sample_food_50g.id, updated_grams, session)
assert meal_food.quantity == expected_quantity
assert meal_food.quantity == updated_grams
# Test for bulk_upload_meals would require creating a mock UploadFile and CSV content
# This is more complex and might be deferred or tested manually if the tool's capabilities are limited.
@@ -131,7 +132,7 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
"person": person,
"date": date_str,
"food_id": sample_food_100g.id,
"quantity": grams, # 75 grams
"grams": grams, # 75 grams
"meal_time": "Breakfast"
}
)
@@ -142,7 +143,7 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
tracked_meal = session.query(Meal).filter(Meal.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 == 0.75 # 75g / 100g serving = 0.75 multiplier
assert meal_food.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"""
@@ -150,7 +151,8 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
date_str = "2023-01-02"
# Create a tracked day and meal
tracked_day = TrackedDay(person=person, date="2023-01-02", is_modified=False)
from datetime import date
tracked_day = TrackedDay(person=person, date=date(2023, 1, 2), is_modified=False)
session.add(tracked_day)
session.commit()
session.refresh(tracked_day)
@@ -166,8 +168,8 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
session.refresh(tracked_meal)
# Add initial foods
meal_food_100g = MealFood(meal_id=meal.id, food_id=sample_food_100g.id, quantity=1.0) # 100g
meal_food_50g = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=2.0) # 100g
meal_food_100g = MealFood(meal_id=meal.id, food_id=sample_food_100g.id, quantity=100.0) # 100g
meal_food_50g = MealFood(meal_id=meal.id, food_id=sample_food_50g.id, quantity=100.0) # 100g
session.add_all([meal_food_100g, meal_food_50g])
session.commit()
session.refresh(meal_food_100g)
@@ -175,8 +177,8 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
# Update quantities: 100g food to 200g, 50g food to 75g
updated_foods_data = [
{"id": meal_food_100g.id, "food_id": sample_food_100g.id, "quantity": 200.0, "is_custom": False},
{"id": meal_food_50g.id, "food_id": sample_food_50g.id, "quantity": 75.0, "is_custom": False}
{"id": meal_food_100g.id, "food_id": sample_food_100g.id, "grams": 200.0, "is_custom": False},
{"id": meal_food_50g.id, "food_id": sample_food_50g.id, "grams": 75.0, "is_custom": False}
]
response = client.post(
@@ -194,10 +196,10 @@ def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == sample_food_100g.id
).first()
assert tracked_food_100g.quantity == 2.0 # 200g / 100g serving = 2.0
assert tracked_food_100g.quantity == 200.0
tracked_food_50g = session.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == sample_food_50g.id
).first()
assert tracked_food_50g.quantity == 1.5 # 75g / 50g serving = 1.5
assert tracked_food_50g.quantity == 75.0

View File

@@ -92,7 +92,7 @@ class TestMealFoods:
"""Test POST /meals/{meal_id}/add_food"""
response = client.post(f"/meals/{sample_meal.id}/add_food", data={
"food_id": sample_food.id,
"quantity": 2.5
"grams": 250
})
assert response.status_code == 200
data = response.json()

View File

@@ -0,0 +1,196 @@
import pytest
from app.database import (
calculate_meal_nutrition,
Food,
Meal,
MealFood,
)
def 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
def test_zero_serving_size_handling(db_session):
"""Test handling of zero serving_size - should not divide by zero"""
food = Food(
name="Test Food Zero Serving",
serving_size=0.0,
serving_unit="g",
calories=100.0
)
db_session.add(food)
db_session.commit()
meal = Meal(name="Test Meal Zero")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=100.0
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
# Multiplier should be 0, so no nutrition added
assert nutrition['calories'] == 0.0
def test_small_gram_values(db_session):
"""Test very small gram values (e.g., 0.1g)"""
food = Food(
name="Test Food Small",
serving_size=100.0,
serving_unit="g",
calories=100.0
)
db_session.add(food)
db_session.commit()
meal = Meal(name="Test Meal Small")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=0.1 # Very small amount
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
# Should be 0.001x multiplier
assert nutrition['calories'] == 0.1
def test_large_gram_values(db_session):
"""Test large gram values (e.g., 10000g)"""
food = Food(
name="Test Food Large",
serving_size=100.0,
serving_unit="g",
calories=100.0
)
db_session.add(food)
db_session.commit()
meal = Meal(name="Test Meal Large")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=food.id,
quantity=10000.0 # Very large amount
)
db_session.add(meal_food)
db_session.commit()
nutrition = calculate_meal_nutrition(meal, db_session)
# Should be 100x multiplier
assert nutrition['calories'] == 10000.0
def test_invalid_serving_size(db_session):
"""Test invalid/non-numeric serving_size values"""
# First create a valid food to test with
valid_food = Food(
name="Test Food Valid",
serving_size=100.0,
serving_unit="g",
calories=100.0
)
db_session.add(valid_food)
db_session.commit()
# Now create a meal and add the valid food
meal = Meal(name="Test Meal Valid")
db_session.add(meal)
db_session.commit()
meal_food = MealFood(
meal_id=meal.id,
food_id=valid_food.id,
quantity=100.0
)
db_session.add(meal_food)
db_session.commit()
# Test that the calculation works with valid serving_size
nutrition = calculate_meal_nutrition(meal, db_session)
assert nutrition['calories'] == 100.0
# Now test with None serving_size by updating the food
valid_food.serving_size = None
db_session.commit()
# Recalculate - should handle None gracefully
nutrition = calculate_meal_nutrition(meal, db_session)
assert nutrition['calories'] == 0

View File

@@ -280,7 +280,7 @@ class TestTrackerEdit:
# Update the food quantity via API
response = client.post("/tracker/update_tracked_food", json={
"tracked_food_id": tracked_food.id,
"quantity": 3.0,
"grams": 300.0,
"is_custom": True
})
assert response.status_code == 200
@@ -290,7 +290,7 @@ class TestTrackerEdit:
# Verify the update
db_session.commit()
updated_food = db_session.query(TrackedMealFood).get(tracked_food.id)
assert updated_food.quantity == 3.0
assert updated_food.quantity == 300.0
assert updated_food.quantity != original_quantity
@@ -332,7 +332,7 @@ class TestTrackerSaveAsNewMeal:
"tracked_meal_id": tracked_meal.id,
"new_meal_name": new_meal_name,
"foods": [
{"food_id": sample_food.id, "quantity": 3.0}
{"food_id": sample_food.id, "grams": 300.0}
]
})
assert response.status_code == 200
@@ -404,7 +404,7 @@ class TestTrackerAddFood:
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": sample_food.id,
"quantity": 150.0,
"grams": 150.0,
"meal_time": "Dinner"
})
assert response.status_code == 200
@@ -422,7 +422,7 @@ class TestTrackerAddFood:
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 == 1.5
assert tracked_meal.meal.meal_foods[0].quantity == 150.0
def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session):
"""
@@ -449,7 +449,7 @@ class TestTrackerAddFood:
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": food.id,
"quantity": grams_to_add,
"grams": grams_to_add,
"meal_time": "Snack 1"
})
assert response.status_code == 200
@@ -469,7 +469,7 @@ class TestTrackerAddFood:
# 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 == expected_servings
assert tracked_meal.meal.meal_foods[0].quantity == grams_to_add
# Verify nutrition calculation
day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session)