fix: corrige validacion de permisos de bedelia y gestion de inscripciones
This commit is contained in:
@@ -72,6 +72,13 @@ SYSTEM_MODULES = [
|
|||||||
'name_en': 'Occupancy Metrics',
|
'name_en': 'Occupancy Metrics',
|
||||||
'icon': 'bi-graph-up',
|
'icon': 'bi-graph-up',
|
||||||
'description': 'Visualización y análisis de porcentajes de ocupación por aula (día, semana y mes).'
|
'description': 'Visualización y análisis de porcentajes de ocupación por aula (día, semana y mes).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'enrollment',
|
||||||
|
'name': 'Inscripciones y Cursadas',
|
||||||
|
'name_en': 'Enrollments & Courses',
|
||||||
|
'icon': 'bi-diagram-3',
|
||||||
|
'description': 'Gestión de inscripciones de alumnos a comisiones y asignación de docentes.'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1174,7 +1174,7 @@ def audit_logs():
|
|||||||
@login_required
|
@login_required
|
||||||
def commissions_list():
|
def commissions_list():
|
||||||
"""List all commissions with filters - accessible by Bedelia and Admin"""
|
"""List all commissions with filters - accessible by Bedelia and Admin"""
|
||||||
if not (current_user.has_permission('academic', 'read') or current_user.can_manage()):
|
if not (current_user.has_permission('enrollment', 'read_write') or current_user.is_admin() or current_user.is_bedelia()):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
from app.models.subject import Subject, Commission
|
from app.models.subject import Subject, Commission
|
||||||
@@ -1241,7 +1241,7 @@ def commissions_list():
|
|||||||
@login_required
|
@login_required
|
||||||
def commission_detail(id):
|
def commission_detail(id):
|
||||||
"""Detail view of a commission: enrolled students and teacher assignment"""
|
"""Detail view of a commission: enrolled students and teacher assignment"""
|
||||||
if not (current_user.has_permission('academic', 'read') or current_user.can_manage()):
|
if not (current_user.has_permission('enrollment', 'read_write') or current_user.is_admin() or current_user.is_bedelia()):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
from app.models.subject import Commission
|
from app.models.subject import Commission
|
||||||
|
|||||||
+33
-4
@@ -107,7 +107,9 @@ def init_database():
|
|||||||
'import': 'read_write',
|
'import': 'read_write',
|
||||||
'optimizer': 'read_write',
|
'optimizer': 'read_write',
|
||||||
'users': 'read_write',
|
'users': 'read_write',
|
||||||
'milestone_types': 'read_write'
|
'milestone_types': 'read_write',
|
||||||
|
'metrics': 'read_write',
|
||||||
|
'enrollment': 'read_write'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -123,7 +125,9 @@ def init_database():
|
|||||||
'import': 'none',
|
'import': 'none',
|
||||||
'optimizer': 'read',
|
'optimizer': 'read',
|
||||||
'users': 'none',
|
'users': 'none',
|
||||||
'milestone_types': 'read'
|
'milestone_types': 'read',
|
||||||
|
'metrics': 'none',
|
||||||
|
'enrollment': 'read'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -139,7 +143,9 @@ def init_database():
|
|||||||
'import': 'read_write',
|
'import': 'read_write',
|
||||||
'optimizer': 'none',
|
'optimizer': 'none',
|
||||||
'users': 'none',
|
'users': 'none',
|
||||||
'milestone_types': 'read_write'
|
'milestone_types': 'read_write',
|
||||||
|
'metrics': 'read_write',
|
||||||
|
'enrollment': 'read_write'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -155,7 +161,9 @@ def init_database():
|
|||||||
'import': 'none',
|
'import': 'none',
|
||||||
'optimizer': 'none',
|
'optimizer': 'none',
|
||||||
'users': 'none',
|
'users': 'none',
|
||||||
'milestone_types': 'read'
|
'milestone_types': 'read',
|
||||||
|
'metrics': 'none',
|
||||||
|
'enrollment': 'read'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -188,6 +196,27 @@ def init_database():
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print("[OK] Roles y matriz de permisos configurados correctamente.")
|
print("[OK] Roles y matriz de permisos configurados correctamente.")
|
||||||
|
|
||||||
|
# 3b. Deduplicar rol Bedelia con acento (Bedelía) si existe
|
||||||
|
print("[*] Verificando duplicados del rol Bedelia...")
|
||||||
|
bedelia_accented = Role.query.filter(
|
||||||
|
Role.name.in_(['Bedelía', 'Bedel\u00eda', 'BEDELÍA'])
|
||||||
|
).first()
|
||||||
|
bedelia_canonical = Role.query.filter_by(name='Bedelia').first()
|
||||||
|
if bedelia_accented and bedelia_canonical and bedelia_accented.id != bedelia_canonical.id:
|
||||||
|
# Migrate all users from accented role to canonical
|
||||||
|
from app.models.user import User
|
||||||
|
affected = User.query.filter_by(role_id=bedelia_accented.id).all()
|
||||||
|
for u in affected:
|
||||||
|
u.role_id = bedelia_canonical.id
|
||||||
|
u.role = 'BEDELIA'
|
||||||
|
print(f" [~] Usuario {u.email} migrado de '{bedelia_accented.name}' a 'Bedelia'")
|
||||||
|
db.session.flush()
|
||||||
|
db.session.delete(bedelia_accented)
|
||||||
|
db.session.commit()
|
||||||
|
print(f" [OK] Rol duplicado '{bedelia_accented.name}' eliminado. {len(affected)} usuario(s) migrado(s).")
|
||||||
|
else:
|
||||||
|
print(" [OK] Sin duplicados de Bedelia.")
|
||||||
|
|
||||||
# 4. Verificar o crear usuarios institucionales de cada rol
|
# 4. Verificar o crear usuarios institucionales de cada rol
|
||||||
print("[*] Verificando usuarios institucionales y credenciales...")
|
print("[*] Verificando usuarios institucionales y credenciales...")
|
||||||
|
|||||||
@@ -0,0 +1,539 @@
|
|||||||
|
"""
|
||||||
|
test_enrollment_and_commissions.py
|
||||||
|
Tests for the enrollment management system:
|
||||||
|
- StudentEnrollment model CRUD
|
||||||
|
- Commission management routes (Bedelia/Admin)
|
||||||
|
- /mis-materias routes per role (Docente, Alumno, Admin, Bedelia)
|
||||||
|
- RBAC enrollment permissions
|
||||||
|
- Bedelia role deduplication (no accented duplicate)
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from app import create_app, db
|
||||||
|
from app.models.user import User
|
||||||
|
from app.models.role import Role, SYSTEM_MODULES
|
||||||
|
from app.models.subject import Subject, Commission
|
||||||
|
from app.models.enrollment import StudentEnrollment
|
||||||
|
|
||||||
|
|
||||||
|
def _login_as(client, user_id):
|
||||||
|
"""Helper: inject Flask-Login session for the given user id."""
|
||||||
|
with client.session_transaction() as sess:
|
||||||
|
sess['_user_id'] = str(user_id)
|
||||||
|
sess['_fresh'] = True
|
||||||
|
|
||||||
|
|
||||||
|
class TestStudentEnrollmentModel(unittest.TestCase):
|
||||||
|
"""Unit tests for the StudentEnrollment SQLAlchemy model."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = create_app()
|
||||||
|
self.app.config['TESTING'] = True
|
||||||
|
self.app.config['WTF_CSRF_ENABLED'] = False
|
||||||
|
self.ctx = self.app.app_context()
|
||||||
|
self.ctx.push()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.ctx.pop()
|
||||||
|
|
||||||
|
def test_table_exists(self):
|
||||||
|
"""student_enrollments table must exist in the database."""
|
||||||
|
result = db.session.execute(
|
||||||
|
db.text("SELECT to_regclass('public.student_enrollments')")
|
||||||
|
).scalar()
|
||||||
|
self.assertIsNotNone(result, "Tabla student_enrollments no existe en la BD")
|
||||||
|
|
||||||
|
def test_model_columns(self):
|
||||||
|
"""StudentEnrollment must expose all required columns."""
|
||||||
|
cols = {c.name for c in StudentEnrollment.__table__.columns}
|
||||||
|
for expected in ('id', 'student_id', 'commission_id', 'status', 'enrolled_at', 'notes'):
|
||||||
|
self.assertIn(expected, cols, f"Columna faltante: {expected}")
|
||||||
|
|
||||||
|
def test_unique_constraint_name(self):
|
||||||
|
"""Unique constraint uq_student_commission must exist."""
|
||||||
|
constraint_names = {c.name for c in StudentEnrollment.__table__.constraints}
|
||||||
|
self.assertIn('uq_student_commission', constraint_names)
|
||||||
|
|
||||||
|
def test_valid_statuses(self):
|
||||||
|
"""to_dict must return a status in the accepted set."""
|
||||||
|
alumno = User.query.filter(
|
||||||
|
User.role.ilike('%ALUMNO%'), User.is_active == True
|
||||||
|
).first()
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
|
||||||
|
if not alumno or not comm:
|
||||||
|
self.skipTest("Se requiere al menos un alumno y una comision activa en la BD")
|
||||||
|
|
||||||
|
# Check if already enrolled to avoid integrity error
|
||||||
|
existing = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=alumno.id, commission_id=comm.id
|
||||||
|
).first()
|
||||||
|
if not existing:
|
||||||
|
enr = StudentEnrollment(
|
||||||
|
student_id=alumno.id,
|
||||||
|
commission_id=comm.id,
|
||||||
|
status='activo',
|
||||||
|
notes='Test enrollment'
|
||||||
|
)
|
||||||
|
db.session.add(enr)
|
||||||
|
db.session.commit()
|
||||||
|
enrolled = enr
|
||||||
|
else:
|
||||||
|
enrolled = existing
|
||||||
|
|
||||||
|
d = enrolled.to_dict()
|
||||||
|
self.assertIn(d['status'], ['activo', 'retirado', 'condicional'])
|
||||||
|
self.assertEqual(d['student_id'], alumno.id)
|
||||||
|
self.assertEqual(d['commission_id'], comm.id)
|
||||||
|
|
||||||
|
# Cleanup (only if we created it)
|
||||||
|
if not existing:
|
||||||
|
db.session.delete(enrolled)
|
||||||
|
# Restore current_students counter
|
||||||
|
comm.current_students = max(0, (comm.current_students or 1) - 1)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def test_relationships_accessible(self):
|
||||||
|
"""student and commission relationships must resolve without error."""
|
||||||
|
enr = StudentEnrollment.query.first()
|
||||||
|
if enr:
|
||||||
|
_ = enr.student
|
||||||
|
_ = enr.commission
|
||||||
|
|
||||||
|
|
||||||
|
class TestRBACEnrollmentModule(unittest.TestCase):
|
||||||
|
"""Tests for the enrollment RBAC module and role permissions."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = create_app()
|
||||||
|
self.app.config['TESTING'] = True
|
||||||
|
self.ctx = self.app.app_context()
|
||||||
|
self.ctx.push()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.ctx.pop()
|
||||||
|
|
||||||
|
def test_enrollment_module_in_system_modules(self):
|
||||||
|
"""SYSTEM_MODULES must contain an 'enrollment' module."""
|
||||||
|
module_ids = [m['id'] for m in SYSTEM_MODULES]
|
||||||
|
self.assertIn('enrollment', module_ids,
|
||||||
|
"Modulo 'enrollment' no encontrado en SYSTEM_MODULES")
|
||||||
|
|
||||||
|
def test_bedelia_has_enrollment_read_write(self):
|
||||||
|
"""Bedelia role must have read_write on enrollment."""
|
||||||
|
bedelia = Role.query.filter_by(name='Bedelia').first()
|
||||||
|
self.assertIsNotNone(bedelia, "Rol 'Bedelia' no existe en la BD")
|
||||||
|
self.assertEqual(bedelia.get_permission('enrollment'), 'read_write')
|
||||||
|
self.assertTrue(bedelia.has_permission('enrollment', 'read_write'))
|
||||||
|
|
||||||
|
def test_admin_has_enrollment_read_write(self):
|
||||||
|
"""Admin role must have read_write on enrollment."""
|
||||||
|
admin = Role.query.filter_by(name='Admin').first()
|
||||||
|
self.assertIsNotNone(admin)
|
||||||
|
self.assertEqual(admin.get_permission('enrollment'), 'read_write')
|
||||||
|
|
||||||
|
def test_docente_has_enrollment_read(self):
|
||||||
|
"""Docente role must have read on enrollment (not write)."""
|
||||||
|
docente = Role.query.filter_by(name='Docente').first()
|
||||||
|
self.assertIsNotNone(docente)
|
||||||
|
self.assertTrue(docente.has_permission('enrollment', 'read'))
|
||||||
|
self.assertFalse(docente.has_permission('enrollment', 'read_write'))
|
||||||
|
|
||||||
|
def test_alumno_has_enrollment_read(self):
|
||||||
|
"""Alumno role must have read on enrollment."""
|
||||||
|
alumno_role = Role.query.filter_by(name='Alumno').first()
|
||||||
|
self.assertIsNotNone(alumno_role)
|
||||||
|
self.assertTrue(alumno_role.has_permission('enrollment', 'read'))
|
||||||
|
|
||||||
|
def test_no_duplicate_bedelia_roles(self):
|
||||||
|
"""There must be exactly one Bedelia role (no accented duplicate)."""
|
||||||
|
bedelia_variants = Role.query.filter(
|
||||||
|
Role.name.in_(['Bedelia', 'Bedelía', 'BEDELÍA'])
|
||||||
|
).all()
|
||||||
|
names = [r.name for r in bedelia_variants]
|
||||||
|
self.assertNotIn('Bedelía', names,
|
||||||
|
"Rol duplicado 'Bedelía' (con acento) todavía existe en la BD")
|
||||||
|
self.assertEqual(len(bedelia_variants), 1,
|
||||||
|
f"Se encontraron {len(bedelia_variants)} variantes de Bedelia: {names}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommissionsManagementRoutes(unittest.TestCase):
|
||||||
|
"""Integration tests for /admin/commissions/ routes (Bedelia/Admin)."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = create_app()
|
||||||
|
self.app.config['TESTING'] = True
|
||||||
|
self.app.config['WTF_CSRF_ENABLED'] = False
|
||||||
|
self.client = self.app.test_client()
|
||||||
|
self.ctx = self.app.app_context()
|
||||||
|
self.ctx.push()
|
||||||
|
|
||||||
|
# Login as admin (first active user)
|
||||||
|
self.admin = User.query.filter_by(email='admin@edu-space.com').first()
|
||||||
|
self.bedelia = User.query.filter_by(email='bedelia@edu-space.com').first()
|
||||||
|
self.docente = User.query.filter_by(email='docente@edu-space.com').first()
|
||||||
|
self.alumno_user = User.query.filter_by(email='alumno@edu-space.com').first()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.ctx.pop()
|
||||||
|
|
||||||
|
def _login(self, user):
|
||||||
|
_login_as(self.client, user.id)
|
||||||
|
|
||||||
|
def test_commissions_list_accessible_by_admin(self):
|
||||||
|
"""Admin can access /admin/commissions."""
|
||||||
|
self._login(self.admin)
|
||||||
|
res = self.client.get('/admin/commissions')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
html = res.get_data(as_text=True)
|
||||||
|
self.assertIn('Gestión de Comisiones', html)
|
||||||
|
|
||||||
|
def test_commissions_list_accessible_by_bedelia(self):
|
||||||
|
"""Bedelia can access /admin/commissions."""
|
||||||
|
if not self.bedelia:
|
||||||
|
self.skipTest("No bedelia user in DB")
|
||||||
|
self._login(self.bedelia)
|
||||||
|
res = self.client.get('/admin/commissions')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
def test_commissions_list_forbidden_for_docente(self):
|
||||||
|
"""Docente without manage permissions should get 403 on /admin/commissions."""
|
||||||
|
if not self.docente:
|
||||||
|
self.skipTest("No docente user in DB")
|
||||||
|
self._login(self.docente)
|
||||||
|
res = self.client.get('/admin/commissions')
|
||||||
|
# Docente has 'academic: read' → can_manage() is False → 403
|
||||||
|
self.assertIn(res.status_code, [403, 302])
|
||||||
|
|
||||||
|
def test_commission_detail_accessible_by_admin(self):
|
||||||
|
"""Commission detail page returns 200 for an existing commission."""
|
||||||
|
self._login(self.admin)
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
res = self.client.get(f'/admin/commissions/{comm.id}')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
html = res.get_data(as_text=True)
|
||||||
|
self.assertIn('Alumnos Inscriptos', html)
|
||||||
|
self.assertIn('Docente Asignado', html)
|
||||||
|
|
||||||
|
def test_commission_detail_accessible_by_bedelia(self):
|
||||||
|
"""Bedelia can view commission detail."""
|
||||||
|
if not self.bedelia:
|
||||||
|
self.skipTest("No bedelia user in DB")
|
||||||
|
self._login(self.bedelia)
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
res = self.client.get(f'/admin/commissions/{comm.id}')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
def test_commission_detail_nonexistent_returns_404(self):
|
||||||
|
"""Non-existent commission ID must return 404."""
|
||||||
|
self._login(self.admin)
|
||||||
|
res = self.client.get('/admin/commissions/999999')
|
||||||
|
self.assertEqual(res.status_code, 404)
|
||||||
|
|
||||||
|
def test_assign_teacher_post(self):
|
||||||
|
"""Bedelia can POST to assign a teacher to a commission."""
|
||||||
|
if not self.bedelia or not self.docente:
|
||||||
|
self.skipTest("Need bedelia and docente users")
|
||||||
|
self._login(self.bedelia)
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
|
||||||
|
res = self.client.post(
|
||||||
|
f'/admin/commissions/{comm.id}/assign-teacher',
|
||||||
|
data={'teacher_id': self.docente.id},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
# Verify assignment persisted
|
||||||
|
db.session.refresh(comm)
|
||||||
|
self.assertEqual(comm.teacher_id, self.docente.id)
|
||||||
|
|
||||||
|
def test_enroll_student_and_unenroll(self):
|
||||||
|
"""Bedelia can enroll and unenroll an alumno in a commission."""
|
||||||
|
if not self.bedelia or not self.alumno_user:
|
||||||
|
self.skipTest("Need bedelia and alumno users")
|
||||||
|
self._login(self.bedelia)
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
|
||||||
|
# Clean up any prior enrollment to ensure idempotent test
|
||||||
|
prior = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).first()
|
||||||
|
if prior:
|
||||||
|
db.session.delete(prior)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
prev_count = comm.current_students or 0
|
||||||
|
|
||||||
|
# Enroll
|
||||||
|
res = self.client.post(
|
||||||
|
f'/admin/commissions/{comm.id}/enroll-student',
|
||||||
|
data={'student_id': self.alumno_user.id, 'notes': 'Test'},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
enr = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).first()
|
||||||
|
self.assertIsNotNone(enr, "Inscripción no encontrada tras POST enroll")
|
||||||
|
self.assertEqual(enr.status, 'activo')
|
||||||
|
db.session.refresh(comm)
|
||||||
|
self.assertEqual(comm.current_students, prev_count + 1)
|
||||||
|
|
||||||
|
# Unenroll
|
||||||
|
res2 = self.client.post(
|
||||||
|
f'/admin/commissions/{comm.id}/unenroll-student/{self.alumno_user.id}',
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
self.assertEqual(res2.status_code, 200)
|
||||||
|
enr_after = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).first()
|
||||||
|
self.assertIsNone(enr_after, "Inscripción debería haber sido eliminada")
|
||||||
|
db.session.refresh(comm)
|
||||||
|
self.assertEqual(comm.current_students, prev_count)
|
||||||
|
|
||||||
|
def test_update_enrollment_status(self):
|
||||||
|
"""Bedelia can change enrollment status (activo→condicional→retirado)."""
|
||||||
|
if not self.bedelia or not self.alumno_user:
|
||||||
|
self.skipTest("Need bedelia and alumno users")
|
||||||
|
self._login(self.bedelia)
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
|
||||||
|
# Ensure enrollment exists
|
||||||
|
enr = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).first()
|
||||||
|
if not enr:
|
||||||
|
enr = StudentEnrollment(
|
||||||
|
student_id=self.alumno_user.id,
|
||||||
|
commission_id=comm.id,
|
||||||
|
status='activo'
|
||||||
|
)
|
||||||
|
db.session.add(enr)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
for new_status in ['condicional', 'retirado', 'activo']:
|
||||||
|
res = self.client.post(
|
||||||
|
f'/admin/commissions/{comm.id}/update-enrollment/{self.alumno_user.id}',
|
||||||
|
data={'status': new_status},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
db.session.refresh(enr)
|
||||||
|
self.assertEqual(enr.status, new_status)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
db.session.delete(enr)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def test_duplicate_enrollment_rejected(self):
|
||||||
|
"""Enrolling the same student twice in the same commission is rejected."""
|
||||||
|
if not self.bedelia or not self.alumno_user:
|
||||||
|
self.skipTest("Need bedelia and alumno users")
|
||||||
|
self._login(self.bedelia)
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
|
||||||
|
# Ensure a clean start
|
||||||
|
prior = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).first()
|
||||||
|
if not prior:
|
||||||
|
enr = StudentEnrollment(
|
||||||
|
student_id=self.alumno_user.id,
|
||||||
|
commission_id=comm.id,
|
||||||
|
status='activo'
|
||||||
|
)
|
||||||
|
db.session.add(enr)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Second enroll attempt
|
||||||
|
res = self.client.post(
|
||||||
|
f'/admin/commissions/{comm.id}/enroll-student',
|
||||||
|
data={'student_id': self.alumno_user.id},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
# Should see a warning message, and only one enrollment should exist
|
||||||
|
count = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).count()
|
||||||
|
self.assertEqual(count, 1)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class TestMySubjectsRoutes(unittest.TestCase):
|
||||||
|
"""/mis-materias route tests per role."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app = create_app()
|
||||||
|
self.app.config['TESTING'] = True
|
||||||
|
self.app.config['WTF_CSRF_ENABLED'] = False
|
||||||
|
self.client = self.app.test_client()
|
||||||
|
self.ctx = self.app.app_context()
|
||||||
|
self.ctx.push()
|
||||||
|
|
||||||
|
self.admin = User.query.filter_by(email='admin@edu-space.com').first()
|
||||||
|
self.bedelia = User.query.filter_by(email='bedelia@edu-space.com').first()
|
||||||
|
self.docente = User.query.filter_by(email='docente@edu-space.com').first()
|
||||||
|
self.alumno_user = User.query.filter_by(email='alumno@edu-space.com').first()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.ctx.pop()
|
||||||
|
|
||||||
|
def _login(self, user):
|
||||||
|
_login_as(self.client, user.id)
|
||||||
|
|
||||||
|
def test_mis_materias_requires_login(self):
|
||||||
|
"""Unauthenticated request to /mis-materias/ must redirect to login."""
|
||||||
|
res = self.client.get('/mis-materias/')
|
||||||
|
self.assertIn(res.status_code, [302, 401])
|
||||||
|
|
||||||
|
def test_mis_materias_admin_ok(self):
|
||||||
|
"""Admin sees all commissions at /mis-materias/."""
|
||||||
|
self._login(self.admin)
|
||||||
|
res = self.client.get('/mis-materias/')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
html = res.get_data(as_text=True)
|
||||||
|
self.assertIn('Todas las Comisiones', html)
|
||||||
|
|
||||||
|
def test_mis_materias_bedelia_ok(self):
|
||||||
|
"""Bedelia sees all commissions at /mis-materias/."""
|
||||||
|
if not self.bedelia:
|
||||||
|
self.skipTest("No bedelia user")
|
||||||
|
self._login(self.bedelia)
|
||||||
|
res = self.client.get('/mis-materias/')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
def test_mis_materias_docente_ok(self):
|
||||||
|
"""Docente gets 200 at /mis-materias/ (even with no assigned commissions)."""
|
||||||
|
if not self.docente:
|
||||||
|
self.skipTest("No docente user")
|
||||||
|
self._login(self.docente)
|
||||||
|
res = self.client.get('/mis-materias/')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
html = res.get_data(as_text=True)
|
||||||
|
self.assertIn('Mis Materias', html)
|
||||||
|
|
||||||
|
def test_mis_materias_alumno_ok(self):
|
||||||
|
"""Alumno gets 200 at /mis-materias/ (even with no enrollments)."""
|
||||||
|
if not self.alumno_user:
|
||||||
|
self.skipTest("No alumno user")
|
||||||
|
self._login(self.alumno_user)
|
||||||
|
res = self.client.get('/mis-materias/')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
html = res.get_data(as_text=True)
|
||||||
|
self.assertIn('Mis Materias', html)
|
||||||
|
|
||||||
|
def test_mis_materias_detail_enrolled_alumno(self):
|
||||||
|
"""Alumno enrolled in a commission can see its detail page."""
|
||||||
|
if not self.alumno_user:
|
||||||
|
self.skipTest("No alumno user")
|
||||||
|
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
|
||||||
|
# Create temporary enrollment
|
||||||
|
enr = StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).first()
|
||||||
|
cleanup_needed = False
|
||||||
|
if not enr:
|
||||||
|
enr = StudentEnrollment(
|
||||||
|
student_id=self.alumno_user.id,
|
||||||
|
commission_id=comm.id,
|
||||||
|
status='activo'
|
||||||
|
)
|
||||||
|
db.session.add(enr)
|
||||||
|
db.session.commit()
|
||||||
|
cleanup_needed = True
|
||||||
|
|
||||||
|
self._login(self.alumno_user)
|
||||||
|
res = self.client.get(f'/mis-materias/{comm.id}')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
html = res.get_data(as_text=True)
|
||||||
|
self.assertIn('Tu inscripción', html)
|
||||||
|
|
||||||
|
if cleanup_needed:
|
||||||
|
db.session.delete(enr)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def test_mis_materias_detail_forbidden_for_unenrolled_alumno(self):
|
||||||
|
"""Alumno NOT enrolled in a commission gets 403 on its detail page."""
|
||||||
|
if not self.alumno_user:
|
||||||
|
self.skipTest("No alumno user")
|
||||||
|
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
|
||||||
|
# Ensure alumno is NOT enrolled
|
||||||
|
StudentEnrollment.query.filter_by(
|
||||||
|
student_id=self.alumno_user.id, commission_id=comm.id
|
||||||
|
).delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
self._login(self.alumno_user)
|
||||||
|
res = self.client.get(f'/mis-materias/{comm.id}')
|
||||||
|
self.assertEqual(res.status_code, 403)
|
||||||
|
|
||||||
|
def test_mis_materias_detail_docente_own_commission(self):
|
||||||
|
"""Docente can see detail of their own assigned commission."""
|
||||||
|
if not self.docente:
|
||||||
|
self.skipTest("No docente user")
|
||||||
|
|
||||||
|
comm = Commission.query.filter_by(active=True).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No active commission in DB")
|
||||||
|
|
||||||
|
# Assign commission to docente
|
||||||
|
prev_teacher = comm.teacher_id
|
||||||
|
comm.teacher_id = self.docente.id
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
self._login(self.docente)
|
||||||
|
res = self.client.get(f'/mis-materias/{comm.id}')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
html = res.get_data(as_text=True)
|
||||||
|
self.assertIn(comm.subject.name, html)
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
comm.teacher_id = prev_teacher
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def test_mis_materias_detail_docente_unauthorized_commission(self):
|
||||||
|
"""Docente cannot see a commission they are not assigned to."""
|
||||||
|
if not self.docente:
|
||||||
|
self.skipTest("No docente user")
|
||||||
|
|
||||||
|
# Find a commission where docente is NOT the teacher
|
||||||
|
comm = Commission.query.filter(
|
||||||
|
Commission.active == True,
|
||||||
|
Commission.teacher_id != self.docente.id
|
||||||
|
).first()
|
||||||
|
if not comm:
|
||||||
|
self.skipTest("No commission without this docente in DB")
|
||||||
|
|
||||||
|
self._login(self.docente)
|
||||||
|
res = self.client.get(f'/mis-materias/{comm.id}')
|
||||||
|
self.assertEqual(res.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user