#!/usr/bin/env python3 """ Database Migration Script for Project Model ========================================== This script safely migrates your existing database to add the Project model and associate QR codes with projects. The script will: 1. Backup your current database 2. Create the new projects table 3. Add project_id column to qr_codes table 4. Create some sample projects (optional) 5. Provide rollback instructions Usage: python migrate_projects.py Requirements: - Your existing Flask app with database models - Database write permissions """ import os import sys import shutil from datetime import datetime from sqlalchemy import create_engine, text, inspect from sqlalchemy.orm import sessionmaker # Add your app to the Python path sys.path.append(os.path.dirname(os.path.abspath(__file__))) try: from app import app, db, User, QRCode, Project from dotenv import load_dotenv except ImportError as e: print(f"Error importing app modules: {e}") print("Make sure this script is in the same directory as your app.py file") sys.exit(1) # Load environment variables load_dotenv() # Configuration BACKUP_DIR = "database_backups" MIGRATION_VERSION = "v1.1_add_project_model" def create_backup_directory(): """Create backup directory if it doesn't exist""" if not os.path.exists(BACKUP_DIR): os.makedirs(BACKUP_DIR) print(f"โœ“ Created backup directory: {BACKUP_DIR}") def backup_database(): """Create a backup of the current database""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_filename = f"backup_{MIGRATION_VERSION}_{timestamp}.db" backup_path = os.path.join(BACKUP_DIR, backup_filename) # Get database path from config db_url = app.config['SQLALCHEMY_DATABASE_URI'] if db_url.startswith('sqlite:///'): # SQLite database db_path = db_url.replace('sqlite:///', '') if os.path.exists(db_path): shutil.copy2(db_path, backup_path) print(f"โœ“ Database backed up to: {backup_path}") return backup_path else: print(f"โš  Database file not found: {db_path}") return None else: print("โš  Non-SQLite databases require manual backup") print("Please ensure you have a recent backup before proceeding") return None def validate_current_database(): """Validate the current database structure and data""" print("\n๐Ÿ” Validating current database...") try: with app.app_context(): # Check if required tables exist inspector = inspect(db.engine) tables = inspector.get_table_names() required_tables = ['users', 'qr_codes'] for table in required_tables: if table not in tables: print(f"โŒ Required table '{table}' not found") return False print(f"โœ“ Table '{table}' exists") # Check current data users = User.query.all() qr_codes = QRCode.query.all() print(f"โœ“ Found {len(users)} users in database") print(f"โœ“ Found {len(qr_codes)} QR codes in database") # Check if projects table already exists if 'projects' in tables: print("โš  Projects table already exists - migration may have been run before") projects = Project.query.all() print(f"โœ“ Found {len(projects)} existing projects") print("โœ“ Database validation passed") return True except Exception as e: print(f"โŒ Database validation failed: {e}") return False def perform_migration(): """Perform the actual migration""" print("\n๐Ÿš€ Starting migration...") try: with app.app_context(): # Create all tables (this will create the projects table if it doesn't exist) db.create_all() print("โœ“ Database tables created/updated") # Check if project_id column exists in qr_codes table inspector = inspect(db.engine) qr_columns = inspector.get_columns('qr_codes') qr_column_names = [col['name'] for col in qr_columns] if 'project_id' not in qr_column_names: # Add project_id column to qr_codes table print("โž• Adding project_id column to qr_codes table...") db.session.execute(text('ALTER TABLE qr_codes ADD COLUMN project_id INTEGER')) db.session.commit() print("โœ“ project_id column added to qr_codes table") else: print("โœ“ project_id column already exists in qr_codes table") print("โœ“ Migration completed successfully!") return True except Exception as e: print(f"โŒ Migration failed: {e}") import traceback traceback.print_exc() return False def create_sample_projects(): """Create some sample projects (optional)""" print("\n๐Ÿ“ Creating sample projects...") try: with app.app_context(): # Get the first admin user to assign as creator admin_user = User.query.filter_by(role='admin').first() creator_id = admin_user.id if admin_user else None # Check if any projects exist existing_projects = Project.query.count() if existing_projects > 0: print(f"โœ“ Found {existing_projects} existing projects - skipping sample creation") return True # Sample projects sample_projects = [ { 'name': 'Office Locations', 'description': 'QR codes for various office locations and facilities' }, { 'name': 'Events', 'description': 'QR codes for company events and meetings' }, { 'name': 'Training Materials', 'description': 'QR codes for training sessions and educational content' } ] created_count = 0 for project_data in sample_projects: # Check if project with this name already exists existing = Project.query.filter_by(name=project_data['name']).first() if not existing: project = Project( name=project_data['name'], description=project_data['description'], created_by=creator_id ) db.session.add(project) created_count += 1 if created_count > 0: db.session.commit() print(f"โœ“ Created {created_count} sample projects") else: print("โœ“ Sample projects already exist") return True except Exception as e: print(f"โŒ Failed to create sample projects: {e}") return False def test_new_functionality(): """Test the new project functionality""" print("\n๐Ÿงช Testing new project functionality...") try: with app.app_context(): # Test Project model projects = Project.query.all() print(f"โœ“ Can query projects: {len(projects)} found") # Test QRCode project relationship qr_codes = QRCode.query.all() for qr in qr_codes[:3]: # Test first 3 QR codes project = qr.project # This should not raise an error print(f"โœ“ QR code '{qr.name}' project: {project.name if project else 'None'}") # Test Project.qr_codes relationship if projects: first_project = projects[0] qr_count = first_project.qr_count print(f"โœ“ Project '{first_project.name}' has {qr_count} QR codes") print("โœ“ New functionality tests passed") return True except Exception as e: print(f"โŒ Functionality tests failed: {e}") import traceback traceback.print_exc() return False def display_summary(): """Display migration summary""" print("\n๐Ÿ“Š MIGRATION SUMMARY") print("=" * 50) try: with app.app_context(): users = User.query.count() projects = Project.query.count() qr_codes = QRCode.query.count() print(f"๐Ÿ‘ฅ Users: {users}") print(f"๐Ÿ“ Projects: {projects}") print(f"๐Ÿ”— QR Codes: {qr_codes}") # Show project distribution if projects > 0: print(f"\n๐Ÿ“ Project Details:") for project in Project.query.all(): print(f" โ€ข {project.name}: {project.qr_count} QR codes") # Show unassigned QR codes unassigned = QRCode.query.filter_by(project_id=None).count() if unassigned > 0: print(f"\nโš ๏ธ {unassigned} QR codes are not assigned to any project") except Exception as e: print(f"Error generating summary: {e}") def rollback_instructions(): """Show rollback instructions""" print("\n๐Ÿ”„ ROLLBACK INSTRUCTIONS") print("=" * 50) print("If you need to rollback this migration:") print("1. Stop your application") print("2. Restore the database backup:") print(" - For SQLite: Replace your database file with the backup") print(" - For other databases: Restore from your backup") print("3. Remove the project_id column from qr_codes table:") print(" ALTER TABLE qr_codes DROP COLUMN project_id;") print("4. Drop the projects table:") print(" DROP TABLE projects;") print("5. Update your app.py to remove Project model and related code") print("6. Restart your application") def main(): """Main migration process""" print("๐Ÿ—ƒ๏ธ PROJECT MODEL MIGRATION") print("=" * 50) print("This will add project functionality to your QR code system.") print("Projects allow you to organize QR codes into logical groups.") print("\nWhat this migration does:") print("โ€ข Creates a new 'projects' table") print("โ€ข Adds 'project_id' column to 'qr_codes' table") print("โ€ข Creates sample projects (optional)") print("โ€ข Updates relationships between models") # Confirm migration response = input("\nProceed with migration? (y/N): ").strip().lower() if response not in ['y', 'yes']: print("Migration cancelled.") sys.exit(0) # Step 1: Create backup directory create_backup_directory() # Step 2: Backup database backup_path = backup_database() if not backup_path: response = input("No backup created. Continue anyway? (y/N): ").strip().lower() if response not in ['y', 'yes']: print("Migration cancelled for safety.") sys.exit(0) # Step 3: Validate current database if not validate_current_database(): print("โŒ Database validation failed. Migration cancelled.") sys.exit(1) # Step 4: Perform migration if not perform_migration(): print("โŒ Migration failed. Please check the errors above.") sys.exit(1) # Step 5: Create sample projects create_sample = input("\nCreate sample projects? (Y/n): ").strip().lower() if create_sample not in ['n', 'no']: create_sample_projects() # Step 6: Test new functionality if not test_new_functionality(): print("โŒ Functionality testing failed. Migration may be incomplete.") sys.exit(1) # Step 7: Display summary display_summary() # Step 8: Show rollback instructions show_rollback = input("\nWould you like to see rollback instructions? (y/N): ").strip().lower() if show_rollback in ['y', 'yes']: rollback_instructions() print("\n๐Ÿš€ Migration completed successfully!") print("You can now:") print("โ€ข Create and manage projects") print("โ€ข Associate QR codes with projects") print("โ€ข Use the project dropdown in QR code forms") print("โ€ข View project statistics and organization") if __name__ == "__main__": main()