mirror of
https://github.com/sstent/foodplanner.git
synced 2025-12-06 08:01:47 +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
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = sqlite:////app/data/meal_planner.db
|
||||
sqlalchemy.url = sqlite:////app/meal_planner.db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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
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,
|
||||
"date": date_str,
|
||||
"food_id": sample_food_100g.id,
|
||||
"grams": grams, # 75 grams
|
||||
"quantity": grams, # 75 grams
|
||||
"meal_time": "Breakfast"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user