From 7ffc57a7a85dec3f6e34306dc0489fee5ee4f053 Mon Sep 17 00:00:00 2001 From: sstent Date: Wed, 1 Oct 2025 12:40:58 -0700 Subject: [PATCH] fixing the db migrations --- ..._change_food_serving_size_to_float_and_.py | 38 ++++++++++++ app/api/routes/tracker.py | 17 +++--- app/database.py | 17 +----- main.py | 61 +++++++++++-------- ...ner_2025-10-01_18-47-31.db:Zone.Identifier | 0 templates/modals/add_single_food.html | 18 +++++- tests/test_tracker.py | 58 +++++++++++++++++- 7 files changed, 158 insertions(+), 51 deletions(-) create mode 100644 alembic/versions/2295851db11e_change_food_serving_size_to_float_and_.py create mode 100644 meal_planner_2025-10-01_18-47-31.db:Zone.Identifier diff --git a/alembic/versions/2295851db11e_change_food_serving_size_to_float_and_.py b/alembic/versions/2295851db11e_change_food_serving_size_to_float_and_.py new file mode 100644 index 0000000..81dfc73 --- /dev/null +++ b/alembic/versions/2295851db11e_change_food_serving_size_to_float_and_.py @@ -0,0 +1,38 @@ +"""Change Food.serving_size to Float and Pydantic models + +Revision ID: 2295851db11e +Revises: cf94fca21104 +Create Date: 2025-10-01 11:24:54.801648 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2295851db11e' +down_revision = 'cf94fca21104' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('foods', schema=None) as batch_op: + batch_op.alter_column('serving_size', + existing_type=sa.VARCHAR(), + type_=sa.Float(), + existing_nullable=True) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('foods', schema=None) as batch_op: + batch_op.alter_column('serving_size', + existing_type=sa.Float(), + type_=sa.VARCHAR(), + existing_nullable=True) + + # ### end Alembic commands ### diff --git a/app/api/routes/tracker.py b/app/api/routes/tracker.py index 4a11727..4a0448d 100644 --- a/app/api/routes/tracker.py +++ b/app/api/routes/tracker.py @@ -713,19 +713,22 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db) db.commit() db.refresh(tracked_day) - # The quantity is already in grams, so no conversion needed - quantity = grams - - # Create a new Meal for this single food entry - # This allows it to be treated like any other meal in the tracker view + # Convert grams to a quantity multiplier based on serving size food_item = db.query(Food).filter(Food.id == food_id).first() if not food_item: return {"status": "error", "message": "Food not found"} - + + if food_item.serving_size > 0: + quantity = grams / food_item.serving_size + else: + quantity = 1.0 # Default to 1 serving if serving size is not set + + # Create a new Meal for this single food entry + # This allows it to be treated like any other meal in the tracker view new_meal = Meal(name=food_item.name, meal_type="single_food", meal_time=meal_time) db.add(new_meal) db.flush() # Flush to get the new meal ID - + # Link the food to the new meal meal_food = MealFood(meal_id=new_meal.id, food_id=food_id, quantity=quantity) db.add(meal_food) diff --git a/app/database.py b/app/database.py index 2a863b4..a2d4c2f 100644 --- a/app/database.py +++ b/app/database.py @@ -14,7 +14,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', '.') +DATABASE_PATH = os.getenv('DATABASE_PATH', '/app/data') 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" @@ -29,7 +29,7 @@ class Food(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, unique=True, index=True) - serving_size = Column(String) + serving_size = Column(Float) serving_unit = Column(String) calories = Column(Float) protein = Column(Float) @@ -322,18 +322,7 @@ def calculate_meal_nutrition(meal, db: Session): for meal_food in meal.meal_foods: food = meal_food.food - grams = meal_food.quantity # quantity is now grams - - # Convert grams to a multiplier of serving size for nutrition calculation - try: - serving_size_value = float(food.serving_size) - except ValueError: - serving_size_value = 1 # Fallback if serving_size is not a number - - if serving_size_value == 0: - multiplier = 0 # Avoid division by zero - else: - multiplier = grams / serving_size_value + multiplier = meal_food.quantity totals['calories'] += food.calories * multiplier totals['protein'] += food.protein * multiplier diff --git a/main.py b/main.py index 30dee51..cfcba28 100644 --- a/main.py +++ b/main.py @@ -166,7 +166,7 @@ def test_sqlite_connection(db_path): dir_perm = stat.filemode(dir_stat.st_mode) dir_uid = dir_stat.st_uid dir_gid = dir_stat.st_gid - logging.info(f"DEBUG: Database directory permissions: {dir_perm}, UID:{dir_uid}, GID:{dir_gid}") + logging.info(f"DEBUG: Database directory permissions: {dir_perm}, UID:{dir_uid}, GID:{dir_gid}, CWD: {os.getcwd()}") # Test write access test_file = os.path.join(db_dir, "write_test.txt") @@ -223,36 +223,45 @@ def test_sqlite_connection(db_path): logging.error(f"DEBUG: SQLite connection test failed: {e}", exc_info=True) return False +def table_exists(engine, table_name): + from sqlalchemy import inspect + inspector = inspect(engine) + return inspector.has_table(table_name) + +def table_has_content(engine, table_name): + from sqlalchemy import text + with engine.connect() as conn: + result = conn.execute(text(f"SELECT COUNT(*) FROM {table_name}")).scalar() + return result > 0 + def run_migrations(): logging.info("DEBUG: Starting database setup...") try: - # Extract database path from URL - db_path = DATABASE_URL.split("///")[1] - logging.info(f"DEBUG: Database path extracted: {db_path}") + alembic_cfg = Config("alembic.ini") - # Create directory if needed - db_dir = os.path.dirname(db_path) - logging.info(f"DEBUG: Database directory: {db_dir}") - if not os.path.exists(db_dir): - logging.info(f"DEBUG: Creating database directory: {db_dir}") - os.makedirs(db_dir, exist_ok=True) - logging.info(f"DEBUG: Database directory created successfully") + # Create a new engine for checking tables + from sqlalchemy import create_engine + db_url = DATABASE_URL + temp_engine = create_engine(db_url) + + # Check if the database is old and needs to be stamped + has_alembic_version = table_exists(temp_engine, 'alembic_version') + has_foods = table_exists(temp_engine, 'foods') + alembic_version_has_content = has_alembic_version and table_has_content(temp_engine, 'alembic_version') + + logging.info(f"DEBUG: has_alembic_version: {has_alembic_version}, has_foods: {has_foods}, alembic_version_has_content: {alembic_version_has_content}") + + if has_foods and (not has_alembic_version or not alembic_version_has_content): + logging.info("DEBUG: Existing database detected. Stamping with initial migration.") + command.stamp(alembic_cfg, "head") + logging.info("DEBUG: Database stamped successfully.") else: - logging.info(f"DEBUG: Database directory already exists") - - # Test SQLite connection - logging.info("DEBUG: Testing SQLite connection...") - if not test_sqlite_connection(db_path): - logging.error("DEBUG: SQLite connection test failed") - raise Exception("SQLite connection test failed") - logging.info("DEBUG: SQLite connection test passed") - - # Create all tables using SQLAlchemy directly instead of alembic - logging.info("DEBUG: Creating database tables using SQLAlchemy...") - Base.metadata.create_all(bind=engine) - logging.info("DEBUG: Database tables created successfully.") - - logging.info("DEBUG: Database setup completed, returning to caller") + logging.info("DEBUG: No stamping needed or fresh database.") + + # Now, run upgrades + logging.info("DEBUG: Running alembic upgrade...") + command.upgrade(alembic_cfg, "head") + logging.info("DEBUG: Database migrations run successfully.") except Exception as e: logging.error(f"DEBUG: Failed to setup database: {e}", exc_info=True) raise diff --git a/meal_planner_2025-10-01_18-47-31.db:Zone.Identifier b/meal_planner_2025-10-01_18-47-31.db:Zone.Identifier new file mode 100644 index 0000000..e69de29 diff --git a/templates/modals/add_single_food.html b/templates/modals/add_single_food.html index f8b0b14..603eb5b 100644 --- a/templates/modals/add_single_food.html +++ b/templates/modals/add_single_food.html @@ -12,15 +12,16 @@
- {% for food in foods %} - + {% endfor %}
+
@@ -30,6 +31,19 @@ + \ No newline at end of file diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 7e6eb00..4c159ba 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -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 == 100.0 + assert tracked_meal.meal.meal_foods[0].quantity == 1.0 def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session): @@ -422,7 +422,61 @@ class TestTrackerAddFood: 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 == 150.0 + assert tracked_meal.meal.meal_foods[0].quantity == 1.5 + + def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session): + """ + Test that when a single food is added to the tracker, the quantity provided in grams + is correctly converted to servings based on the food's serving size. + """ + # Create a food with a known serving size + food = Food(name="Apple", serving_size=150, serving_unit="g", calories=52, protein=0.3, carbs=14, fat=0.2, fiber=2.4, sugar=10, sodium=1) + db_session.add(food) + db_session.commit() + db_session.refresh(food) + + # Create a tracked day + tracked_day = TrackedDay(person="Sarah", date=date.today(), is_modified=False) + db_session.add(tracked_day) + db_session.commit() + db_session.refresh(tracked_day) + + # Add food directly to tracker with a specific gram quantity + grams_to_add = 300.0 + expected_servings = grams_to_add / float(food.serving_size) + + response = client.post("/tracker/add_food", json={ + "person": "Sarah", + "date": date.today().isoformat(), + "food_id": food.id, + "quantity": grams_to_add, + "meal_time": "Snack 1" + }) + assert response.status_code == 200 + data = response.json() + assert data["status"] == "success" + + # Verify that a new tracked meal was created with the food + tracked_meals = db_session.query(TrackedMeal).filter( + TrackedMeal.tracked_day_id == tracked_day.id, + TrackedMeal.meal_time == "Snack 1" + ).all() + assert len(tracked_meals) == 1 + + tracked_meal = tracked_meals[0] + assert tracked_meal.meal.name == food.name + + # Verify the food is in the tracked meal's foods and quantity is in servings + assert len(tracked_meal.meal.meal_foods) == 1 + assert tracked_meal.meal.meal_foods[0].food_id == food.id + assert tracked_meal.meal.meal_foods[0].quantity == expected_servings + + # Verify nutrition calculation + day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session) + assert day_nutrition["calories"] == food.calories * expected_servings + assert day_nutrition["protein"] == food.protein * expected_servings + assert day_nutrition["carbs"] == food.carbs * expected_servings + assert day_nutrition["fat"] == food.fat * expected_servings class TestTrackedMealQuantity: