mirror of
https://github.com/sstent/foodplanner.git
synced 2026-01-25 11:11:36 +00:00
refactored
This commit is contained in:
29
TDD_RULES.md
29
TDD_RULES.md
@@ -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
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
92
plan.md
@@ -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
|
||||
Reference in New Issue
Block a user