feat(install): agregar opcion --empty-academic en install.sh e init_db.py para inicializar base de datos sin datos demo

This commit is contained in:
2026-09-23 20:44:08 -03:00
parent ddd1714eb2
commit 95d2aac323
5 changed files with 196 additions and 102 deletions
+3
View File
@@ -37,6 +37,9 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1
- Sustitución de listas fijas/mockeadas en `frontend/src/routes/schedule.js` por consulta dinámica a `/commissions` en vivo, permitiendo reservar aulas para cualquiera de las comisiones reales de la plataforma.
- **Soporte Multisede Consistente en Aulas:**
- Resolución robusta de `building_id` y flag `is_virtual` en `frontend/src/routes/classrooms.js`, eliminando hardcodes y asegurando que las aulas físicas pertenezcan a la sede seleccionada.
- **Inicialización de Base de Datos Limpia sin Datos Demo (`--empty-academic` / `--no-demo`):**
- **Soporte en `install.sh` y Menú Interactivo:** Incorporación de la opción `sudo bash install.sh --empty-academic` (o `--no-demo`) y opción `[3]` en el menú interactivo para crear la base de datos limpia desde cero, omitiendo la inyección de snapshots con carreras, materias, comisiones, aulas o reservas heredadas.
- **Inicializador de Esquema `init_db.py`:** Nuevo flag `--empty-academic` que genera el esquema de tablas completo, los roles y permisos RBAC, los usuarios institucionales y las tipificaciones de hitos, purgando y omitiendo cualquier edificio, aula física o virtual, carrera, materia, comisión o reserva demo por defecto.
- **Personalización de Interfaz y Saludo en Dashboard (Configuración Global):**
- **Nueva Pestaña de Interfaz & Dashboard (`/admin/settings`):** Módulo en el panel de configuración global para personalizar elementos visuales del sistema.
- **Desactivación Opcional del Saludo y Selector de Rol:** Switch interactivo para habilitar o deshabilitar el banner superior `¡Hola, {Usuario}!` y el selector de perspectiva de roles en `dashboard.html`. Al deshabilitarlo, el usuario accede directamente a los indicadores y accesos operativos de su rol sin distracciones ni dudas de contexto.
+14 -3
View File
@@ -218,8 +218,7 @@ node src/app.js
Para servidores Debian 12 / Ubuntu en Proxmox LXC o VPS, el repositorio incluye el instalador unificado [install.sh](install.sh) en la raíz:
```bash
# Despliegue interactivo (pregunta si mantener o restaurar la BD si ya existe):
# Despliegue interactivo (pregunta si mantener BD, restaurar snapshot o crear base limpia):
sudo bash install.sh
# Despliegue desatendido manteniendo la base de datos existente:
@@ -228,10 +227,22 @@ sudo bash install.sh --keep-db
# Despliegue desatendido preservando la base de datos y los usuarios/credenciales configurados:
sudo bash install.sh --keep-db --keep-users
# Despliegue desatendido recreando la base de datos desde cero (DROP y restore snapshot):
# Despliegue desatendido con base de datos limpia sin carreras, materias, ciclos lectivos, aulas ni edificios demo:
sudo bash install.sh --empty-academic
# Despliegue desatendido recreando la base de datos desde cero con snapshot de muestra (DROP y restore classrooms_db.sql):
sudo bash install.sh --fresh-db
```
También es posible inicializar directamente la base de datos limpia desde Python en cualquier entorno:
```bash
# Inicialización estándar con datos de muestra:
python backend/init_db.py
# Inicialización limpia (sin carreras, materias, ciclos lectivos, aulas, edificios ni reservas):
python backend/init_db.py --empty-academic
```
El script configura automáticamente:
1. Entorno Python 3.10+ y virtualenv en `backend/venv` con Gunicorn y servicio `admin-edu-space-backend.service` (puerto 5000).
2. Entorno Node.js 20 LTS en `frontend/` con servicio `admin-edu-space-frontend.service` (puerto 3000).
+2
View File
@@ -162,6 +162,8 @@ Fase 6: Despliegue Producción e Infraest.[███████░░░░░
- [x] Preservación garantizada de usuarios y roles RBAC ante reseteos de datos de cursada.
4. **Configuración y Personalización UI:**
- [x] Panel de configuración global (`/admin/settings`) con opción para habilitar o deshabilitar el saludo superior y banner de rol en el dashboard.
5. **Inicialización y Despliegue con Base Limpia:**
- [x] Soporte `--empty-academic` / `--no-demo` en `install.sh` e `init_db.py` para inicializar el sistema con roles y usuarios intactos pero con tablas académicas y de espacios totalmente vacías para configuración desde cero.
---
+141 -90
View File
@@ -8,6 +8,7 @@ if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8')
from app import create_app, db
from app import models
from app.models.user import User
from app.models.role import Role, Permission, SYSTEM_MODULES
from sqlalchemy import text
@@ -313,6 +314,49 @@ def init_database():
# 4. Verificar o crear usuarios institucionales de cada rol
keep_users = ('--keep-users' in sys.argv) or (os.environ.get('KEEP_USERS', '').lower() in ['1', 'true', 'yes'])
empty_academic = any(arg in sys.argv for arg in ['--empty-academic', '--no-demo', '--clean-db', '--without-demo-data', '--skip-demo']) or \
(os.environ.get('EMPTY_ACADEMIC', '').lower() in ['1', 'true', 'yes'])
if empty_academic:
print("[*] Opción --empty-academic activa: Se configurará la base de datos limpia SIN carreras, materias, ciclos lectivos, aulas, edificios ni reservas por defecto.")
try:
from app.models.reservation import Reservation
from app.models.grade import MilestoneGrade
from app.models.milestone import AcademicMilestone
from app.models.subject import CommissionTeacher, Commission, Subject
from app.models.enrollment import StudentEnrollment
from app.models.career import Career
from app.models.classroom import ClassroomResource, Classroom
from app.models.building import Building
from app.models.academic_term import AcademicTerm
Reservation.query.delete()
try:
MilestoneGrade.query.delete()
AcademicMilestone.query.delete()
except Exception:
pass
try:
CommissionTeacher.query.delete()
except Exception:
pass
StudentEnrollment.query.delete()
Commission.query.delete()
Subject.query.delete()
Career.query.delete()
try:
ClassroomResource.query.delete()
except Exception:
pass
Classroom.query.delete()
Building.query.delete()
AcademicTerm.query.delete()
db.session.commit()
print(" [OK] Tablas de carreras, materias, comisiones, aulas, edificios y reservas aseguradas vacías.")
except Exception as pe:
db.session.rollback()
print(f" [!] Nota durante verificación de tablas académicas vacías: {pe}")
if keep_users:
print("[*] Opción --keep-users activa: Preservando usuarios, roles y contraseñas existentes intactos (no se modifican ni pisan).")
else:
@@ -380,33 +424,36 @@ def init_database():
print(f" [OK] Usuario {u_info['email']} validado (Rol: {u_info['role_name']}).")
# 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'
if not empty_academic:
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.")
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.")
else:
print("[*] Modo --empty-academic: Omitiendo creación de edificios institucionales por defecto.")
# 6. Inicializar catalogo dinamico de tipificaciones de hitos academicos
print("[*] Verificando catalogo de tipificaciones de hitos academicos...")
@@ -432,76 +479,80 @@ def init_database():
print(f"[OK] Catalogo de tipificaciones inicializado ({MilestoneType.query.count()} tipos disponibles).")
# 7. Inicializar aulas, materias, comisiones y reservas de muestra
from app.models.classroom import Classroom
from app.models.subject import Subject, Commission
from app.models.reservation import Reservation, ReservationStatus
from datetime import datetime, timedelta, date
if not empty_academic:
from app.models.classroom import Classroom
from app.models.subject import Subject, Commission
from app.models.reservation import Reservation, ReservationStatus
from datetime import datetime, timedelta, date
if Classroom.query.count() == 0:
print("[*] Generando catalogo de aulas...")
for floor in ['PB', '1', '2', '3']:
for num in range(1, 8):
code = f'{floor}0{num}' if floor != 'PB' else f'PB-0{num}'
c = Classroom(
code=code,
building=b_central.name,
building_id=b_central.id,
floor=floor,
capacity=30 + (num * 5),
description=f'Aula física estándar {code} en {b_central.name}',
if Classroom.query.count() == 0 and 'b_central' in locals() and b_central and 'b_virtual' in locals() and b_virtual:
print("[*] Generando catalogo de aulas...")
for floor in ['PB', '1', '2', '3']:
for num in range(1, 8):
code = f'{floor}0{num}' if floor != 'PB' else f'PB-0{num}'
c = Classroom(
code=code,
building=b_central.name,
building_id=b_central.id,
floor=floor,
capacity=30 + (num * 5),
description=f'Aula física estándar {code} en {b_central.name}',
is_active=True
)
db.session.add(c)
for v in range(1, 4):
vc = Classroom(
code=f'VIRTUAL-0{v}',
building=b_virtual.name,
building_id=b_virtual.id,
floor='0',
capacity=100,
description=f'Sala virtual Microsoft Teams {v}',
is_active=True
)
db.session.add(c)
db.session.add(vc)
db.session.commit()
print(f"[OK] {Classroom.query.count()} aulas creadas.")
for v in range(1, 4):
vc = Classroom(
code=f'VIRTUAL-0{v}',
building=b_virtual.name,
building_id=b_virtual.id,
floor='0',
capacity=100,
description=f'Sala virtual Microsoft Teams {v}',
is_active=True
)
db.session.add(vc)
db.session.commit()
print(f"[OK] {Classroom.query.count()} aulas creadas.")
# Materia y Comisión
s = Subject.query.filter_by(code='ARQ-101').first()
if not s:
s = Subject(name='Arquitectura de Software', code='ARQ-101', is_active=True)
db.session.add(s)
db.session.commit()
# Materia y Comisión
s = Subject.query.filter_by(code='ARQ-101').first()
if not s:
s = Subject(name='Arquitectura de Software', code='ARQ-101', is_active=True)
db.session.add(s)
db.session.commit()
comm = Commission.query.filter_by(code='COM-A').first()
if not comm:
comm = Commission(code='COM-A', subject_id=s.id, shift='Noche', semester=2, year=2026, max_students=40, is_active=True)
db.session.add(comm)
db.session.commit()
comm = Commission.query.filter_by(code='COM-A').first()
if not comm:
comm = Commission(code='COM-A', subject_id=s.id, shift='Noche', semester=2, year=2026, max_students=40, is_active=True)
db.session.add(comm)
db.session.commit()
# Reservas de Hoy
if Reservation.query.count() == 0:
print("[*] Generando reservas de muestra...")
today = date.today()
classrooms = Classroom.query.all()
docente = User.query.filter_by(email='docente@edu-space.com').first()
for idx, cl in enumerate(classrooms[:19]):
start_hour = 8 + (idx % 6) * 2
r = Reservation(
classroom_id=cl.id,
commission_id=comm.id,
user_id=docente.id if docente else 1,
start_time=datetime.combine(today, datetime.min.time()) + timedelta(hours=start_hour),
end_time=datetime.combine(today, datetime.min.time()) + timedelta(hours=start_hour + 2),
purpose='Clase Regular',
status=ReservationStatus.CONFIRMED.value,
shift='Noche' if start_hour >= 18 else 'Tarde' if start_hour >= 13 else 'Mañana',
expected_attendees=cl.capacity - 5
)
db.session.add(r)
db.session.commit()
print(f"[OK] {Reservation.query.count()} reservas creadas.")
# Reservas de Hoy
if Reservation.query.count() == 0:
print("[*] Generando reservas de muestra...")
today = date.today()
classrooms = Classroom.query.all()
docente = User.query.filter_by(email='docente@edu-space.com').first()
for idx, cl in enumerate(classrooms[:19]):
start_hour = 8 + (idx % 6) * 2
r = Reservation(
classroom_id=cl.id,
commission_id=comm.id,
user_id=docente.id if docente else 1,
start_time=datetime.combine(today, datetime.min.time()) + timedelta(hours=start_hour),
end_time=datetime.combine(today, datetime.min.time()) + timedelta(hours=start_hour + 2),
purpose='Clase Regular',
status=ReservationStatus.CONFIRMED.value,
shift='Noche' if start_hour >= 18 else 'Tarde' if start_hour >= 13 else 'Mañana',
expected_attendees=cl.capacity - 5
)
db.session.add(r)
db.session.commit()
print(f"[OK] {Reservation.query.count()} reservas creadas.")
else:
print("[*] Modo --empty-academic: Omitiendo creación de aulas, materias, comisiones y reservas de muestra.")
print("[OK] Base de datos creada con tablas vacías para gestión académica y de espacios desde cero.")
print("\n[LISTO] Base de datos inicializada y lista para usar.")
+36 -9
View File
@@ -21,6 +21,7 @@ hdr() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}\n"; }
# ─── Argumentos de Línea de Comandos ───────────────────────────────────────────
ARG_DB_ACTION=""
ARG_KEEP_USERS=false
ARG_EMPTY_ACADEMIC=false
for arg in "$@"; do
case "$arg" in
--keep-db)
@@ -32,13 +33,19 @@ for arg in "$@"; do
--fresh-db|--restore-db)
ARG_DB_ACTION="fresh"
;;
--empty-academic|--no-demo|--clean-db|--without-demo-data)
ARG_DB_ACTION="fresh"
ARG_EMPTY_ACADEMIC=true
;;
-h|--help)
echo "Uso: sudo bash install.sh [OPCIONES]"
echo "Opciones:"
echo " --keep-db Mantiene la base de datos existente si ya contiene datos."
echo " --keep-users Preserva los usuarios existentes sin volver a crearlos o pisar sus contraseñas."
echo " --fresh-db Restaura la base de datos desde cero (DROP y recarga snapshot)."
echo " -h, --help Muestra esta ayuda."
echo " --keep-db Mantiene la base de datos existente si ya contiene datos."
echo " --keep-users Preserva los usuarios existentes sin volver a crearlos o pisar sus contraseñas."
echo " --fresh-db Restaura la base de datos desde cero (DROP y recarga snapshot classrooms_db.sql)."
echo " --empty-academic Crea la base de datos limpia sin carreras, materias, ciclos lectivos, aulas, edificios ni reservas demo."
echo " --no-demo Alias de --empty-academic."
echo " -h, --help Muestra esta ayuda."
exit 0
;;
*)
@@ -117,6 +124,7 @@ info "Directorio Frontend: ${FRONTEND_DIR}"
info "Usuario de Servicio: ${APP_USER}"
info "Preservar Base de Datos: ${ARG_DB_ACTION:-auto (interactivo/detección)}"
info "Preservar Usuarios: ${ARG_KEEP_USERS}"
info "Base Limpia (Sin Demo): ${ARG_EMPTY_ACADEMIC}"
# ─── 1. Paquetes Base del Sistema ──────────────────────────────────────────────
hdr "1. Actualizando repositorios y paquetes base"
@@ -340,22 +348,32 @@ if [ "$DB_EXISTS" = true ] && [ "$TABLE_COUNT" -gt 0 ]; then
info "Bandera --keep-db recibida: Se mantiene la base de datos existente intacta."
DO_RESTORE_FRESH=false
elif [[ "$ARG_DB_ACTION" == "fresh" ]]; then
warn "Bandera --fresh-db recibida: Se recreará la base de datos desde cero."
if [ "$ARG_EMPTY_ACADEMIC" = true ]; then
warn "Bandera --empty-academic recibida: Se inicializará la base de datos limpia sin datos demo."
else
warn "Bandera --fresh-db recibida: Se recreará la base de datos desde cero con snapshot."
fi
DO_RESTORE_FRESH=true
elif [ -t 0 ]; then
echo -e "\n${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BOLD} ¿Qué deseas hacer con la base de datos PostgreSQL existente?${RESET}"
echo -e " [1] ${GREEN}${BOLD}Mantener la base de datos actual${RESET} (Recomendado: preserva registros y sincroniza esquema)"
echo -e " [2] ${RED}${BOLD}Restaurar desde cero${RESET} (DROP DATABASE y recargar snapshot ${SQL_BACKUP_FILE##*/})"
echo -e " [2] ${RED}${BOLD}Restaurar desde cero con datos de muestra${RESET} (DROP DATABASE y recargar snapshot ${SQL_BACKUP_FILE##*/})"
echo -e " [3] ${CYAN}${BOLD}Base de datos limpia sin datos demo${RESET} (DROP DATABASE y crear tablas vacías sin carreras, materias, aulas ni sedes)"
echo -e "${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
DB_CHOICE=""
read -t 30 -p " Ingresá tu opción [1/2] (por defecto 1 en 30s): " DB_CHOICE || DB_CHOICE="1"
read -t 30 -p " Ingresá tu opción [1/2/3] (por defecto 1 en 30s): " DB_CHOICE || DB_CHOICE="1"
DB_CHOICE="${DB_CHOICE:-1}"
if [[ "$DB_CHOICE" == "2" ]]; then
warn "Opción [2] seleccionada: Restauración destructiva desde cero."
warn "Opción [2] seleccionada: Restauración destructiva desde snapshot demo."
DO_RESTORE_FRESH=true
ARG_EMPTY_ACADEMIC=false
elif [[ "$DB_CHOICE" == "3" ]]; then
warn "Opción [3] seleccionada: Base de datos limpia sin carreras, materias, aulas ni sedes demo."
DO_RESTORE_FRESH=true
ARG_EMPTY_ACADEMIC=true
else
ok "Opción [1] seleccionada: Mantener base de datos existente."
DO_RESTORE_FRESH=false
@@ -401,7 +419,12 @@ hdr "9. Aplicando esquema y datos en la base de datos"
cd "$BACKEND_DIR"
if [ "$DO_RESTORE_FRESH" = true ]; then
if [[ -f "$SQL_BACKUP_FILE" ]]; then
if [ "$ARG_EMPTY_ACADEMIC" = true ]; then
info "Inicializando base de datos limpia (sin datos demo de carreras, materias, aulas o sedes)..."
pg_cmd -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true
pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" > /dev/null 2>&1
ok "Base de datos '${DB_NAME}' creada limpia (sin snapshot demo)"
elif [[ -f "$SQL_BACKUP_FILE" ]]; then
BACKUP_SIZE=$(du -h "$SQL_BACKUP_FILE" | awk '{print $1}')
info "Restaurando snapshot '${SQL_BACKUP_FILE##*/}' (${BACKUP_SIZE})..."
pg_cmd -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true
@@ -436,6 +459,10 @@ if [ "$ARG_KEEP_USERS" = true ]; then
INIT_DB_EXTRA_FLAGS+=( "--keep-users" )
info "Bandera --keep-users activada: init_db.py preservará los usuarios y contraseñas sin modificarlos ni recrearlos."
fi
if [ "$ARG_EMPTY_ACADEMIC" = true ]; then
INIT_DB_EXTRA_FLAGS+=( "--empty-academic" )
info "Bandera --empty-academic activada: init_db.py creará las tablas vacías sin carreras, materias, ciclos lectivos, aulas, edificios ni reservas."
fi
LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \
PYTHONPATH="${BACKEND_DIR}" FLASK_APP=wsgi.py "${VENV_DIR}/bin/python" -X utf8 init_db.py "${INIT_DB_EXTRA_FLAGS[@]}"