feat(sprint4): completar optimizador heuristico concurrente, integracion Spring Boot, correccion de login y fixes visuales

This commit is contained in:
Carlos Tello
2026-09-05 02:20:47 -03:00
parent dd93b8c07a
commit 69d545b718
17 changed files with 1021 additions and 91 deletions
+3 -1
View File
@@ -2,9 +2,11 @@
from app.routes.api.auth import api_auth_bp
from app.routes.api.classrooms import api_classrooms_bp
from app.routes.api.reservations import api_reservations_bp
from app.routes.api.optimizer import api_optimizer_bp
__all__ = [
'api_auth_bp',
'api_classrooms_bp',
'api_reservations_bp'
'api_reservations_bp',
'api_optimizer_bp'
]
+105
View File
@@ -0,0 +1,105 @@
from flask import Blueprint, request, jsonify, g
from pydantic import ValidationError
from app.schemas.optimizer_dto import OptimizerJobRequestDTO, SpringBootSyncPayloadDTO
from app.services.optimizer_service import OptimizerService
from app.utils.jwt_decorators import jwt_required
api_optimizer_bp = Blueprint('api_optimizer', __name__, url_prefix='/api/v1/optimizer')
@api_optimizer_bp.route('/jobs', methods=['POST'])
@jwt_required
def submit_optimization_job():
"""
Despacha un trabajo de optimización heurística asíncrono.
Retorna inmediatamente con HTTP 202 Accepted y el identificador de trabajo para sondeo.
"""
data = request.get_json(silent=True) or {}
try:
dto = OptimizerJobRequestDTO(**data)
except ValidationError as ex:
return jsonify({
'error': 'UnprocessableEntity',
'message': 'Error de validación en los parámetros del optimizador.',
'details': ex.errors()
}), 422
user_id = g.current_user.id if hasattr(g, 'current_user') and g.current_user else 1
job = OptimizerService.submit_job(
commission_ids=dto.commission_ids,
admin_user_id=user_id,
start_date=dto.start_date,
end_date=dto.end_date,
generations=dto.generations,
population_size=dto.population_size,
workers=dto.workers
)
return jsonify({
'job_id': job.job_id,
'status': job.status.value,
'poll_url': f'/api/v1/optimizer/jobs/{job.job_id}',
'message': 'Trabajo de optimización puesto en cola con éxito.'
}), 202
@api_optimizer_bp.route('/jobs/<string:job_id>', methods=['GET'])
@jwt_required
def get_optimization_job(job_id: str):
"""
Obtiene el estado, progreso y resultados de un trabajo de optimización en cola.
"""
job = OptimizerService.get_job(job_id)
if not job:
return jsonify({
'error': 'NotFound',
'message': f'Trabajo de optimización {job_id} no encontrado.'
}), 404
return jsonify(job.to_dict()), 200
@api_optimizer_bp.route('/jobs', methods=['GET'])
@jwt_required
def list_optimization_jobs():
"""
Lista los trabajos de optimización más recientes.
"""
limit = request.args.get('limit', default=20, type=int)
jobs = OptimizerService.list_jobs(limit=limit)
return jsonify({
'total': len(jobs),
'jobs': jobs
}), 200
@api_optimizer_bp.route('/spring-boot/sync', methods=['POST'])
@jwt_required
def sync_with_spring_boot():
"""
Endpoint interoperable para la sincronización y resolución de horarios con microservicios Spring Boot.
Recibe el payload en formato Spring Boot (camelCase) y retorna el cronograma optimizado.
"""
data = request.get_json(silent=True) or {}
try:
dto = SpringBootSyncPayloadDTO(**data)
except ValidationError as ex:
return jsonify({
'error': 'UnprocessableEntity',
'message': 'Error de validación en el payload Spring Boot.',
'details': ex.errors()
}), 422
user_id = g.current_user.id if hasattr(g, 'current_user') and g.current_user else 1
try:
result = OptimizerService.sync_with_spring_boot(dto, admin_user_id=user_id)
return jsonify(result), 200
except Exception as ex:
return jsonify({
'requestId': dto.requestId,
'status': 'ERROR',
'error': str(ex)
}), 500
+27 -2
View File
@@ -8,15 +8,18 @@ from datetime import datetime
auth_bp = Blueprint('auth', __name__)
from sqlalchemy import func
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""Handle user login"""
"""Handle user login with case-insensitive email lookup and whitespace trimming"""
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
clean_email = (form.email.data or '').strip().lower()
user = User.query.filter(func.lower(User.email) == clean_email).first()
if user and user.check_password(form.password.data):
if not user.is_active:
@@ -25,6 +28,19 @@ def login():
login_user(user, remember=form.remember_me.data)
user.update_last_login()
# Registrar en bitácora de auditoría
try:
from app.services.audit_service import AuditService
AuditService.log(
action='LOGIN',
module='auth',
user_id=user.id,
user_email=user.email,
details=f"Inicio de sesión exitoso ({user.role})"
)
except Exception:
pass
# Si el usuario tiene idioma configurado en su perfil, sobrescribe el navegador en toda la sesión
if user.preferred_language and user.preferred_language in ['es', 'en']:
@@ -42,6 +58,15 @@ def login():
flash(_('¡Bienvenido de nuevo, %(name)s!', name=user_full_name), 'success')
return redirect(next_page)
else:
try:
from app.services.audit_service import AuditService
AuditService.log(
action='LOGIN_FAILED',
module='auth',
details=f"Intento fallido de login con email: {clean_email}"
)
except Exception:
pass
flash(_('Correo electrónico o contraseña inválidos.'), 'error')
return render_template('auth/login.html', form=form)
+3 -3
View File
@@ -39,8 +39,8 @@ def optimize_reservations():
except ValueError:
return jsonify({'error': 'Invalid end_date format. Use ISO format.'}), 400
# Check if user has permission (admin or teacher)
if current_user.role not in ['ADMIN', 'teacher']:
# Check if user has permission (admin, teacher or RBAC optimizer write)
if not (current_user.is_admin() or current_user.has_permission('optimizer', 'read_write') or current_user.role in ['ADMIN', 'teacher']):
return jsonify({'error': 'Insufficient permissions to optimize reservations'}), 403
# Initialize optimizer and run optimization
@@ -84,7 +84,7 @@ def apply_optimization():
return jsonify({'error': 'reservations must be a list'}), 400
# Check if user has permission
if current_user.role not in ['ADMIN', 'teacher']:
if not (current_user.is_admin() or current_user.has_permission('optimizer', 'read_write') or current_user.role in ['ADMIN', 'teacher']):
return jsonify({'error': 'Insufficient permissions to apply reservations'}), 403
# Initialize optimizer and apply reservations