mirror of
https://github.com/sstent/foodplanner.git
synced 2025-12-06 08:01:47 +00:00
updated the tracker to have more features
This commit is contained in:
@@ -345,6 +345,117 @@ async def tracker_reset_to_plan(request: Request, db: Session = Depends(get_db))
|
|||||||
logging.error(f"DEBUG: Error resetting to plan: {e}")
|
logging.error(f"DEBUG: Error resetting to plan: {e}")
|
||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
@router.get("/tracker/get_tracked_meal_foods/{tracked_meal_id}")
|
||||||
|
async def get_tracked_meal_foods(tracked_meal_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Get foods associated with a tracked meal"""
|
||||||
|
try:
|
||||||
|
tracked_meal = db.query(TrackedMeal).options(
|
||||||
|
joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food),
|
||||||
|
joinedload(TrackedMeal.tracked_foods).joinedload(TrackedMealFood.food)
|
||||||
|
).filter(TrackedMeal.id == tracked_meal_id).first()
|
||||||
|
|
||||||
|
if not tracked_meal:
|
||||||
|
raise HTTPException(status_code=404, detail="Tracked meal not found")
|
||||||
|
|
||||||
|
# Combine foods from the base meal and custom tracked foods
|
||||||
|
meal_foods_data = []
|
||||||
|
for meal_food in tracked_meal.meal.meal_foods:
|
||||||
|
meal_foods_data.append({
|
||||||
|
"id": meal_food.id,
|
||||||
|
"food_id": meal_food.food.id,
|
||||||
|
"food_name": meal_food.food.name,
|
||||||
|
"quantity": meal_food.quantity,
|
||||||
|
"serving_unit": meal_food.food.serving_unit,
|
||||||
|
"serving_size": meal_food.food.serving_size,
|
||||||
|
"is_custom": False
|
||||||
|
})
|
||||||
|
|
||||||
|
for tracked_food in tracked_meal.tracked_foods:
|
||||||
|
meal_foods_data.append({
|
||||||
|
"id": tracked_food.id,
|
||||||
|
"food_id": tracked_food.food.id,
|
||||||
|
"food_name": tracked_food.food.name,
|
||||||
|
"quantity": tracked_food.quantity,
|
||||||
|
"serving_unit": tracked_food.food.serving_unit,
|
||||||
|
"serving_size": tracked_food.food.serving_size,
|
||||||
|
"is_custom": True
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"status": "success", "meal_foods": meal_foods_data}
|
||||||
|
|
||||||
|
except HTTPException as he:
|
||||||
|
logging.error(f"DEBUG: HTTP Error getting tracked meal foods: {he.detail}")
|
||||||
|
return {"status": "error", "message": he.detail}
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"DEBUG: Error getting tracked meal foods: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
@router.post("/tracker/add_food_to_tracked_meal")
|
||||||
|
async def add_food_to_tracked_meal(data: dict = Body(...), db: Session = Depends(get_db)):
|
||||||
|
"""Add a food to an existing tracked meal"""
|
||||||
|
try:
|
||||||
|
tracked_meal_id = data.get("tracked_meal_id")
|
||||||
|
food_id = data.get("food_id")
|
||||||
|
quantity = float(data.get("quantity", 1.0))
|
||||||
|
|
||||||
|
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.id == tracked_meal_id).first()
|
||||||
|
if not tracked_meal:
|
||||||
|
raise HTTPException(status_code=404, detail="Tracked meal not found")
|
||||||
|
|
||||||
|
food = db.query(Food).filter(Food.id == food_id).first()
|
||||||
|
if not food:
|
||||||
|
raise HTTPException(status_code=404, detail="Food not found")
|
||||||
|
|
||||||
|
# Create a new MealFood entry for the tracked meal's associated meal
|
||||||
|
meal_food = MealFood(
|
||||||
|
meal_id=tracked_meal.meal_id,
|
||||||
|
food_id=food_id,
|
||||||
|
quantity=quantity
|
||||||
|
)
|
||||||
|
db.add(meal_food)
|
||||||
|
|
||||||
|
# Mark the tracked day as modified
|
||||||
|
tracked_meal.tracked_day.is_modified = True
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
except HTTPException as he:
|
||||||
|
db.rollback()
|
||||||
|
logging.error(f"DEBUG: HTTP Error adding food to tracked meal: {he.detail}")
|
||||||
|
return {"status": "error", "message": he.detail}
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logging.error(f"DEBUG: Error adding food to tracked meal: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
@router.delete("/tracker/remove_food_from_tracked_meal/{meal_food_id}")
|
||||||
|
async def remove_food_from_tracked_meal(meal_food_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Remove a food from a tracked meal"""
|
||||||
|
try:
|
||||||
|
meal_food = db.query(MealFood).filter(MealFood.id == meal_food_id).first()
|
||||||
|
if not meal_food:
|
||||||
|
raise HTTPException(status_code=404, detail="Meal food not found")
|
||||||
|
|
||||||
|
# Mark the tracked day as modified
|
||||||
|
tracked_meal = db.query(TrackedMeal).filter(TrackedMeal.meal_id == meal_food.meal_id).first()
|
||||||
|
if tracked_meal:
|
||||||
|
tracked_meal.tracked_day.is_modified = True
|
||||||
|
|
||||||
|
db.delete(meal_food)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {"status": "success"}
|
||||||
|
|
||||||
|
except HTTPException as he:
|
||||||
|
db.rollback()
|
||||||
|
logging.error(f"DEBUG: HTTP Error removing food from tracked meal: {he.detail}")
|
||||||
|
return {"status": "error", "message": he.detail}
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
logging.error(f"DEBUG: Error removing food from tracked meal: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/tracker/save_as_new_meal")
|
@router.post("/tracker/save_as_new_meal")
|
||||||
async def save_as_new_meal(data: dict = Body(...), db: Session = Depends(get_db)):
|
async def save_as_new_meal(data: dict = Body(...), db: Session = Depends(get_db)):
|
||||||
"""Save an edited tracked meal as a new meal/variant"""
|
"""Save an edited tracked meal as a new meal/variant"""
|
||||||
|
|||||||
@@ -8,13 +8,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" id="editTrackedMealId" value="">
|
<input type="hidden" id="editTrackedMealId" value="">
|
||||||
<div id="editMealFoodsList">
|
<div class="row">
|
||||||
<!-- Foods will be loaded here -->
|
<div class="col-md-6">
|
||||||
</div>
|
<h6>Add Food to Tracked Meal</h6>
|
||||||
<div class="mt-3">
|
<form id="addFoodToTrackedMealForm">
|
||||||
<button class="btn btn-outline-primary" onclick="addFoodToTrackedMeal()">
|
<input type="hidden" id="tracked_meal_id_for_food" name="tracked_meal_id">
|
||||||
<i class="bi bi-plus"></i> Add Food
|
<div class="mb-3">
|
||||||
</button>
|
<label class="form-label">Select Food</label>
|
||||||
|
<select class="form-control" id="foodSelectTrackedMeal" name="food_id" required>
|
||||||
|
<option value="">Choose food...</option>
|
||||||
|
{% for food in foods %}
|
||||||
|
<option value="{{ food.id }}">{{ food.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Quantity</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" name="quantity" value="1" required>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-success" onclick="addFoodToTrackedMeal()">Add Food</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Current Foods in Tracked Meal</h6>
|
||||||
|
<div id="editMealFoodsList">
|
||||||
|
<!-- Foods will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -341,6 +341,7 @@
|
|||||||
|
|
||||||
// Load foods for editing
|
// Load foods for editing
|
||||||
async function loadTrackedMealFoods(trackedMealId) {
|
async function loadTrackedMealFoods(trackedMealId) {
|
||||||
|
document.getElementById('tracked_meal_id_for_food').value = trackedMealId;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/tracker/get_tracked_meal_foods/${trackedMealId}`);
|
const response = await fetch(`/tracker/get_tracked_meal_foods/${trackedMealId}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -349,25 +350,21 @@
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
data.meal_foods.forEach(food => {
|
if (data.meal_foods.length === 0) {
|
||||||
const foodDiv = document.createElement('div');
|
container.innerHTML = '<em>No foods added yet</em>';
|
||||||
foodDiv.className = 'mb-2 p-2 border rounded';
|
} else {
|
||||||
foodDiv.innerHTML = `
|
data.meal_foods.forEach(food => {
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
const foodDiv = document.createElement('div');
|
||||||
<div>
|
foodDiv.className = 'd-flex justify-content-between align-items-center mb-2 p-2 bg-light rounded';
|
||||||
<strong>${food.food_name}</strong>
|
foodDiv.innerHTML = `
|
||||||
<small class="text-muted">(${food.serving_size} ${food.serving_unit})</small>
|
<span>${food.quantity} × ${food.food_name}</span>
|
||||||
</div>
|
<button class="btn btn-sm btn-outline-danger" onclick="removeFoodFromTrackedMeal(${food.id})">
|
||||||
<div>
|
<i class="bi bi-trash"></i>
|
||||||
<input type="number" class="form-control w-25 d-inline me-2" value="${food.quantity}" data-food-id="${food.id}" min="0" step="0.1">
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" onclick="removeTrackedFood(${food.id})">
|
`;
|
||||||
<i class="bi bi-trash"></i>
|
container.appendChild(foodDiv);
|
||||||
</button>
|
});
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(foodDiv);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading meal foods:', error);
|
console.error('Error loading meal foods:', error);
|
||||||
@@ -375,27 +372,55 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add food to tracked meal
|
// Add food to tracked meal
|
||||||
function addFoodToTrackedMeal() {
|
async function addFoodToTrackedMeal() {
|
||||||
// Implementation for adding new food - would need a food selection modal
|
const form = document.getElementById('addFoodToTrackedMealForm');
|
||||||
alert('Add food functionality to be implemented');
|
const formData = new FormData(form);
|
||||||
|
const trackedMealId = formData.get('tracked_meal_id');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/tracker/add_food_to_tracked_meal', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
tracked_meal_id: parseInt(formData.get('tracked_meal_id')),
|
||||||
|
food_id: parseInt(formData.get('food_id')),
|
||||||
|
quantity: parseFloat(formData.get('quantity'))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.status === 'success') {
|
||||||
|
// Reset form and reload current foods
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('tracked_meal_id_for_food').value = trackedMealId;
|
||||||
|
await loadTrackedMealFoods(trackedMealId);
|
||||||
|
} else {
|
||||||
|
alert('Error adding food: ' + result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error adding food: ' + error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove tracked food
|
// Remove food from tracked meal
|
||||||
async function removeTrackedFood(trackedFoodId) {
|
async function removeFoodFromTrackedMeal(mealFoodId) {
|
||||||
if (confirm('Remove this food from the tracked meal?')) {
|
if (confirm('Remove this food from the tracked meal?')) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/tracker/remove_tracked_food/${trackedFoodId}`, {
|
const response = await fetch(`/tracker/remove_food_from_tracked_meal/${mealFoodId}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
loadTrackedMealFoods(document.getElementById('editTrackedMealId').value);
|
const trackedMealId = document.getElementById('editTrackedMealId').value;
|
||||||
|
await loadTrackedMealFoods(trackedMealId);
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + result.message);
|
alert('Error removing food: ' + result.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error: ' + error.message);
|
alert('Error removing food: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
tests/test_edit_tracked_meal.py
Normal file
138
tests/test_edit_tracked_meal.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
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", quantity=1.0)
|
||||||
|
session.add(tracked_meal)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(tracked_meal)
|
||||||
|
|
||||||
|
return food1, food2, meal1, tracked_day, tracked_meal
|
||||||
|
|
||||||
|
def test_get_tracked_meal_foods_endpoint(client: TestClient, session: TestingSessionLocal):
|
||||||
|
"""Test retrieving foods for a tracked meal"""
|
||||||
|
food1, food2, meal1, tracked_day, tracked_meal = create_test_data(session)
|
||||||
|
|
||||||
|
response = client.get(f"/tracker/get_tracked_meal_foods/{tracked_meal.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert len(data["meal_foods"]) == 2
|
||||||
|
|
||||||
|
# Check if food details are correct
|
||||||
|
food_names = [f["food_name"] for f in data["meal_foods"]]
|
||||||
|
assert "Apple" in food_names
|
||||||
|
assert "Banana" in food_names
|
||||||
|
|
||||||
|
# Check quantities
|
||||||
|
for food_data in data["meal_foods"]:
|
||||||
|
if food_data["food_name"] == "Apple":
|
||||||
|
assert food_data["quantity"] == 150.0
|
||||||
|
elif food_data["food_name"] == "Banana":
|
||||||
|
assert food_data["quantity"] == 100.0
|
||||||
|
|
||||||
|
def test_add_food_to_tracked_meal_endpoint(client: TestClient, session: TestingSessionLocal):
|
||||||
|
"""Test adding a new food to an existing tracked meal"""
|
||||||
|
food1, food2, meal1, tracked_day, tracked_meal = create_test_data(session)
|
||||||
|
|
||||||
|
# Create a new food to add
|
||||||
|
food3 = Food(name="Orange", serving_size=130, serving_unit="g", calories=62, protein=1.2, carbs=15, fat=0.2, fiber=3.1, sugar=12, sodium=0)
|
||||||
|
session.add(food3)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(food3)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/tracker/add_food_to_tracked_meal",
|
||||||
|
json={
|
||||||
|
"tracked_meal_id": tracked_meal.id,
|
||||||
|
"food_id": food3.id,
|
||||||
|
"quantity": 200
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "success"
|
||||||
|
|
||||||
|
# Verify the food was added to the meal associated with the tracked meal
|
||||||
|
updated_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
|
||||||
|
assert len(updated_meal_foods) == 3 # Original 2 + new 1
|
||||||
|
|
||||||
|
# Check the new food's quantity
|
||||||
|
orange_meal_food = next(mf for mf in updated_meal_foods if mf.food_id == food3.id)
|
||||||
|
assert orange_meal_food.quantity == 200
|
||||||
|
|
||||||
|
def test_remove_food_from_tracked_meal_endpoint(client: TestClient, session: TestingSessionLocal):
|
||||||
|
"""Test removing a food from a tracked meal"""
|
||||||
|
food1, food2, meal1, tracked_day, tracked_meal = create_test_data(session)
|
||||||
|
|
||||||
|
# Get the meal_food_id for food1
|
||||||
|
meal_food_to_remove = session.query(MealFood).filter(
|
||||||
|
MealFood.meal_id == meal1.id,
|
||||||
|
MealFood.food_id == food1.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
response = client.delete(f"/tracker/remove_food_from_tracked_meal/{meal_food_to_remove.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "success"
|
||||||
|
|
||||||
|
# Verify the food was removed from the meal associated with the tracked meal
|
||||||
|
updated_meal_foods = session.query(MealFood).filter(MealFood.meal_id == meal1.id).all()
|
||||||
|
assert len(updated_meal_foods) == 1 # Original 2 - removed 1
|
||||||
|
assert updated_meal_foods[0].food_id == food2.id # Only food2 should remain
|
||||||
Reference in New Issue
Block a user