fixing db tables and onplace upgrde

This commit is contained in:
2025-10-02 05:46:58 -07:00
parent ed5839e222
commit 342eceff1f
9 changed files with 271 additions and 12 deletions

View File

@@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:////app/data/meal_planner.db
sqlalchemy.url = sqlite:////app/meal_planner.db
[post_write_hooks]

View File

@@ -687,10 +687,11 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
person = data.get("person")
date_str = data.get("date")
food_id = data.get("food_id")
grams = float(data.get("grams", 1.0))
grams = float(data.get("quantity", 1.0))
meal_time = data.get("meal_time")
logging.info(f"DEBUG: Adding single food to tracker - person={person}, date={date_str}, food_id={food_id}, grams={grams}, meal_time={meal_time}")
logging.info(f"BUG HUNT: Received raw data: {data}")
logging.info(f"BUG HUNT: Parsed grams: {grams}")
# Parse date
from datetime import datetime

View File

@@ -25,7 +25,7 @@ import os
# Database setup - Use SQLite for easier setup
# Use environment variables if set, otherwise use defaults
# Use current directory for database
DATABASE_PATH = os.getenv('DATABASE_PATH', '/app/data')
DATABASE_PATH = os.getenv('DATABASE_PATH', '/app')
DATABASE_URL = os.getenv('DATABASE_URL', f'sqlite:///{DATABASE_PATH}/meal_planner.db')
# For production, use PostgreSQL: DATABASE_URL = "postgresql://username:password@localhost/meal_planner"

120
bug_repro_test.md Normal file
View File

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

View File

@@ -25,8 +25,6 @@ import shutil
import sqlite3
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Import database components from the database module
from app.database import DATABASE_URL, engine, Base, get_db, SessionLocal, Food, Meal, MealFood, Plan, Template, TemplateMeal, WeeklyMenu, WeeklyMenuDay, TrackedMeal, FoodCreate, FoodResponse, calculate_meal_nutrition, calculate_day_nutrition, calculate_day_nutrition_tracked
@@ -36,6 +34,13 @@ async def lifespan(app: FastAPI):
# Startup
logging.info("DEBUG: Startup event triggered")
run_migrations()
# Re-apply logging configuration after Alembic might have altered it
logging.getLogger().setLevel(logging.INFO)
for handler in logging.getLogger().handlers:
handler.setLevel(logging.INFO)
logging.info("DEBUG: Logging re-configured to INFO level.")
logging.info("DEBUG: Startup event completed")
# Schedule the backup job - temporarily disabled for debugging

View File

@@ -83,7 +83,7 @@
<div class="col">
<div class="d-flex justify-content-between small text-muted">
<span>• {{ meal_food.food.name }}</span>
<span class="text-end">{{ meal_food.food.serving_size }} {{ meal_food.food.serving_unit }}</span>
<span class="text-end">{{ meal_food.quantity }} {{ meal_food.food.serving_unit }}</span>
</div>
</div>
{% endfor %}

133
tests/test_add_food_bug.py Normal file
View File

@@ -0,0 +1,133 @@
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base, Food, Meal, MealFood, TrackedDay, TrackedMeal, get_db
from main import app
from datetime import date
import os
import tempfile
@pytest.fixture(scope="function")
def test_engine():
"""Create a temporary test database engine for each test"""
# Create temporary database file
db_fd, db_path = tempfile.mkstemp(suffix='.db')
database_url = f"sqlite:///{db_path}"
# Create engine and tables
engine = create_engine(database_url, connect_args={"check_same_thread": False})
Base.metadata.create_all(bind=engine)
yield engine
# Cleanup
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture(scope="function")
def db_session(test_engine):
"""Provide a database session for tests using the test engine"""
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)
session = TestingSessionLocal()
yield session
session.close()
@pytest.fixture(scope="function")
def test_client(test_engine):
"""Create a test client with test database"""
def override_get_db():
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine):
"""
Test that the quantity from the add food endpoint is saved correctly as grams.
This test reproduces the bug where the backend expects "grams" but frontend sends "quantity".
"""
# Create a session for initial setup
setup_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)()
try:
# Create a test food using setup session
food = Food(
name="Test Food",
serving_size=100.0,
serving_unit="g",
calories=100.0,
protein=10.0,
carbs=20.0,
fat=5.0
)
setup_session.add(food)
setup_session.commit()
setup_session.refresh(food)
# Simulate the frontend request: sends "quantity" key (as the frontend does)
response = test_client.post(
"/tracker/add_food",
json={
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": food.id,
"quantity": 50.0, # User enters 50 grams
"meal_time": "Snack 1"
}
)
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
# Create a new session to query the committed data
query_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)()
try:
# Find the created Meal
created_meal = query_session.query(Meal).order_by(Meal.id.desc()).first()
assert created_meal is not None
assert created_meal.name == "Test Food"
assert created_meal.meal_type == "single_food"
# Find the MealFood
meal_food = query_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first()
assert meal_food is not None
assert meal_food.food_id == food.id
# This assertion fails because the backend used data.get("grams", 1.0), so quantity=1.0 instead of 50.0
# After the fix changing to data.get("quantity", 1.0), it will pass
assert meal_food.quantity == 50.0, f"Expected quantity 50.0, but got {meal_food.quantity}"
# Also verify TrackedDay and TrackedMeal were created
tracked_day = query_session.query(TrackedDay).filter(
TrackedDay.person == "Sarah",
TrackedDay.date == date.today()
).first()
assert tracked_day is not None
assert tracked_day.is_modified is True
tracked_meal = query_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
assert tracked_meal is not None
assert tracked_meal.meal_id == created_meal.id
assert tracked_meal.meal_time == "Snack 1"
finally:
query_session.close()
finally:
setup_session.close()

View File

@@ -132,7 +132,7 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
"person": person,
"date": date_str,
"food_id": sample_food_100g.id,
"grams": grams, # 75 grams
"quantity": grams, # 75 grams
"meal_time": "Breakfast"
}
)

View File

@@ -389,7 +389,7 @@ class TestTrackerAddFood:
# Verify the food is in the tracked meal's foods
assert len(tracked_meal.meal.meal_foods) == 1
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
assert tracked_meal.meal.meal_foods[0].quantity == 1.0
assert tracked_meal.meal.meal_foods[0].quantity == 100.0
def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session):
@@ -404,7 +404,7 @@ class TestTrackerAddFood:
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": sample_food.id,
"grams": 150.0,
"quantity": 150.0,
"meal_time": "Dinner"
})
assert response.status_code == 200
@@ -449,7 +449,7 @@ class TestTrackerAddFood:
"person": "Sarah",
"date": date.today().isoformat(),
"food_id": food.id,
"grams": grams_to_add,
"quantity": grams_to_add,
"meal_time": "Snack 1"
})
assert response.status_code == 200