fixing meal edit on teampltes page

This commit is contained in:
2025-10-02 08:22:40 -07:00
parent 342eceff1f
commit ecd8c375f7
18 changed files with 636 additions and 1458 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
.kilocode/
*.pyc
__pycache__/
data/

View File

@@ -1,11 +1,17 @@
from logging.config import fileConfig
import logging
import os
import sys
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# Add the project root to the Python path
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
from app.database import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
@@ -17,9 +23,7 @@ if config.config_file_name is not None:
# add your model's MetaData object here
# for 'autogenerate' support
# We create an empty metadata object since we're not using autogenerate
# and we have explicit migration files
target_metadata = None
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
@@ -43,7 +47,7 @@ def run_migrations_offline() -> None:
url = os.getenv('DATABASE_URL', config.get_main_option("sqlalchemy.url"))
context.configure(
url=url,
target_metadata=None,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
@@ -72,7 +76,7 @@ def run_migrations_online() -> None:
with connectable.connect() as connection:
logging.info("DEBUG: Database connection established for alembic")
context.configure(
connection=connection, target_metadata=None
connection=connection, target_metadata=target_metadata
)
logging.info("DEBUG: Alembic context configured")

View File

@@ -0,0 +1,30 @@
"""Add is_deleted to TrackedMealFood
Revision ID: 2498205b9e48
Revises: d0c142fbf0b0
Create Date: 2025-10-02 13:22:15.674346
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '2498205b9e48'
down_revision: Union[str, None] = 'd0c142fbf0b0'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tracked_meal_foods', sa.Column('is_deleted', sa.Boolean(), nullable=True, server_default='0'))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('tracked_meal_foods', 'is_deleted')
# ### end Alembic commands ###

View File

@@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session, joinedload
from datetime import date, datetime, timedelta
import logging
from typing import List, Optional, Union
# Import from the database module
@@ -14,8 +13,7 @@ router = APIRouter()
# Tracker tab - Main page
@router.get("/tracker", response_class=HTMLResponse)
async def tracker_page(request: Request, person: str = "Sarah", date: str = None, db: Session = Depends(get_db)):
logging.info(f"DEBUG: Tracker page requested with person={person}, date={date}")
try:
from datetime import datetime, timedelta
# If no date provided, use today
@@ -40,11 +38,14 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
db.add(tracked_day)
db.commit()
db.refresh(tracked_day)
logging.info(f"DEBUG: Created new tracked day for {person} on {current_date}")
# Get tracked meals for this day with eager loading of meal foods
tracked_meals = db.query(TrackedMeal).options(
joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food)
joinedload(TrackedMeal.meal)
.joinedload(Meal.meal_foods)
.joinedload(MealFood.food),
joinedload(TrackedMeal.tracked_foods)
.joinedload(TrackedMealFood.food)
).filter(
TrackedMeal.tracked_day_id == tracked_day.id
).all()
@@ -61,8 +62,6 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
# Calculate day totals
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
logging.info(f"DEBUG: Rendering tracker page with {len(tracked_meals)} tracked meals")
return templates.TemplateResponse("tracker.html", {
"request": request,
"person": person,
@@ -77,6 +76,15 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
"foods": foods
})
except Exception as e:
# Return a detailed error page instead of generic Internal Server Error
return templates.TemplateResponse("error.html", {
"request": request,
"error_title": "Error Loading Tracker",
"error_message": f"An error occurred while loading the tracker page: {str(e)}",
"error_details": f"Person: {person}, Date: {date}"
}, status_code=500)
# Tracker API Routes
@router.post("/tracker/add_meal")
async def tracker_add_meal(request: Request, db: Session = Depends(get_db)):
@@ -88,7 +96,6 @@ async def tracker_add_meal(request: Request, db: Session = Depends(get_db)):
meal_id = form_data.get("meal_id")
meal_time = form_data.get("meal_time")
logging.info(f"DEBUG: Adding meal to tracker - person={person}, date={date_str}, meal_id={meal_id}, meal_time={meal_time}")
# Parse date
from datetime import datetime
@@ -119,19 +126,16 @@ async def tracker_add_meal(request: Request, db: Session = Depends(get_db)):
db.commit()
logging.info(f"DEBUG: Successfully added meal to tracker")
return {"status": "success"}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error adding meal to tracker: {e}")
return {"status": "error", "message": str(e)}
@router.delete("/tracker/remove_meal/{tracked_meal_id}")
async def tracker_remove_meal(tracked_meal_id: int, db: Session = Depends(get_db)):
"""Remove a meal from the tracker"""
try:
logging.info(f"DEBUG: Removing tracked meal with ID: {tracked_meal_id}")
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first()
if not tracked_meal:
@@ -144,12 +148,10 @@ async def tracker_remove_meal(tracked_meal_id: int, db: Session = Depends(get_db
db.delete(tracked_meal)
db.commit()
logging.info(f"DEBUG: Successfully removed tracked meal")
return {"status": "success"}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error removing tracked meal: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/save_template")
@@ -164,7 +166,6 @@ async def tracker_save_template(request: Request, db: Session = Depends(get_db))
if not all([person, date_str, template_name]):
raise HTTPException(status_code=400, detail="Missing required form data.")
logging.info(f"debug: saving template - name={template_name}, person={person}, date={date_str}")
# 1. Check if template name already exists
existing_template = db.query(Template).filter(Template.name == template_name).first()
@@ -202,12 +203,10 @@ async def tracker_save_template(request: Request, db: Session = Depends(get_db))
db.add(template_meal_entry)
db.commit()
logging.info(f"debug: successfully saved template '{template_name}' with {len(tracked_meals)} meals.")
return {"status": "success", "message": "Template saved successfully."}
except Exception as e:
db.rollback()
logging.error(f"debug: error saving template: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/apply_template")
@@ -219,7 +218,6 @@ async def tracker_apply_template(request: Request, db: Session = Depends(get_db)
date_str = form_data.get("date")
template_id = form_data.get("template_id")
logging.info(f"DEBUG: Applying template - template_id={template_id}, person={person}, date={date_str}")
# Parse date
from datetime import datetime
@@ -267,12 +265,10 @@ async def tracker_apply_template(request: Request, db: Session = Depends(get_db)
db.commit()
logging.info(f"DEBUG: Successfully applied template with {len(template_meals)} meals")
return {"status": "success"}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error applying template: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/update_tracked_food")
@@ -283,7 +279,6 @@ async def update_tracked_food(request: Request, data: dict = Body(...), db: Sess
grams = float(data.get("grams", 1.0))
is_custom = data.get("is_custom", False)
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()
@@ -319,12 +314,10 @@ async def update_tracked_food(request: Request, data: dict = Body(...), db: Sess
db.commit()
logging.info(f"DEBUG: Successfully updated tracked food grams")
return {"status": "success"}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error updating tracked food: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/reset_to_plan")
@@ -335,7 +328,6 @@ async def tracker_reset_to_plan(request: Request, db: Session = Depends(get_db))
person = form_data.get("person")
date_str = form_data.get("date")
logging.info(f"DEBUG: Resetting to plan - person={person}, date={date_str}")
# Parse date
from datetime import datetime
@@ -360,59 +352,64 @@ async def tracker_reset_to_plan(request: Request, db: Session = Depends(get_db))
db.commit()
logging.info(f"DEBUG: Successfully reset to plan")
return {"status": "success"}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error resetting to plan: {e}")
return {"status": "error", "message": str(e)}
@router.get("/tracker/get_tracked_meal_foods/{tracked_meal_id}")
async def get_tracked_meal_foods(tracked_meal_id: int, db: Session = Depends(get_db)):
"""Get foods associated with a tracked meal"""
logging.info(f"DEBUG: get_tracked_meal_foods called for tracked_meal_id: {tracked_meal_id}")
try:
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first()
logging.info(f"DEBUG: Tracked meal found: {tracked_meal.id if tracked_meal else 'None'}")
if not tracked_meal:
raise HTTPException(status_code=404, detail="Tracked meal not found")
# Load the associated Meal and its foods
meal = db.query(Meal).options(joinedload(Meal.meal_foods).joinedload(MealFood.food)).filter(Meal.id == tracked_meal.meal_id).first()
logging.info(f"DEBUG: Associated meal found: {meal.id if meal else 'None'}")
if not meal:
raise HTTPException(status_code=404, detail="Associated meal not found")
# Load custom tracked foods for this tracked meal
tracked_foods = db.query(TrackedMealFood).options(joinedload(TrackedMealFood.food)).filter(TrackedMealFood.tracked_meal_id == tracked_meal_id).all()
logging.info(f"DEBUG: Found {len(tracked_foods)} custom tracked foods.")
# Combine foods from the base meal and custom tracked foods, handling overrides
# New override-based logic
meal_foods_data = []
base_foods = {mf.food_id: mf for mf in meal.meal_foods}
overrides = {tf.food_id: tf for tf in tracked_foods}
# Keep track of food_ids that have been overridden by TrackedMealFood entries
# These should not be added from the base meal definition
overridden_food_ids = {tf.food_id for tf in tracked_foods}
logging.info(f"DEBUG: Overridden food IDs: {overridden_food_ids}")
for meal_food in meal.meal_foods:
# Only add meal_food if it hasn't been overridden by a TrackedMealFood
if meal_food.food_id not in overridden_food_ids:
# 1. Handle base meal foods, applying overrides where they exist
for food_id, base_meal_food in base_foods.items():
if food_id in overrides:
override_food = overrides[food_id]
if not override_food.is_deleted:
# This food is overridden, use the override's data
meal_foods_data.append({
"id": meal_food.id,
"food_id": meal_food.food.id,
"food_name": meal_food.food.name,
"quantity": meal_food.quantity,
"serving_unit": meal_food.food.serving_unit,
"serving_size": meal_food.food.serving_size,
"id": override_food.id,
"food_id": override_food.food.id,
"food_name": override_food.food.name,
"quantity": override_food.quantity,
"serving_unit": override_food.food.serving_unit,
"serving_size": override_food.food.serving_size,
"is_custom": True # It's an override, so treat as custom
})
else:
# No override exists, use the base meal food data
meal_foods_data.append({
"id": base_meal_food.id,
"food_id": base_meal_food.food.id,
"food_name": base_meal_food.food.name,
"quantity": base_meal_food.quantity,
"serving_unit": base_meal_food.food.serving_unit,
"serving_size": base_meal_food.food.serving_size,
"is_custom": False
})
logging.info(f"DEBUG: Added {len(meal_foods_data)} meal foods (excluding overridden).")
for tracked_food in tracked_foods:
# 2. Add new foods that are not in the base meal
for food_id, tracked_food in overrides.items():
if food_id not in base_foods and not tracked_food.is_deleted:
meal_foods_data.append({
"id": tracked_food.id,
"food_id": tracked_food.food.id,
@@ -422,21 +419,17 @@ async def get_tracked_meal_foods(tracked_meal_id: int, db: Session = Depends(get
"serving_size": tracked_food.food.serving_size,
"is_custom": True
})
logging.info(f"DEBUG: Added {len(tracked_foods)} custom tracked foods.")
logging.info(f"DEBUG: Total meal foods data items: {len(meal_foods_data)}")
return {"status": "success", "meal_foods": meal_foods_data}
except HTTPException as he:
logging.error(f"DEBUG: HTTP Error getting tracked meal foods: {he.detail}")
return {"status": "error", "message": he.detail}
except Exception as e:
logging.error(f"DEBUG: Error getting tracked meal foods: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/add_food_to_tracked_meal")
async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends(get_db)):
"""Add a food to an existing tracked meal"""
"""Add a food to an existing tracked meal by creating a TrackedMealFood entry."""
try:
tracked_meal_id = data.get("tracked_meal_id")
food_id = data.get("food_id")
@@ -450,13 +443,14 @@ async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends
if not food:
raise HTTPException(status_code=404, detail="Food not found")
# Create a new MealFood entry for the tracked meal's associated meal
meal_food = MealFood(
meal_id=tracked_meal.meal_id,
# Create a new TrackedMealFood entry to associate the food with the tracked meal
tracked_meal_food = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
food_id=food_id,
quantity=grams
quantity=grams,
is_override=False # This is a new addition, not an override
)
db.add(meal_food)
db.add(tracked_meal_food)
# Mark the tracked day as modified
tracked_meal.tracked_day.is_modified = True
@@ -466,92 +460,77 @@ async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends
except HTTPException as he:
db.rollback()
logging.error(f"DEBUG: HTTP Error adding food to tracked meal: {he.detail}")
return {"status": "error", "message": he.detail}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error adding food to tracked meal: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/update_tracked_meal_foods")
async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depends(get_db)):
"""Update quantities of multiple foods in a tracked meal"""
logging.info(f"DEBUG: update_tracked_meal_foods called for tracked_meal_id: {data.get('tracked_meal_id')}")
"""Update, add, or remove foods from a tracked meal using an override system."""
try:
tracked_meal_id = data.get("tracked_meal_id")
foods_data = data.get("foods", [])
logging.info(f"DEBUG: Foods data received: {foods_data}")
removed_food_ids = data.get("removed_food_ids", [])
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first()
logging.info(f"DEBUG: Tracked meal found: {tracked_meal.id if tracked_meal else 'None'}")
if not tracked_meal:
raise HTTPException(status_code=404, detail="Tracked meal not found")
# Process removals: mark existing foods as deleted
for food_id_to_remove in removed_food_ids:
# Check if an override already exists
override = db.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal_id,
TrackedMealFood.food_id == food_id_to_remove
).first()
if override:
override.is_deleted = True
else:
# If no override exists, create one to mark the food as deleted
new_override = TrackedMealFood(
tracked_meal_id=tracked_meal_id,
food_id=food_id_to_remove,
quantity=0, # Quantity is irrelevant for a deleted item
is_override=True,
is_deleted=True
)
db.add(new_override)
# Process updates and additions
for food_data in foods_data:
food_id = food_data.get("food_id")
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}, grams: {grams}, is_custom: {is_custom}, item_id: {item_id}")
print(f" Processing food_id {food_id} with grams {grams}")
if is_custom:
tracked_food = db.query(TrackedMealFood).filter(TrackedMealFood.id == item_id).first()
if tracked_food:
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=grams
)
db.add(new_tracked_food)
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)
# Or if it's still a MealFood
existing_tracked_food = db.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
# Check if an override entry already exists for this food
existing_override = db.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal_id,
TrackedMealFood.food_id == food_id
).first()
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 = float(grams)
logging.info(f"DEBUG: Updated existing TrackedMealFood {existing_tracked_food.id} (override) to grams {grams}")
if existing_override:
# If an override exists, update its quantity and ensure it's not marked as deleted
print(f" Found existing override for food_id {food_id}. Updating quantity to {grams}.")
existing_override.quantity = grams
existing_override.is_deleted = False
else:
# If it's not a TrackedMealFood, it must be a MealFood
meal_food = db.query(MealFood).filter(
# If no override exists, it's either a modification of a base food or a new addition
print(f" No existing override for food_id {food_id}. Creating new entry.")
base_meal_food = db.query(MealFood).filter(
MealFood.meal_id == tracked_meal.meal_id,
MealFood.food_id == food_id
).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 grams changed, convert to TrackedMealFood
if meal_food.quantity != grams:
new_tracked_food = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
is_override = base_meal_food is not None
print(f" Is this an override of a base meal food? {is_override}")
new_entry = TrackedMealFood(
tracked_meal_id=tracked_meal_id,
food_id=food_id,
quantity=float(grams),
is_override=True
quantity=grams,
is_override=is_override
)
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 grams {grams} and deleted original MealFood.")
else:
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=float(grams)
)
db.add(new_tracked_food)
logging.warning(f"DEBUG: Fallback: Added new TrackedMealFood for food_id {food_id} with grams {grams}. Original MealFood not found.")
db.add(new_entry)
# Mark the tracked day as modified
tracked_meal.tracked_day.is_modified = True
@@ -561,66 +540,12 @@ async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depend
except HTTPException as he:
db.rollback()
logging.error(f"DEBUG: HTTP Error updating tracked meal foods: {he.detail}")
return {"status": "error", "message": he.detail}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error updating tracked meal foods: {e}")
return {"status": "error", "message": str(e)}
@router.delete("/tracker/remove_food_from_tracked_meal/{meal_food_id}")
async def remove_food_from_tracked_meal(meal_food_id: int, db: Session = Depends(get_db)):
"""Remove a food from a tracked meal"""
try:
meal_food = db.query(MealFood).filter(MealFood.id == meal_food_id).first()
if not meal_food:
raise HTTPException(status_code=404, detail="Meal food not found")
# Mark the tracked day as modified
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.meal_id == meal_food.meal_id).first()
if tracked_meal:
tracked_meal.tracked_day.is_modified = True
db.delete(meal_food)
db.commit()
return {"status": "success"}
except HTTPException as he:
db.rollback()
logging.error(f"DEBUG: HTTP Error removing food from tracked meal: {he.detail}")
return {"status": "error", "message": he.detail}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error removing food from tracked meal: {e}")
return {"status": "error", "message": str(e)}
@router.delete("/tracker/remove_custom_food_from_tracked_meal/{tracked_meal_food_id}")
async def remove_custom_food_from_tracked_meal(tracked_meal_food_id: int, db: Session = Depends(get_db)):
"""Remove a custom food from a tracked meal"""
try:
tracked_meal_food = db.query(TrackedMealFood).filter(TrackedMealFood.id == tracked_meal_food_id).first()
if not tracked_meal_food:
raise HTTPException(status_code=404, detail="Tracked meal food not found")
# Mark the tracked day as modified
tracked_meal = tracked_meal_food.tracked_meal
if tracked_meal:
tracked_meal.tracked_day.is_modified = True
db.delete(tracked_meal_food)
db.commit()
return {"status": "success"}
except HTTPException as he:
db.rollback()
logging.error(f"DEBUG: HTTP Error removing custom food from tracked meal: {he.detail}")
return {"status": "error", "message": he.detail}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error removing custom food from tracked meal: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/save_as_new_meal")
async def save_as_new_meal(data: dict = Body(...), db: Session = Depends(get_db)):
@@ -673,11 +598,9 @@ async def save_as_new_meal(data: dict = Body(...), db: Session = Depends(get_db)
except HTTPException as he:
db.rollback()
logging.error(f"DEBUG: HTTP Error saving as new meal: {he.detail}")
return {"status": "error", "message": he.detail}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error saving as new meal: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/add_food")
@@ -690,8 +613,6 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
grams = float(data.get("quantity", 1.0))
meal_time = data.get("meal_time")
logging.info(f"BUG HUNT: Received raw data: {data}")
logging.info(f"BUG HUNT: Parsed grams: {grams}")
# Parse date
from datetime import datetime
@@ -739,16 +660,13 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
db.commit()
logging.info(f"DEBUG: Successfully added single food to tracker")
return {"status": "success"}
except ValueError as ve:
db.rollback()
logging.error(f"DEBUG: Error adding single food to tracker: {ve}")
return {"status": "error", "message": str(ve)}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error adding single food to tracker: {e}")
return {"status": "error", "message": str(e)}
@router.get("/detailed_tracked_day", response_class=HTMLResponse, name="detailed_tracked_day")
@@ -756,8 +674,7 @@ async def detailed_tracked_day(request: Request, person: str = "Sarah", date: Op
"""
Displays a detailed view of a tracked day, including all meals and their food breakdowns.
"""
logging.info(f"DEBUG: Detailed tracked day page requested with person={person}, date={date}")
try:
# If no date is provided, default to today's date
if not date:
current_date = date.today()
@@ -765,13 +682,12 @@ async def detailed_tracked_day(request: Request, person: str = "Sarah", date: Op
try:
current_date = datetime.fromisoformat(date).date()
except ValueError:
logging.error(f"DEBUG: Invalid date format for date: {date}")
return templates.TemplateResponse("detailed.html", {
"request": request, "title": "Invalid Date",
"error": "Invalid date format. Please use YYYY-MM-DD.",
"day_totals": {},
"person": person
})
return templates.TemplateResponse("error.html", {
"request": request,
"error_title": "Invalid Date Format",
"error_message": "The date format is invalid. Please use YYYY-MM-DD format.",
"error_details": f"Date provided: {date}"
}, status_code=400)
tracked_day = db.query(TrackedDay).filter(
TrackedDay.person == person,
@@ -866,5 +782,13 @@ async def detailed_tracked_day(request: Request, person: str = "Sarah", date: Op
if not meal_details:
context["message"] = "No meals tracked for this day."
logging.info(f"DEBUG: Rendering tracked day details with context: {context}")
return templates.TemplateResponse("detailed_tracked_day.html", context)
except Exception as e:
# Return a detailed error page instead of generic Internal Server Error
return templates.TemplateResponse("error.html", {
"request": request,
"error_title": "Error Loading Detailed View",
"error_message": f"An error occurred while loading the detailed view: {str(e)}",
"error_details": f"Person: {person}, Date: {date}"
}, status_code=500)

View File

@@ -21,6 +21,7 @@ from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from datetime import date, datetime
import os
import logging
# Database setup - Use SQLite for easier setup
# Use environment variables if set, otherwise use defaults
@@ -161,6 +162,7 @@ class TrackedMealFood(Base):
food_id = Column(Integer, ForeignKey("foods.id"))
quantity = Column(Float, default=1.0) # Custom quantity for this tracked instance
is_override = Column(Boolean, default=False) # True if overriding original meal food, False if addition
is_deleted = Column(Boolean, default=False) # True if this food has been deleted from the meal
tracked_meal = relationship("TrackedMeal", back_populates="tracked_foods")
food = relationship("Food")
@@ -401,7 +403,6 @@ def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0,
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
}
# Base meal nutrition
base_nutrition = calculate_meal_nutrition(tracked_meal.meal, db)
for key in totals:

View File

@@ -1,120 +0,0 @@
# Bug Reproduction Test Plan
This document outlines the test case required to reproduce the quantity calculation bug in the "add food" modal.
## Test File
Create a new file at `tests/test_add_food_bug.py`.
## Test Case
The following pytest test should be implemented in `tests/test_add_food_bug.py`. This test will simulate the buggy behavior by creating a food with a non-standard serving size and then asserting that the stored quantity is incorrect when a specific number of "servings" is added via the API.
```python
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.database import Food, Meal, MealFood, TrackedDay, TrackedMeal
from datetime import date
def test_add_food_with_serving_size_multiplier(client: TestClient, db_session: Session):
"""
Simulates the bug where the quantity is a multiple of the serving size.
This test will fail if the bug exists.
"""
# 1. Create a food with a serving size of 30g
food = Food(
name="Test Cracker",
serving_size=30.0,
serving_unit="g",
calories=120,
protein=2,
carbs=25,
fat=2
)
db_session.add(food)
db_session.commit()
# 2. Simulate adding the food via the API
# The user enters "2" in the quantity field, but some faulty client-side
# logic multiplies it by the serving size (2 * 30 = 60) before sending.
# We are simulating the faulty request here.
response = client.post(
"/tracker/add_food",
json={
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": food.id,
"grams": 60.0, # This is what the backend receives
"meal_time": "Snack 1"
}
)
assert response.status_code == 200
assert response.json()["status"] == "success"
# 3. Verify the stored quantity
# Find the MealFood that was just created.
# The bug is that the backend stores 60g, instead of what the user *thought* they entered (2 servings, which should be stored as 60g).
# The user's report is that the quantity is a multiple.
# A correct implementation would just store the grams value.
# This test asserts the buggy behavior to prove it exists.
# The endpoint creates a new Meal and a new TrackedMeal for the single food.
# We need to find the most recently created one.
created_meal = db_session.query(Meal).order_by(Meal.id.desc()).first()
assert created_meal is not None
assert created_meal.name == "Test Cracker"
meal_food = db_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first()
assert meal_food is not None
# This assertion will pass if the bug exists, because the backend is saving the wrong value.
# The goal is to make this test *fail* by fixing the backend logic.
# A correct implementation would require the frontend to always send grams.
# If the user enters "2" servings of a 30g serving size food, the frontend *should* send 60g.
# The bug description is a bit ambiguous. Let's clarify the assertion.
# The user said "the quantity value is a multiple of the serving size not in grams".
# This implies if they enter "60" in the grams field, it might be getting multiplied AGAIN.
# Let's write the test to check for THAT.
# Re-simulating based on a clearer interpretation of the bug report.
# The user enters "60" grams. The faulty logic might be `60 * 30 = 1800`.
# Let's create a more precise test.
# Delete the previous test data to be safe.
db_session.delete(meal_food)
db_session.delete(created_meal)
db_session.commit()
# Re-run with a clearer scenario
response = client.post(
"/tracker/add_food",
json={
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": food.id,
"grams": 2.0, # User wants 2 grams
"meal_time": "Snack 1"
}
)
assert response.status_code == 200
created_meal = db_session.query(Meal).order_by(Meal.id.desc()).first()
meal_food = db_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first()
# The bug is that this is NOT 2.0, but something else.
# Let's assume the bug is `quantity * serving_size`. So `2.0 * 30.0 = 60.0`
# A failing test should assert the expected *correct* value.
assert meal_food.quantity == 2.0, f"Quantity should be 2.0, but was {meal_food.quantity}"
```
## Instructions for Implementation
1. A developer in `code` mode should create the file `tests/test_add_food_bug.py`.
2. The code above should be added to this file.
3. The test should be run using the command from the TDD rules to confirm that it fails as expected, thus reproducing the bug.
```bash
docker compose build; docker compose run --remove-orphans foodtracker pytest tests/test_add_food_bug.py

View File

@@ -1,21 +0,0 @@
--- templates/detailed.html
+++ templates/detailed.html
@@ -1,3 +1,7 @@
+{# Look for the meal details section and ensure it shows food breakdown #}
+{% for meal_detail in meal_details %}
+ {# Existing meal header code... #}
+
+ {# ADD FOOD BREAKDOWN SECTION: #}
+ <div class="food-breakdown">
+ <h4>Food Breakdown:</h4>
+ <ul>
+ {% for food in meal_detail.foods %}
+ <li>{{ food.quantity }}g {{ food.name }}
+ {% if food.serving_size and food.serving_unit %}
+ ({{ food.serving_size }}{{ food.serving_unit }})
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+{% endfor %}

View File

@@ -4,7 +4,7 @@ services:
ports:
- "8999:8999"
environment:
- DATABASE_URL=sqlite:////app/meal_planner.db
- DATABASE_URL=sqlite:////app/data/meal_planner.db
volumes:
- ./alembic:/app/alembic
- ./meal_planner.db:/app/meal_planner.db
- ./data:/app/data

View File

@@ -1,158 +0,0 @@
Fix Detailed View Food Breakdown - Implementation Plan
Problem Statement
The detailed view (/detailed route) is incorrectly calculating and displaying per-food nutrition values:
Display Issue: Shows "34.0 × 34.0g" instead of "34.0g" in the Serving column
Calculation Issue: Multiplies nutrition by quantity directly instead of calculating proper multiplier (quantity ÷ serving_size)
Current incorrect calculation:
python'calories': mf.food.calories * mf.quantity # Wrong: 125cal * 34g = 4250cal
Should be:
pythonmultiplier = mf.quantity / mf.food.serving_size # 34g / 34g = 1.0
'calories': mf.food.calories * multiplier # 125cal * 1.0 = 125cal
Files to Modify
app/api/routes/plans.py - Fix calculation logic in detailed() function
templates/detailed.html - Update serving column display
Implementation Steps
Step 1: Fix Template View Calculation (plans.py)
Location: app/api/routes/plans.py, in the detailed() function around lines 190-220
Find this section (for template meals):
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,
'num_servings': num_servings,
'serving_size': mf.food.serving_size,
'serving_unit': mf.food.serving_unit,
'calories': mf.food.calories * num_servings, # May be wrong
'protein': mf.food.protein * num_servings,
# ... etc
})
Replace with:
pythonfor mf in tm.meal.meal_foods:
try:
serving_size = float(mf.food.serving_size)
multiplier = mf.quantity / serving_size if serving_size > 0 else 0
except (ValueError, TypeError):
multiplier = 0
foods.append({
'name': mf.food.name,
'quantity': mf.quantity, # Grams used in this meal
'serving_unit': mf.food.serving_unit,
# Calculate nutrition for the actual amount used
'calories': (mf.food.calories or 0) * multiplier,
'protein': (mf.food.protein or 0) * multiplier,
'carbs': (mf.food.carbs or 0) * multiplier,
'fat': (mf.food.fat or 0) * multiplier,
'fiber': (mf.food.fiber or 0) * multiplier,
'sodium': (mf.food.sodium or 0) * multiplier,
})
Step 2: Fix Tracked Day View Calculation (plans.py)
Location: Same file, around lines 247-280 (in the tracked meals section)
Find this section:
pythonfor mf in tracked_meal.meal.meal_foods:
foods.append({
'name': mf.food.name,
'quantity': mf.quantity,
'serving_size': mf.food.serving_size,
'serving_unit': mf.food.serving_unit,
})
Replace with (add nutrition calculations):
pythonfor mf in tracked_meal.meal.meal_foods:
try:
serving_size = float(mf.food.serving_size)
multiplier = mf.quantity / serving_size if serving_size > 0 else 0
except (ValueError, TypeError):
multiplier = 0
foods.append({
'name': mf.food.name,
'quantity': mf.quantity,
'serving_unit': mf.food.serving_unit,
'calories': (mf.food.calories or 0) * multiplier,
'protein': (mf.food.protein or 0) * multiplier,
'carbs': (mf.food.carbs or 0) * multiplier,
'fat': (mf.food.fat or 0) * multiplier,
'fiber': (mf.food.fiber or 0) * multiplier,
'sodium': (mf.food.sodium or 0) * multiplier,
})
Step 3: Fix Template Display
Location: templates/detailed.html
Find the Serving column display (likely something like):
html<td>{{ food.total_grams }} × {{ food.serving_size }}{{ food.serving_unit }}</td>
or
html<td>{{ food.quantity }} × {{ food.serving_size }}{{ food.serving_unit }}</td>
Replace with:
html<td>{{ food.quantity }}{{ food.serving_unit }}</td>
This will show "34.0g" instead of "34.0 × 34.0g"
Testing Checklist
After making changes, test these scenarios:
Test 1: Basic Calculation
Food with 100g serving size, 100 calories
Add 50g to meal
Should show: "50g" and "50 calories"
Test 2: Your Current Example
Pea Protein: 34g serving, 125 cal/serving
Add 34g to meal
Should show: "34.0g" and "125 calories"
NOT "4250 calories"
Test 3: Fractional Servings
Food with 100g serving size, 200 calories
Add 150g to meal
Should show: "150g" and "300 calories"
Test 4: Template View
View a template from the detailed page
Verify food breakdown shows correct grams and nutrition
Test 5: Tracked Day View
View a tracked day from the detailed page
Verify food breakdown shows correct grams and nutrition
Code Quality Notes
Why Use Multiplier Pattern?
pythonmultiplier = quantity / serving_size
nutrition_value = base_nutrition * multiplier
This is consistent with:
calculate_meal_nutrition() function
The standardization plan
Makes the math explicit and debuggable
Error Handling
The try/except block handles:
Non-numeric serving_size values
Division by zero
NULL values (though migration confirmed none exist)
Expected Results
Before:
Serving: 34.0 × 34.0g
Calories: 4250
Protein: 952.0g
After:
Serving: 34.0g
Calories: 125
Protein: 28.0g

View File

@@ -1,125 +0,0 @@
#!/usr/bin/env python3
"""
Script to fix the incomplete tracker.py file
"""
def fix_tracker_file():
file_path = "app/api/routes/tracker.py"
with open(file_path, 'r') as f:
content = f.read()
# Check if file is incomplete (ends abruptly)
if content.strip().endswith('@router.post("/tracker/save_template")'):
print("File is incomplete, adding missing content...")
missing_content = '''async def tracker_save_template(request: Request, db: Session = Depends(get_db)):
"""save current day's meals as template"""
try:
form_data = await request.form()
person = form_data.get("person")
date_str = form_data.get("date")
template_name = form_data.get("template_name")
logging.info(f"debug: saving template - name={template_name}, person={person}, date={date_str}")
# Parse date
from datetime import datetime
date = datetime.fromisoformat(date_str).date()
# Get tracked day and meals
tracked_day = db.query(TrackedDay).filter(
TrackedDay.person == person,
TrackedDay.date == date
).first()
if not tracked_day:
return {"status": "error", "message": "No tracked day found"}
tracked_meals = db.query(TrackedMeal).filter(
TrackedMeal.tracked_day_id == tracked_day.id
).all()
if not tracked_meals:
return {"status": "error", "message": "No tracked meals found"}
# Create new template
template = Template(name=template_name)
db.add(template)
db.flush()
# Add meals to template
for tracked_meal in tracked_meals:
template_meal = TemplateMeal(
template_id=template.id,
meal_id=tracked_meal.meal_id,
meal_time=tracked_meal.meal_time
)
db.add(template_meal)
db.commit()
return {"status": "success", "message": "Template saved successfully"}
except Exception as e:
db.rollback()
logging.error(f"debug: error saving template: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/apply_template")
async def tracker_apply_template(request: Request, db: Session = Depends(get_db)):
"""apply template to current day"""
try:
form_data = await request.form()
person = form_data.get("person")
date_str = form_data.get("date")
template_id = form_data.get("template_id")
logging.info(f"debug: applying template - template_id={template_id}, person={person}, date={date_str}")
# Parse date
from datetime import datetime
date = datetime.fromisoformat(date_str).date()
# Get template meals
template_meals = db.query(TemplateMeal).filter(
TemplateMeal.template_id == template_id
).all()
if not template_meals:
return {"status": "error", "message": "Template has no meals"}
# Get or create tracked day
tracked_day = db.query(TrackedDay).filter(
TrackedDay.person == person,
TrackedDay.date == date
).first()
if not tracked_day:
tracked_day = TrackedDay(person=person, date=date, is_modified=True)
db.add(tracked_day)
db.flush()
# Clear existing meals and add template meals
db.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).delete()
for template_meal in template_meals:
tracked_meal = TrackedMeal(
tracked_day_id=tracked_day.id,
meal_id=template_meal.meal_id,
meal_time=template_meal.meal_time
)
db.add(tracked_meal)
tracked_day.is_modified = True
db.commit()
return {"status": "success", "message": "Template applied successfully"}
except Exception as e:
db.rollback()
logging.error(f"debug: error applying template: {e}")
return {"status": "error", "message": str(e)}'''
# Append the missing content
with open(file_path, 'a') as f:
f.write('\n' + missing_content)
print("Tracker.py file fixed successfully!")
else:
print("Tracker.py file appears to be complete")
if __name__ == "__main__":
fix_tracker_file()

View File

@@ -1,160 +0,0 @@
--- app/api/routes/plans.py.orig
+++ app/api/routes/plans.py.fixed
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body
from fastapi.responses import HTMLResponse, RedirectResponse
-from sqlalchemy.orm import Session
+from sqlalchemy.orm import Session, joinedload
from datetime import date, datetime, timedelta
import logging
from typing import List, Optional
@@ -9,7 +9,7 @@
# Import database module
from app.database import get_db, Food, Meal, MealFood, Plan, Template, TemplateMeal, WeeklyMenu, WeeklyMenuDay, TrackedDay, TrackedMeal, calculate_meal_nutrition, calculate_day_nutrition
from main import templates
-
+from app.database import calculate_tracked_meal_nutrition, calculate_day_nutrition_tracked
router = APIRouter()
# Plan tab
@@ -156,6 +156,7 @@
"""render detailed view for a specific day or template"""
from datetime import datetime, date
import logging
+ from sqlalchemy.orm import joinedload
logging.info(f"debug: detailed page requested url: {request.url.path}, query_params: {request.query_params}")
logging.info(f"debug: detailed page requested person={person}, plan_date={plan_date}, template_id={template_id}")
@@ -189,6 +190,20 @@
for tm in template_meals:
meal_nutrition = calculate_meal_nutrition(tm.meal, db)
meal_details.append({
+ 'plan': {'meal': tm.meal, 'meal_time': tm.meal_time},
+ 'nutrition': meal_nutrition,
+ 'foods': [] # Template view should show individual foods
+ })
+ # ADD FOOD BREAKDOWN FOR TEMPLATES
+ foods = []
+ for mf in tm.meal.meal_foods:
+ foods.append({
+ 'name': mf.food.name,
+ 'quantity': mf.quantity,
+ 'serving_size': mf.food.serving_size,
+ 'serving_unit': mf.food.serving_unit,
+ })
+ meal_details[-1]['foods'] = foods
+ # Accumulate nutrition totals
+ for key in template_nutrition:
+ if key in meal_nutrition:
+ template_nutrition[key] += meal_nutrition[key]
@@ -232,42 +247,64 @@
plan_date_obj = datetime.fromisoformat(plan_date).date()
except ValueError:
logging.error(f"debug: invalid date format plan_date: {plan_date}")
- return templates.TemplateResponse("detailed.html", {
+ return templates.TemplateResponse(request, "detailed.html", {
"request": request,
"title": "Invalid date",
"error": "Invalid date format. Please use YYYY-MM-DD.",
"day_totals": {},
"templates": templates_list,
- "person": person
+ "person": person,
+ "is_tracked_view": True
})
- logging.info(f"debug: loading plan for {person} on {plan_date_obj}")
- plans = db.query(Plan).filter(Plan.person == person, Plan.date == plan_date_obj).all()
- logging.info(f"debug: found {len(plans)} plans for {person} on {plan_date_obj}")
-
- day_totals = calculate_day_nutrition(plans, db)
+ logging.info(f"debug: loading TRACKED meals for {person} on {plan_date_obj}")
+
+ # Get tracked day and meals instead of planned meals
+ tracked_day = db.query(TrackedDay).filter(
+ TrackedDay.person == person,
+ TrackedDay.date == plan_date_obj
+ ).first()
+
meal_details = []
- for plan in plans:
- meal_nutrition = calculate_meal_nutrition(plan.meal, db)
- foods = []
- for mf in plan.meal.meal_foods:
- foods.append({
- 'name': mf.food.name,
- 'quantity': mf.quantity,
- 'serving_size': mf.food.serving_size,
- 'serving_unit': mf.food.serving_unit,
- 'calories': mf.food.calories * mf.quantity,
- 'protein': mf.food.protein * mf.quantity,
- 'carbs': mf.food.carbs * mf.quantity,
- 'fat': mf.food.fat * mf.quantity,
- 'fiber': (mf.food.fiber or 0) * mf.quantity,
- 'sodium': (mf.food.sodium or 0) * mf.quantity,
- })
- meal_details.append({
- 'plan': plan,
- 'nutrition': meal_nutrition,
- 'foods': foods
- })
+ day_totals = {'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0, 'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0}
+
+ if tracked_day:
+ tracked_meals = db.query(TrackedMeal).filter(
+ TrackedMeal.tracked_day_id == tracked_day.id
+ ).options(joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food)).all()
+
+ logging.info(f"debug: found {len(tracked_meals)} tracked meals for {person} on {plan_date_obj}")
+
+ for tracked_meal in tracked_meals:
+ meal_nutrition = calculate_tracked_meal_nutrition(tracked_meal, db)
+ foods = []
+
+ # Show base meal foods
+ for mf in tracked_meal.meal.meal_foods:
+ foods.append({
+ 'name': mf.food.name,
+ 'quantity': mf.quantity,
+ 'serving_size': mf.food.serving_size,
+ 'serving_unit': mf.food.serving_unit,
+ })
+
+ # Show custom tracked foods (overrides/additions)
+ for tracked_food in tracked_meal.tracked_foods:
+ foods.append({
+ 'name': f"{tracked_food.food.name} {'(override)' if tracked_food.is_override else '(addition)'}",
+ 'quantity': tracked_food.quantity,
+ 'serving_size': tracked_food.food.serving_size,
+ 'serving_unit': tracked_food.food.serving_unit,
+ })
+
+ meal_details.append({
+ 'plan': tracked_meal, # Use tracked_meal instead of plan
+ 'nutrition': meal_nutrition,
+ 'foods': foods
+ })
+
+ # Accumulate day totals
+ for key in day_totals:
+ if key in meal_nutrition:
+ day_totals[key] += meal_nutrition[key]
context = {
"request": request,
@@ -276,10 +313,11 @@
"day_totals": day_totals,
"person": person,
"plan_date": plan_date_obj,
- "templates": templates_list
+ "templates": templates_list,
+ "is_tracked_view": True # Add flag to indicate this is tracked view
}
-
- if not meal_details:
- context["message"] = "No meals planned for this day."
+
+ if not meal_details and tracked_day:
+ context["message"] = "No meals tracked for this day."
logging.info(f"debug: rendering plan details context: {context}")

View File

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

38
templates/error.html Normal file
View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="alert alert-danger">
<h4 class="alert-heading">
<i class="bi bi-exclamation-triangle-fill"></i> {{ error_title }}
</h4>
<p>{{ error_message }}</p>
{% if error_details %}
<hr>
<p class="mb-0">
<small>Details: {{ error_details }}</small>
</p>
{% endif %}
</div>
<div class="card mt-3">
<div class="card-header">
<h5>What to do next?</h5>
</div>
<div class="card-body">
<ul>
<li>Check if the database is properly connected</li>
<li>Verify that all required database tables exist</li>
<li>Try refreshing the page</li>
<li>If the error persists, check the server logs for more details</li>
</ul>
<button class="btn btn-primary" onclick="window.location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh Page
</button>
<button class="btn btn-secondary" onclick="window.location.href='/'">
<i class="bi bi-house"></i> Go to Home
</button>
</div>
</div>
</div>
{% endblock %}

View File

@@ -79,13 +79,36 @@
<!-- Food Breakdown -->
<div class="ms-3">
<div class="row row-cols-1 row-cols-sm-2">
{% set overrides = {} %}
{% for tmf in tracked_meal.tracked_foods %}
{% if not tmf.is_deleted %}
{% set _ = overrides.update({tmf.food_id: tmf}) %}
{% endif %}
{% endfor %}
{% set displayed_food_ids = [] %}
{# Display base meal foods, applying overrides #}
{% for meal_food in tracked_meal.meal.meal_foods %}
{% if meal_food.food_id not in overrides %}
<div class="col">
<div class="d-flex justify-content-between small text-muted">
<span>• {{ meal_food.food.name }}</span>
<span class="text-end">{{ meal_food.quantity }} {{ meal_food.food.serving_unit }}</span>
</div>
</div>
{% endif %}
{% set _ = displayed_food_ids.append(meal_food.food_id) %}
{% endfor %}
{# Display overridden and new foods #}
{% for food_id, tmf in overrides.items() %}
<div class="col">
<div class="d-flex justify-content-between small text-muted">
<span>• {{ tmf.food.name }}</span>
<span class="text-end">{{ tmf.quantity }} g</span>
</div>
</div>
{% endfor %}
</div>
{% if not tracked_meal.meal.meal_foods %}
@@ -376,13 +399,19 @@
const foods = [];
inputs.forEach(input => {
foods.push({
const foodData = {
id: parseInt(input.dataset.itemId),
food_id: parseInt(input.dataset.foodId),
quantity: parseFloat(input.value), // Quantity is now grams
grams: parseFloat(input.value), // Renamed to grams to match backend
is_custom: input.dataset.isCustom === 'true'
};
foods.push(foodData);
});
});
console.log('Payload being sent to /tracker/update_tracked_meal_foods:', JSON.stringify({
tracked_meal_id: trackedMealId,
foods: foods
}, null, 2));
try {
const response = await fetch('/tracker/update_tracked_meal_foods', {
@@ -447,19 +476,6 @@
}
}
// Update quantity on input change (real-time update)
document.addEventListener('input', function(e) {
if (e.target.type === 'number' && e.target.dataset.foodId) {
const trackedFoodId = e.target.dataset.foodId;
const quantity = parseFloat(e.target.value);
fetch('/tracker/update_tracked_food', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tracked_food_id: trackedFoodId, quantity: quantity })
}).catch(error => console.error('Error updating quantity:', error));
}
});
// Show add single food modal and pre-select meal time
function addSingleFoodToTime(mealTime) {
document.getElementById('addSingleFoodMealTime').value = mealTime;

102
test_edit_meal.sh Executable file
View File

@@ -0,0 +1,102 @@
#!/bin/bash
# Curl-based test script for exercising the Edit Tracked Meal modal functionality.
# This script demonstrates how to:
# 1. Update the quantity of a food in a tracked meal.
# 2. Remove a food from a tracked meal.
# 3. Add a new food to a tracked meal.
#
# Prerequisites:
# - The FastAPI server must be running (e.g., uvicorn main:app --reload --port 8000).
# - You need a valid tracked_meal_id (from the database or API response).
# - You need valid food_id(s) (from the foods table or API).
#
# How to find IDs:
# 1. tracked_meal_id: Use the GET /tracker endpoint or query the database:
# SELECT id FROM tracked_meal WHERE tracked_day_id = (SELECT id FROM tracked_day WHERE person = 'Sarah' AND date = '2025-10-02');
# 2. food_id: Use the GET /api/foods endpoint or query:
# SELECT id, name FROM food LIMIT 5;
#
# Base URL (adjust if your server is on a different host/port)
BASE_URL="http://localhost:8999"
# Set your specific IDs here (replace with actual values)
TRACKED_MEAL_ID=1 # Example: Replace with your tracked_meal_id
FOOD_ID_TO_UPDATE=1 # Example: Food to update quantity
FOOD_ID_TO_REMOVE=2 # Example: Food to remove
FOOD_ID_TO_ADD=3 # Example: New food to add
echo "Testing Edit Tracked Meal functionality..."
echo "Using tracked_meal_id: $TRACKED_MEAL_ID"
echo ""
# 1. Update Quantity of a Food
echo "1. Updating quantity of food $FOOD_ID_TO_UPDATE to 200g..."
curl -X POST "$BASE_URL/tracker/update_tracked_meal_foods" \
-H "Content-Type: application/json" \
-d '{
"tracked_meal_id": '"$TRACKED_MEAL_ID"',
"foods": [
{
"food_id": '"$FOOD_ID_TO_UPDATE"',
"grams": 200.0
}
],
"removed_food_ids": []
}'
echo ""
echo "Expected response: {\"status\": \"success\"}"
echo ""
# Verify the update (optional: GET the foods)
echo "2. Verifying updated foods..."
curl -X GET "$BASE_URL/tracker/get_tracked_meal_foods/$TRACKED_MEAL_ID"
echo ""
echo "----------------------------------------"
echo ""
# 3. Remove a Food
echo "3. Removing food $FOOD_ID_TO_REMOVE..."
curl -X POST "$BASE_URL/tracker/update_tracked_meal_foods" \
-H "Content-Type: application/json" \
-d '{
"tracked_meal_id": '"$TRACKED_MEAL_ID"',
"foods": [],
"removed_food_ids": ['"$FOOD_ID_TO_REMOVE"']
}'
echo ""
echo "Expected response: {\"status\": \"success\"}"
echo ""
# Verify removal
echo "4. Verifying removed foods..."
curl -X GET "$BASE_URL/tracker/get_tracked_meal_foods/$TRACKED_MEAL_ID"
echo ""
echo "----------------------------------------"
echo ""
# 5. Add a New Food
echo "5. Adding new food $FOOD_ID_TO_ADD with 150g..."
curl -X POST "$BASE_URL/tracker/add_food_to_tracked_meal" \
-H "Content-Type: application/json" \
-d '{
"tracked_meal_id": '"$TRACKED_MEAL_ID"',
"food_id": '"$FOOD_ID_TO_ADD"',
"grams": 150.0
}'
echo ""
echo "Expected response: {\"status\": \"success\"}"
echo ""
# Verify addition
echo "6. Verifying added foods..."
curl -X GET "$BASE_URL/tracker/get_tracked_meal_foods/$TRACKED_MEAL_ID"
echo ""
echo "Script completed. Check responses for success/error messages."
echo "Note: After running, the tracked day will be marked as modified."

View File

@@ -89,72 +89,63 @@ def test_get_tracked_meal_foods_endpoint(client: TestClient, session: TestingSes
def test_edit_tracked_meal_with_override_flow(client: TestClient, session: TestingSessionLocal):
"""
Test the full flow of editing a tracked meal, overriding a food, and then retrieving its foods.
This test aims to reproduce the "Error loading tracked meal foods" bug.
Test the full flow of editing a tracked meal, overriding a food's quantity,
and verifying the new override system.
"""
food1, food2, meal1, tracked_day, tracked_meal = create_test_data(session)
# 1. Simulate adding a meal (already done by create_test_data, so tracked_meal exists)
# 2. Simulate updating a food in the tracked meal to create an override
# This will call /tracker/update_tracked_meal_foods
# Get the original MealFood for food1
# 1. Get the original MealFood for food1 (Apple)
original_meal_food1 = session.query(MealFood).filter(
MealFood.meal_id == meal1.id,
MealFood.food_id == food1.id
).first()
assert original_meal_food1 is not None
# Prepare update data: update food1 quantity (should create a TrackedMealFood and delete original MealFood)
# 2. Prepare update data: update food1's quantity and keep food2 the same.
updated_foods_data = [
{"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
{"id": original_meal_food1.id, "food_id": food1.id, "grams": 175.0, "is_custom": False},
]
# 3. Call the update endpoint
response_update = client.post(
"/tracker/update_tracked_meal_foods",
json={
"tracked_meal_id": tracked_meal.id,
"foods": updated_foods_data
"foods": updated_foods_data,
"removed_food_ids": []
}
)
assert response_update.status_code == 200
assert response_update.json()["status"] == "success"
session.expire_all() # Ensure a fresh load from the database
session.expire_all()
# Verify original MealFood for food1 is deleted
deleted_meal_food1 = session.query(MealFood).filter(MealFood.id == original_meal_food1.id).first()
assert deleted_meal_food1 is None
# Verify a TrackedMealFood for food1 now exists
overridden_tracked_food1 = session.query(TrackedMealFood).filter(
# 4. Verify that a new TrackedMealFood override was created for food1
override_food = session.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == food1.id
).first()
assert overridden_tracked_food1 is not None
assert overridden_tracked_food1.quantity == 175.0
assert override_food is not None
assert override_food.quantity == 175.0
assert override_food.is_override is True
# 3. Now, try to get the tracked meal foods again, which is where the bug occurs
# This will call /tracker/get_tracked_meal_foods
# 5. Verify the original MealFood still exists
assert session.query(MealFood).filter(MealFood.id == original_meal_food1.id).first() is not None
# 6. Get the foods for the tracked meal and check the final state
response_get = client.get(f"/tracker/get_tracked_meal_foods/{tracked_meal.id}")
assert response_get.status_code == 200
data_get = response_get.json()
assert data_get["status"] == "success"
assert len(data_get["meal_foods"]) == 2
# Verify the contents of the returned meal_foods
food_names = [f["food_name"] for f in data_get["meal_foods"]]
assert "Apple" in food_names
assert "Banana" in food_names
for food_data in data_get["meal_foods"]:
if food_data["food_name"] == "Apple":
assert food_data["quantity"] == 175.0
assert food_data["is_custom"] == True
elif food_data["food_name"] == "Banana":
assert food_data["quantity"] == 100.0
assert food_data["is_custom"] == False
food_map = {f["food_name"]: f for f in data_get["meal_foods"]}
assert "Apple" in food_map
assert "Banana" in food_map
assert food_map["Apple"]["quantity"] == 175.0
assert food_map["Apple"]["is_custom"] is True # It's an override
assert food_map["Banana"]["quantity"] == 100.0
assert food_map["Banana"]["is_custom"] is False # It's from the base meal
def test_update_tracked_meal_foods_endpoint(client: TestClient, session: TestingSessionLocal):
@@ -217,30 +208,120 @@ def test_add_food_to_tracked_meal_endpoint(client: TestClient, session: TestingS
data = response.json()
assert data["status"] == "success"
# Verify the food was added to the meal associated with the tracked meal
updated_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
assert len(updated_meal_foods) == 3 # Original 2 + new 1
# Verify the food was added as a TrackedMealFood, not a MealFood
new_tracked_food = session.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == food3.id
).first()
assert new_tracked_food is not None
assert new_tracked_food.quantity == 200
assert new_tracked_food.is_override is False # It's a new addition
# Check the new food's quantity
orange_meal_food = next(mf for mf in updated_meal_foods if mf.food_id == food3.id)
assert orange_meal_food.quantity == 200
# Verify the base meal is unchanged
base_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
assert len(base_meal_foods) == 2
def test_remove_food_from_tracked_meal_endpoint(client: TestClient, session: TestingSessionLocal):
"""Test removing a food from a tracked meal"""
def test_edit_tracked_meal_bug_scenario(client: TestClient, session: TestingSessionLocal):
"""
Simulates the full bug scenario described:
1. Start with a meal with 2 foods.
2. Add a 3rd food.
3. Delete one of the original foods.
4. Update the quantity of the other original food.
5. Save and verify the state.
"""
food1, food2, meal1, tracked_day, tracked_meal = create_test_data(session)
# Get the meal_food_id for food1
meal_food_to_remove = session.query(MealFood).filter(
MealFood.meal_id == meal1.id,
MealFood.food_id == food1.id
).first()
# 1. Initial state: tracked_meal with food1 (Apple) and food2 (Banana)
response = client.delete(f"/tracker/remove_food_from_tracked_meal/{meal_food_to_remove.id}")
assert response.status_code == 200
data = response.json()
# 2. Add a 3rd food (Orange)
food3 = Food(name="Orange", serving_size=130, serving_unit="g", calories=62, protein=1.2, carbs=15, fat=0.2)
session.add(food3)
session.commit()
session.refresh(food3)
add_food_payload = {
"tracked_meal_id": tracked_meal.id,
"food_id": food3.id,
"grams": 200
}
response_add = client.post("/tracker/add_food_to_tracked_meal", json=add_food_payload)
assert response_add.status_code == 200
assert response_add.json()["status"] == "success"
# Verify Orange was added as a TrackedMealFood
orange_tmf = session.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == food3.id
).first()
assert orange_tmf is not None
assert orange_tmf.quantity == 200
# 3. Delete an original food (Apple, food1)
# This requires an update call with the food removed from the list
# 4. Update quantity of the other original food (Banana, food2)
# Simulate the data sent from the frontend after edits
final_foods_payload = [
# food1 (Apple) is omitted, signifying deletion
{"id": None, "food_id": food2.id, "grams": 125.0, "is_custom": False}, # Banana quantity updated
{"id": orange_tmf.id, "food_id": food3.id, "grams": 210.0, "is_custom": True} # Orange quantity updated
]
removed_food_ids = [food1.id]
update_payload = {
"tracked_meal_id": tracked_meal.id,
"foods": final_foods_payload,
"removed_food_ids": removed_food_ids
}
response_update = client.post("/tracker/update_tracked_meal_foods", json=update_payload)
assert response_update.status_code == 200
assert response_update.json()["status"] == "success"
session.expire_all()
# 5. Verify the final state
# There should be one override for the deleted food (Apple)
deleted_apple_override = session.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == food1.id,
TrackedMealFood.is_deleted == True
).first()
assert deleted_apple_override is not None
# There should be one override for the updated food (Banana)
updated_banana_override = session.query(TrackedMealFood).filter(
TrackedMealFood.tracked_meal_id == tracked_meal.id,
TrackedMealFood.food_id == food2.id
).first()
assert updated_banana_override is not None
assert updated_banana_override.quantity == 125.0
# The added food (Orange) should be updated
updated_orange_tmf = session.query(TrackedMealFood).filter(
TrackedMealFood.id == orange_tmf.id
).first()
assert updated_orange_tmf is not None
assert updated_orange_tmf.quantity == 210.0
# Let's check the get_tracked_meal_foods endpoint to be sure
response_get = client.get(f"/tracker/get_tracked_meal_foods/{tracked_meal.id}")
assert response_get.status_code == 200
data = response_get.json()
assert data["status"] == "success"
# Verify the food was removed from the meal associated with the tracked meal
updated_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
assert len(updated_meal_foods) == 1 # Original 2 - removed 1
assert updated_meal_foods[0].food_id == food2.id # Only food2 should remain
# The final list should contain Banana and Orange, but not Apple
final_food_names = [f["food_name"] for f in data["meal_foods"]]
assert "Apple" not in final_food_names
assert "Banana" in final_food_names
assert "Orange" in final_food_names
for food_data in data["meal_foods"]:
if food_data["food_name"] == "Banana":
assert food_data["quantity"] == 125.0
elif food_data["food_name"] == "Orange":
assert food_data["quantity"] == 210.0

View File

@@ -1,115 +0,0 @@
--- app/api/routes/tracker.py
+++ app/api/routes/tracker.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body
+from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body, UploadFile, File
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session, joinedload
from datetime import date, datetime, timedelta
@@ -110,4 +110,94 @@
except Exception as e:
db.rollback()
logging.error(f"debug: error removing tracked meal: {e}")
- return {"status": "error", "message": str(e)}
+ return {"status": "error", "message": str(e)}
+
+@router.post("/tracker/save_template")
+async def tracker_save_template(request: Request, db: Session = Depends(get_db)):
+ """save current day's meals as template"""
+ try:
+ form_data = await request.form()
+ person = form_data.get("person")
+ date_str = form_data.get("date")
+ template_name = form_data.get("template_name")
+ logging.info(f"debug: saving template - name={template_name}, person={person}, date={date_str}")
+
+ # Parse date
+ from datetime import datetime
+ date = datetime.fromisoformat(date_str).date()
+
+ # Get tracked day and meals
+ tracked_day = db.query(TrackedDay).filter(
+ TrackedDay.person == person,
+ TrackedDay.date == date
+ ).first()
+ if not tracked_day:
+ return {"status": "error", "message": "No tracked day found"}
+
+ tracked_meals = db.query(TrackedMeal).filter(
+ TrackedMeal.tracked_day_id == tracked_day.id
+ ).all()
+
+ if not tracked_meals:
+ return {"status": "error", "message": "No tracked meals found"}
+
+ # Create new template
+ template = Template(name=template_name)
+ db.add(template)
+ db.flush()
+
+ # Add meals to template
+ for tracked_meal in tracked_meals:
+ template_meal = TemplateMeal(
+ template_id=template.id,
+ meal_id=tracked_meal.meal_id,
+ meal_time=tracked_meal.meal_time
+ )
+ db.add(template_meal)
+
+ db.commit()
+ return {"status": "success", "message": "Template saved successfully"}
+ except Exception as e:
+ db.rollback()
+ logging.error(f"debug: error saving template: {e}")
+ return {"status": "error", "message": str(e)}
+
+@router.post("/tracker/apply_template")
+async def tracker_apply_template(request: Request, db: Session = Depends(get_db)):
+ """apply template to current day"""
+ try:
+ form_data = await request.form()
+ person = form_data.get("person")
+ date_str = form_data.get("date")
+ template_id = form_data.get("template_id")
+ logging.info(f"debug: applying template - template_id={template_id}, person={person}, date={date_str}")
+
+ # Parse date
+ from datetime import datetime
+ date = datetime.fromisoformat(date_str).date()
+
+ # Get template meals
+ template_meals = db.query(TemplateMeal).filter(
+ TemplateMeal.template_id == template_id
+ ).all()
+
+ if not template_meals:
+ return {"status": "error", "message": "Template has no meals"}
+
+ # Get or create tracked day
+ tracked_day = db.query(TrackedDay).filter(
+ TrackedDay.person == person,
+ TrackedDay.date == date
+ ).first()
+ if not tracked_day:
+ tracked_day = TrackedDay(person=person, date=date, is_modified=True)
+ db.add(tracked_day)
+ db.flush()
+
+ # Clear existing meals and add template meals
+ db.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).delete()
+
+ for template_meal in template_meals:
+ tracked_meal = TrackedMeal(
+ tracked_day_id=tracked_day.id,
+ meal_id=template_meal.meal_id,
+ meal_time=template_meal.meal_time
+ )
+ db.add(tracked_meal)
+
+ tracked_day.is_modified = True
+ db.commit()
+ return {"status": "success", "message": "Template applied successfully"}
+ except Exception as e:
+ db.rollback()
+ logging.error(f"debug: error applying template: {e}")
+ return {"status": "error", "message": str(e)}