mirror of
https://github.com/sstent/foodplanner.git
synced 2025-12-06 08:01:47 +00:00
fixing meal edit on teampltes page
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
.kilocode/
|
.kilocode/
|
||||||
*.pyc
|
*.pyc
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
data/
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from sqlalchemy import engine_from_config
|
from sqlalchemy import engine_from_config
|
||||||
from sqlalchemy import pool
|
from sqlalchemy import pool
|
||||||
|
|
||||||
from alembic import context
|
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
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
config = context.config
|
config = context.config
|
||||||
@@ -17,9 +23,7 @@ if config.config_file_name is not None:
|
|||||||
|
|
||||||
# add your model's MetaData object here
|
# add your model's MetaData object here
|
||||||
# for 'autogenerate' support
|
# for 'autogenerate' support
|
||||||
# We create an empty metadata object since we're not using autogenerate
|
target_metadata = Base.metadata
|
||||||
# and we have explicit migration files
|
|
||||||
target_metadata = None
|
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
# other values from the config, defined by the needs of env.py,
|
||||||
# can be acquired:
|
# can be acquired:
|
||||||
@@ -43,7 +47,7 @@ def run_migrations_offline() -> None:
|
|||||||
url = os.getenv('DATABASE_URL', config.get_main_option("sqlalchemy.url"))
|
url = os.getenv('DATABASE_URL', config.get_main_option("sqlalchemy.url"))
|
||||||
context.configure(
|
context.configure(
|
||||||
url=url,
|
url=url,
|
||||||
target_metadata=None,
|
target_metadata=target_metadata,
|
||||||
literal_binds=True,
|
literal_binds=True,
|
||||||
dialect_opts={"paramstyle": "named"},
|
dialect_opts={"paramstyle": "named"},
|
||||||
)
|
)
|
||||||
@@ -72,7 +76,7 @@ def run_migrations_online() -> None:
|
|||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
logging.info("DEBUG: Database connection established for alembic")
|
logging.info("DEBUG: Database connection established for alembic")
|
||||||
context.configure(
|
context.configure(
|
||||||
connection=connection, target_metadata=None
|
connection=connection, target_metadata=target_metadata
|
||||||
)
|
)
|
||||||
logging.info("DEBUG: Alembic context configured")
|
logging.info("DEBUG: Alembic context configured")
|
||||||
|
|
||||||
|
|||||||
@@ -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 ###
|
||||||
@@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body
|
|||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
import logging
|
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
# Import from the database module
|
# Import from the database module
|
||||||
@@ -14,68 +13,77 @@ router = APIRouter()
|
|||||||
# Tracker tab - Main page
|
# Tracker tab - Main page
|
||||||
@router.get("/tracker", response_class=HTMLResponse)
|
@router.get("/tracker", response_class=HTMLResponse)
|
||||||
async def tracker_page(request: Request, person: str = "Sarah", date: str = None, db: Session = Depends(get_db)):
|
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
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
# If no date provided, use today
|
||||||
# If no date provided, use today
|
if not date:
|
||||||
if not date:
|
current_date = datetime.now().date()
|
||||||
current_date = datetime.now().date()
|
else:
|
||||||
else:
|
current_date = datetime.fromisoformat(date).date()
|
||||||
current_date = datetime.fromisoformat(date).date()
|
|
||||||
|
# Calculate previous and next dates
|
||||||
# Calculate previous and next dates
|
prev_date = (current_date - timedelta(days=1)).isoformat()
|
||||||
prev_date = (current_date - timedelta(days=1)).isoformat()
|
next_date = (current_date + timedelta(days=1)).isoformat()
|
||||||
next_date = (current_date + timedelta(days=1)).isoformat()
|
|
||||||
|
# Get or create tracked day
|
||||||
# Get or create tracked day
|
tracked_day = db.query(TrackedDay).filter(
|
||||||
tracked_day = db.query(TrackedDay).filter(
|
TrackedDay.person == person,
|
||||||
TrackedDay.person == person,
|
TrackedDay.date == current_date
|
||||||
TrackedDay.date == current_date
|
).first()
|
||||||
).first()
|
|
||||||
|
if not tracked_day:
|
||||||
if not tracked_day:
|
# Create new tracked day
|
||||||
# Create new tracked day
|
tracked_day = TrackedDay(person=person, date=current_date, is_modified=False)
|
||||||
tracked_day = TrackedDay(person=person, date=current_date, is_modified=False)
|
db.add(tracked_day)
|
||||||
db.add(tracked_day)
|
db.commit()
|
||||||
db.commit()
|
db.refresh(tracked_day)
|
||||||
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(
|
||||||
# Get tracked meals for this day with eager loading of meal foods
|
joinedload(TrackedMeal.meal)
|
||||||
tracked_meals = db.query(TrackedMeal).options(
|
.joinedload(Meal.meal_foods)
|
||||||
joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food)
|
.joinedload(MealFood.food),
|
||||||
).filter(
|
joinedload(TrackedMeal.tracked_foods)
|
||||||
TrackedMeal.tracked_day_id == tracked_day.id
|
.joinedload(TrackedMealFood.food)
|
||||||
).all()
|
).filter(
|
||||||
|
TrackedMeal.tracked_day_id == tracked_day.id
|
||||||
# Get all meals for dropdown
|
).all()
|
||||||
meals = db.query(Meal).all()
|
|
||||||
|
# Get all meals for dropdown
|
||||||
# Get all templates for template dropdown
|
meals = db.query(Meal).all()
|
||||||
templates_list = db.query(Template).all()
|
|
||||||
|
# Get all templates for template dropdown
|
||||||
|
templates_list = db.query(Template).all()
|
||||||
|
|
||||||
# Get all foods for dropdown
|
# Get all foods for dropdown
|
||||||
foods = db.query(Food).all()
|
foods = db.query(Food).all()
|
||||||
|
|
||||||
|
# Calculate day totals
|
||||||
|
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("tracker.html", {
|
||||||
|
"request": request,
|
||||||
|
"person": person,
|
||||||
|
"current_date": current_date,
|
||||||
|
"prev_date": prev_date,
|
||||||
|
"next_date": next_date,
|
||||||
|
"tracked_meals": tracked_meals,
|
||||||
|
"is_modified": tracked_day.is_modified,
|
||||||
|
"day_totals": day_totals,
|
||||||
|
"meals": meals,
|
||||||
|
"templates": templates_list,
|
||||||
|
"foods": foods
|
||||||
|
})
|
||||||
|
|
||||||
# Calculate day totals
|
except Exception as e:
|
||||||
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
|
# Return a detailed error page instead of generic Internal Server Error
|
||||||
|
return templates.TemplateResponse("error.html", {
|
||||||
logging.info(f"DEBUG: Rendering tracker page with {len(tracked_meals)} tracked meals")
|
"request": request,
|
||||||
|
"error_title": "Error Loading Tracker",
|
||||||
return templates.TemplateResponse("tracker.html", {
|
"error_message": f"An error occurred while loading the tracker page: {str(e)}",
|
||||||
"request": request,
|
"error_details": f"Person: {person}, Date: {date}"
|
||||||
"person": person,
|
}, status_code=500)
|
||||||
"current_date": current_date,
|
|
||||||
"prev_date": prev_date,
|
|
||||||
"next_date": next_date,
|
|
||||||
"tracked_meals": tracked_meals,
|
|
||||||
"is_modified": tracked_day.is_modified,
|
|
||||||
"day_totals": day_totals,
|
|
||||||
"meals": meals,
|
|
||||||
"templates": templates_list,
|
|
||||||
"foods": foods
|
|
||||||
})
|
|
||||||
|
|
||||||
# Tracker API Routes
|
# Tracker API Routes
|
||||||
@router.post("/tracker/add_meal")
|
@router.post("/tracker/add_meal")
|
||||||
@@ -88,7 +96,6 @@ async def tracker_add_meal(request: Request, db: Session = Depends(get_db)):
|
|||||||
meal_id = form_data.get("meal_id")
|
meal_id = form_data.get("meal_id")
|
||||||
meal_time = form_data.get("meal_time")
|
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
|
# Parse date
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -119,19 +126,16 @@ async def tracker_add_meal(request: Request, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logging.info(f"DEBUG: Successfully added meal to tracker")
|
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error adding meal to tracker: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.delete("/tracker/remove_meal/{tracked_meal_id}")
|
@router.delete("/tracker/remove_meal/{tracked_meal_id}")
|
||||||
async def tracker_remove_meal(tracked_meal_id: int, db: Session = Depends(get_db)):
|
async def tracker_remove_meal(tracked_meal_id: int, db: Session = Depends(get_db)):
|
||||||
"""Remove a meal from the tracker"""
|
"""Remove a meal from the tracker"""
|
||||||
try:
|
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()
|
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first()
|
||||||
if not tracked_meal:
|
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.delete(tracked_meal)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logging.info(f"DEBUG: Successfully removed tracked meal")
|
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
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")
|
@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]):
|
if not all([person, date_str, template_name]):
|
||||||
raise HTTPException(status_code=400, detail="Missing required form data.")
|
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
|
# 1. Check if template name already exists
|
||||||
existing_template = db.query(Template).filter(Template.name == template_name).first()
|
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.add(template_meal_entry)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
logging.info(f"debug: successfully saved template '{template_name}' with {len(tracked_meals)} meals.")
|
|
||||||
return {"status": "success", "message": "Template saved successfully."}
|
return {"status": "success", "message": "Template saved successfully."}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"debug: error saving template: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/tracker/apply_template")
|
@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")
|
date_str = form_data.get("date")
|
||||||
template_id = form_data.get("template_id")
|
template_id = form_data.get("template_id")
|
||||||
|
|
||||||
logging.info(f"DEBUG: Applying template - template_id={template_id}, person={person}, date={date_str}")
|
|
||||||
|
|
||||||
# Parse date
|
# Parse date
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -267,12 +265,10 @@ async def tracker_apply_template(request: Request, db: Session = Depends(get_db)
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logging.info(f"DEBUG: Successfully applied template with {len(template_meals)} meals")
|
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error applying template: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/tracker/update_tracked_food")
|
@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))
|
grams = float(data.get("grams", 1.0))
|
||||||
is_custom = data.get("is_custom", False)
|
is_custom = data.get("is_custom", False)
|
||||||
|
|
||||||
logging.info(f"DEBUG: Updating tracked food {tracked_food_id} grams to {grams}")
|
|
||||||
|
|
||||||
if is_custom:
|
if is_custom:
|
||||||
tracked_food = db.query(TrackedMealFood).filter(TrackedMealFood.id == tracked_food_id).first()
|
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()
|
db.commit()
|
||||||
|
|
||||||
logging.info(f"DEBUG: Successfully updated tracked food grams")
|
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error updating tracked food: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/tracker/reset_to_plan")
|
@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")
|
person = form_data.get("person")
|
||||||
date_str = form_data.get("date")
|
date_str = form_data.get("date")
|
||||||
|
|
||||||
logging.info(f"DEBUG: Resetting to plan - person={person}, date={date_str}")
|
|
||||||
|
|
||||||
# Parse date
|
# Parse date
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -360,83 +352,84 @@ async def tracker_reset_to_plan(request: Request, db: Session = Depends(get_db))
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logging.info(f"DEBUG: Successfully reset to plan")
|
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error resetting to plan: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.get("/tracker/get_tracked_meal_foods/{tracked_meal_id}")
|
@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)):
|
async def get_tracked_meal_foods(tracked_meal_id: int, db: Session = Depends(get_db)):
|
||||||
"""Get foods associated with a tracked meal"""
|
"""Get foods associated with a tracked meal"""
|
||||||
logging.info(f"DEBUG: get_tracked_meal_foods called for tracked_meal_id: {tracked_meal_id}")
|
|
||||||
try:
|
try:
|
||||||
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first()
|
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:
|
if not tracked_meal:
|
||||||
raise HTTPException(status_code=404, detail="Tracked meal not found")
|
raise HTTPException(status_code=404, detail="Tracked meal not found")
|
||||||
|
|
||||||
# Load the associated Meal and its foods
|
# 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()
|
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:
|
if not meal:
|
||||||
raise HTTPException(status_code=404, detail="Associated meal not found")
|
raise HTTPException(status_code=404, detail="Associated meal not found")
|
||||||
|
|
||||||
# Load custom tracked foods for this tracked meal
|
# 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()
|
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 = []
|
meal_foods_data = []
|
||||||
|
base_foods = {mf.food_id: mf for mf in meal.meal_foods}
|
||||||
# Keep track of food_ids that have been overridden by TrackedMealFood entries
|
overrides = {tf.food_id: tf for tf in tracked_foods}
|
||||||
# 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:
|
# 1. Handle base meal foods, applying overrides where they exist
|
||||||
# Only add meal_food if it hasn't been overridden by a TrackedMealFood
|
for food_id, base_meal_food in base_foods.items():
|
||||||
if meal_food.food_id not in overridden_food_ids:
|
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": 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({
|
meal_foods_data.append({
|
||||||
"id": meal_food.id,
|
"id": base_meal_food.id,
|
||||||
"food_id": meal_food.food.id,
|
"food_id": base_meal_food.food.id,
|
||||||
"food_name": meal_food.food.name,
|
"food_name": base_meal_food.food.name,
|
||||||
"quantity": meal_food.quantity,
|
"quantity": base_meal_food.quantity,
|
||||||
"serving_unit": meal_food.food.serving_unit,
|
"serving_unit": base_meal_food.food.serving_unit,
|
||||||
"serving_size": meal_food.food.serving_size,
|
"serving_size": base_meal_food.food.serving_size,
|
||||||
"is_custom": False
|
"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
|
||||||
meal_foods_data.append({
|
for food_id, tracked_food in overrides.items():
|
||||||
"id": tracked_food.id,
|
if food_id not in base_foods and not tracked_food.is_deleted:
|
||||||
"food_id": tracked_food.food.id,
|
meal_foods_data.append({
|
||||||
"food_name": tracked_food.food.name,
|
"id": tracked_food.id,
|
||||||
"quantity": tracked_food.quantity,
|
"food_id": tracked_food.food.id,
|
||||||
"serving_unit": tracked_food.food.serving_unit,
|
"food_name": tracked_food.food.name,
|
||||||
"serving_size": tracked_food.food.serving_size,
|
"quantity": tracked_food.quantity,
|
||||||
"is_custom": True
|
"serving_unit": tracked_food.food.serving_unit,
|
||||||
})
|
"serving_size": tracked_food.food.serving_size,
|
||||||
logging.info(f"DEBUG: Added {len(tracked_foods)} custom tracked foods.")
|
"is_custom": True
|
||||||
logging.info(f"DEBUG: Total meal foods data items: {len(meal_foods_data)}")
|
})
|
||||||
|
|
||||||
return {"status": "success", "meal_foods": meal_foods_data}
|
return {"status": "success", "meal_foods": meal_foods_data}
|
||||||
|
|
||||||
except HTTPException as he:
|
except HTTPException as he:
|
||||||
logging.error(f"DEBUG: HTTP Error getting tracked meal foods: {he.detail}")
|
|
||||||
return {"status": "error", "message": he.detail}
|
return {"status": "error", "message": he.detail}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"DEBUG: Error getting tracked meal foods: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/tracker/add_food_to_tracked_meal")
|
@router.post("/tracker/add_food_to_tracked_meal")
|
||||||
async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends(get_db)):
|
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:
|
try:
|
||||||
tracked_meal_id = data.get("tracked_meal_id")
|
tracked_meal_id = data.get("tracked_meal_id")
|
||||||
food_id = data.get("food_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:
|
if not food:
|
||||||
raise HTTPException(status_code=404, detail="Food not found")
|
raise HTTPException(status_code=404, detail="Food not found")
|
||||||
|
|
||||||
# Create a new MealFood entry for the tracked meal's associated meal
|
# Create a new TrackedMealFood entry to associate the food with the tracked meal
|
||||||
meal_food = MealFood(
|
tracked_meal_food = TrackedMealFood(
|
||||||
meal_id=tracked_meal.meal_id,
|
tracked_meal_id=tracked_meal.id,
|
||||||
food_id=food_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
|
# Mark the tracked day as modified
|
||||||
tracked_meal.tracked_day.is_modified = True
|
tracked_meal.tracked_day.is_modified = True
|
||||||
@@ -466,93 +460,78 @@ async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends
|
|||||||
|
|
||||||
except HTTPException as he:
|
except HTTPException as he:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: HTTP Error adding food to tracked meal: {he.detail}")
|
|
||||||
return {"status": "error", "message": he.detail}
|
return {"status": "error", "message": he.detail}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error adding food to tracked meal: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/tracker/update_tracked_meal_foods")
|
@router.post("/tracker/update_tracked_meal_foods")
|
||||||
async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depends(get_db)):
|
async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depends(get_db)):
|
||||||
"""Update quantities of multiple foods in a tracked meal"""
|
"""Update, add, or remove foods from a tracked meal using an override system."""
|
||||||
logging.info(f"DEBUG: update_tracked_meal_foods called for tracked_meal_id: {data.get('tracked_meal_id')}")
|
|
||||||
try:
|
try:
|
||||||
tracked_meal_id = data.get("tracked_meal_id")
|
tracked_meal_id = data.get("tracked_meal_id")
|
||||||
foods_data = data.get("foods", [])
|
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()
|
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:
|
if not tracked_meal:
|
||||||
raise HTTPException(status_code=404, detail="Tracked meal not found")
|
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:
|
for food_data in foods_data:
|
||||||
food_id = food_data.get("food_id")
|
food_id = food_data.get("food_id")
|
||||||
grams = float(food_data.get("grams", 1.0))
|
grams = float(food_data.get("grams", 1.0))
|
||||||
is_custom = food_data.get("is_custom", False)
|
print(f" Processing food_id {food_id} with grams {grams}")
|
||||||
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}")
|
|
||||||
|
|
||||||
if is_custom:
|
# Check if an override entry already exists for this food
|
||||||
tracked_food = db.query(TrackedMealFood).filter(TrackedMealFood.id == item_id).first()
|
existing_override = db.query(TrackedMealFood).filter(
|
||||||
if tracked_food:
|
TrackedMealFood.tracked_meal_id == tracked_meal_id,
|
||||||
tracked_food.quantity = grams
|
TrackedMealFood.food_id == food_id
|
||||||
logging.info(f"DEBUG: Updated existing custom tracked food {item_id} to grams {grams}")
|
).first()
|
||||||
else:
|
|
||||||
# If it's a new custom food being added
|
if existing_override:
|
||||||
new_tracked_food = TrackedMealFood(
|
# If an override exists, update its quantity and ensure it's not marked as deleted
|
||||||
tracked_meal_id=tracked_meal.id,
|
print(f" Found existing override for food_id {food_id}. Updating quantity to {grams}.")
|
||||||
food_id=food_id,
|
existing_override.quantity = grams
|
||||||
quantity=grams
|
existing_override.is_deleted = False
|
||||||
)
|
|
||||||
db.add(new_tracked_food)
|
|
||||||
logging.info(f"DEBUG: Added new custom tracked food for food_id {food_id} with grams {grams}")
|
|
||||||
else:
|
else:
|
||||||
# This is a food from the original meal definition
|
# If no override exists, it's either a modification of a base food or a new addition
|
||||||
# We need to check if it's already a TrackedMealFood (meaning it was overridden)
|
print(f" No existing override for food_id {food_id}. Creating new entry.")
|
||||||
# Or if it's still a MealFood
|
base_meal_food = db.query(MealFood).filter(
|
||||||
existing_tracked_food = db.query(TrackedMealFood).filter(
|
MealFood.meal_id == tracked_meal.meal_id,
|
||||||
TrackedMealFood.tracked_meal_id == tracked_meal.id,
|
MealFood.food_id == food_id
|
||||||
TrackedMealFood.food_id == food_id
|
|
||||||
).first()
|
).first()
|
||||||
logging.info(f"DEBUG: Checking for existing TrackedMealFood for food_id {food_id}: {existing_tracked_food.id if existing_tracked_food else 'None'}")
|
|
||||||
|
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=grams,
|
||||||
|
is_override=is_override
|
||||||
|
)
|
||||||
|
db.add(new_entry)
|
||||||
|
|
||||||
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}")
|
|
||||||
else:
|
|
||||||
# If it's not a TrackedMealFood, it must be a MealFood
|
|
||||||
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,
|
|
||||||
food_id=food_id,
|
|
||||||
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 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.")
|
|
||||||
|
|
||||||
# Mark the tracked day as modified
|
# Mark the tracked day as modified
|
||||||
tracked_meal.tracked_day.is_modified = True
|
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:
|
except HTTPException as he:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: HTTP Error updating tracked meal foods: {he.detail}")
|
|
||||||
return {"status": "error", "message": he.detail}
|
return {"status": "error", "message": he.detail}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error updating tracked meal foods: {e}")
|
|
||||||
return {"status": "error", "message": str(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")
|
@router.post("/tracker/save_as_new_meal")
|
||||||
async def save_as_new_meal(data: dict = Body(...), db: Session = Depends(get_db)):
|
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:
|
except HTTPException as he:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: HTTP Error saving as new meal: {he.detail}")
|
|
||||||
return {"status": "error", "message": he.detail}
|
return {"status": "error", "message": he.detail}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error saving as new meal: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/tracker/add_food")
|
@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))
|
grams = float(data.get("quantity", 1.0))
|
||||||
meal_time = data.get("meal_time")
|
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
|
# Parse date
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -739,16 +660,13 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
logging.info(f"DEBUG: Successfully added single food to tracker")
|
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
|
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error adding single food to tracker: {ve}")
|
|
||||||
return {"status": "error", "message": str(ve)}
|
return {"status": "error", "message": str(ve)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logging.error(f"DEBUG: Error adding single food to tracker: {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.get("/detailed_tracked_day", response_class=HTMLResponse, name="detailed_tracked_day")
|
@router.get("/detailed_tracked_day", response_class=HTMLResponse, name="detailed_tracked_day")
|
||||||
@@ -756,115 +674,121 @@ 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.
|
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()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
current_date = datetime.fromisoformat(date).date()
|
||||||
|
except ValueError:
|
||||||
|
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)
|
||||||
|
|
||||||
# If no date is provided, default to today's date
|
tracked_day = db.query(TrackedDay).filter(
|
||||||
if not date:
|
TrackedDay.person == person,
|
||||||
current_date = date.today()
|
TrackedDay.date == current_date
|
||||||
else:
|
).first()
|
||||||
try:
|
|
||||||
current_date = datetime.fromisoformat(date).date()
|
if not tracked_day:
|
||||||
except ValueError:
|
return templates.TemplateResponse("detailed_tracked_day.html", {
|
||||||
logging.error(f"DEBUG: Invalid date format for date: {date}")
|
"request": request, "title": "No Tracked Day Found",
|
||||||
return templates.TemplateResponse("detailed.html", {
|
"error": "No tracked meals found for this day.",
|
||||||
"request": request, "title": "Invalid Date",
|
|
||||||
"error": "Invalid date format. Please use YYYY-MM-DD.",
|
|
||||||
"day_totals": {},
|
"day_totals": {},
|
||||||
"person": person
|
"person": person,
|
||||||
})
|
"plan_date": current_date # Pass current_date for consistent template behavior
|
||||||
|
|
||||||
tracked_day = db.query(TrackedDay).filter(
|
|
||||||
TrackedDay.person == person,
|
|
||||||
TrackedDay.date == current_date
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not tracked_day:
|
|
||||||
return templates.TemplateResponse("detailed_tracked_day.html", {
|
|
||||||
"request": request, "title": "No Tracked Day Found",
|
|
||||||
"error": "No tracked meals found for this day.",
|
|
||||||
"day_totals": {},
|
|
||||||
"person": person,
|
|
||||||
"plan_date": current_date # Pass current_date for consistent template behavior
|
|
||||||
})
|
|
||||||
|
|
||||||
tracked_meals = db.query(TrackedMeal).options(
|
|
||||||
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()
|
|
||||||
|
|
||||||
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
|
|
||||||
|
|
||||||
meal_details = []
|
|
||||||
for tracked_meal in tracked_meals:
|
|
||||||
meal_nutrition = calculate_meal_nutrition(tracked_meal.meal, db) # Base meal nutrition
|
|
||||||
|
|
||||||
foods = []
|
|
||||||
# Add foods from the base meal definition
|
|
||||||
for mf in tracked_meal.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 # Fallback for invalid serving_size
|
|
||||||
|
|
||||||
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,
|
|
||||||
'protein': mf.food.protein * num_servings,
|
|
||||||
'carbs': mf.food.carbs * num_servings,
|
|
||||||
'fat': mf.food.fat * num_servings,
|
|
||||||
'fiber': (mf.food.fiber or 0) * num_servings,
|
|
||||||
'sugar': (mf.food.sugar or 0) * num_servings,
|
|
||||||
'sodium': (mf.food.sodium or 0) * num_servings,
|
|
||||||
'calcium': (mf.food.calcium or 0) * num_servings,
|
|
||||||
})
|
|
||||||
# Add custom tracked foods (overrides or additions)
|
|
||||||
for tmf in tracked_meal.tracked_foods:
|
|
||||||
try:
|
|
||||||
serving_size_value = float(tmf.food.serving_size)
|
|
||||||
num_servings = tmf.quantity / serving_size_value if serving_size_value != 0 else 0
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
num_servings = 0 # Fallback for invalid serving_size
|
|
||||||
|
|
||||||
foods.append({
|
|
||||||
'name': tmf.food.name,
|
|
||||||
'total_grams': tmf.quantity,
|
|
||||||
'num_servings': num_servings,
|
|
||||||
'serving_size': tmf.food.serving_size,
|
|
||||||
'serving_unit': tmf.food.serving_unit,
|
|
||||||
'calories': tmf.food.calories * num_servings,
|
|
||||||
'protein': tmf.food.protein * num_servings,
|
|
||||||
'carbs': tmf.food.carbs * num_servings,
|
|
||||||
'fat': tmf.food.fat * num_servings,
|
|
||||||
'fiber': (tmf.food.fiber or 0) * num_servings,
|
|
||||||
'sugar': (tmf.food.sugar or 0) * num_servings,
|
|
||||||
'sodium': (tmf.food.sodium or 0) * num_servings,
|
|
||||||
'calcium': (tmf.food.calcium or 0) * num_servings,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
meal_details.append({
|
tracked_meals = db.query(TrackedMeal).options(
|
||||||
'plan': {'meal': tracked_meal.meal, 'meal_time': tracked_meal.meal_time},
|
joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food),
|
||||||
'nutrition': meal_nutrition,
|
joinedload(TrackedMeal.tracked_foods).joinedload(TrackedMealFood.food)
|
||||||
'foods': foods
|
).filter(
|
||||||
})
|
TrackedMeal.tracked_day_id == tracked_day.id
|
||||||
|
).all()
|
||||||
|
|
||||||
context = {
|
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
|
||||||
"request": request,
|
|
||||||
"title": f"Detailed Day for {person} on {current_date.strftime('%B %d, %Y')}",
|
|
||||||
"meal_details": meal_details,
|
|
||||||
"day_totals": day_totals,
|
|
||||||
"person": person,
|
|
||||||
"plan_date": current_date # Renamed from current_date to plan_date for consistency with detailed.html
|
|
||||||
}
|
|
||||||
|
|
||||||
if not meal_details:
|
meal_details = []
|
||||||
context["message"] = "No meals tracked for this day."
|
for tracked_meal in tracked_meals:
|
||||||
|
meal_nutrition = calculate_meal_nutrition(tracked_meal.meal, db) # Base meal nutrition
|
||||||
|
|
||||||
logging.info(f"DEBUG: Rendering tracked day details with context: {context}")
|
foods = []
|
||||||
return templates.TemplateResponse("detailed_tracked_day.html", context)
|
# Add foods from the base meal definition
|
||||||
|
for mf in tracked_meal.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 # Fallback for invalid serving_size
|
||||||
|
|
||||||
|
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,
|
||||||
|
'protein': mf.food.protein * num_servings,
|
||||||
|
'carbs': mf.food.carbs * num_servings,
|
||||||
|
'fat': mf.food.fat * num_servings,
|
||||||
|
'fiber': (mf.food.fiber or 0) * num_servings,
|
||||||
|
'sugar': (mf.food.sugar or 0) * num_servings,
|
||||||
|
'sodium': (mf.food.sodium or 0) * num_servings,
|
||||||
|
'calcium': (mf.food.calcium or 0) * num_servings,
|
||||||
|
})
|
||||||
|
# Add custom tracked foods (overrides or additions)
|
||||||
|
for tmf in tracked_meal.tracked_foods:
|
||||||
|
try:
|
||||||
|
serving_size_value = float(tmf.food.serving_size)
|
||||||
|
num_servings = tmf.quantity / serving_size_value if serving_size_value != 0 else 0
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
num_servings = 0 # Fallback for invalid serving_size
|
||||||
|
|
||||||
|
foods.append({
|
||||||
|
'name': tmf.food.name,
|
||||||
|
'total_grams': tmf.quantity,
|
||||||
|
'num_servings': num_servings,
|
||||||
|
'serving_size': tmf.food.serving_size,
|
||||||
|
'serving_unit': tmf.food.serving_unit,
|
||||||
|
'calories': tmf.food.calories * num_servings,
|
||||||
|
'protein': tmf.food.protein * num_servings,
|
||||||
|
'carbs': tmf.food.carbs * num_servings,
|
||||||
|
'fat': tmf.food.fat * num_servings,
|
||||||
|
'fiber': (tmf.food.fiber or 0) * num_servings,
|
||||||
|
'sugar': (tmf.food.sugar or 0) * num_servings,
|
||||||
|
'sodium': (tmf.food.sodium or 0) * num_servings,
|
||||||
|
'calcium': (tmf.food.calcium or 0) * num_servings,
|
||||||
|
})
|
||||||
|
|
||||||
|
meal_details.append({
|
||||||
|
'plan': {'meal': tracked_meal.meal, 'meal_time': tracked_meal.meal_time},
|
||||||
|
'nutrition': meal_nutrition,
|
||||||
|
'foods': foods
|
||||||
|
})
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"request": request,
|
||||||
|
"title": f"Detailed Day for {person} on {current_date.strftime('%B %d, %Y')}",
|
||||||
|
"meal_details": meal_details,
|
||||||
|
"day_totals": day_totals,
|
||||||
|
"person": person,
|
||||||
|
"plan_date": current_date # Renamed from current_date to plan_date for consistency with detailed.html
|
||||||
|
}
|
||||||
|
|
||||||
|
if not meal_details:
|
||||||
|
context["message"] = "No meals tracked for this day."
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -21,6 +21,7 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
# Database setup - Use SQLite for easier setup
|
# Database setup - Use SQLite for easier setup
|
||||||
# Use environment variables if set, otherwise use defaults
|
# Use environment variables if set, otherwise use defaults
|
||||||
@@ -161,6 +162,7 @@ class TrackedMealFood(Base):
|
|||||||
food_id = Column(Integer, ForeignKey("foods.id"))
|
food_id = Column(Integer, ForeignKey("foods.id"))
|
||||||
quantity = Column(Float, default=1.0) # Custom quantity for this tracked instance
|
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_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")
|
tracked_meal = relationship("TrackedMeal", back_populates="tracked_foods")
|
||||||
food = relationship("Food")
|
food = relationship("Food")
|
||||||
@@ -401,7 +403,6 @@ def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
|
|||||||
'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0,
|
'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0,
|
||||||
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
|
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Base meal nutrition
|
# Base meal nutrition
|
||||||
base_nutrition = calculate_meal_nutrition(tracked_meal.meal, db)
|
base_nutrition = calculate_meal_nutrition(tracked_meal.meal, db)
|
||||||
for key in totals:
|
for key in totals:
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8999:8999"
|
- "8999:8999"
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=sqlite:////app/meal_planner.db
|
- DATABASE_URL=sqlite:////app/data/meal_planner.db
|
||||||
volumes:
|
volumes:
|
||||||
- ./alembic:/app/alembic
|
- ./alembic:/app/alembic
|
||||||
- ./meal_planner.db:/app/meal_planner.db
|
- ./data:/app/data
|
||||||
158
fix_detailed.md
158
fix_detailed.md
@@ -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
|
|
||||||
125
fix_tracer.py
125
fix_tracer.py
@@ -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()
|
|
||||||
160
plans.patch
160
plans.patch
@@ -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}")
|
|
||||||
@@ -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
38
templates/error.html
Normal 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 %}
|
||||||
@@ -79,13 +79,36 @@
|
|||||||
<!-- Food Breakdown -->
|
<!-- Food Breakdown -->
|
||||||
<div class="ms-3">
|
<div class="ms-3">
|
||||||
<div class="row row-cols-1 row-cols-sm-2">
|
<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 %}
|
{% for meal_food in tracked_meal.meal.meal_foods %}
|
||||||
<div class="col">
|
{% if meal_food.food_id not in overrides %}
|
||||||
<div class="d-flex justify-content-between small text-muted">
|
<div class="col">
|
||||||
<span>• {{ meal_food.food.name }}</span>
|
<div class="d-flex justify-content-between small text-muted">
|
||||||
<span class="text-end">{{ meal_food.quantity }} {{ meal_food.food.serving_unit }}</span>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% if not tracked_meal.meal.meal_foods %}
|
{% if not tracked_meal.meal.meal_foods %}
|
||||||
@@ -376,13 +399,19 @@
|
|||||||
|
|
||||||
const foods = [];
|
const foods = [];
|
||||||
inputs.forEach(input => {
|
inputs.forEach(input => {
|
||||||
foods.push({
|
const foodData = {
|
||||||
id: parseInt(input.dataset.itemId),
|
id: parseInt(input.dataset.itemId),
|
||||||
food_id: parseInt(input.dataset.foodId),
|
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'
|
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 {
|
try {
|
||||||
const response = await fetch('/tracker/update_tracked_meal_foods', {
|
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
|
// Show add single food modal and pre-select meal time
|
||||||
function addSingleFoodToTime(mealTime) {
|
function addSingleFoodToTime(mealTime) {
|
||||||
document.getElementById('addSingleFoodMealTime').value = mealTime;
|
document.getElementById('addSingleFoodMealTime').value = mealTime;
|
||||||
|
|||||||
102
test_edit_meal.sh
Executable file
102
test_edit_meal.sh
Executable 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."
|
||||||
@@ -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):
|
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.
|
Test the full flow of editing a tracked meal, overriding a food's quantity,
|
||||||
This test aims to reproduce the "Error loading tracked meal foods" bug.
|
and verifying the new override system.
|
||||||
"""
|
"""
|
||||||
food1, food2, meal1, tracked_day, tracked_meal = create_test_data(session)
|
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)
|
# 1. Get the original MealFood for food1 (Apple)
|
||||||
# 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
|
|
||||||
original_meal_food1 = session.query(MealFood).filter(
|
original_meal_food1 = session.query(MealFood).filter(
|
||||||
MealFood.meal_id == meal1.id,
|
MealFood.meal_id == meal1.id,
|
||||||
MealFood.food_id == food1.id
|
MealFood.food_id == food1.id
|
||||||
).first()
|
).first()
|
||||||
assert original_meal_food1 is not None
|
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 = [
|
updated_foods_data = [
|
||||||
{"id": original_meal_food1.id, "food_id": food1.id, "grams": 175.0, "is_custom": False}, # Original MealFood, but quantity changed
|
{"id": original_meal_food1.id, "food_id": food1.id, "grams": 175.0, "is_custom": False},
|
||||||
{"id": None, "food_id": food2.id, "grams": 100.0, "is_custom": False} # Unchanged original MealFood
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 3. Call the update endpoint
|
||||||
response_update = client.post(
|
response_update = client.post(
|
||||||
"/tracker/update_tracked_meal_foods",
|
"/tracker/update_tracked_meal_foods",
|
||||||
json={
|
json={
|
||||||
"tracked_meal_id": tracked_meal.id,
|
"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.status_code == 200
|
||||||
assert response_update.json()["status"] == "success"
|
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
|
# 4. Verify that a new TrackedMealFood override was created for food1
|
||||||
deleted_meal_food1 = session.query(MealFood).filter(MealFood.id == original_meal_food1.id).first()
|
override_food = session.query(TrackedMealFood).filter(
|
||||||
assert deleted_meal_food1 is None
|
|
||||||
|
|
||||||
# Verify a TrackedMealFood for food1 now exists
|
|
||||||
overridden_tracked_food1 = session.query(TrackedMealFood).filter(
|
|
||||||
TrackedMealFood.tracked_meal_id == tracked_meal.id,
|
TrackedMealFood.tracked_meal_id == tracked_meal.id,
|
||||||
TrackedMealFood.food_id == food1.id
|
TrackedMealFood.food_id == food1.id
|
||||||
).first()
|
).first()
|
||||||
assert overridden_tracked_food1 is not None
|
assert override_food is not None
|
||||||
assert overridden_tracked_food1.quantity == 175.0
|
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
|
# 5. Verify the original MealFood still exists
|
||||||
# This will call /tracker/get_tracked_meal_foods
|
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}")
|
response_get = client.get(f"/tracker/get_tracked_meal_foods/{tracked_meal.id}")
|
||||||
assert response_get.status_code == 200
|
assert response_get.status_code == 200
|
||||||
data_get = response_get.json()
|
data_get = response_get.json()
|
||||||
assert data_get["status"] == "success"
|
assert data_get["status"] == "success"
|
||||||
assert len(data_get["meal_foods"]) == 2
|
assert len(data_get["meal_foods"]) == 2
|
||||||
|
|
||||||
# Verify the contents of the returned meal_foods
|
food_map = {f["food_name"]: f for f in data_get["meal_foods"]}
|
||||||
food_names = [f["food_name"] for f in data_get["meal_foods"]]
|
assert "Apple" in food_map
|
||||||
assert "Apple" in food_names
|
assert "Banana" in food_map
|
||||||
assert "Banana" in food_names
|
assert food_map["Apple"]["quantity"] == 175.0
|
||||||
|
assert food_map["Apple"]["is_custom"] is True # It's an override
|
||||||
for food_data in data_get["meal_foods"]:
|
assert food_map["Banana"]["quantity"] == 100.0
|
||||||
if food_data["food_name"] == "Apple":
|
assert food_map["Banana"]["is_custom"] is False # It's from the base meal
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_tracked_meal_foods_endpoint(client: TestClient, session: TestingSessionLocal):
|
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()
|
data = response.json()
|
||||||
assert data["status"] == "success"
|
assert data["status"] == "success"
|
||||||
|
|
||||||
# Verify the food was added to the meal associated with the tracked meal
|
# Verify the food was added as a TrackedMealFood, not a MealFood
|
||||||
updated_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
|
new_tracked_food = session.query(TrackedMealFood).filter(
|
||||||
assert len(updated_meal_foods) == 3 # Original 2 + new 1
|
TrackedMealFood.tracked_meal_id == tracked_meal.id,
|
||||||
|
TrackedMealFood.food_id == food3.id
|
||||||
# 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
|
|
||||||
|
|
||||||
def test_remove_food_from_tracked_meal_endpoint(client: TestClient, session: TestingSessionLocal):
|
|
||||||
"""Test removing a food from a tracked meal"""
|
|
||||||
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()
|
).first()
|
||||||
|
assert new_tracked_food is not None
|
||||||
response = client.delete(f"/tracker/remove_food_from_tracked_meal/{meal_food_to_remove.id}")
|
assert new_tracked_food.quantity == 200
|
||||||
assert response.status_code == 200
|
assert new_tracked_food.is_override is False # It's a new addition
|
||||||
data = response.json()
|
|
||||||
assert data["status"] == "success"
|
|
||||||
|
|
||||||
# Verify the food was removed from the meal associated with the tracked meal
|
# Verify the base meal is unchanged
|
||||||
updated_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
|
base_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
|
||||||
assert len(updated_meal_foods) == 1 # Original 2 - removed 1
|
assert len(base_meal_foods) == 2
|
||||||
assert updated_meal_foods[0].food_id == food2.id # Only food2 should remain
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 1. Initial state: tracked_meal with food1 (Apple) and food2 (Banana)
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# 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
|
||||||
115
tracker.patch
115
tracker.patch
@@ -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)}
|
|
||||||
Reference in New Issue
Block a user