feat: completar fases 3 y 4 del roadmap mvp (gradebook, drag-and-drop, impersonacion) y actualizar README
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from app import create_app, db
|
||||
from app.models.subject import Subject, Commission
|
||||
from app.models.milestone import MilestoneType, AcademicMilestone
|
||||
from app.models.user import User
|
||||
from app.models.role import Role
|
||||
from app.models.enrollment import StudentEnrollment
|
||||
from app.models.grade import MilestoneGrade
|
||||
from app.services.gradebook_service import GradebookService
|
||||
|
||||
|
||||
class TestPhase3Gradebook(unittest.TestCase):
|
||||
"""Pruebas unitarias e integrales para la Fase 3 del Roadmap MVP (UniCABA)."""
|
||||
|
||||
def setUp(self):
|
||||
self.app = create_app()
|
||||
self.app_context = self.app.app_context()
|
||||
self.app_context.push()
|
||||
db.create_all()
|
||||
|
||||
# Roles
|
||||
self.role_alumno = Role.query.filter_by(name='Alumno').first()
|
||||
if not self.role_alumno:
|
||||
self.role_alumno = Role(name='Alumno', description='Rol Alumno')
|
||||
db.session.add(self.role_alumno)
|
||||
|
||||
self.role_docente = Role.query.filter_by(name='Docente').first()
|
||||
if not self.role_docente:
|
||||
self.role_docente = Role(name='Docente', description='Rol Docente')
|
||||
db.session.add(self.role_docente)
|
||||
db.session.commit()
|
||||
|
||||
ts = int(datetime.utcnow().timestamp())
|
||||
|
||||
# Docente
|
||||
self.docente = User(
|
||||
name=f'Profesor Calificador {ts}',
|
||||
email=f'docente_calif_{ts}@unicaba.edu.ar',
|
||||
role_id=self.role_docente.id,
|
||||
role='Docente',
|
||||
is_active=True
|
||||
)
|
||||
self.docente.set_password('Secret123!')
|
||||
db.session.add(self.docente)
|
||||
|
||||
# Alumnos
|
||||
self.alumno1 = User(
|
||||
name=f'Estudiante Uno {ts}',
|
||||
email=f'alumno1_{ts}@unicaba.edu.ar',
|
||||
role_id=self.role_alumno.id,
|
||||
role='Alumno',
|
||||
is_active=True
|
||||
)
|
||||
self.alumno1.set_password('Secret123!')
|
||||
db.session.add(self.alumno1)
|
||||
|
||||
self.alumno2 = User(
|
||||
name=f'Estudiante Dos {ts}',
|
||||
email=f'alumno2_{ts}@unicaba.edu.ar',
|
||||
role_id=self.role_alumno.id,
|
||||
role='Alumno',
|
||||
is_active=True
|
||||
)
|
||||
self.alumno2.set_password('Secret123!')
|
||||
db.session.add(self.alumno2)
|
||||
|
||||
# Materia y Comisión
|
||||
self.subject = Subject(
|
||||
name=f'Evaluación y Métricas {ts}',
|
||||
code=f'EVAL{ts % 10000:04d}',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(self.subject)
|
||||
db.session.flush()
|
||||
|
||||
self.commission = Commission(
|
||||
subject_id=self.subject.id,
|
||||
code='C1',
|
||||
semester='1C',
|
||||
year=2026,
|
||||
teacher_id=self.docente.id,
|
||||
max_students=40,
|
||||
active=True
|
||||
)
|
||||
db.session.add(self.commission)
|
||||
db.session.flush()
|
||||
|
||||
# Matrículas
|
||||
self.enr1 = StudentEnrollment(
|
||||
student_id=self.alumno1.id,
|
||||
commission_id=self.commission.id,
|
||||
status='activo'
|
||||
)
|
||||
self.enr2 = StudentEnrollment(
|
||||
student_id=self.alumno2.id,
|
||||
commission_id=self.commission.id,
|
||||
status='activo'
|
||||
)
|
||||
db.session.add_all([self.enr1, self.enr2])
|
||||
|
||||
# Tipos de hito
|
||||
self.mtype_parcial = MilestoneType.query.filter_by(code='parcial_1').first()
|
||||
if not self.mtype_parcial:
|
||||
self.mtype_parcial = MilestoneType(
|
||||
name='Parcial 1',
|
||||
code='parcial_1',
|
||||
color='#0d6efd',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(self.mtype_parcial)
|
||||
|
||||
self.mtype_tp = MilestoneType.query.filter_by(code='tp_1').first()
|
||||
if not self.mtype_tp:
|
||||
self.mtype_tp = MilestoneType(
|
||||
name='Trabajo Práctico 1',
|
||||
code='tp_1',
|
||||
color='#198754',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(self.mtype_tp)
|
||||
db.session.commit()
|
||||
|
||||
# Hitos de la comisión
|
||||
now = datetime.utcnow()
|
||||
self.hito1 = AcademicMilestone(
|
||||
title='Primer Parcial Teórico',
|
||||
materia_codigo=self.subject.code,
|
||||
subject_id=self.subject.id,
|
||||
commission_id=self.commission.id,
|
||||
milestone_type_id=self.mtype_parcial.id,
|
||||
date=now + timedelta(days=5),
|
||||
classroom='Aula 101',
|
||||
is_active=True
|
||||
)
|
||||
self.hito2 = AcademicMilestone(
|
||||
title='Entrega de TP Obligatorio',
|
||||
materia_codigo=self.subject.code,
|
||||
subject_id=self.subject.id,
|
||||
commission_id=self.commission.id,
|
||||
milestone_type_id=self.mtype_tp.id,
|
||||
date=now + timedelta(days=12),
|
||||
classroom='Campus Virtual',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add_all([self.hito1, self.hito2])
|
||||
db.session.commit()
|
||||
|
||||
def tearDown(self):
|
||||
db.session.rollback()
|
||||
self.app_context.pop()
|
||||
|
||||
def test_gradebook_matrix_initial(self):
|
||||
"""Verifica que la matriz de notas inicialice vacía pero con los alumnos e hitos correctos."""
|
||||
gb = GradebookService.get_commission_gradebook(self.commission.id)
|
||||
self.assertIn('students', gb)
|
||||
self.assertEqual(len(gb['students']), 2)
|
||||
self.assertEqual(len(gb['milestones']), 2)
|
||||
self.assertFalse(gb['is_closed'])
|
||||
self.assertIsNone(gb['acta_number'])
|
||||
|
||||
# Los alumnos deben figurar como 'Sin Calificar'
|
||||
for s in gb['students']:
|
||||
self.assertEqual(s['condition'], 'Sin Calificar')
|
||||
self.assertIsNone(s['average_score'])
|
||||
|
||||
def test_save_grade_valid_and_absent(self):
|
||||
"""Verifica la carga de notas numéricas y estados de ausente."""
|
||||
# Alumno 1 saca 8.5 en hito 1
|
||||
g1 = GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=8.5,
|
||||
is_absent=False,
|
||||
feedback='Excelente razonamiento teórico.',
|
||||
grader_id=self.docente.id
|
||||
)
|
||||
self.assertEqual(g1.score, 8.5)
|
||||
self.assertTrue(g1.is_passed)
|
||||
self.assertEqual(g1.display_score, '8.5')
|
||||
|
||||
# Alumno 2 estuvo ausente en hito 1
|
||||
g2 = GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno2.id,
|
||||
score=None,
|
||||
is_absent=True,
|
||||
feedback='Ausente con aviso.',
|
||||
grader_id=self.docente.id
|
||||
)
|
||||
self.assertIsNone(g2.score)
|
||||
self.assertTrue(g2.is_absent)
|
||||
self.assertFalse(g2.is_passed)
|
||||
self.assertEqual(g2.display_score, 'Ausente')
|
||||
|
||||
def test_save_grade_validation_range(self):
|
||||
"""Bloqueo de notas fuera del rango estándar (1.00 a 10.00)."""
|
||||
with self.assertRaises(ValueError):
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=10.5
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=0.5
|
||||
)
|
||||
|
||||
def test_academic_condition_calculation(self):
|
||||
"""Verifica el cálculo de Promociona, Regular y Desaprobado."""
|
||||
# Alumno 1: 8.0 en hito 1 y 9.0 en hito 2 -> Promedio 8.50 -> Promociona
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=8.0
|
||||
)
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito2.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=9.0
|
||||
)
|
||||
|
||||
# Alumno 2: 5.0 en hito 1 y 6.0 en hito 2 -> Promedio 5.50 -> Regular
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno2.id,
|
||||
score=5.0
|
||||
)
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito2.id,
|
||||
student_id=self.alumno2.id,
|
||||
score=6.0
|
||||
)
|
||||
|
||||
gb = GradebookService.get_commission_gradebook(self.commission.id)
|
||||
al1_data = next(s for s in gb['students'] if s['student_id'] == self.alumno1.id)
|
||||
al2_data = next(s for s in gb['students'] if s['student_id'] == self.alumno2.id)
|
||||
|
||||
self.assertEqual(al1_data['average_score'], 8.5)
|
||||
self.assertEqual(al1_data['condition'], 'Promociona')
|
||||
|
||||
self.assertEqual(al2_data['average_score'], 5.5)
|
||||
self.assertEqual(al2_data['condition'], 'Regular')
|
||||
|
||||
def test_close_gradebook_and_immutability(self):
|
||||
"""Cierre formal del acta de regularidad y bloqueo de modificaciones posteriores."""
|
||||
# Calificar
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=7.0
|
||||
)
|
||||
|
||||
# Cerrar acta
|
||||
res = GradebookService.close_gradebook(
|
||||
commission_id=self.commission.id,
|
||||
user_id=self.docente.id,
|
||||
notes='Cierre final de cursada primer cuatrimestre.'
|
||||
)
|
||||
self.assertTrue(res['success'])
|
||||
self.assertIn('ACTA-2026-1C-', res['acta_number'])
|
||||
|
||||
# Intentar modificar nota en acta cerrada debe ser rechazado
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=10.0
|
||||
)
|
||||
self.assertIn('cerrada', str(ctx.exception).lower())
|
||||
|
||||
def test_student_grades_and_countdown(self):
|
||||
"""Verifica la consulta individual de notas y la cuenta regresiva para el alumno."""
|
||||
# Calificar al alumno 1
|
||||
GradebookService.save_single_grade(
|
||||
commission_id=self.commission.id,
|
||||
milestone_id=self.hito1.id,
|
||||
student_id=self.alumno1.id,
|
||||
score=8.0,
|
||||
feedback='Buen trabajo en el examen.'
|
||||
)
|
||||
|
||||
# Consulta del alumno
|
||||
grades = GradebookService.get_student_grades(self.alumno1.id)
|
||||
self.assertEqual(len(grades), 1)
|
||||
self.assertEqual(grades[0]['subject_name'], self.subject.name)
|
||||
self.assertEqual(len(grades[0]['milestones']), 2)
|
||||
|
||||
# Próximos hitos con cuenta regresiva
|
||||
upcoming = GradebookService.get_student_upcoming_milestones(self.alumno1.id)
|
||||
self.assertGreaterEqual(len(upcoming), 1)
|
||||
self.assertIn('countdown_display', upcoming[0])
|
||||
self.assertIn('days_remaining', upcoming[0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,283 @@
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from app import create_app, db
|
||||
from app.models.subject import Subject, Commission
|
||||
from app.models.classroom import Classroom
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
from app.models.user import User
|
||||
from app.models.role import Role
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.models.genetic_algorithm import ReservationOptimizer
|
||||
from app.services.jwt_service import JWTService
|
||||
|
||||
|
||||
class TestPhase4InteractiveImpersonation(unittest.TestCase):
|
||||
"""Pruebas unitarias e integrales para la Fase 4 del Roadmap MVP (UniCABA)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.app = create_app()
|
||||
cls.app.config['TESTING'] = True
|
||||
cls.app_context = cls.app.app_context()
|
||||
cls.app_context.push()
|
||||
db.create_all()
|
||||
|
||||
# Roles
|
||||
cls.role_admin = Role.query.filter_by(name='Admin').first()
|
||||
if not cls.role_admin:
|
||||
cls.role_admin = Role(name='Admin', description='Rol Administrador')
|
||||
db.session.add(cls.role_admin)
|
||||
|
||||
cls.role_alumno = Role.query.filter_by(name='Alumno').first()
|
||||
if not cls.role_alumno:
|
||||
cls.role_alumno = Role(name='Alumno', description='Rol Alumno')
|
||||
db.session.add(cls.role_alumno)
|
||||
|
||||
cls.role_docente = Role.query.filter_by(name='Docente').first()
|
||||
if not cls.role_docente:
|
||||
cls.role_docente = Role(name='Docente', description='Rol Docente')
|
||||
db.session.add(cls.role_docente)
|
||||
db.session.commit()
|
||||
|
||||
ts = int(datetime.utcnow().timestamp())
|
||||
|
||||
# Admin
|
||||
cls.admin = User(
|
||||
name=f'Super Administrador {ts}',
|
||||
email=f'admin_p4_{ts}@unicaba.edu.ar',
|
||||
role_id=cls.role_admin.id,
|
||||
role='Admin',
|
||||
is_active=True
|
||||
)
|
||||
cls.admin.set_password('AdminSecret123!')
|
||||
db.session.add(cls.admin)
|
||||
|
||||
# Docente
|
||||
cls.docente = User(
|
||||
name=f'Profesor Fase 4 {ts}',
|
||||
email=f'docente_p4_{ts}@unicaba.edu.ar',
|
||||
role_id=cls.role_docente.id,
|
||||
role='Docente',
|
||||
is_active=True
|
||||
)
|
||||
cls.docente.set_password('DocSecret123!')
|
||||
db.session.add(cls.docente)
|
||||
|
||||
# Alumno
|
||||
cls.alumno = User(
|
||||
name=f'Estudiante Fase 4 {ts}',
|
||||
email=f'alumno_p4_{ts}@unicaba.edu.ar',
|
||||
role_id=cls.role_alumno.id,
|
||||
role='Alumno',
|
||||
is_active=True
|
||||
)
|
||||
cls.alumno.set_password('AlumSecret123!')
|
||||
db.session.add(cls.alumno)
|
||||
|
||||
# Aulas
|
||||
cls.aula1 = Classroom(
|
||||
code=f'AULA-P4-1-{ts}',
|
||||
capacity=40,
|
||||
floor='1',
|
||||
building='Edificio Central',
|
||||
is_active=True
|
||||
)
|
||||
cls.aula2 = Classroom(
|
||||
code=f'AULA-P4-2-{ts}',
|
||||
capacity=25,
|
||||
floor='2',
|
||||
building='Edificio Central',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(cls.aula1)
|
||||
db.session.add(cls.aula2)
|
||||
|
||||
# Materia y Comisión
|
||||
cls.subject = Subject(
|
||||
name=f'Gestión Interactiva {ts}',
|
||||
code=f'GI4-{ts}',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(cls.subject)
|
||||
db.session.flush()
|
||||
|
||||
cls.commission = Commission(
|
||||
subject_id=cls.subject.id,
|
||||
teacher_id=cls.docente.id,
|
||||
code='C1',
|
||||
shift='Mañana',
|
||||
schedule='Lunes 08:00 a 12:00',
|
||||
year=2026,
|
||||
semester='1C',
|
||||
max_students=30,
|
||||
active=True
|
||||
)
|
||||
db.session.add(cls.commission)
|
||||
db.session.commit()
|
||||
|
||||
cls.admin_id = cls.admin.id
|
||||
cls.docente_id = cls.docente.id
|
||||
cls.alumno_id = cls.alumno.id
|
||||
cls.aula1_id = cls.aula1.id
|
||||
cls.aula2_id = cls.aula2.id
|
||||
cls.commission_id = cls.commission.id
|
||||
|
||||
cls.client = cls.app.test_client()
|
||||
|
||||
# Tokens
|
||||
cls.admin_token = JWTService.create_access_token(cls.admin)
|
||||
cls.alumno_token = JWTService.create_access_token(cls.alumno)
|
||||
cls.docente_token = JWTService.create_access_token(cls.docente)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
db.session.remove()
|
||||
cls.app_context.pop()
|
||||
|
||||
def tearDown(self):
|
||||
db.session.remove()
|
||||
|
||||
def test_admin_impersonate_user_and_audit(self):
|
||||
"""Superadmin inicia sesión como alumno y se genera bitácora inmutable."""
|
||||
res = self.client.post(
|
||||
'/api/v1/admin/impersonate',
|
||||
headers={'Authorization': f'Bearer {self.admin_token}'},
|
||||
json={'target_user_id': self.alumno_id}
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
data = res.get_json()
|
||||
self.assertEqual(data.get('status'), 'success')
|
||||
alumno = db.session.get(User, self.alumno_id)
|
||||
self.assertEqual(data['target_user']['email'], alumno.email)
|
||||
|
||||
# Verificar auditoría
|
||||
audit = AuditLog.query.filter_by(action='IMPERSONATE_START', entity_id=self.alumno_id).first()
|
||||
self.assertIsNotNone(audit)
|
||||
self.assertEqual(audit.user_id, self.admin_id)
|
||||
self.assertIn('inició sesión temporal', audit.details)
|
||||
|
||||
def test_non_admin_cannot_impersonate(self):
|
||||
"""Usuarios no administradores (docente/alumno) tienen denegado el modo impersonación."""
|
||||
# Intento por alumno
|
||||
res_alum = self.client.post(
|
||||
'/api/v1/admin/impersonate',
|
||||
headers={'Authorization': f'Bearer {self.alumno_token}'},
|
||||
json={'target_user_id': self.docente_id}
|
||||
)
|
||||
self.assertEqual(res_alum.status_code, 403)
|
||||
|
||||
# Intento por docente
|
||||
res_doc = self.client.post(
|
||||
'/api/v1/admin/impersonate',
|
||||
headers={'Authorization': f'Bearer {self.docente_token}'},
|
||||
json={'target_user_id': self.alumno_id}
|
||||
)
|
||||
self.assertEqual(res_doc.status_code, 403)
|
||||
|
||||
def test_impersonated_request_context_with_header(self):
|
||||
"""La cabecera X-Impersonate-User sustituye la identidad para las consultas pero preserva admin real."""
|
||||
res = self.client.get(
|
||||
'/api/v1/students/my-grades',
|
||||
headers={
|
||||
'Authorization': f'Bearer {self.admin_token}',
|
||||
'X-Impersonate-User': str(self.alumno_id)
|
||||
}
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
data = res.get_json()
|
||||
self.assertEqual(data.get('student_id'), self.alumno_id)
|
||||
self.assertTrue(data.get('is_impersonating'))
|
||||
|
||||
def test_stop_impersonating_and_audit(self):
|
||||
"""Finalización formal de la impersonación con registro en bitácora."""
|
||||
res = self.client.post(
|
||||
'/api/v1/admin/stop-impersonating',
|
||||
headers={
|
||||
'Authorization': f'Bearer {self.admin_token}',
|
||||
'X-Impersonate-User': str(self.alumno_id)
|
||||
}
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
audit = AuditLog.query.filter_by(action='IMPERSONATE_END', user_id=self.admin_id).first()
|
||||
self.assertIsNotNone(audit)
|
||||
self.assertEqual(audit.user_id, self.admin_id)
|
||||
|
||||
def test_drag_update_reservation_and_conflict_detection(self):
|
||||
"""Actualización interactiva de reserva por arrastre con detección de colisión física."""
|
||||
now = datetime.utcnow().replace(hour=8, minute=0, second=0, microsecond=0) + timedelta(days=2)
|
||||
end = now + timedelta(hours=4)
|
||||
|
||||
# 1. Crear reserva existente en aula1
|
||||
res1 = Reservation(
|
||||
classroom_id=self.aula1_id,
|
||||
commission_id=self.commission_id,
|
||||
user_id=self.docente_id,
|
||||
start_time=now,
|
||||
end_time=end,
|
||||
purpose='Clase regular',
|
||||
expected_attendees=25,
|
||||
status=ReservationStatus.CONFIRMED.value
|
||||
)
|
||||
db.session.add(res1)
|
||||
db.session.commit()
|
||||
|
||||
# 2. Arrastre exitoso hacia aula2 (vacía en ese horario)
|
||||
res_drag_ok = self.client.post(
|
||||
'/api/v1/reservations/drag-update',
|
||||
headers={'Authorization': f'Bearer {self.admin_token}'},
|
||||
json={
|
||||
'reservation_id': res1.id,
|
||||
'classroom_id': self.aula2_id,
|
||||
'start_time': now.isoformat(),
|
||||
'end_time': end.isoformat()
|
||||
}
|
||||
)
|
||||
self.assertEqual(res_drag_ok.status_code, 200)
|
||||
self.assertEqual(res_drag_ok.get_json()['reservation']['classroom_id'], self.aula2_id)
|
||||
|
||||
# 3. Intentar arrastrar una nueva reserva sobre aula2 en el mismo horario (colisión física)
|
||||
res_drag_conflict = self.client.post(
|
||||
'/api/v1/reservations/drag-update',
|
||||
headers={'Authorization': f'Bearer {self.admin_token}'},
|
||||
json={
|
||||
'commission_id': self.commission_id,
|
||||
'classroom_id': self.aula2_id,
|
||||
'start_time': now.isoformat(),
|
||||
'end_time': end.isoformat()
|
||||
}
|
||||
)
|
||||
self.assertEqual(res_drag_conflict.status_code, 409)
|
||||
data_err = res_drag_conflict.get_json()
|
||||
self.assertEqual(data_err.get('status'), 'conflict')
|
||||
|
||||
def test_genetic_algorithm_shift_schedule_and_apply(self):
|
||||
"""Optimizador genético calcula horarios según turno real y persiste en BD."""
|
||||
optimizer = ReservationOptimizer()
|
||||
comm = db.session.get(Commission, self.commission_id)
|
||||
|
||||
# Verificar que calcula según turno Mañana
|
||||
pref = optimizer._calculate_preferred_time(comm)
|
||||
self.assertEqual(pref['start'].hour, 8)
|
||||
self.assertEqual(pref['end'].hour, 12)
|
||||
|
||||
# Probar aplicación masiva de resultados válidos
|
||||
res_data = [{
|
||||
'commission_id': self.commission_id,
|
||||
'classroom_id': self.aula1_id,
|
||||
'user_id': self.admin_id,
|
||||
'start_time': (datetime.utcnow() + timedelta(days=10)).isoformat(),
|
||||
'end_time': (datetime.utcnow() + timedelta(days=10, hours=3)).isoformat(),
|
||||
'purpose': 'Asignación Óptima con IA',
|
||||
'expected_attendees': 20,
|
||||
'status': 'CONFIRMED'
|
||||
}]
|
||||
|
||||
applied = optimizer.apply_optimized_reservations(res_data)
|
||||
self.assertEqual(len(applied), 1)
|
||||
self.assertEqual(applied[0].classroom_id, self.aula1_id)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user