From d1bf3b817d5daf94ed70d37246c2cc3766f7836d Mon Sep 17 00:00:00 2001 From: sstent Date: Mon, 29 Sep 2025 12:59:38 -0700 Subject: [PATCH] tryiong to fix the details page --- main.py | 69 ++++++++++++++++++++---- plan.md | 120 +++++++++++++++++++++++++++-------------- tests/test_detailed.py | 78 +++++++++++++++++++++++++-- 3 files changed, 215 insertions(+), 52 deletions(-) diff --git a/main.py b/main.py index ba7e0c9..773f099 100644 --- a/main.py +++ b/main.py @@ -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 or_ from sqlalchemy.orm import sessionmaker, Session, relationship, declarative_base +from sqlalchemy.orm import joinedload from pydantic import BaseModel, ConfigDict from typing import List, Optional from datetime import date, datetime @@ -309,6 +310,11 @@ class WeeklyMenuDayExport(BaseModel): day_of_week: int template_id: int +class WeeklyMenuDayDetail(BaseModel): + day_of_week: int + template_id: int + template_name: str + class WeeklyMenuExport(BaseModel): id: int name: str @@ -316,6 +322,13 @@ class WeeklyMenuExport(BaseModel): 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): meal_id: int meal_time: str @@ -596,11 +609,11 @@ async def root(request: Request): # Admin Section @app.get("/admin", response_class=HTMLResponse) 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) 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) 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)): weekly_menus = db.query(WeeklyMenu).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", { "request": request, - "weekly_menus": weekly_menus, + "weekly_menus": weekly_menus_data, "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") async def create_weekly_menu(request: Request, db: Session = Depends(get_db)): """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() if not template: 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", "error": "Template not found", "day_totals": {} @@ -1894,7 +1945,7 @@ async def detailed(request: Request, person: str = "Sarah", plan_date: str = Non "person": person } 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: # 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() except ValueError: 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", "error": "Invalid date format. Please use YYYY-MM-DD.", "day_totals": {} @@ -1948,11 +1999,11 @@ async def detailed(request: Request, person: str = "Sarah", plan_date: str = Non "plan_date": plan_date_obj } 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 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", "error": "Please provide either a plan date or a template ID.", "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]) async def get_templates_api(db: Session = Depends(get_db)): """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 = [] for t in templates: diff --git a/plan.md b/plan.md index 728489b..76295a1 100644 --- a/plan.md +++ b/plan.md @@ -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 -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. +## Current Test Coverage +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 -- `tests/test_detailed.py` +## New Tests to Add -## 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` -- **Description**: This test will check the `/detailed` route when a valid `plan_date` is provided. -- **Steps**: - 1. Create mock data: a `Food`, a `Meal`, a `MealFood`, and a `Plan` for a specific date. - 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. + meal = Meal(name="Fruit Snack", meal_type="snack", meal_time="Snack") + session.add(meal) + session.commit() + session.refresh(meal) -### 2. Test with `template_id` -- **Description**: This test will check the `/detailed` route when a valid `template_id` is provided. -- **Steps**: - 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. + meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=1.0) + session.add(meal_food) + session.commit() -### 3. Test with Invalid `plan_date` -- **Description**: This test will ensure the route handles an invalid `plan_date` gracefully. -- **Steps**: - 1. Send a GET request to `/detailed` with a non-existent `plan_date`. - 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. + test_date = date.today() + plan = Plan(person="Sarah", date=test_date, meal_id=meal.id, meal_time="Snack") + session.add(plan) + session.commit() -### 4. Test with Invalid `template_id` -- **Description**: This test will ensure the route handles an invalid `template_id` gracefully. -- **Steps**: - 1. Send a GET request to `/detailed` with a non-existent `template_id`. - 2. Assert that the response status code is 200. - 3. Assert that the response contains a message indicating that the template was not found. + # 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 + 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 +``` -## 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: -- Imports for `pytest`, `TestClient`, and the necessary models from `main.py`. -- A `TestClient` instance for making requests to the application. -- Fixtures to set up and tear down the test database for each test function to ensure test isolation. + # 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 +``` -This plan provides a clear path for a developer to implement the required tests. \ No newline at end of file +## 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 \ No newline at end of file diff --git a/tests/test_detailed.py b/tests/test_detailed.py index 0ad66f7..5f6b088 100644 --- a/tests/test_detailed.py +++ b/tests/test_detailed.py @@ -5,8 +5,18 @@ from sqlalchemy.orm import sessionmaker from main import app, get_db, Base, Food, Meal, MealFood, Plan, Template, TemplateMeal from datetime import date, timedelta -# Setup test database -SQLALCHEMY_DATABASE_URL = "sqlite:///./test_detailed.db" +# Setup test database to match Docker environment +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}) 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 "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): # Create mock data 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()}") 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 + def test_detailed_page_with_template_id(client, session): # Create mock data 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 "Banana Smoothie" in response.text + def test_detailed_page_with_invalid_plan_date(client): 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()}") 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 + def test_detailed_page_with_invalid_template_id(client): response = client.get(f"/detailed?template_id=99999") assert response.status_code == 200 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