diff --git a/app/api/routes/tracker.py b/app/api/routes/tracker.py index d8fcbdc..f5c11ba 100644 --- a/app/api/routes/tracker.py +++ b/app/api/routes/tracker.py @@ -50,6 +50,10 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None TrackedMeal.tracked_day_id == tracked_day.id ).all() + # Filter out deleted tracked foods from each tracked meal + for tracked_meal in tracked_meals: + tracked_meal.tracked_foods = [tf for tf in tracked_meal.tracked_foods if not tf.is_deleted] + # Get all meals for dropdown meals = db.query(Meal).all() @@ -472,9 +476,6 @@ async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depend tracked_meal_id = data.get("tracked_meal_id") foods_data = data.get("foods", []) removed_food_ids = data.get("removed_food_ids", []) - print(f"Received update for tracked_meal_id: {tracked_meal_id}") - print(f" Foods data: {foods_data}") - print(f" Removed food IDs: {removed_food_ids}") tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first() if not tracked_meal: @@ -482,18 +483,15 @@ async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depend # Process removals: mark existing foods as deleted for food_id_to_remove in removed_food_ids: - print(f" Processing removal for food_id: {food_id_to_remove}") # Check if an override already exists override = db.query(TrackedMealFood).filter( TrackedMealFood.tracked_meal_id == tracked_meal_id, TrackedMealFood.food_id == food_id_to_remove ).first() if override: - print(f" Found existing override for food_id {food_id_to_remove}. Marking as deleted.") override.is_deleted = True else: # If no override exists, create one to mark the food as deleted - print(f" No existing override for food_id {food_id_to_remove}. Creating new deleted override.") new_override = TrackedMealFood( tracked_meal_id=tracked_meal_id, food_id=food_id_to_remove, @@ -502,7 +500,6 @@ async def update_tracked_meal_foods(data: dict = Body(...), db: Session = Depend is_deleted=True ) db.add(new_override) - print(f" New override created: {new_override.is_deleted}") # Process updates and additions for food_data in foods_data: @@ -717,6 +714,10 @@ async def detailed_tracked_day(request: Request, person: str = "Sarah", date: Op TrackedMeal.tracked_day_id == tracked_day.id ).all() + # Filter out deleted tracked foods from each tracked meal + for tracked_meal in tracked_meals: + tracked_meal.tracked_foods = [tf for tf in tracked_meal.tracked_foods if not tf.is_deleted] + day_totals = calculate_day_nutrition_tracked(tracked_meals, db) meal_details = [] diff --git a/templates/tracker.html b/templates/tracker.html index cb574dd..9e78ebc 100644 --- a/templates/tracker.html +++ b/templates/tracker.html @@ -80,9 +80,11 @@
{% set overrides = {} %} + {% set non_deleted_override_ids = [] %} {% for tmf in tracked_meal.tracked_foods %} {% if not tmf.is_deleted %} {% set _ = overrides.update({tmf.food_id: tmf}) %} + {% set _ = non_deleted_override_ids.append(tmf.food_id) %} {% endif %} {% endfor %} @@ -90,7 +92,7 @@ {# Display base meal foods, applying overrides #} {% for meal_food in tracked_meal.meal.meal_foods %} - {% if meal_food.food_id not in overrides %} + {% if meal_food.food_id not in non_deleted_override_ids %}
• {{ meal_food.food.name }} @@ -344,7 +346,10 @@ } // Edit tracked meal + let removedFoodIds = []; + function editTrackedMeal(trackedMealId) { + removedFoodIds = []; // Reset the array when a new meal is edited document.getElementById('editTrackedMealId').value = trackedMealId; loadTrackedMealFoods(trackedMealId); new bootstrap.Modal(document.getElementById('editTrackedMealModal')).show(); @@ -375,7 +380,7 @@
g -
@@ -393,6 +398,21 @@ } } + function removeFoodFromTrackedMeal(foodId, isCustom) { + // Find the button that was clicked + const button = event.target.closest('button'); + if (button) { + // Find the parent div and remove it + const foodDiv = button.closest('.d-flex'); + if (foodDiv) { + const foodDataId = parseInt(foodDiv.querySelector('input').dataset.foodId); + removedFoodIds.push(foodDataId); + foodDiv.remove(); + console.log('Removed food with ID:', foodDataId, 'and removedFoodIds is now:', removedFoodIds); + } + } + } + async function saveTrackedMeal() { const trackedMealId = document.getElementById('editTrackedMealId').value; const inputs = document.querySelectorAll('#editMealFoodsList input[type="number"]'); @@ -408,19 +428,19 @@ foods.push(foodData); }); - console.log('Payload being sent to /tracker/update_tracked_meal_foods:', JSON.stringify({ + const payload = { tracked_meal_id: trackedMealId, - foods: foods - }, null, 2)); + foods: foods, + removed_food_ids: removedFoodIds + }; + + console.log('Payload being sent to /tracker/update_tracked_meal_foods:', JSON.stringify(payload, null, 2)); try { const response = await fetch('/tracker/update_tracked_meal_foods', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tracked_meal_id: trackedMealId, - foods: foods - }) + body: JSON.stringify(payload) }); const result = await response.json(); if (result.status === 'success') { diff --git a/tests/test_delete_food_from_meal.py b/tests/test_delete_food_from_meal.py new file mode 100644 index 0000000..60376e2 --- /dev/null +++ b/tests/test_delete_food_from_meal.py @@ -0,0 +1,121 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from main import app +from app.database import Base, get_db, Food, Meal, MealFood, TrackedDay, TrackedMeal, TrackedMealFood +from datetime import date + +# Setup for in-memory SQLite database for testing +SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +@pytest.fixture(name="session") +def session_fixture(): + Base.metadata.create_all(engine) + db = TestingSessionLocal() + try: + yield db + finally: + db.close() + Base.metadata.drop_all(engine) + +@pytest.fixture(name="client") +def client_fixture(session): + def override_get_db(): + yield session + app.dependency_overrides[get_db] = override_get_db + yield TestClient(app) + app.dependency_overrides.clear() + +def create_test_data(session: TestingSessionLocal): + food1 = Food(name="Apple", serving_size=100, serving_unit="g", calories=52, protein=0.3, carbs=14, fat=0.2, fiber=2.4, sugar=10.4, sodium=1) + food2 = Food(name="Banana", serving_size=100, serving_unit="g", calories=89, protein=1.1, carbs=23, fat=0.3, fiber=2.6, sugar=12.2, sodium=1) + session.add_all([food1, food2]) + session.commit() + session.refresh(food1) + session.refresh(food2) + + meal1 = Meal(name="Fruit Salad", meal_type="custom", meal_time="Breakfast") + session.add(meal1) + session.commit() + session.refresh(meal1) + + meal_food1 = MealFood(meal_id=meal1.id, food_id=food1.id, quantity=150) + meal_food2 = MealFood(meal_id=meal1.id, food_id=food2.id, quantity=100) + session.add_all([meal_food1, meal_food2]) + session.commit() + + tracked_day = TrackedDay(person="Sarah", date=date.today(), is_modified=False) + session.add(tracked_day) + session.commit() + session.refresh(tracked_day) + + tracked_meal = TrackedMeal(tracked_day_id=tracked_day.id, meal_id=meal1.id, meal_time="Breakfast") + session.add(tracked_meal) + session.commit() + session.refresh(tracked_meal) + + return food1, food2, meal1, tracked_day, tracked_meal + +def test_delete_food_from_tracked_meal(client: TestClient, session: TestingSessionLocal): + """ + Test deleting a food from a tracked meal. This simulates the user removing a food + from the edit meal modal. + """ + food1, food2, meal1, tracked_day, tracked_meal = create_test_data(session) + + # We want to delete food1 (Apple) and keep food2 (Banana) + removed_food_ids = [food1.id] + + # The 'foods' payload will only contain the food we are keeping. + # The id and is_custom fields are based on what the frontend would send. + original_meal_food2 = session.query(MealFood).filter(MealFood.meal_id == meal1.id, MealFood.food_id == food2.id).first() + + update_payload = { + "tracked_meal_id": tracked_meal.id, + "foods": [ + {"id": original_meal_food2.id, "food_id": food2.id, "grams": 100.0, "is_custom": False}, + ], + "removed_food_ids": removed_food_ids + } + + response_update = client.post("/tracker/update_tracked_meal_foods", json=update_payload) + assert response_update.status_code == 200 + assert response_update.json()["status"] == "success" + + session.expire_all() + + # Verify that an override was created for the deleted food (Apple) + deleted_apple_override = session.query(TrackedMealFood).filter( + TrackedMealFood.tracked_meal_id == tracked_meal.id, + TrackedMealFood.food_id == food1.id, + TrackedMealFood.is_deleted == True + ).first() + assert deleted_apple_override is not None + + # Verify that the food we kept (Banana) does not have an override marked as deleted + banana_override = session.query(TrackedMealFood).filter( + TrackedMealFood.tracked_meal_id == tracked_meal.id, + TrackedMealFood.food_id == food2.id, + TrackedMealFood.is_deleted == False + ).first() + # In this flow, an override for Banana might not be created if the quantity is unchanged. + # The key is that it's not marked as deleted. + + # Finally, check the get_tracked_meal_foods endpoint to ensure 'Apple' is gone + response_get = client.get(f"/tracker/get_tracked_meal_foods/{tracked_meal.id}") + assert response_get.status_code == 200 + data = response_get.json() + assert data["status"] == "success" + + final_food_names = [f["food_name"] for f in data["meal_foods"]] + assert "Apple" not in final_food_names + assert "Banana" in final_food_names \ No newline at end of file