Phase 3.1: Enhanced Chore Logging and Reporting System
This commit is contained in:
53
backend/migrations/001_add_birthday_field.py
Normal file
53
backend/migrations/001_add_birthday_field.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Database migration: Add birthday field to users table.
|
||||
|
||||
This script safely adds a birthday column to the users table.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from app.core.database import engine, SessionLocal
|
||||
from sqlalchemy import text
|
||||
|
||||
def add_birthday_column():
|
||||
"""Add birthday column to users table."""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
print("=" * 70)
|
||||
print("MIGRATION: Add Birthday Field to Users")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Check if column already exists
|
||||
result = db.execute(text("PRAGMA table_info(users)"))
|
||||
columns = [row[1] for row in result.fetchall()]
|
||||
|
||||
if 'birthday' in columns:
|
||||
print("✅ Birthday column already exists. No migration needed.")
|
||||
return
|
||||
|
||||
print("📋 Adding birthday column to users table...")
|
||||
|
||||
# Add birthday column (DATE type, nullable)
|
||||
db.execute(text("ALTER TABLE users ADD COLUMN birthday DATE"))
|
||||
db.commit()
|
||||
|
||||
print("✅ Birthday column added successfully!")
|
||||
print()
|
||||
print("Migration complete!")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
add_birthday_column()
|
||||
81
backend/migrations/002_add_multi_user_chores.py
Normal file
81
backend/migrations/002_add_multi_user_chores.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Database migration: Add support for multiple users per chore.
|
||||
|
||||
This creates a chore_assignments junction table to allow
|
||||
multiple users to be assigned to a single chore.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from app.core.database import engine, SessionLocal
|
||||
from sqlalchemy import text
|
||||
|
||||
def create_chore_assignments_table():
|
||||
"""Create chore_assignments table for many-to-many relationship."""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
print("=" * 70)
|
||||
print("MIGRATION: Add Multiple Users Per Chore Support")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Check if table already exists
|
||||
result = db.execute(text("SELECT name FROM sqlite_master WHERE type='table' AND name='chore_assignments'"))
|
||||
if result.fetchone():
|
||||
print("✅ chore_assignments table already exists. No migration needed.")
|
||||
return
|
||||
|
||||
print("📋 Creating chore_assignments table...")
|
||||
|
||||
# Create junction table
|
||||
db.execute(text("""
|
||||
CREATE TABLE chore_assignments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chore_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
completed_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (chore_id) REFERENCES chores (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
UNIQUE(chore_id, user_id)
|
||||
)
|
||||
"""))
|
||||
|
||||
print("✅ chore_assignments table created!")
|
||||
print()
|
||||
|
||||
print("📋 Migrating existing chore assignments...")
|
||||
|
||||
# Migrate existing assigned_user_id data to new table
|
||||
# For each chore with an assigned_user_id, create an assignment record
|
||||
result = db.execute(text("""
|
||||
INSERT INTO chore_assignments (chore_id, user_id, completed_at)
|
||||
SELECT id, assigned_user_id, completed_at
|
||||
FROM chores
|
||||
WHERE assigned_user_id IS NOT NULL
|
||||
"""))
|
||||
|
||||
migrated_count = result.rowcount
|
||||
db.commit()
|
||||
|
||||
print(f"✅ Migrated {migrated_count} existing chore assignments!")
|
||||
print()
|
||||
print("⚠️ NOTE: The assigned_user_id column in chores table is now deprecated.")
|
||||
print(" It will be kept for backwards compatibility but not used.")
|
||||
print()
|
||||
print("Migration complete!")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_chore_assignments_table()
|
||||
56
backend/migrations/003_add_image_fields.py
Normal file
56
backend/migrations/003_add_image_fields.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Add image fields to users and chores tables
|
||||
|
||||
Revision ID: 003
|
||||
Created: 2026-02-02
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def get_db_path():
|
||||
"""Get database path"""
|
||||
# Use absolute path
|
||||
return r"D:\Hosted\familyhub\backend\data\family_hub.db"
|
||||
|
||||
def upgrade():
|
||||
"""Add image fields to users and chores"""
|
||||
db_path = get_db_path()
|
||||
print(f"Connecting to database: {db_path}")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"ERROR: Database file not found at {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Add avatar_url to users table
|
||||
cursor.execute("""
|
||||
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);
|
||||
""")
|
||||
print("✓ Added avatar_url to users table")
|
||||
|
||||
# Add image_url to chores table
|
||||
cursor.execute("""
|
||||
ALTER TABLE chores ADD COLUMN image_url VARCHAR(500);
|
||||
""")
|
||||
print("✓ Added image_url to chores table")
|
||||
|
||||
conn.commit()
|
||||
print("\n✅ Migration 003 completed successfully!")
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e).lower():
|
||||
print(f"⚠️ Column already exists: {e}")
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def downgrade():
|
||||
"""Remove image fields"""
|
||||
print("Note: SQLite doesn't support DROP COLUMN easily.")
|
||||
print("To rollback, you would need to recreate the tables.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
upgrade()
|
||||
40
backend/migrations/004_add_assignment_type.py
Normal file
40
backend/migrations/004_add_assignment_type.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Add assignment_type to chores table."""
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import from app
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
def upgrade():
|
||||
"""Add assignment_type column to chores table."""
|
||||
db_path = Path(__file__).parent.parent / "data" / "family_hub.db"
|
||||
|
||||
print(f"Connecting to database: {db_path}")
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Add assignment_type column (default 'any_one' for backward compatibility)
|
||||
print("Adding assignment_type column to chores table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE chores
|
||||
ADD COLUMN assignment_type VARCHAR(20) DEFAULT 'any_one'
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
print("✅ Successfully added assignment_type column")
|
||||
print(" Values: 'any_one' (only one person needs to complete)")
|
||||
print(" 'all_assigned' (all assigned people must complete)")
|
||||
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e).lower():
|
||||
print("⚠️ Column assignment_type already exists, skipping...")
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
upgrade()
|
||||
print("\n✅ Migration completed!")
|
||||
79
backend/migrations/005_add_completion_logs.py
Normal file
79
backend/migrations/005_add_completion_logs.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Add chore_completion_logs table for comprehensive chore tracking."""
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import from app
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
def upgrade():
|
||||
"""Create chore_completion_logs table."""
|
||||
db_path = Path(__file__).parent.parent / "data" / "family_hub.db"
|
||||
|
||||
print(f"Connecting to database: {db_path}")
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if table already exists
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='chore_completion_logs'
|
||||
""")
|
||||
|
||||
if cursor.fetchone():
|
||||
print("⚠️ Table chore_completion_logs already exists, skipping...")
|
||||
return
|
||||
|
||||
# Create chore_completion_logs table
|
||||
print("Creating chore_completion_logs table...")
|
||||
cursor.execute("""
|
||||
CREATE TABLE chore_completion_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chore_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
completed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
notes TEXT,
|
||||
verified_by_user_id INTEGER,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (chore_id) REFERENCES chores(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (verified_by_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for better query performance
|
||||
print("Creating indexes...")
|
||||
cursor.execute("""
|
||||
CREATE INDEX idx_completion_logs_chore_id
|
||||
ON chore_completion_logs(chore_id)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX idx_completion_logs_user_id
|
||||
ON chore_completion_logs(user_id)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX idx_completion_logs_completed_at
|
||||
ON chore_completion_logs(completed_at)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
print("✅ Successfully created chore_completion_logs table")
|
||||
print(" Features:")
|
||||
print(" - Tracks every chore completion instance")
|
||||
print(" - Optional notes for each completion")
|
||||
print(" - Optional verification by another user")
|
||||
print(" - Full historical data for reporting")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error during migration: {e}")
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
upgrade()
|
||||
print("\n✅ Migration completed!")
|
||||
37
backend/migrations/add_user_fields.py
Normal file
37
backend/migrations/add_user_fields.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Add discord_id and profile_picture to users table."""
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
def migrate():
|
||||
"""Add new columns to users table."""
|
||||
db_path = Path("/app/data/familyhub.db")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Add discord_id column
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN discord_id VARCHAR(100)")
|
||||
print("✓ Added discord_id column")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e):
|
||||
print("✓ discord_id column already exists")
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
# Add profile_picture column
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN profile_picture VARCHAR(500)")
|
||||
print("✓ Added profile_picture column")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e):
|
||||
print("✓ profile_picture column already exists")
|
||||
else:
|
||||
raise
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✓ Migration complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
227
backend/migrations/init_db.py
Normal file
227
backend/migrations/init_db.py
Normal file
@@ -0,0 +1,227 @@
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Add the app directory to the path
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.db.base import Base
|
||||
from app.models.user import User
|
||||
from app.models.chore import Chore
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
# Database setup
|
||||
DATABASE_URL = "sqlite:///./family_hub.db"
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def init_db():
|
||||
"""Initialize database with schema and demo data"""
|
||||
print("Creating all tables...")
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# Check if users already exist
|
||||
existing_users = db.query(User).count()
|
||||
if existing_users > 0:
|
||||
print(f"Database already has {existing_users} users. Skipping initialization.")
|
||||
return
|
||||
|
||||
print("Creating family members...")
|
||||
# Create family members
|
||||
users_data = [
|
||||
{"username": "lou", "email": "lou@family.local", "full_name": "Lou", "is_admin": False},
|
||||
{"username": "jess", "email": "jess@family.local", "full_name": "Jess", "is_admin": True},
|
||||
{"username": "william", "email": "william@family.local", "full_name": "William", "is_admin": False},
|
||||
{"username": "xander", "email": "xander@family.local", "full_name": "Xander", "is_admin": False},
|
||||
{"username": "bella", "email": "bella@family.local", "full_name": "Bella", "is_admin": False},
|
||||
]
|
||||
|
||||
users = {}
|
||||
for user_data in users_data:
|
||||
user = User(
|
||||
username=user_data["username"],
|
||||
email=user_data["email"],
|
||||
full_name=user_data["full_name"],
|
||||
hashed_password=get_password_hash("password123"),
|
||||
is_admin=user_data["is_admin"],
|
||||
is_active=True,
|
||||
discord_id=None,
|
||||
profile_picture=None
|
||||
)
|
||||
db.add(user)
|
||||
users[user_data["username"]] = user
|
||||
print(f" + Created user: {user_data['full_name']} ({user_data['username']})")
|
||||
|
||||
db.commit()
|
||||
|
||||
# Refresh to get IDs
|
||||
for user in users.values():
|
||||
db.refresh(user)
|
||||
|
||||
print("\nCreating demo chores...")
|
||||
# Create demo chores with various statuses and assignments
|
||||
today = datetime.now()
|
||||
|
||||
demo_chores = [
|
||||
# Daily chores
|
||||
{
|
||||
"title": "Feed the Dog",
|
||||
"description": "Give Rex his breakfast and dinner",
|
||||
"room": "Kitchen",
|
||||
"frequency": "daily",
|
||||
"assignment_type": "any_one",
|
||||
"status": "completed",
|
||||
"assigned_user_id": users["william"].id,
|
||||
"due_date": today.replace(hour=23, minute=59, second=59),
|
||||
"completed_at": today.replace(hour=8, minute=30)
|
||||
},
|
||||
{
|
||||
"title": "Take Out Trash",
|
||||
"description": "Empty all bins and take to curb",
|
||||
"room": "Kitchen",
|
||||
"frequency": "daily",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["xander"].id,
|
||||
"due_date": today.replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
{
|
||||
"title": "Wash Dishes",
|
||||
"description": "Load and run the dishwasher",
|
||||
"room": "Kitchen",
|
||||
"frequency": "daily",
|
||||
"assignment_type": "any_one",
|
||||
"status": "in_progress",
|
||||
"assigned_user_id": users["bella"].id,
|
||||
"due_date": today.replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
# Weekly chores
|
||||
{
|
||||
"title": "Vacuum Living Room",
|
||||
"description": "Vacuum carpets and under furniture",
|
||||
"room": "Living Room",
|
||||
"frequency": "weekly",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["lou"].id,
|
||||
"due_date": (today + timedelta(days=3)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
{
|
||||
"title": "Clean Bathrooms",
|
||||
"description": "Scrub toilets, sinks, and showers",
|
||||
"room": "Bathroom",
|
||||
"frequency": "weekly",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["jess"].id,
|
||||
"due_date": (today + timedelta(days=2)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
{
|
||||
"title": "Mow Lawn",
|
||||
"description": "Mow front and back yards",
|
||||
"room": "Yard",
|
||||
"frequency": "weekly",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["william"].id,
|
||||
"due_date": (today + timedelta(days=5)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
# Monthly chores
|
||||
{
|
||||
"title": "Change Air Filters",
|
||||
"description": "Replace HVAC air filters throughout house",
|
||||
"room": "Utility Room",
|
||||
"frequency": "monthly",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["lou"].id,
|
||||
"due_date": (today + timedelta(days=15)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
{
|
||||
"title": "Deep Clean Fridge",
|
||||
"description": "Empty, wipe down shelves, check expiry dates",
|
||||
"room": "Kitchen",
|
||||
"frequency": "monthly",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["jess"].id,
|
||||
"due_date": (today + timedelta(days=20)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
# On-trigger chores (no specific schedule)
|
||||
{
|
||||
"title": "Water Plants",
|
||||
"description": "Check soil moisture and water as needed",
|
||||
"room": "Living Room",
|
||||
"frequency": "on_trigger",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["bella"].id,
|
||||
"due_date": today.replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
{
|
||||
"title": "Sort Recycling",
|
||||
"description": "Separate recyclables into proper bins",
|
||||
"room": "Garage",
|
||||
"frequency": "on_trigger",
|
||||
"assignment_type": "any_one",
|
||||
"status": "completed",
|
||||
"assigned_user_id": users["xander"].id,
|
||||
"due_date": (today - timedelta(days=1)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": (today - timedelta(days=1)).replace(hour=14, minute=20)
|
||||
},
|
||||
# Some overdue chores
|
||||
{
|
||||
"title": "Organize Garage",
|
||||
"description": "Sort tools and clean workspace",
|
||||
"room": "Garage",
|
||||
"frequency": "monthly",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["william"].id,
|
||||
"due_date": (today - timedelta(days=3)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
{
|
||||
"title": "Clean Windows",
|
||||
"description": "Wash all interior and exterior windows",
|
||||
"room": "Whole House",
|
||||
"frequency": "monthly",
|
||||
"assignment_type": "any_one",
|
||||
"status": "pending",
|
||||
"assigned_user_id": users["xander"].id,
|
||||
"due_date": (today - timedelta(days=1)).replace(hour=23, minute=59, second=59),
|
||||
"completed_at": None
|
||||
},
|
||||
]
|
||||
|
||||
for chore_data in demo_chores:
|
||||
chore = Chore(**chore_data, created_by=users["jess"].id)
|
||||
db.add(chore)
|
||||
status_marker = "[DONE]" if chore_data["status"] == "completed" else "[WIP]" if chore_data["status"] == "in_progress" else "[TODO]"
|
||||
print(f" {status_marker} {chore_data['title']} - {chore_data['frequency']} ({chore_data['room']})")
|
||||
|
||||
db.commit()
|
||||
print(f"\n[SUCCESS] Database initialized with {len(users_data)} users and {len(demo_chores)} demo chores!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error initializing database: {e}")
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
Reference in New Issue
Block a user