import sys # Asegurar codificación UTF-8 en consola de Windows if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8') if hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(encoding='utf-8') from app import create_app, db from app.models.user import User from app.models.role import Role, Permission, SYSTEM_MODULES from sqlalchemy import text def init_database(): app = create_app() with app.app_context(): try: print("[*] Conectando a la base de datos y sincronizando esquema...") # 1. Crear tablas que no existan db.create_all() # 2. Migraciones incrementales seguras para bases de datos existentes db.session.execute(text(""" ALTER TABLE users ADD COLUMN IF NOT EXISTS preferred_language VARCHAR(10) DEFAULT NULL; """)) db.session.execute(text(""" ALTER TABLE users ADD COLUMN IF NOT EXISTS theme_preference VARCHAR(10) DEFAULT 'auto' NOT NULL; """)) db.session.execute(text(""" ALTER TABLE users ADD COLUMN IF NOT EXISTS role_id INTEGER REFERENCES roles(id) ON DELETE SET NULL; """)) db.session.execute(text(""" ALTER TABLE subjects ADD COLUMN IF NOT EXISTS career_id INTEGER REFERENCES careers(id) ON DELETE SET NULL; """)) db.session.execute(text(""" ALTER TABLE subjects ALTER COLUMN code TYPE VARCHAR(50); """)) db.session.execute(text(""" CREATE TABLE IF NOT EXISTS buildings ( id SERIAL PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, code VARCHAR(50) UNIQUE, address VARCHAR(255), floors INTEGER DEFAULT 1 NOT NULL, description TEXT, is_active BOOLEAN DEFAULT TRUE NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() ); """)) db.session.execute(text(""" CREATE TABLE IF NOT EXISTS audit_logs ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, user_email VARCHAR(255), action VARCHAR(50) NOT NULL, module VARCHAR(50) NOT NULL, entity_id INTEGER, details TEXT, ip_address VARCHAR(45), created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL ); CREATE INDEX IF NOT EXISTS ix_audit_logs_action ON audit_logs (action); CREATE INDEX IF NOT EXISTS ix_audit_logs_module ON audit_logs (module); CREATE INDEX IF NOT EXISTS ix_audit_logs_created_at ON audit_logs (created_at); """)) db.session.execute(text(""" ALTER TABLE classrooms ADD COLUMN IF NOT EXISTS building_id INTEGER REFERENCES buildings(id) ON DELETE SET NULL; """)) db.session.execute(text(""" ALTER TABLE commissions ADD COLUMN IF NOT EXISTS shift VARCHAR(30); """)) db.session.execute(text(""" ALTER TABLE reservations ADD COLUMN IF NOT EXISTS shift VARCHAR(30); """)) db.session.commit() print("[OK] Esquema y tablas actualizadas con éxito.") # 3. Crear o sincronizar roles predeterminados y matriz de permisos print("[*] Configurando roles institucionales y permisos RBAC...") default_roles = [ { 'name': 'Admin', 'description': 'Control total institucional, infraestructura, gestion academica y accesos RBAC', 'is_system': True, 'permissions': { 'classrooms': 'read_write', 'buildings': 'read_write', 'reservations': 'read_write', 'schedule': 'read_write', 'academic': 'read_write', 'import': 'read_write', 'optimizer': 'read_write', 'users': 'read_write', 'milestone_types': 'read_write' } }, { 'name': 'Docente', 'description': 'Gestion de reservas de cursada, consulta de cronograma y materias', 'is_system': False, 'permissions': { 'classrooms': 'read', 'buildings': 'read', 'reservations': 'read_write', 'schedule': 'read_write', 'academic': 'read', 'import': 'none', 'optimizer': 'read', 'users': 'none', 'milestone_types': 'read' } }, { 'name': 'Bedelia', 'description': 'Gestion operativa de aulas, sedes, reservas, comisiones, importacion y cartelera', 'is_system': False, 'permissions': { 'classrooms': 'read_write', 'buildings': 'read_write', 'reservations': 'read_write', 'schedule': 'read_write', 'academic': 'read_write', 'import': 'read_write', 'optimizer': 'none', 'users': 'none', 'milestone_types': 'read_write' } }, { 'name': 'Alumno', 'description': 'Acceso de solo lectura al cronograma general, asignaturas y aulas', 'is_system': False, 'permissions': { 'classrooms': 'read', 'buildings': 'read', 'reservations': 'read', 'schedule': 'read', 'academic': 'read', 'import': 'none', 'optimizer': 'none', 'users': 'none', 'milestone_types': 'read' } } ] module_ids = [m['id'] for m in SYSTEM_MODULES] admin_role = None for r_data in default_roles: role = Role.query.filter_by(name=r_data['name']).first() if not role: role = Role( name=r_data['name'], description=r_data['description'], is_system=r_data['is_system'] ) db.session.add(role) db.session.flush() print(f" [+] Rol '{role.name}' creado.") else: role.description = r_data['description'] role.is_system = r_data['is_system'] if role.name == 'Admin': admin_role = role # Sincronizar permisos del rol for mod in module_ids: level = r_data['permissions'].get(mod, 'none') role.set_permission(mod, level) db.session.commit() print("[OK] Roles y matriz de permisos configurados correctamente.") # 4. Verificar o crear usuario administrador inicial print("[*] Verificando usuario administrador inicial...") admin = User.query.filter_by(email="admin@edu-space.com").first() if not admin: admin = User( email="admin@edu-space.com", name="System Administrator", role="ADMIN", role_id=admin_role.id if admin_role else None, is_active=True, preferred_language="es", theme_preference="auto" ) admin.set_password("admin123") db.session.add(admin) db.session.commit() print("[OK] Usuario inicial creado exitosamente:") print(" Email: admin@edu-space.com") print(" Password: admin123") print(f" Rol: {admin_role.name if admin_role else 'ADMIN'}") else: # Sincronizar rol y atributos del admin existente si faltan if admin_role and admin.role_id != admin_role.id: admin.role_id = admin_role.id admin.role = "ADMIN" if not admin.preferred_language: admin.preferred_language = "es" if not admin.theme_preference: admin.theme_preference = "auto" db.session.commit() print(f"[INFO] El usuario admin (admin@edu-space.com) esta activo y vinculado al rol '{admin_role.name if admin_role else 'ADMIN'}'.") # 5. Inicializar edificios institucionales oficiales print("[*] Verificando edificios institucionales...") from app.models.building import Building b_central = Building.query.filter_by(name='Edificio Central').first() if not b_central: b_central = Building( name='Edificio Central', code='CENTRAL', address='Tte. Gral. Juan Domingo Peron 802, CABA', floors=4, description='Sede central institucional de UniCABA. Aulas de grado, auditorio y laboratorios.' ) db.session.add(b_central) else: b_central.address = 'Tte. Gral. Juan Domingo Peron 802, CABA' b_virtual = Building.query.filter_by(name='Campus Virtual').first() if not b_virtual: b_virtual = Building( name='Campus Virtual', code='VIRTUAL', address='Entorno Digital / Microsoft Teams - Moodle', floors=0, description='Entorno de aulas virtuales y clases a distancia' ) db.session.add(b_virtual) db.session.commit() print("[OK] Edificios institucionales configurados con dirección oficial.") # 6. Inicializar catalogo dinamico de tipificaciones de hitos academicos print("[*] Verificando catalogo de tipificaciones de hitos academicos...") from app.models.milestone import MilestoneType, BASE_MILESTONE_TYPES for item in BASE_MILESTONE_TYPES: mt = MilestoneType.query.filter_by(code=item['code']).first() if not mt: mt = MilestoneType( name=item['name'], code=item['code'], color=item['color'], description=item['description'], sort_order=item['sort_order'], is_active=True ) db.session.add(mt) else: if not mt.color: mt.color = item['color'] if not mt.sort_order: mt.sort_order = item['sort_order'] db.session.commit() print(f"[OK] Catalogo de tipificaciones inicializado ({MilestoneType.query.count()} tipos disponibles).") print("\n[LISTO] Base de datos inicializada y lista para usar.") except Exception as e: # Safe print to avoid UnicodeEncodeError in exception strings try: print(f"\n[ERROR] Error al inicializar la base de datos: {str(e)}") except UnicodeEncodeError: print("\n[ERROR] Error al inicializar la base de datos (se omiten caracteres no-ascii).") print("\nVerifica que:") print("1. El servicio de PostgreSQL este corriendo.") print("2. La base de datos 'classrooms_db' haya sido creada.") print("3. La variable DATABASE_URL en el archivo .env tenga el usuario y contraseña correctos.") sys.exit(1) if __name__ == "__main__": init_database()