refactored

This commit is contained in:
2025-09-29 17:27:59 -07:00
parent 0ed8e3e77d
commit d034c7349d
7 changed files with 0 additions and 485 deletions

View File

@@ -1,29 +0,0 @@
# TDD Development Rules
This document outlines the rules for Test-Driven Development (TDD) using pytest within our containerized environment.
## Principles
- **Test-First Approach**: All new features must begin with writing a failing test.
- **Debug-Driven Tests**: When a bug is identified during debugging, a new pytest must be created to reproduce the bug and validate its fix.
## Pytest Execution
Pytest should be run within the Docker Compose environment to ensure consistency and isolation.
### Running Specific Tests
To run tests for a specific file:
```bash
docker compose build
docker compose run --remove-orphans foodtracker pytest tests/<test filename>
```
### Running All Tests
To run all tests in the project:
```bash
docker compose build
docker compose run --remove-orphans foodtracker pytest

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
"""
Database migration script to add new fields for food sources and meal times.
Run this script to update the database schema.
"""
import sqlite3
from sqlalchemy import create_engine, text
from main import Base, engine
def migrate_database():
"""Add new columns to existing tables"""
# Connect to database
conn = sqlite3.connect('./meal_planner.db')
cursor = conn.cursor()
try:
# Check if source column exists in foods table
cursor.execute("PRAGMA table_info(foods)")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
if 'source' not in column_names:
print("Adding 'source' column to foods table...")
cursor.execute("ALTER TABLE foods ADD COLUMN source TEXT DEFAULT 'manual'")
print("✓ Added source column to foods table")
# Check if meal_time column exists in meals table
cursor.execute("PRAGMA table_info(meals)")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
if 'meal_time' not in column_names:
print("Adding 'meal_time' column to meals table...")
cursor.execute("ALTER TABLE meals ADD COLUMN meal_time TEXT DEFAULT 'Breakfast'")
print("✓ Added meal_time column to meals table")
# Check if meal_time column exists in plans table
cursor.execute("PRAGMA table_info(plans)")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
if 'meal_time' not in column_names:
print("Adding 'meal_time' column to plans table...")
cursor.execute("ALTER TABLE plans ADD COLUMN meal_time TEXT DEFAULT 'Breakfast'")
print("✓ Added meal_time column to plans table")
# Update existing records to have proper source values
print("Updating existing food records with source information...")
# Set source to 'csv' for foods that might have been imported
# Note: This is a heuristic - you may need to adjust based on your data
cursor.execute("""
UPDATE foods
SET source = 'csv'
WHERE name LIKE '%(%'
AND source = 'manual'
""")
conn.commit()
print("✓ Migration completed successfully!")
except Exception as e:
print(f"✗ Migration failed: {e}")
conn.rollback()
finally:
conn.close()
if __name__ == "__main__":
print("Starting database migration...")
migrate_database()
print("Migration script completed.")

View File

@@ -1,61 +0,0 @@
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
import re
import os
DATABASE_URL = f"sqlite:///{os.getenv('DATABASE_PATH', './data')}/meal_planner.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class Food(Base):
__tablename__ = "foods"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
brand = Column(String, default="") # New field
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def migrate_food_brands():
db = next(get_db())
foods = db.query(Food).all()
updated_count = 0
for food in foods:
# Check if the name contains a brand in parentheses
match = re.search(r'\s*\((\w[^)]*)\)$', food.name)
if match:
brand_name = match.group(1).strip()
# If brand is found and not already set, update
if not food.brand and brand_name:
food.brand = brand_name
# Optionally remove brand from name
food.name = re.sub(r'\s*\((\w[^)]*)\)$', '', food.name).strip()
updated_count += 1
print(f"Updated food '{food.name}' with brand '{food.brand}'")
db.commit()
print(f"Migration complete. Updated {updated_count} food brands.")
db.close()
if __name__ == "__main__":
print("Starting food brand migration...")
# This will add the 'brand' column if it doesn't exist.
# Note: For SQLite, ALTER TABLE ADD COLUMN is limited.
# If the column already exists and you're just populating, Base.metadata.create_all() is fine.
# If you're adding a new column to an existing table, you might need Alembic for proper migrations.
# For this task, we'll assume the column is added manually or via a previous step.
# Base.metadata.create_all(bind=engine) # This line should only be run if the table/column is new and not yet in DB
# We need to reflect the existing table schema to ensure the 'brand' column is known
# by SQLAlchemy before attempting to set its value.
# For a real-world scenario, a proper migration tool like Alembic would handle schema changes.
# For this simplified example, we assume the 'brand' column already exists in the DB or will be added manually.
migrate_food_brands()

View File

@@ -1,128 +0,0 @@
#!/usr/bin/env python3
"""
Migration script to convert Plan.date from String to Date type
and convert existing "DayX" values to actual calendar dates.
"""
import sqlite3
from datetime import datetime, timedelta
def migrate_plans_to_dates():
"""Convert existing DayX plans to actual dates"""
# Connect to database
conn = sqlite3.connect('meal_planner.db')
cursor = conn.cursor()
try:
# Check if migration is needed
cursor.execute("PRAGMA table_info(plans)")
columns = cursor.fetchall()
date_column_type = None
for col in columns:
if col[1] == 'date': # column name
date_column_type = col[2] # column type
break
if date_column_type and 'DATE' in date_column_type.upper():
print("Migration already completed - date column is already DATE type")
return
print("Starting migration from String to Date...")
# Create backup
print("Creating backup of plans table...")
cursor.execute("""
CREATE TABLE plans_backup AS
SELECT * FROM plans
""")
# Add new date column
print("Adding new date column...")
cursor.execute("""
ALTER TABLE plans ADD COLUMN date_new DATE
""")
# Convert DayX to actual dates (starting from today as Day1)
print("Converting DayX values to dates...")
today = datetime.now().date()
# Get all unique day values
cursor.execute("SELECT DISTINCT date FROM plans WHERE date LIKE 'Day%'")
day_values = cursor.fetchall()
for (day_str,) in day_values:
if day_str.startswith('Day'):
try:
day_num = int(day_str[3:]) # Extract number from "Day1", "Day2", etc.
# Convert to date (Day1 = today, Day2 = tomorrow, etc.)
actual_date = today + timedelta(days=day_num - 1)
cursor.execute("""
UPDATE plans
SET date_new = ?
WHERE date = ?
""", (actual_date.isoformat(), day_str))
print(f"Converted {day_str} to {actual_date.isoformat()}")
except (ValueError, IndexError) as e:
print(f"Error converting {day_str}: {e}")
# Handle any non-DayX dates (if they exist)
cursor.execute("""
UPDATE plans
SET date_new = date
WHERE date NOT LIKE 'Day%' AND date_new IS NULL
""")
# Recreate table with new structure (SQLite doesn't support DROP COLUMN with indexes)
print("Recreating table with new structure...")
cursor.execute("""
CREATE TABLE plans_new (
id INTEGER PRIMARY KEY,
person VARCHAR NOT NULL,
date DATE NOT NULL,
meal_id INTEGER NOT NULL REFERENCES meals(id)
)
""")
# Copy data to new table
cursor.execute("""
INSERT INTO plans_new (id, person, date, meal_id)
SELECT id, person, date_new, meal_id FROM plans
""")
# Drop old table and rename new one
cursor.execute("DROP TABLE plans")
cursor.execute("ALTER TABLE plans_new RENAME TO plans")
# Create index on new date column
cursor.execute("CREATE INDEX ix_plans_date ON plans(date)")
conn.commit()
print("Migration completed successfully!")
# Show summary
cursor.execute("SELECT COUNT(*) FROM plans")
total_plans = cursor.fetchone()[0]
print(f"Total plans migrated: {total_plans}")
except Exception as e:
print(f"Migration failed: {e}")
conn.rollback()
# Restore backup if something went wrong
try:
cursor.execute("DROP TABLE IF EXISTS plans")
cursor.execute("ALTER TABLE plans_backup RENAME TO plans")
print("Restored from backup")
except Exception as backup_error:
print(f"Failed to restore backup: {backup_error}")
finally:
conn.close()
if __name__ == "__main__":
migrate_plans_to_dates()

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""
Migration script to add is_modified column to tracked_days table.
Run this script to add the modification tracking functionality.
"""
import sqlite3
import os
def migrate_add_is_modified():
"""Add is_modified column to tracked_days table"""
db_path = "./meal_planner.db"
if not os.path.exists(db_path):
print("Database file not found. Please run the main application first to create the database.")
return
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check if column already exists
cursor.execute("PRAGMA table_info(tracked_days)")
columns = cursor.fetchall()
column_names = [col[1] for col in columns]
if 'is_modified' in column_names:
print("Column 'is_modified' already exists in tracked_days table.")
conn.close()
return
print("Adding is_modified column to tracked_days table...")
# Add the column with default value 0 (False)
cursor.execute("ALTER TABLE tracked_days ADD COLUMN is_modified INTEGER DEFAULT 0")
# Update existing rows to have is_modified = 0 (not modified)
cursor.execute("UPDATE tracked_days SET is_modified = 0 WHERE is_modified IS NULL")
conn.commit()
conn.close()
print("Migration completed successfully!")
print("Added column: is_modified (INTEGER, DEFAULT 0)")
except Exception as e:
print(f"Migration failed: {e}")
if 'conn' in locals():
conn.close()
if __name__ == "__main__":
migrate_add_is_modified()

View File

@@ -1,50 +0,0 @@
#!/usr/bin/env python3
"""
Migration script to add tracker tables for meal tracking functionality.
Run this script to add the necessary tables for the Tracker tab.
"""
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Date
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
# Database setup
DATABASE_URL = f"sqlite:///{os.getenv('DATABASE_PATH', './data')}/meal_planner.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# New models for tracker functionality
class TrackedDay(Base):
"""Represents a day being tracked (separate from planned days)"""
__tablename__ = "tracked_days"
id = Column(Integer, primary_key=True, index=True)
person = Column(String, index=True) # Sarah or Stuart
date = Column(Date, index=True) # Date being tracked
class TrackedMeal(Base):
"""Represents a meal tracked for a specific day"""
__tablename__ = "tracked_meals"
id = Column(Integer, primary_key=True, index=True)
tracked_day_id = Column(Integer) # Will add FK constraint later
meal_id = Column(Integer) # Will add FK constraint later
meal_time = Column(String) # Breakfast, Lunch, Dinner, Snack 1, Snack 2, Beverage 1, Beverage 2
quantity = Column(Float, default=1.0) # Quantity multiplier (e.g., 1.5 for 1.5 servings)
def migrate_tracker_tables():
"""Create the new tracker tables"""
try:
print("Creating tracker tables...")
Base.metadata.create_all(bind=engine)
print("Migration completed successfully!")
print("New tables created:")
print("- tracked_days: Stores individual days being tracked")
print("- tracked_meals: Stores meals for each tracked day")
except Exception as e:
print(f"Migration failed: {e}")
if __name__ == "__main__":
migrate_tracker_tables()

92
plan.md
View File

@@ -1,92 +0,0 @@
# Plan for Adding New Tests to test_detailed.py
## 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
## 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
## New Tests to Add
### 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)
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
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
```
### 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)
# 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
```
## 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