fixing the db migrations

This commit is contained in:
2025-10-01 12:40:58 -07:00
parent 63b3575797
commit 7ffc57a7a8
7 changed files with 158 additions and 51 deletions

View File

@@ -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 ###

View File

@@ -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)

View File

@@ -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

61
main.py
View File

@@ -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

View File

@@ -12,15 +12,16 @@
<input type="hidden" name="date" value="{{ current_date.isoformat() }}">
<div class="mb-3">
<label class="form-label">Select Food</label>
<select class="form-control" name="food_id" required>
<select class="form-control" name="food_id" required onchange="updateServingSizeNote(this)">
<option value="">Choose food...</option>
{% for food in foods %}
<option value="{{ food.id }}">{{ food.name }}</option>
<option value="{{ food.id }}" data-serving-size="{{ food.serving_size }}">{{ food.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Quantity (g)</label>
<span id="servingSizeNote" class="text-muted small ms-2"></span>
<input type="number" step="0.1" class="form-control" name="quantity" value="1.0" min="0.1" required>
</div>
<input type="hidden" name="meal_time" id="addSingleFoodMealTime">
@@ -30,6 +31,19 @@
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitAddSingleFood()">Add Food</button>
</div>
<script>
function updateServingSizeNote(selectElement) {
const selectedOption = selectElement.options[selectElement.selectedIndex];
const servingSize = selectedOption.getAttribute('data-serving-size');
const servingSizeNote = document.getElementById('servingSizeNote');
if (servingSize) {
servingSizeNote.textContent = `(Serving size: ${servingSize}g)`;
} else {
servingSizeNote.textContent = '';
}
}
</script>
</div>
</div>
</div>

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 == 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: