Files

311 lines
11 KiB
Python

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()