mirror of
https://github.com/sstent/foodplanner.git
synced 2026-01-25 03:01:35 +00:00
fixing the db migrations
This commit is contained in:
@@ -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 ###
|
||||
@@ -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)
|
||||
|
||||
@@ -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
61
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
|
||||
|
||||
0
meal_planner_2025-10-01_18-47-31.db:Zone.Identifier
Normal file
0
meal_planner_2025-10-01_18-47-31.db:Zone.Identifier
Normal 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>
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user