mirror of
https://github.com/sstent/foodplanner.git
synced 2026-02-06 00:51:35 +00:00
fixing db tables and onplace upgrde
This commit is contained in:
@@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = sqlite:////app/data/meal_planner.db
|
sqlalchemy.url = sqlite:////app/meal_planner.db
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|||||||
@@ -687,10 +687,11 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
|
|||||||
person = data.get("person")
|
person = data.get("person")
|
||||||
date_str = data.get("date")
|
date_str = data.get("date")
|
||||||
food_id = data.get("food_id")
|
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")
|
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
|
# Parse date
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import os
|
|||||||
# 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
|
||||||
# Use current directory for database
|
# 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')
|
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"
|
# For production, use PostgreSQL: DATABASE_URL = "postgresql://username:password@localhost/meal_planner"
|
||||||
|
|||||||
120
bug_repro_test.md
Normal file
120
bug_repro_test.md
Normal 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
|
||||||
9
main.py
9
main.py
@@ -25,8 +25,6 @@ import shutil
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
|
|
||||||
# Import database components from the database module
|
# 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
|
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
|
# Startup
|
||||||
logging.info("DEBUG: Startup event triggered")
|
logging.info("DEBUG: Startup event triggered")
|
||||||
run_migrations()
|
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")
|
logging.info("DEBUG: Startup event completed")
|
||||||
|
|
||||||
# Schedule the backup job - temporarily disabled for debugging
|
# Schedule the backup job - temporarily disabled for debugging
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex justify-content-between small text-muted">
|
<div class="d-flex justify-content-between small text-muted">
|
||||||
<span>• {{ meal_food.food.name }}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
133
tests/test_add_food_bug.py
Normal file
133
tests/test_add_food_bug.py
Normal 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()
|
||||||
@@ -132,7 +132,7 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
|
|||||||
"person": person,
|
"person": person,
|
||||||
"date": date_str,
|
"date": date_str,
|
||||||
"food_id": sample_food_100g.id,
|
"food_id": sample_food_100g.id,
|
||||||
"grams": grams, # 75 grams
|
"quantity": grams, # 75 grams
|
||||||
"meal_time": "Breakfast"
|
"meal_time": "Breakfast"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ class TestTrackerAddFood:
|
|||||||
# Verify the food is in the tracked meal's foods
|
# Verify the food is in the tracked meal's foods
|
||||||
assert len(tracked_meal.meal.meal_foods) == 1
|
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].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):
|
def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session):
|
||||||
@@ -404,7 +404,7 @@ class TestTrackerAddFood:
|
|||||||
"person": "Sarah",
|
"person": "Sarah",
|
||||||
"date": date.today().isoformat(),
|
"date": date.today().isoformat(),
|
||||||
"food_id": sample_food.id,
|
"food_id": sample_food.id,
|
||||||
"grams": 150.0,
|
"quantity": 150.0,
|
||||||
"meal_time": "Dinner"
|
"meal_time": "Dinner"
|
||||||
})
|
})
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -449,7 +449,7 @@ class TestTrackerAddFood:
|
|||||||
"person": "Sarah",
|
"person": "Sarah",
|
||||||
"date": date.today().isoformat(),
|
"date": date.today().isoformat(),
|
||||||
"food_id": food.id,
|
"food_id": food.id,
|
||||||
"grams": grams_to_add,
|
"quantity": grams_to_add,
|
||||||
"meal_time": "Snack 1"
|
"meal_time": "Snack 1"
|
||||||
})
|
})
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
Reference in New Issue
Block a user