mirror of
https://github.com/sstent/foodplanner.git
synced 2026-04-17 02:13:40 +00:00
Compare commits
12 Commits
7fc17967e0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e411a8f6c4 | |||
| e68a1f52af | |||
| e47af2c839 | |||
| 7099577f92 | |||
| ced3c63f92 | |||
| 5c73ce9caf | |||
| 0e4cb5a5ed | |||
| b834e89a97 | |||
| 00600a76fa | |||
| cc6b4ca145 | |||
| f0430c810b | |||
| 326a82ea5d |
4
.github/workflows/nomad-deploy-dev.yml
vendored
4
.github/workflows/nomad-deploy-dev.yml
vendored
@@ -51,6 +51,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Deploying container version: ${{ steps.container_version.outputs.sha }}"
|
echo "Deploying container version: ${{ steps.container_version.outputs.sha }}"
|
||||||
nomad status
|
nomad status
|
||||||
nomad job run
|
nomad job run \
|
||||||
-var="container_version=${{ steps.container_version.outputs.sha }}"
|
-var="container_version=${{ steps.container_version.outputs.sha }}" \
|
||||||
foodplanner-dev.nomad
|
foodplanner-dev.nomad
|
||||||
|
|||||||
27
alembic/versions/7fdcc454e056_add_name_to_tracked_meal.py
Normal file
27
alembic/versions/7fdcc454e056_add_name_to_tracked_meal.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""add_name_to_tracked_meal
|
||||||
|
|
||||||
|
Revision ID: 7fdcc454e056
|
||||||
|
Revises: e1c2d8d5c1a8
|
||||||
|
Create Date: 2026-02-24 06:29:46.441129
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '7fdcc454e056'
|
||||||
|
down_revision: Union[str, None] = 'e1c2d8d5c1a8'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('tracked_meals', sa.Column('name', sa.String(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table('tracked_meals') as batch_op:
|
||||||
|
batch_op.drop_column('name')
|
||||||
@@ -15,7 +15,10 @@ router = APIRouter()
|
|||||||
@router.get("/meals", response_class=HTMLResponse)
|
@router.get("/meals", response_class=HTMLResponse)
|
||||||
async def meals_page(request: Request, db: Session = Depends(get_db)):
|
async def meals_page(request: Request, db: Session = Depends(get_db)):
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
meals = db.query(Meal).options(joinedload(Meal.meal_foods).joinedload(MealFood.food)).all()
|
# Filter out single food entries and snapshots
|
||||||
|
meals = db.query(Meal).filter(
|
||||||
|
Meal.meal_type.notin_(["single_food", "tracked_snapshot"])
|
||||||
|
).options(joinedload(Meal.meal_foods).joinedload(MealFood.food)).all()
|
||||||
foods = db.query(Food).all()
|
foods = db.query(Food).all()
|
||||||
return templates.TemplateResponse("meals.html",
|
return templates.TemplateResponse("meals.html",
|
||||||
{"request": request, "meals": meals, "foods": foods})
|
{"request": request, "meals": meals, "foods": foods})
|
||||||
|
|||||||
@@ -746,23 +746,25 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
|
|||||||
# Store grams directly
|
# Store grams directly
|
||||||
quantity = grams
|
quantity = grams
|
||||||
|
|
||||||
# Create a new Meal for this single food entry
|
# Create tracked meal entry without a parent Meal template
|
||||||
# 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=grams)
|
|
||||||
db.add(meal_food)
|
|
||||||
|
|
||||||
# Create tracked meal entry
|
|
||||||
tracked_meal = TrackedMeal(
|
tracked_meal = TrackedMeal(
|
||||||
tracked_day_id=tracked_day.id,
|
tracked_day_id=tracked_day.id,
|
||||||
meal_id=new_meal.id,
|
meal_id=None,
|
||||||
meal_time=meal_time
|
meal_time=meal_time,
|
||||||
|
name=food_item.name
|
||||||
)
|
)
|
||||||
db.add(tracked_meal)
|
db.add(tracked_meal)
|
||||||
|
db.flush() # Flush to get the tracked_meal ID
|
||||||
|
|
||||||
|
# Link the food directly to the tracked meal via TrackedMealFood
|
||||||
|
new_entry = TrackedMealFood(
|
||||||
|
tracked_meal_id=tracked_meal.id,
|
||||||
|
food_id=food_id,
|
||||||
|
quantity=grams,
|
||||||
|
is_override=False,
|
||||||
|
is_deleted=False
|
||||||
|
)
|
||||||
|
db.add(new_entry)
|
||||||
|
|
||||||
# Mark day as modified
|
# Mark day as modified
|
||||||
tracked_day.is_modified = True
|
tracked_day.is_modified = True
|
||||||
|
|||||||
@@ -148,8 +148,9 @@ class TrackedMeal(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
tracked_day_id = Column(Integer, ForeignKey("tracked_days.id"))
|
tracked_day_id = Column(Integer, ForeignKey("tracked_days.id"))
|
||||||
meal_id = Column(Integer, ForeignKey("meals.id"))
|
meal_id = Column(Integer, ForeignKey("meals.id"), nullable=True)
|
||||||
meal_time = Column(String) # Breakfast, Lunch, Dinner, Snack 1, Snack 2, Beverage 1, Beverage 2
|
meal_time = Column(String) # Breakfast, Lunch, Dinner, Snack 1, Snack 2, Beverage 1, Beverage 2
|
||||||
|
name = Column(String, nullable=True) # For single food items or custom names
|
||||||
|
|
||||||
tracked_day = relationship("TrackedDay", back_populates="tracked_meals")
|
tracked_day = relationship("TrackedDay", back_populates="tracked_meals")
|
||||||
meal = relationship("Meal")
|
meal = relationship("Meal")
|
||||||
@@ -427,9 +428,11 @@ def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
|
|||||||
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
|
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# 1. Get base foods from the meal
|
# 1. Get base foods from the meal (if it exists)
|
||||||
# access via relationship, assume eager loading or lazy loading
|
# access via relationship, assume eager loading or lazy loading
|
||||||
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
|
base_foods = {}
|
||||||
|
if tracked_meal.meal:
|
||||||
|
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
|
||||||
|
|
||||||
# 2. Get tracked foods (overrides, deletions, additions)
|
# 2. Get tracked foods (overrides, deletions, additions)
|
||||||
tracked_foods = tracked_meal.tracked_foods
|
tracked_foods = tracked_meal.tracked_foods
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] **Track: Refactor the meal tracking system to decouple 'Journal Logs' from 'Cookbook Recipes'**
|
- [x] **Track: Refactor the meal tracking system to decouple 'Journal Logs' from 'Cookbook Recipes'**
|
||||||
*Link: [./tracks/meal_tracker_refactor_20250223/](./tracks/meal_tracker_refactor_20250223/)*
|
*Link: [./tracks/meal_tracker_refactor_20250223/](./tracks/meal_tracker_refactor_20250223/)*
|
||||||
|
|||||||
@@ -2,27 +2,27 @@
|
|||||||
|
|
||||||
This plan outlines the steps for refactoring the meal tracking system to decouple "Journal Logs" from "Cookbook Recipes," resolving database pollution and improving system structure.
|
This plan outlines the steps for refactoring the meal tracking system to decouple "Journal Logs" from "Cookbook Recipes," resolving database pollution and improving system structure.
|
||||||
|
|
||||||
## Phase 1: Preparation & Schema Updates
|
## Phase 1: Preparation & Schema Updates [checkpoint: 326a82e]
|
||||||
- [ ] Task: Create a new branch for the refactoring track.
|
- [x] Task: Create a new branch for the refactoring track.
|
||||||
- [ ] Task: Add the 'name' column to the 'TrackedMeal' table and make 'meal_id' nullable in 'app/database.py'.
|
- [x] Task: Add the 'name' column to the 'TrackedMeal' table and make 'meal_id' nullable in 'app/database.py'.
|
||||||
- [ ] Task: Create and run an Alembic migration for the schema changes.
|
- [x] Task: Create and run an Alembic migration for the schema changes.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Schema Updates' (Protocol in workflow.md)
|
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Preparation & Schema Updates' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 2: Logic & Calculation Updates
|
## Phase 2: Logic & Calculation Updates [checkpoint: cc6b4ca]
|
||||||
- [ ] Task: Write failing unit tests for 'calculate_tracked_meal_nutrition' with 'meal_id=None'.
|
- [ ] Task: Write failing unit tests for 'calculate_tracked_meal_nutrition' with 'meal_id=None'.
|
||||||
- [ ] Task: Implement support for 'meal_id=None' in 'calculate_tracked_meal_nutrition' within 'app/database.py'.
|
- [ ] Task: Implement support for 'meal_id=None' in 'calculate_tracked_meal_nutrition' within 'app/database.py'.
|
||||||
- [ ] Task: Write failing unit tests for the refactored 'tracker_add_food' endpoint.
|
- [ ] Task: Write failing unit tests for the refactored 'tracker_add_food' endpoint.
|
||||||
- [ ] Task: Refactor the 'tracker_add_food' route in 'app/api/routes/tracker.py' to use the new 'TrackedMeal' structure.
|
- [ ] Task: Refactor the 'tracker_add_food' route in 'app/api/routes/tracker.py' to use the new 'TrackedMeal' structure.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Logic & Calculation Updates' (Protocol in workflow.md)
|
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Logic & Calculation Updates' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 3: UI & Cookbook Refinement
|
## Phase 3: UI & Cookbook Refinement [checkpoint: b834e89]
|
||||||
- [ ] Task: Update the 'tracker.html' template to display 'TrackedMeal.name' for template-less logs.
|
- [ ] Task: Update the 'tracker.html' template to display 'TrackedMeal.name' for template-less logs.
|
||||||
- [ ] Task: Update the Meals page in 'app/api/routes/meals.py' to filter out 'single_food' and 'snapshot' types.
|
- [ ] Task: Update the Meals page in 'app/api/routes/meals.py' to filter out 'single_food' and 'snapshot' types.
|
||||||
- [ ] Task: Write failing E2E tests for the new tracking workflow.
|
- [ ] Task: Write failing E2E tests for the new tracking workflow.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: UI & Cookbook Refinement' (Protocol in workflow.md)
|
- [ ] Task: Conductor - User Manual Verification 'Phase 3: UI & Cookbook Refinement' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 4: Database Migration & Cleanup
|
## Phase 4: Database Migration & Cleanup [checkpoint: 5c73ce9]
|
||||||
- [ ] Task: Create a Python migration script for cleaning up existing 'single_food' entries.
|
- [x] Task: Create a Python migration script for cleaning up existing 'single_food' entries.
|
||||||
- [ ] Task: Run the migration script on the development PostgreSQL database.
|
- [x] Task: Run the migration script on the development PostgreSQL database.
|
||||||
- [ ] Task: Verify the database state and ensure no orphans remain.
|
- [x] Task: Verify the database state and ensure no orphans remain.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Database Migration & Cleanup' (Protocol in workflow.md)
|
- [x] Task: Conductor - User Manual Verification 'Phase 4: Database Migration & Cleanup' (Protocol in workflow.md)
|
||||||
|
|||||||
@@ -70,15 +70,15 @@
|
|||||||
{% for tracked_meal in meals_for_time %}
|
{% for tracked_meal in meals_for_time %}
|
||||||
{# 1. Create stable slugs #}
|
{# 1. Create stable slugs #}
|
||||||
{% set meal_time_slug = meal_time|slugify %}
|
{% set meal_time_slug = meal_time|slugify %}
|
||||||
{% set meal_name_safe = tracked_meal.meal.name|slugify %}
|
{% set display_meal_name = (tracked_meal.name or tracked_meal.meal.name) if (tracked_meal.name or tracked_meal.meal) else "Unnamed Meal" %}
|
||||||
|
{% set meal_name_safe = display_meal_name|slugify %}
|
||||||
|
|
||||||
{# 2. Construct the core Unique Meal ID for non-ambiguous locating #}
|
{# 2. Construct the core Unique Meal ID for non-ambiguous locating #}
|
||||||
{% set unique_meal_id = meal_time_slug + '-' + meal_name_safe + '-' + loop.index|string %}
|
{% set unique_meal_id = meal_time_slug + '-' + meal_name_safe + '-' + loop.index|string %}
|
||||||
<div class="mb-3 p-3 bg-light rounded" data-testid="meal-card-{{ unique_meal_id }}">
|
<div class="mb-3 p-3 bg-light rounded" data-testid="meal-card-{{ unique_meal_id }}">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<div>
|
<div>
|
||||||
<strong data-testid="meal-name-{{ unique_meal_id }}">{{ tracked_meal.meal.name
|
<strong data-testid="meal-name-{{ unique_meal_id }}">{{ display_meal_name }}</strong>
|
||||||
}}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||||
@@ -126,6 +126,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{# Display base meal foods, applying overrides #}
|
{# Display base meal foods, applying overrides #}
|
||||||
|
{% if tracked_meal.meal %}
|
||||||
{% for meal_food in tracked_meal.meal.meal_foods %}
|
{% for meal_food in tracked_meal.meal.meal_foods %}
|
||||||
{% if meal_food.food_id not in deleted_food_ids and meal_food.food_id not in
|
{% if meal_food.food_id not in deleted_food_ids and meal_food.food_id not in
|
||||||
overrides.keys() %}
|
overrides.keys() %}
|
||||||
@@ -162,6 +163,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# Display overridden/new foods #}
|
{# Display overridden/new foods #}
|
||||||
{% for food_id, tmf in overrides.items() %}
|
{% for food_id, tmf in overrides.items() %}
|
||||||
|
|||||||
@@ -98,20 +98,9 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
|
|||||||
query_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)()
|
query_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Find the created Meal
|
# Verify NO new Meal was created
|
||||||
created_meal = query_session.query(Meal).order_by(Meal.id.desc()).first()
|
meals = query_session.query(Meal).all()
|
||||||
assert created_meal is not None
|
assert len(meals) == 0
|
||||||
assert created_meal.name == "Test Food"
|
|
||||||
assert created_meal.meal_type == "single_food"
|
|
||||||
|
|
||||||
# Find the MealFood
|
|
||||||
meal_food = query_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first()
|
|
||||||
assert meal_food is not None
|
|
||||||
assert meal_food.food_id == food.id
|
|
||||||
|
|
||||||
# This assertion fails because the backend used data.get("grams", 1.0), so quantity=1.0 instead of 50.0
|
|
||||||
# After the fix changing to data.get("quantity", 1.0), it will pass
|
|
||||||
assert meal_food.quantity == 50.0, f"Expected quantity 50.0, but got {meal_food.quantity}"
|
|
||||||
|
|
||||||
# Also verify TrackedDay and TrackedMeal were created
|
# Also verify TrackedDay and TrackedMeal were created
|
||||||
tracked_day = query_session.query(TrackedDay).filter(
|
tracked_day = query_session.query(TrackedDay).filter(
|
||||||
@@ -123,8 +112,16 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
|
|||||||
|
|
||||||
tracked_meal = query_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
|
tracked_meal = query_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
|
||||||
assert tracked_meal is not None
|
assert tracked_meal is not None
|
||||||
assert tracked_meal.meal_id == created_meal.id
|
assert tracked_meal.meal_id is None
|
||||||
|
assert tracked_meal.name == "Test Food"
|
||||||
assert tracked_meal.meal_time == "Snack 1"
|
assert tracked_meal.meal_time == "Snack 1"
|
||||||
|
|
||||||
|
# Find the TrackedMealFood
|
||||||
|
from app.database import TrackedMealFood
|
||||||
|
tmf = query_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||||
|
assert tmf is not None
|
||||||
|
assert tmf.food_id == food.id
|
||||||
|
assert tmf.quantity == 50.0
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
query_session.close()
|
query_session.close()
|
||||||
|
|||||||
@@ -140,10 +140,13 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
|
|||||||
assert response.json()["status"] == "success"
|
assert response.json()["status"] == "success"
|
||||||
|
|
||||||
# Verify the tracked meal food quantity
|
# Verify the tracked meal food quantity
|
||||||
tracked_meal = session.query(Meal).filter(Meal.name == sample_food_100g.name).first()
|
tracked_meal = session.query(TrackedMeal).filter(TrackedMeal.name == sample_food_100g.name).first()
|
||||||
assert tracked_meal is not None
|
assert tracked_meal is not None
|
||||||
meal_food = session.query(MealFood).filter(MealFood.meal_id == tracked_meal.id).first()
|
assert tracked_meal.meal_id is None
|
||||||
assert meal_food.quantity == grams
|
|
||||||
|
tmf = session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||||
|
assert tmf is not None
|
||||||
|
assert tmf.quantity == grams
|
||||||
|
|
||||||
def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g, sample_food_50g):
|
def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g, sample_food_50g):
|
||||||
"""Test updating tracked meal foods with grams input"""
|
"""Test updating tracked meal foods with grams input"""
|
||||||
|
|||||||
113
tests/test_tracked_meal_refactor.py
Normal file
113
tests/test_tracked_meal_refactor.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import pytest
|
||||||
|
from app.database import Food, TrackedMeal, TrackedMealFood, calculate_tracked_meal_nutrition
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
def test_calculate_tracked_meal_nutrition_no_meal_template(db_session: Session):
|
||||||
|
"""Test nutrition calculation for a tracked meal with no parent meal template (meal_id=None)"""
|
||||||
|
# Create a food
|
||||||
|
food = Food(
|
||||||
|
name="Test Food",
|
||||||
|
serving_size=100.0,
|
||||||
|
serving_unit="g",
|
||||||
|
calories=100.0,
|
||||||
|
protein=10.0,
|
||||||
|
carbs=20.0,
|
||||||
|
fat=5.0,
|
||||||
|
fiber=5.0,
|
||||||
|
sugar=10.0,
|
||||||
|
sodium=100.0,
|
||||||
|
calcium=50.0
|
||||||
|
)
|
||||||
|
db_session.add(food)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(food)
|
||||||
|
|
||||||
|
# Create a tracked meal without a template
|
||||||
|
tracked_meal = TrackedMeal(
|
||||||
|
meal_id=None,
|
||||||
|
meal_time="Snack",
|
||||||
|
name="Single Food Log"
|
||||||
|
)
|
||||||
|
db_session.add(tracked_meal)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(tracked_meal)
|
||||||
|
|
||||||
|
# Add a tracked food entry to it
|
||||||
|
tracked_food = TrackedMealFood(
|
||||||
|
tracked_meal_id=tracked_meal.id,
|
||||||
|
food_id=food.id,
|
||||||
|
quantity=200.0, # 2 servings
|
||||||
|
is_override=False,
|
||||||
|
is_deleted=False
|
||||||
|
)
|
||||||
|
db_session.add(tracked_food)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(tracked_food)
|
||||||
|
|
||||||
|
# Calculate nutrition
|
||||||
|
nutrition = calculate_tracked_meal_nutrition(tracked_meal, db_session)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
assert nutrition['calories'] == 200.0
|
||||||
|
assert nutrition['protein'] == 20.0
|
||||||
|
assert nutrition['carbs'] == 40.0
|
||||||
|
assert nutrition['fat'] == 10.0
|
||||||
|
assert nutrition['fiber'] == 10.0
|
||||||
|
assert nutrition['sugar'] == 20.0
|
||||||
|
assert nutrition['sodium'] == 200.0
|
||||||
|
assert nutrition['calcium'] == 100.0
|
||||||
|
assert nutrition['net_carbs'] == 30.0
|
||||||
|
assert nutrition['protein_pct'] == 40.0 # (20 * 4) / 200 = 80 / 200 = 40%
|
||||||
|
assert nutrition['carbs_pct'] == 80.0 # (40 * 4) / 200 = 160 / 200 = 80%
|
||||||
|
assert nutrition['fat_pct'] == 45.0 # (10 * 9) / 200 = 90 / 200 = 45%
|
||||||
|
|
||||||
|
def test_tracker_add_food_api_no_new_meal(client, db_session: Session):
|
||||||
|
"""Test /tracker/add_food endpoint to ensure it doesn't create redundant Meal templates"""
|
||||||
|
# Create a food
|
||||||
|
food = Food(
|
||||||
|
name="API Test Food",
|
||||||
|
serving_size=100.0,
|
||||||
|
serving_unit="g",
|
||||||
|
calories=100.0,
|
||||||
|
protein=10.0,
|
||||||
|
carbs=20.0,
|
||||||
|
fat=5.0
|
||||||
|
)
|
||||||
|
db_session.add(food)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(food)
|
||||||
|
|
||||||
|
from app.database import Meal
|
||||||
|
initial_meal_count = db_session.query(Meal).count()
|
||||||
|
|
||||||
|
# Call the API
|
||||||
|
response = client.post("/tracker/add_food", json={
|
||||||
|
"person": "Sarah",
|
||||||
|
"date": "2025-02-24",
|
||||||
|
"food_id": food.id,
|
||||||
|
"quantity": 150.0,
|
||||||
|
"meal_time": "Snack"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "success"
|
||||||
|
|
||||||
|
# Verify NO new Meal was created
|
||||||
|
assert db_session.query(Meal).count() == initial_meal_count
|
||||||
|
|
||||||
|
# Verify TrackedMeal exists with meal_id=None and correct name
|
||||||
|
from app.database import TrackedMeal, TrackedDay
|
||||||
|
tracked_day = db_session.query(TrackedDay).filter(TrackedDay.date == "2025-02-24").first()
|
||||||
|
assert tracked_day is not None
|
||||||
|
|
||||||
|
tracked_meal = db_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
|
||||||
|
assert tracked_meal is not None
|
||||||
|
assert tracked_meal.meal_id is None
|
||||||
|
assert tracked_meal.name == "API Test Food"
|
||||||
|
|
||||||
|
# Verify TrackedMealFood exists
|
||||||
|
from app.database import TrackedMealFood
|
||||||
|
tmf = db_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
|
||||||
|
assert tmf is not None
|
||||||
|
assert tmf.food_id == food.id
|
||||||
|
assert tmf.quantity == 150.0
|
||||||
@@ -384,12 +384,13 @@ class TestTrackerAddFood:
|
|||||||
assert len(tracked_meals) == 1
|
assert len(tracked_meals) == 1
|
||||||
|
|
||||||
tracked_meal = tracked_meals[0]
|
tracked_meal = tracked_meals[0]
|
||||||
assert tracked_meal.meal.name == sample_food.name # The meal name should be the food name
|
assert tracked_meal.name == sample_food.name # The meal name should be the food name
|
||||||
|
assert tracked_meal.meal_id is None
|
||||||
|
|
||||||
# Verify the food is in the tracked meal's foods
|
# Verify the food is in the tracked meal's foods
|
||||||
assert len(tracked_meal.meal.meal_foods) == 1
|
assert len(tracked_meal.tracked_foods) == 1
|
||||||
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
|
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
|
||||||
assert tracked_meal.meal.meal_foods[0].quantity == 100.0
|
assert tracked_meal.tracked_foods[0].quantity == 100.0
|
||||||
|
|
||||||
|
|
||||||
def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session):
|
def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session):
|
||||||
@@ -418,11 +419,12 @@ class TestTrackerAddFood:
|
|||||||
assert len(tracked_meals) == 1
|
assert len(tracked_meals) == 1
|
||||||
|
|
||||||
tracked_meal = tracked_meals[0]
|
tracked_meal = tracked_meals[0]
|
||||||
assert tracked_meal.meal.name == sample_food.name
|
assert tracked_meal.name == sample_food.name
|
||||||
|
assert tracked_meal.meal_id is None
|
||||||
|
|
||||||
assert len(tracked_meal.meal.meal_foods) == 1
|
assert len(tracked_meal.tracked_foods) == 1
|
||||||
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
|
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
|
||||||
assert tracked_meal.meal.meal_foods[0].quantity == 150.0
|
assert tracked_meal.tracked_foods[0].quantity == 150.0
|
||||||
|
|
||||||
def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session):
|
def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session):
|
||||||
"""
|
"""
|
||||||
@@ -464,12 +466,13 @@ class TestTrackerAddFood:
|
|||||||
assert len(tracked_meals) == 1
|
assert len(tracked_meals) == 1
|
||||||
|
|
||||||
tracked_meal = tracked_meals[0]
|
tracked_meal = tracked_meals[0]
|
||||||
assert tracked_meal.meal.name == food.name
|
assert tracked_meal.name == food.name
|
||||||
|
assert tracked_meal.meal_id is None
|
||||||
|
|
||||||
# Verify the food is in the tracked meal's foods and quantity is in servings
|
# Verify the food is in the tracked meal's foods
|
||||||
assert len(tracked_meal.meal.meal_foods) == 1
|
assert len(tracked_meal.tracked_foods) == 1
|
||||||
assert tracked_meal.meal.meal_foods[0].food_id == food.id
|
assert tracked_meal.tracked_foods[0].food_id == food.id
|
||||||
assert tracked_meal.meal.meal_foods[0].quantity == grams_to_add
|
assert tracked_meal.tracked_foods[0].quantity == grams_to_add
|
||||||
|
|
||||||
# Verify nutrition calculation
|
# Verify nutrition calculation
|
||||||
day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session)
|
day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session)
|
||||||
|
|||||||
32
tests/tracked_meal_refactor.spec.js
Normal file
32
tests/tracked_meal_refactor.spec.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test('add single food to tracker and verify it is not in meals page', async ({ page }) => {
|
||||||
|
await page.goto('/tracker');
|
||||||
|
|
||||||
|
// Add single food to breakfast
|
||||||
|
await page.locator('[data-testid="add-food-breakfast"]').click();
|
||||||
|
// Select a food (Verification Beans)
|
||||||
|
await page.locator('#addSingleFoodModal select[name="food_id"]').selectOption({ label: 'Verification Beans' });
|
||||||
|
await page.locator('#addSingleFoodModal input[name="quantity"]').fill('200');
|
||||||
|
await page.getByRole('button', { name: 'Add Food', exact: true }).click();
|
||||||
|
|
||||||
|
// Verify it appears in the tracker
|
||||||
|
// The name should be just the food name
|
||||||
|
const mealNameLocator = page.locator('[data-testid^="meal-name-breakfast-verification-beans"]');
|
||||||
|
await expect(mealNameLocator).toBeVisible();
|
||||||
|
await expect(mealNameLocator).toHaveText('Verification Beans');
|
||||||
|
|
||||||
|
// Verify it contains the food with correct quantity
|
||||||
|
const foodRowLocator = page.locator('[data-testid^="food-row-breakfast-verification-beans"][data-testid$="verification-beans"]');
|
||||||
|
await expect(foodRowLocator).toBeVisible();
|
||||||
|
await expect(foodRowLocator).toContainText('Verification Beans');
|
||||||
|
await expect(foodRowLocator).toContainText('200.0 g');
|
||||||
|
|
||||||
|
// Navigate to Meals page
|
||||||
|
await page.goto('/meals');
|
||||||
|
|
||||||
|
// Verify 'Verification Beans' is NOT in the meals list as a meal name
|
||||||
|
// It might be in the ingredients dropdown, but shouldn't be a <strong> heading in a card
|
||||||
|
const mealCardHeading = page.locator('.card-title:has-text("Verification Beans")');
|
||||||
|
await expect(mealCardHeading).not.toBeVisible();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user