tryiong to fix the details page

This commit is contained in:
2025-09-29 12:59:38 -07:00
parent 494320c8f3
commit d1bf3b817d
3 changed files with 215 additions and 52 deletions

69
main.py
View File

@@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Text, Date, Boolean from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Text, Date, Boolean
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.orm import sessionmaker, Session, relationship, declarative_base from sqlalchemy.orm import sessionmaker, Session, relationship, declarative_base
from sqlalchemy.orm import joinedload
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from typing import List, Optional from typing import List, Optional
from datetime import date, datetime from datetime import date, datetime
@@ -309,6 +310,11 @@ class WeeklyMenuDayExport(BaseModel):
day_of_week: int day_of_week: int
template_id: int template_id: int
class WeeklyMenuDayDetail(BaseModel):
day_of_week: int
template_id: int
template_name: str
class WeeklyMenuExport(BaseModel): class WeeklyMenuExport(BaseModel):
id: int id: int
name: str name: str
@@ -316,6 +322,13 @@ class WeeklyMenuExport(BaseModel):
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
class WeeklyMenuDetail(BaseModel):
id: int
name: str
weekly_menu_days: List[WeeklyMenuDayDetail]
model_config = ConfigDict(from_attributes=True)
class TrackedMealExport(BaseModel): class TrackedMealExport(BaseModel):
meal_id: int meal_id: int
meal_time: str meal_time: str
@@ -596,11 +609,11 @@ async def root(request: Request):
# Admin Section # Admin Section
@app.get("/admin", response_class=HTMLResponse) @app.get("/admin", response_class=HTMLResponse)
async def admin_page(request: Request): async def admin_page(request: Request):
return templates.TemplateResponse("admin/index.html", {"request": request}) return templates.TemplateResponse(request, "admin/index.html", {"request": request})
@app.get("/admin/imports", response_class=HTMLResponse) @app.get("/admin/imports", response_class=HTMLResponse)
async def admin_imports_page(request: Request): async def admin_imports_page(request: Request):
return templates.TemplateResponse("admin/imports.html", {"request": request}) return templates.TemplateResponse(request, "admin/imports.html", {"request": request})
@app.get("/admin/backups", response_class=HTMLResponse) @app.get("/admin/backups", response_class=HTMLResponse)
async def admin_backups_page(request: Request): async def admin_backups_page(request: Request):
@@ -1560,12 +1573,50 @@ async def delete_meals(meal_ids: dict = Body(...), db: Session = Depends(get_db)
async def weekly_menu_page(request: Request, db: Session = Depends(get_db)): async def weekly_menu_page(request: Request, db: Session = Depends(get_db)):
weekly_menus = db.query(WeeklyMenu).all() weekly_menus = db.query(WeeklyMenu).all()
templates_list = db.query(Template).all() templates_list = db.query(Template).all()
# Convert WeeklyMenu objects to dictionaries for JSON serialization
weekly_menus_data = []
for wm in weekly_menus:
wm_dict = {
"id": wm.id,
"name": wm.name,
"weekly_menu_days": []
}
for wmd in wm.weekly_menu_days:
wm_dict["weekly_menu_days"].append({
"day_of_week": wmd.day_of_week,
"template_id": wmd.template_id,
"template_name": wmd.template.name if wmd.template else "Unknown"
})
weekly_menus_data.append(wm_dict)
return templates.TemplateResponse("weeklymenu.html", { return templates.TemplateResponse("weeklymenu.html", {
"request": request, "request": request,
"weekly_menus": weekly_menus, "weekly_menus": weekly_menus_data,
"templates": templates_list "templates": templates_list
}) })
@app.get("/api/weeklymenus", response_model=List[WeeklyMenuDetail])
async def get_weekly_menus_api(db: Session = Depends(get_db)):
"""API endpoint to get all weekly menus with template details."""
weekly_menus = db.query(WeeklyMenu).options(joinedload(WeeklyMenu.weekly_menu_days).joinedload(WeeklyMenuDay.template)).all()
results = []
for wm in weekly_menus:
day_details = [
WeeklyMenuDayDetail(
day_of_week=wmd.day_of_week,
template_id=wmd.template_id,
template_name=wmd.template.name if wmd.template else "Unknown"
) for wmd in wm.weekly_menu_days
]
results.append(WeeklyMenuDetail(
id=wm.id,
name=wm.name,
weekly_menu_days=day_details
))
return results
@app.post("/weeklymenu/create") @app.post("/weeklymenu/create")
async def create_weekly_menu(request: Request, db: Session = Depends(get_db)): async def create_weekly_menu(request: Request, db: Session = Depends(get_db)):
"""Create a new weekly menu with template assignments.""" """Create a new weekly menu with template assignments."""
@@ -1853,7 +1904,7 @@ async def detailed(request: Request, person: str = "Sarah", plan_date: str = Non
template = db.query(Template).filter(Template.id == template_id).first() template = db.query(Template).filter(Template.id == template_id).first()
if not template: if not template:
logging.error(f"DEBUG: Template with id {template_id} not found") logging.error(f"DEBUG: Template with id {template_id} not found")
return templates.TemplateResponse("detailed.html", { return templates.TemplateResponse(request, "detailed.html", {
"request": request, "title": "Template Not Found", "request": request, "title": "Template Not Found",
"error": "Template not found", "error": "Template not found",
"day_totals": {} "day_totals": {}
@@ -1894,7 +1945,7 @@ async def detailed(request: Request, person: str = "Sarah", plan_date: str = Non
"person": person "person": person
} }
logging.info(f"DEBUG: Rendering template details with context: {context}") logging.info(f"DEBUG: Rendering template details with context: {context}")
return templates.TemplateResponse("detailed.html", context) return templates.TemplateResponse(request, "detailed.html", context)
if plan_date: if plan_date:
# Show plan details for a specific date # Show plan details for a specific date
@@ -1903,7 +1954,7 @@ async def detailed(request: Request, person: str = "Sarah", plan_date: str = Non
plan_date_obj = datetime.fromisoformat(plan_date).date() plan_date_obj = datetime.fromisoformat(plan_date).date()
except ValueError: except ValueError:
logging.error(f"DEBUG: Invalid date format for plan_date: {plan_date}") logging.error(f"DEBUG: Invalid date format for plan_date: {plan_date}")
return templates.TemplateResponse("detailed.html", { return templates.TemplateResponse(request, "detailed.html", {
"request": request, "title": "Invalid Date", "request": request, "title": "Invalid Date",
"error": "Invalid date format. Please use YYYY-MM-DD.", "error": "Invalid date format. Please use YYYY-MM-DD.",
"day_totals": {} "day_totals": {}
@@ -1948,11 +1999,11 @@ async def detailed(request: Request, person: str = "Sarah", plan_date: str = Non
"plan_date": plan_date_obj "plan_date": plan_date_obj
} }
logging.info(f"DEBUG: Rendering plan details with context: {context}") logging.info(f"DEBUG: Rendering plan details with context: {context}")
return templates.TemplateResponse("detailed.html", context) return templates.TemplateResponse(request, "detailed.html", context)
# If neither plan_date nor template_id is provided, return an error # If neither plan_date nor template_id is provided, return an error
logging.error("DEBUG: Neither plan_date nor template_id were provided") logging.error("DEBUG: Neither plan_date nor template_id were provided")
return templates.TemplateResponse("detailed.html", { return templates.TemplateResponse(request, "detailed.html", {
"request": request, "title": "Error", "request": request, "title": "Error",
"error": "Please provide either a plan date or a template ID.", "error": "Please provide either a plan date or a template ID.",
"day_totals": {} "day_totals": {}
@@ -2305,7 +2356,7 @@ async def templates_page(request: Request, db: Session = Depends(get_db)):
@app.get("/api/templates", response_model=List[TemplateDetail]) @app.get("/api/templates", response_model=List[TemplateDetail])
async def get_templates_api(db: Session = Depends(get_db)): async def get_templates_api(db: Session = Depends(get_db)):
"""API endpoint to get all templates with meal details.""" """API endpoint to get all templates with meal details."""
templates = db.query(Template).options(orm.joinedload(Template.template_meals).joinedload(TemplateMeal.meal)).all() templates = db.query(Template).options(joinedload(Template.template_meals).joinedload(TemplateMeal.meal)).all()
results = [] results = []
for t in templates: for t in templates:

120
plan.md
View File

@@ -1,50 +1,92 @@
# Plan for Pytest of Details Tab # Plan for Adding New Tests to test_detailed.py
This plan outlines the steps to create a comprehensive pytest for the "details" tab in the Food Planner application. ## Overview
This plan outlines the additional tests that need to be added to `tests/test_detailed.py` to cover the following functionality:
- Load today's date by default
- View date should return the meals planned for a date (already covered)
- The template dropdown should show a list of templates available to view
## Objective ## Current Test Coverage
The goal is to create a suite of tests that verify the functionality of the `/detailed` route, ensuring it correctly handles both plan-based and template-based views, as well as invalid inputs. The existing tests in `tests/test_detailed.py` already cover:
- `test_detailed_page_no_params` - when no params are provided
- `test_detailed_page_with_plan_date` - when plan_date is provided
- `test_detailed_page_with_template_id` - when template_id is provided
- `test_detailed_page_with_invalid_plan_date` - when invalid plan_date is provided
- `test_detailed_page_with_invalid_template_id` - when invalid template_id is provided
## File to be Created ## New Tests to Add
- `tests/test_detailed.py`
## Test Cases ### 1. Test Default Date Loading
**Test Name:** `test_detailed_page_default_date`
**Purpose:** Verify that when no plan_date is provided, the detailed page loads with today's date by default
**Implementation:**
```python
def test_detailed_page_default_date(client, session):
# Create mock data for today
food = Food(name="Apple", serving_size="100", serving_unit="g", calories=52, protein=0.3, carbs=14, fat=0.2)
session.add(food)
session.commit()
session.refresh(food)
### 1. Test with `plan_date` meal = Meal(name="Fruit Snack", meal_type="snack", meal_time="Snack")
- **Description**: This test will check the `/detailed` route when a valid `plan_date` is provided. session.add(meal)
- **Steps**: session.commit()
1. Create mock data: a `Food`, a `Meal`, a `MealFood`, and a `Plan` for a specific date. session.refresh(meal)
2. Send a GET request to `/detailed` with the `person` and `plan_date` as query parameters.
3. Assert that the response status code is 200.
4. Assert that the response contains the correct data for the plan.
### 2. Test with `template_id` meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
- **Description**: This test will check the `/detailed` route when a valid `template_id` is provided. session.add(meal_food)
- **Steps**: session.commit()
1. Create mock data: a `Food`, a `Meal`, a `Template`, and a `TemplateMeal`.
2. Send a GET request to `/detailed` with the `template_id` as a query parameter.
3. Assert that the response status code is 200.
4. Assert that the response contains the correct data for the template.
### 3. Test with Invalid `plan_date` test_date = date.today()
- **Description**: This test will ensure the route handles an invalid `plan_date` gracefully. plan = Plan(person="Sarah", date=test_date, meal_id=meal.id, meal_time="Snack")
- **Steps**: session.add(plan)
1. Send a GET request to `/detailed` with a non-existent `plan_date`. session.commit()
2. Assert that the response status code is 200 (as the page should still render).
3. Assert that the response contains a message indicating that no plan was found.
### 4. Test with Invalid `template_id` # Test that when no plan_date is provided, today's date is used by default
- **Description**: This test will ensure the route handles an invalid `template_id` gracefully. response = client.get("/detailed?person=Sarah")
- **Steps**: assert response.status_code == 200
1. Send a GET request to `/detailed` with a non-existent `template_id`. assert "Sarah's Detailed Plan for" in response.text
2. Assert that the response status code is 200. assert test_date.strftime('%B %d, %Y') in response.text # Check if today's date appears in the formatted date
3. Assert that the response contains a message indicating that the template was not found. assert "Fruit Snack" in response.text
```
## Implementation Details ### 2. Test Template Dropdown
**Test Name:** `test_detailed_page_template_dropdown`
**Purpose:** Verify that the template dropdown shows available templates
**Implementation:**
```python
def test_detailed_page_template_dropdown(client, session):
# Create multiple templates
template1 = Template(name="Morning Boost")
template2 = Template(name="Evening Energy")
session.add(template1)
session.add(template2)
session.commit()
session.refresh(template1)
session.refresh(template2)
The `tests/test_detailed.py` file should include: # Test that the template dropdown shows available templates
- Imports for `pytest`, `TestClient`, and the necessary models from `main.py`. response = client.get("/detailed")
- A `TestClient` instance for making requests to the application. assert response.status_code == 200
- Fixtures to set up and tear down the test database for each test function to ensure test isolation.
# Check that the response contains template selection UI elements
assert "Select Template..." in response.text
assert "Morning Boost" in response.text
assert "Evening Energy" in response.text
# Verify that template IDs are present in the dropdown options
assert f'value="{template1.id}"' in response.text
assert f'value="{template2.id}"' in response.text
```
This plan provides a clear path for a developer to implement the required tests. ## Implementation Notes
- Both tests should use the existing session and client fixtures
- The tests should create necessary mock data to ensure proper functionality testing
- The date default test should verify that today's date appears in the response when no date is specified
- The template dropdown test should verify that templates are properly listed in the UI
## Expected Outcome
After implementing these tests, the test coverage for the detailed page will include:
- Default date loading functionality
- Template dropdown functionality
- All existing functionality remains covered

View File

@@ -5,8 +5,18 @@ from sqlalchemy.orm import sessionmaker
from main import app, get_db, Base, Food, Meal, MealFood, Plan, Template, TemplateMeal from main import app, get_db, Base, Food, Meal, MealFood, Plan, Template, TemplateMeal
from datetime import date, timedelta from datetime import date, timedelta
# Setup test database # Setup test database to match Docker environment
SQLALCHEMY_DATABASE_URL = "sqlite:///./test_detailed.db" import os
from pathlib import Path
# Create test database directory if it doesn't exist
test_db_dir = "/app/data"
os.makedirs(test_db_dir, exist_ok=True)
# Use the same database path as Docker container
SQLALCHEMY_DATABASE_URL = "sqlite:////app/data/test_detailed.db"
print(f"Using test database at: {SQLALCHEMY_DATABASE_URL}")
test_engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) test_engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)
@@ -34,6 +44,37 @@ def test_detailed_page_no_params(client):
assert response.status_code == 200 assert response.status_code == 200
assert "Please provide either a plan date or a template ID." in response.text assert "Please provide either a plan date or a template ID." in response.text
def test_detailed_page_default_date(client, session):
# Create mock data for today
food = Food(name="Apple", serving_size="100", serving_unit="g", calories=52, protein=0.3, carbs=14, fat=0.2)
session.add(food)
session.commit()
session.refresh(food)
meal = Meal(name="Fruit Snack", meal_type="snack", meal_time="Snack")
session.add(meal)
session.commit()
session.refresh(meal)
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0)
session.add(meal_food)
session.commit()
test_date = date.today()
plan = Plan(person="Sarah", date=test_date, meal_id=meal.id, meal_time="Snack")
session.add(plan)
session.commit()
# Test that when no plan_date is provided, today's date is used by default
response = client.get("/detailed?person=Sarah")
assert response.status_code == 200
# The apostrophe is HTML-escaped in the template
assert "Sarah's Detailed Plan for" in response.text
assert test_date.strftime('%B %d, %Y') in response.text # Check if today's date appears in the formatted date
assert "Fruit Snack" in response.text
def test_detailed_page_with_plan_date(client, session): def test_detailed_page_with_plan_date(client, session):
# Create mock data # Create mock data
food = Food(name="Apple", serving_size="100", serving_unit="g", calories=52, protein=0.3, carbs=14, fat=0.2) food = Food(name="Apple", serving_size="100", serving_unit="g", calories=52, protein=0.3, carbs=14, fat=0.2)
@@ -57,9 +98,11 @@ def test_detailed_page_with_plan_date(client, session):
response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}") response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}")
assert response.status_code == 200 assert response.status_code == 200
assert "Sarah's Detailed Plan for" in response.text # The apostrophe is HTML-escaped in the template
assert "Sarah's Detailed Plan for" in response.text
assert "Fruit Snack" in response.text assert "Fruit Snack" in response.text
def test_detailed_page_with_template_id(client, session): def test_detailed_page_with_template_id(client, session):
# Create mock data # Create mock data
food = Food(name="Banana", serving_size="100", serving_unit="g", calories=89, protein=1.1, carbs=23, fat=0.3) food = Food(name="Banana", serving_size="100", serving_unit="g", calories=89, protein=1.1, carbs=23, fat=0.3)
@@ -90,14 +133,41 @@ def test_detailed_page_with_template_id(client, session):
assert "Morning Boost Template" in response.text assert "Morning Boost Template" in response.text
assert "Banana Smoothie" in response.text assert "Banana Smoothie" in response.text
def test_detailed_page_with_invalid_plan_date(client): def test_detailed_page_with_invalid_plan_date(client):
invalid_date = date.today() + timedelta(days=100) # A date far in the future invalid_date = date.today() + timedelta(days=100) # A date far in the future
response = client.get(f"/detailed?person=Sarah&plan_date={invalid_date.isoformat()}") response = client.get(f"/detailed?person=Sarah&plan_date={invalid_date.isoformat()}")
assert response.status_code == 200 assert response.status_code == 200
assert "Sarah's Detailed Plan for" in response.text # The apostrophe is HTML-escaped in the template
assert "Sarah's Detailed Plan for" in response.text
assert "No meals planned for this day." in response.text assert "No meals planned for this day." in response.text
def test_detailed_page_with_invalid_template_id(client): def test_detailed_page_with_invalid_template_id(client):
response = client.get(f"/detailed?template_id=99999") response = client.get(f"/detailed?template_id=99999")
assert response.status_code == 200 assert response.status_code == 200
assert "Template Not Found" in response.text assert "Template Not Found" in response.text
def test_detailed_page_template_dropdown(client, session):
# Create multiple templates
template1 = Template(name="Morning Boost")
template2 = Template(name="Evening Energy")
session.add(template1)
session.add(template2)
session.commit()
session.refresh(template1)
session.refresh(template2)
# Test that the template dropdown shows available templates
response = client.get("/detailed")
assert response.status_code == 200
# Check that the response contains template selection UI elements
assert "Select Template..." in response.text
assert "Morning Boost" in response.text
assert "Evening Energy" in response.text
# Verify that template IDs are present in the dropdown options
assert f'value="{template1.id}"' in response.text
assert f'value="{template2.id}"' in response.text