""" Migration: Dynamic QR Code Support ==================================== Applies the following database changes required for the Dynamic QR feature: 1. Adds qr_type column to qr_codes (VARCHAR 20, default 'standard') 2. Creates qr_code_locations table 3. Makes qr_codes.location nullable (was NOT NULL) 4. Makes qr_codes.location_address nullable (was NOT NULL) Usage (run once from the project root): python tools/migration_dynamic_qr_locations.py Fully idempotent — safe to run multiple times without side effects. """ import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import app, db def run_migration(): with app.app_context(): from sqlalchemy import text, inspect as sa_inspect inspector = sa_inspect(db.engine) # ---------------------------------------------------------------- # Step 1: Add qr_type column to qr_codes if it does not exist yet # ---------------------------------------------------------------- existing_cols = {c['name'] for c in inspector.get_columns('qr_codes')} if 'qr_type' not in existing_cols: with db.engine.connect() as conn: conn.execute(text( "ALTER TABLE qr_codes " "ADD COLUMN qr_type VARCHAR(20) NOT NULL DEFAULT 'standard'" )) conn.commit() print("✅ Added column: qr_codes.qr_type (default = 'standard')") else: print("ℹ️ Column qr_codes.qr_type already exists — skipped.") # ---------------------------------------------------------------- # Step 2: Create qr_code_locations table if it does not exist yet # ---------------------------------------------------------------- existing_tables = set(inspector.get_table_names()) if 'qr_code_locations' not in existing_tables: from models.qrcode import QRCodeLocation QRCodeLocation.__table__.create(db.engine, checkfirst=True) print("✅ Created table: qr_code_locations") else: print("ℹ️ Table qr_code_locations already exists — skipped.") # ---------------------------------------------------------------- # Step 3: Make qr_codes.location and location_address nullable # Dynamic QR codes have no single fixed location/address so these # columns must allow NULL. # ---------------------------------------------------------------- # Re-inspect to get current column definitions inspector2 = sa_inspect(db.engine) col_map = {c['name']: c for c in inspector2.get_columns('qr_codes')} location_nullable = col_map.get('location', {}).get('nullable', True) loc_addr_nullable = col_map.get('location_address', {}).get('nullable', True) if not location_nullable or not loc_addr_nullable: with db.engine.connect() as conn: if not location_nullable: conn.execute(text( "ALTER TABLE qr_codes " "MODIFY COLUMN location VARCHAR(100) NULL" )) print("✅ Made qr_codes.location nullable") else: print("ℹ️ qr_codes.location already nullable — skipped.") if not loc_addr_nullable: conn.execute(text( "ALTER TABLE qr_codes " "MODIFY COLUMN location_address TEXT NULL" )) print("✅ Made qr_codes.location_address nullable") else: print("ℹ️ qr_codes.location_address already nullable — skipped.") conn.commit() else: print("ℹ️ qr_codes.location and location_address already nullable — skipped.") # ---------------------------------------------------------------- # Step 4: Add qr_address column to attendance_data if absent # Stores the selected location's address for dynamic QR check-ins # ---------------------------------------------------------------- inspector3 = sa_inspect(db.engine) att_cols = {c['name'] for c in inspector3.get_columns('attendance_data')} if 'qr_address' not in att_cols: with db.engine.connect() as conn: conn.execute(text( "ALTER TABLE attendance_data ADD COLUMN qr_address TEXT NULL" )) conn.commit() print("\u2705 Added column: attendance_data.qr_address") else: print("\u2139\ufe0f Column attendance_data.qr_address already exists \u2014 skipped.") # ---------------------------------------------------------------- # Step 5: Add is_dynamic_qr column to attendance_data if absent # Flags records that came from a Dynamic QR code scan # ---------------------------------------------------------------- if 'is_dynamic_qr' not in att_cols: with db.engine.connect() as conn: conn.execute(text( "ALTER TABLE attendance_data " "ADD COLUMN is_dynamic_qr TINYINT(1) NOT NULL DEFAULT 0" )) conn.commit() print("\u2705 Added column: attendance_data.is_dynamic_qr") else: print("\u2139\ufe0f Column attendance_data.is_dynamic_qr already exists \u2014 skipped.") # ---------------------------------------------------------------- # Step 6: Backfill is_dynamic_qr=1 on old broken records # Records created before this migration where location_name='Dynamic' # are legacy dynamic QR check-ins that were saved with the wrong name. # Mark them so the badge shows correctly in the attendance report. # ---------------------------------------------------------------- with db.engine.connect() as conn: result = conn.execute(text( "UPDATE attendance_data " "SET is_dynamic_qr = 1 " "WHERE location_name = 'Dynamic' AND is_dynamic_qr = 0" )) conn.commit() updated = result.rowcount if updated > 0: print(f"\u2705 Backfilled is_dynamic_qr=1 on {updated} legacy 'Dynamic' record(s)") else: print("\u2139\ufe0f No legacy 'Dynamic' records to backfill.") print("\nMigration complete.") print("All existing QR codes remain fully unaffected (qr_type = 'standard').") print("All existing attendance records default to is_dynamic_qr = 0 (False).") if __name__ == '__main__': run_migration()