mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-05 03:01:13 -06:00
- Add organized screenshot sections for better visual presentation - Include all 12 available screenshots from assets/screenshots/ - Group screenshots into logical categories: * Core Application Views (Dashboard, Projects, Tasks, Clients) * Management & Analytics (Reports, Visual Analytics, Task Management, Admin) * Data Entry & Creation (Log Time, New Task, New Client, New Project) - Improve visual layout with proper spacing and responsive design - Enhance user experience by showcasing full application capabilities
384 lines
13 KiB
Python
384 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Comprehensive Database Migration Script for TimeTracker
|
|
This script can migrate ANY existing database to the new Flask-Migrate system
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import sqlite3
|
|
import psycopg2
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import shutil
|
|
|
|
def run_command(command, description, capture_output=True):
|
|
"""Run a command and handle errors"""
|
|
print(f"\n--- {description} ---")
|
|
print(f"Running: {command}")
|
|
|
|
try:
|
|
if capture_output:
|
|
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
|
print(f"✓ {description} completed successfully")
|
|
if result.stdout:
|
|
print(result.stdout)
|
|
return True
|
|
else:
|
|
subprocess.run(command, shell=True, check=True)
|
|
print(f"✓ {description} completed successfully")
|
|
return True
|
|
except subprocess.CmdProcessError as e:
|
|
print(f"✗ {description} failed:")
|
|
print(f"Error code: {e.returncode}")
|
|
if e.stdout:
|
|
print(f"STDOUT: {e.stdout}")
|
|
if e.stderr:
|
|
print(f"STDERR: {e.stderr}")
|
|
return False
|
|
|
|
def check_flask_migrate_installed():
|
|
"""Check if Flask-Migrate is properly installed"""
|
|
try:
|
|
import flask_migrate
|
|
print("✓ Flask-Migrate is installed")
|
|
return True
|
|
except ImportError:
|
|
print("✗ Flask-Migrate is not installed")
|
|
print("Please install it with: pip install Flask-Migrate")
|
|
return False
|
|
|
|
def detect_database_type():
|
|
"""Detect the type of database being used"""
|
|
db_url = os.getenv('DATABASE_URL', '')
|
|
|
|
if db_url.startswith('postgresql'):
|
|
return 'postgresql', db_url
|
|
elif db_url.startswith('sqlite'):
|
|
return 'sqlite', db_url
|
|
else:
|
|
# Check for common database files
|
|
if os.path.exists('instance/timetracker.db'):
|
|
return 'sqlite', 'sqlite:///instance/timetracker.db'
|
|
elif os.path.exists('timetracker.db'):
|
|
return 'sqlite', 'sqlite:///timetracker.db'
|
|
else:
|
|
return 'unknown', None
|
|
|
|
def backup_database(db_type, db_url):
|
|
"""Create a comprehensive backup of the current database"""
|
|
print("\n--- Creating Database Backup ---")
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
|
|
if db_type == 'postgresql':
|
|
backup_file = f"backup_postgresql_{timestamp}.dump"
|
|
backup_cmd = f'pg_dump --format=custom --dbname="{db_url}" --file={backup_file}'
|
|
print(f"PostgreSQL database detected")
|
|
print(f"Running: {backup_cmd}")
|
|
|
|
try:
|
|
subprocess.run(backup_cmd, shell=True, check=True)
|
|
print(f"✓ Database backed up to: {backup_file}")
|
|
return backup_file
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"✗ PostgreSQL backup failed: {e}")
|
|
print("Please ensure pg_dump is available and you have proper permissions")
|
|
return None
|
|
|
|
elif db_type == 'sqlite':
|
|
# Find the actual database file
|
|
if 'instance/timetracker.db' in db_url:
|
|
db_file = 'instance/timetracker.db'
|
|
elif 'timetracker.db' in db_url:
|
|
db_file = 'timetracker.db'
|
|
else:
|
|
db_file = db_url.replace('sqlite:///', '')
|
|
|
|
if os.path.exists(db_file):
|
|
backup_file = f"backup_sqlite_{timestamp}.db"
|
|
shutil.copy2(db_file, backup_file)
|
|
print(f"✓ SQLite database backed up to: {backup_file}")
|
|
return backup_file
|
|
else:
|
|
print(f"✗ SQLite database file not found: {db_file}")
|
|
return None
|
|
else:
|
|
print("✗ Unknown database type")
|
|
return None
|
|
|
|
def analyze_existing_schema(db_type, db_url):
|
|
"""Analyze the existing database schema to understand what needs to be migrated"""
|
|
print("\n--- Analyzing Existing Database Schema ---")
|
|
|
|
if db_type == 'postgresql':
|
|
try:
|
|
conn = psycopg2.connect(db_url)
|
|
cursor = conn.cursor()
|
|
|
|
# Get list of existing tables
|
|
cursor.execute("""
|
|
SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
ORDER BY table_name
|
|
""")
|
|
existing_tables = [row[0] for row in cursor.fetchall()]
|
|
|
|
# Get table schemas
|
|
table_schemas = {}
|
|
for table in existing_tables:
|
|
cursor.execute(f"""
|
|
SELECT column_name, data_type, is_nullable, column_default
|
|
FROM information_schema.columns
|
|
WHERE table_name = '{table}'
|
|
ORDER BY ordinal_position
|
|
""")
|
|
columns = cursor.fetchall()
|
|
table_schemas[table] = columns
|
|
|
|
conn.close()
|
|
|
|
print(f"✓ Found {len(existing_tables)} existing tables: {existing_tables}")
|
|
return existing_tables, table_schemas
|
|
|
|
except Exception as e:
|
|
print(f"✗ Error analyzing PostgreSQL schema: {e}")
|
|
return [], {}
|
|
|
|
elif db_type == 'sqlite':
|
|
try:
|
|
# Find the actual database file
|
|
if 'instance/timetracker.db' in db_url:
|
|
db_file = 'instance/timetracker.db'
|
|
elif 'timetracker.db' in db_url:
|
|
db_file = 'timetracker.db'
|
|
else:
|
|
db_file = db_url.replace('sqlite:///', '')
|
|
|
|
if not os.path.exists(db_file):
|
|
print(f"✗ SQLite database file not found: {db_file}")
|
|
return [], {}
|
|
|
|
conn = sqlite3.connect(db_file)
|
|
cursor = conn.cursor()
|
|
|
|
# Get list of existing tables
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
existing_tables = [row[0] for row in cursor.fetchall()]
|
|
|
|
# Get table schemas
|
|
table_schemas = {}
|
|
for table in existing_tables:
|
|
cursor.execute(f"PRAGMA table_info({table})")
|
|
columns = cursor.fetchall()
|
|
table_schemas[table] = columns
|
|
|
|
conn.close()
|
|
|
|
print(f"✓ Found {len(existing_tables)} existing tables: {existing_tables}")
|
|
return existing_tables, table_schemas
|
|
|
|
except Exception as e:
|
|
print(f"✗ Error analyzing SQLite schema: {e}")
|
|
return [], {}
|
|
|
|
return [], {}
|
|
|
|
def create_migration_strategy(existing_tables, table_schemas):
|
|
"""Create a migration strategy based on existing schema"""
|
|
print("\n--- Creating Migration Strategy ---")
|
|
|
|
# Define expected tables and their priority
|
|
expected_tables = [
|
|
'clients', 'users', 'projects', 'tasks', 'time_entries',
|
|
'settings', 'invoices', 'invoice_items'
|
|
]
|
|
|
|
missing_tables = [table for table in expected_tables if table not in existing_tables]
|
|
existing_but_modified = []
|
|
|
|
if missing_tables:
|
|
print(f"Tables to create: {missing_tables}")
|
|
|
|
# Check for schema modifications
|
|
for table in existing_tables:
|
|
if table in expected_tables:
|
|
# This table exists, check if it needs modifications
|
|
print(f"Table '{table}' exists - will preserve data")
|
|
|
|
return missing_tables, existing_but_modified
|
|
|
|
def initialize_flask_migrate():
|
|
"""Initialize Flask-Migrate if not already initialized"""
|
|
migrations_dir = Path("migrations")
|
|
|
|
if not migrations_dir.exists():
|
|
print("Initializing Flask-Migrate...")
|
|
return run_command("flask db init", "Initialize Flask-Migrate")
|
|
else:
|
|
print("✓ Migrations directory already exists")
|
|
return True
|
|
|
|
def create_initial_migration():
|
|
"""Create the initial migration that captures the current state"""
|
|
versions_dir = Path("migrations/versions")
|
|
|
|
if not versions_dir.exists() or not list(versions_dir.glob("*.py")):
|
|
print("Creating initial migration...")
|
|
return run_command("flask db migrate -m 'Initial schema from existing database'", "Create initial migration")
|
|
else:
|
|
print("✓ Initial migration already exists")
|
|
return True
|
|
|
|
def stamp_database_with_current_revision():
|
|
"""Mark the database as being at the current migration revision"""
|
|
print("Stamping database with current migration revision...")
|
|
return run_command("flask db stamp head", "Stamp database with current revision")
|
|
|
|
def apply_migrations():
|
|
"""Apply any pending migrations"""
|
|
return run_command("flask db upgrade", "Apply database migrations")
|
|
|
|
def verify_migration_success():
|
|
"""Verify that the migration was successful"""
|
|
print("\n--- Verifying Migration Success ---")
|
|
|
|
# Check migration status
|
|
print("Current migration status:")
|
|
run_command("flask db current", "Show current migration", capture_output=False)
|
|
|
|
# Check migration history
|
|
print("\nMigration history:")
|
|
run_command("flask db history", "Show migration history", capture_output=False)
|
|
|
|
# Test database connection
|
|
try:
|
|
from app import create_app, db
|
|
app = create_app()
|
|
with app.app_context():
|
|
# Try to query the database
|
|
result = db.session.execute(db.text("SELECT 1"))
|
|
print("✓ Database connection test successful")
|
|
return True
|
|
except Exception as e:
|
|
print(f"✗ Database connection test failed: {e}")
|
|
return False
|
|
|
|
def create_data_migration_script(existing_tables, table_schemas):
|
|
"""Create a data migration script for any existing data"""
|
|
print("\n--- Creating Data Migration Script ---")
|
|
|
|
script_content = """#!/usr/bin/env python3
|
|
\"\"\"
|
|
Data Migration Script for Existing Database
|
|
This script handles data migration from old schema to new schema
|
|
\"\"\"
|
|
|
|
from app import create_app, db
|
|
from app.models import User, Project, TimeEntry, Task, Settings, Invoice, Client
|
|
|
|
def migrate_existing_data():
|
|
\"\"\"Migrate existing data to new schema\"\"\"
|
|
app = create_app()
|
|
|
|
with app.app_context():
|
|
print("Starting data migration...")
|
|
|
|
# Add your data migration logic here
|
|
# Example: Migrate old client names to new client table
|
|
|
|
print("Data migration completed")
|
|
|
|
if __name__ == "__main__":
|
|
migrate_existing_data()
|
|
"""
|
|
|
|
script_path = "migrations/migrate_existing_data.py"
|
|
with open(script_path, 'w') as f:
|
|
f.write(script_content)
|
|
|
|
print(f"✓ Data migration script created: {script_path}")
|
|
return script_path
|
|
|
|
def main():
|
|
"""Main migration function"""
|
|
print("=== TimeTracker Comprehensive Database Migration ===")
|
|
print("This script will migrate ANY existing database to the new Flask-Migrate system")
|
|
|
|
# Check prerequisites
|
|
if not check_flask_migrate_installed():
|
|
sys.exit(1)
|
|
|
|
# Detect database type
|
|
db_type, db_url = detect_database_type()
|
|
if not db_url:
|
|
print("✗ Could not detect database configuration")
|
|
print("Please set DATABASE_URL environment variable or ensure database files exist")
|
|
sys.exit(1)
|
|
|
|
print(f"✓ Detected {db_type} database")
|
|
|
|
# Create backup
|
|
print("\n⚠️ IMPORTANT: Creating database backup before proceeding...")
|
|
backup_file = backup_database(db_type, db_url)
|
|
if not backup_file:
|
|
response = input("Failed to create backup. Continue anyway? (y/N): ")
|
|
if response.lower() != 'y':
|
|
print("Migration cancelled.")
|
|
sys.exit(1)
|
|
|
|
# Analyze existing schema
|
|
existing_tables, table_schemas = analyze_existing_schema(db_type, db_url)
|
|
|
|
# Create migration strategy
|
|
missing_tables, modified_tables = create_migration_strategy(existing_tables, table_schemas)
|
|
|
|
# Initialize Flask-Migrate
|
|
if not initialize_flask_migrate():
|
|
print("Failed to initialize Flask-Migrate")
|
|
sys.exit(1)
|
|
|
|
# Create initial migration
|
|
if not create_initial_migration():
|
|
print("Failed to create initial migration")
|
|
sys.exit(1)
|
|
|
|
# Create data migration script if needed
|
|
if existing_tables:
|
|
create_data_migration_script(existing_tables, table_schemas)
|
|
|
|
# Stamp database with current revision
|
|
if not stamp_database_with_current_revision():
|
|
print("Failed to stamp database")
|
|
sys.exit(1)
|
|
|
|
# Apply any pending migrations
|
|
if not apply_migrations():
|
|
print("Failed to apply migrations")
|
|
sys.exit(1)
|
|
|
|
# Verify migration success
|
|
if not verify_migration_success():
|
|
print("Migration verification failed")
|
|
sys.exit(1)
|
|
|
|
# Show final status
|
|
print("\n=== Migration Complete ===")
|
|
print("🎉 Your database has been successfully migrated to Flask-Migrate!")
|
|
|
|
print("\nNext steps:")
|
|
print("1. Test your application to ensure everything works")
|
|
print("2. Review the generated migration files in migrations/versions/")
|
|
print("3. If you have existing data, run the data migration script:")
|
|
print(" python migrations/migrate_existing_data.py")
|
|
print("4. For future schema changes, use:")
|
|
print(" - flask db migrate -m 'Description of changes'")
|
|
print(" - flask db upgrade")
|
|
|
|
print("\nBackup created at:", backup_file)
|
|
print("For more information, see: migrations/README.md")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|