The constraint system is the heart of the Automatización Backend. It ensures that all scheduling decisions comply with business rules using the Chain of Responsibility pattern.
Every classroom assignment must satisfy three core constraints:
Capacity - Enough seats for students
Compatibility - Right type of room (lab vs. lecture)
def validar_compatibilidad(aula: Dict, asignatura: Dict) -> Optional[str]: tipo_aula = aula.get('tipo', '').lower() tipo_asignatura = asignatura.get('tipo', '').lower() # Hibrida rooms work for everything if tipo_aula == 'hibrida': return None # Check compatibility compatible = ( (tipo_asignatura == 'teorica' and tipo_aula == 'teorica') or (tipo_asignatura == 'laboratorio' and tipo_aula == 'laboratorio') or (tipo_asignatura == 'hibrida') # Hibrida courses work in any room ) if not compatible: return f"Asignatura '{asignatura['nombre']}' de tipo '{tipo_asignatura}' no puede usar aula de tipo '{tipo_aula}'" return None
# src/core/restrictions/aulas/aula_no_ocupada_doble_handler.py:9class AulaNoOcupadaDobleHandler(RestrictionHandler): """ Restriction: A classroom cannot have two classes at the same time. """ def __init__(self): super().__init__() self.validador_individual = ValidadorIndividual() self.validador_global = ValidadorGlobal() self.buscador_aulas = BuscadorAulasDisponibles() def validate(self, context: Dict[str, Any]) -> Optional[str]: """ Supports two validation modes: 1. Individual: Check new block against existing schedules 2. Global: Check entire schedule set for conflicts """ nuevo_bloque = context.get("nuevo_bloque") if nuevo_bloque: return self.validador_individual.validar(context) else: return self.validador_global.validar(context) def obtener_aulas_disponibles(self, context: Dict[str, Any]) -> List[str]: """Get all available classrooms for a time slot""" return self.buscador_aulas.obtener_aulas_disponibles(context)
Two schedule blocks overlap if their time ranges intersect:
def horarios_solapan(h1: Dict, h2: Dict) -> bool: """ Check if two time slots overlap. Non-overlapping cases: - h1 ends before h2 starts: h1.end <= h2.start - h2 ends before h1 starts: h2.end <= h1.start All other cases are overlaps. """ inicio1, fin1 = h1['start_time'], h1['end_time'] inicio2, fin2 = h2['start_time'], h2['end_time'] # No overlap if one ends before the other starts return not (fin1 <= inicio2 or fin2 <= inicio1)
Visual Examples
Case 1: No Overlap (h1 before h2)|----h1----| |----h2----|07:00 09:00 10:00 12:00Case 2: No Overlap (h2 before h1) |----h1----||----h2----|07:00 09:00 10:00 12:00Case 3: Overlap (h1 and h2 intersect) |----h1----| |----h2----|07:00 09:00 11:00 13:00Case 4: Complete Overlap (h1 contains h2)|--------h1--------| |----h2----|07:00 09:00 11:00 13:00
def validar_ocupacion( aula_id: str, nuevo_horario: Dict, programaciones_existentes: List[Dict]) -> Optional[str]: """ Check if classroom is available for the new schedule. """ dia = nuevo_horario['dia'] hora_inicio = nuevo_horario['hora_inicio'] hora_fin = nuevo_horario['hora_fin'] # Check all existing schedules for this classroom for prog in programaciones_existentes: # Skip if different classroom if prog['aula_id'] != aula_id: continue # Skip if different day if prog['dia'] != dia: continue # Skip if cancelled if prog.get('estado') == 'cancelado': continue # Check for time overlap if horarios_solapan( {'start_time': hora_inicio, 'end_time': hora_fin}, {'start_time': prog['hora_inicio'], 'end_time': prog['hora_fin']} ): return ( f"Aula '{aula_id}' ya está ocupada el {dia} " f"{prog['hora_inicio']}-{prog['hora_fin']} " f"(Asignatura: {prog['asignatura_id']})" ) return None # Classroom is available
Instead of stopping at first failure, collect all constraint violations:
# Get all errors at onceerrors = handler.validate_all_and_collect_errors(context)if errors: print("This assignment violates:") for error in errors: print(f" - {error}")
To add a new constraint (e.g., “teacher availability”):
1
Create Handler Class
class DocenteDisponibleHandler(RestrictionHandler): def validate(self, context: Dict[str, Any]) -> Optional[str]: docente = context.get('docente') horario = context.get('nuevo_bloque') # Check if teacher is already scheduled if self._tiene_conflicto(docente, horario): return f"Docente {docente['nombre']} ya tiene clase en este horario" return None