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