feat: gestion de materias por rol - Bedelia, Docente y Alumno

- Nuevo modelo StudentEnrollment (tabla student_enrollments)
  con inscripcion unica por alumno/comision, estado y notas

- Migracion automatica en init_db.py (CREATE TABLE IF NOT EXISTS)

- Rutas /admin/commissions/ para Bedelia y Admin:
  listado con filtros, detalle, asignar docente,
  inscribir/dar de baja alumnos, cambiar estado

- Blueprint my_subjects_bp en /mis-materias:
  Docente ve solo sus comisiones asignadas
  Alumno ve solo sus inscripciones activas
  Admin/Bedelia ven todo

- Templates: admin/commissions/list.html, detail.html
            my_subjects/list.html, detail.html

- Navbar actualizado por rol:
  Docente y Alumno: 'Mis Materias'
  Bedelia y Admin: 'Gestion de Comisiones'
This commit is contained in:
Carlos Tello
2026-09-05 12:24:04 -03:00
parent 69d545b718
commit 1fda344248
12 changed files with 1410 additions and 3 deletions
+2 -1
View File
@@ -5,5 +5,6 @@ from .buildings import buildings_bp
from .schedule import schedule_bp
from .genetic_algorithm import genetic_bp, genetic_web_bp
from .admin import admin_bp
from .my_subjects import my_subjects_bp
__all__ = ['main_bp', 'auth_bp', 'classrooms_bp', 'buildings_bp', 'schedule_bp', 'genetic_bp', 'genetic_web_bp', 'admin_bp']
__all__ = ['main_bp', 'auth_bp', 'classrooms_bp', 'buildings_bp', 'schedule_bp', 'genetic_bp', 'genetic_web_bp', 'admin_bp', 'my_subjects_bp']
+252
View File
@@ -1165,3 +1165,255 @@ def audit_logs():
# ---------------------------------------------------------
# 5. GESTION DE COMISIONES (BEDELIA / ADMIN)
# ---------------------------------------------------------
@admin_bp.route('/commissions')
@login_required
def commissions_list():
"""List all commissions with filters - accessible by Bedelia and Admin"""
if not (current_user.has_permission('academic', 'read') or current_user.can_manage()):
abort(403)
from app.models.subject import Subject, Commission
from app.models.user import User
search = request.args.get('search', '').strip()
subject_filter = request.args.get('subject_id', type=int)
teacher_filter = request.args.get('teacher_id', type=int)
shift_filter = request.args.get('shift', '').strip()
active_filter = request.args.get('active', 'true').strip()
page = request.args.get('page', 1, type=int)
per_page = 20
query = Commission.query.join(Subject, Commission.subject_id == Subject.id)
if search:
query = query.filter(
or_(
Commission.code.ilike(f'%{search}%'),
Subject.name.ilike(f'%{search}%'),
Subject.code.ilike(f'%{search}%'),
)
)
if subject_filter:
query = query.filter(Commission.subject_id == subject_filter)
if teacher_filter:
query = query.filter(Commission.teacher_id == teacher_filter)
if shift_filter:
query = query.filter(Commission.shift.ilike(f'%{shift_filter}%'))
if active_filter in ['true', 'false']:
query = query.filter(Commission.active == (active_filter == 'true'))
pagination = query.order_by(Subject.name.asc(), Commission.code.asc()).paginate(
page=page, per_page=per_page, error_out=False
)
subjects = Subject.query.filter_by(is_active=True).order_by(Subject.name.asc()).all()
teachers = User.query.filter(
or_(User.role.ilike('%DOCENTE%'), User.role.ilike('%docent%'), User.role.ilike('%ADMIN%'))
).filter_by(is_active=True).order_by(User.name.asc()).all()
can_edit = (
current_user.has_permission('academic', 'read_write') or
current_user.is_admin() or
current_user.is_bedelia()
)
return render_template(
'admin/commissions/list.html',
commissions=pagination.items,
pagination=pagination,
subjects=subjects,
teachers=teachers,
search=search,
subject_filter=subject_filter,
teacher_filter=teacher_filter,
shift_filter=shift_filter,
active_filter=active_filter,
can_edit=can_edit
)
@admin_bp.route('/commissions/<int:id>')
@login_required
def commission_detail(id):
"""Detail view of a commission: enrolled students and teacher assignment"""
if not (current_user.has_permission('academic', 'read') or current_user.can_manage()):
abort(403)
from app.models.subject import Commission
from app.models.enrollment import StudentEnrollment
from app.models.user import User
commission = Commission.query.get_or_404(id)
enrollments = (
StudentEnrollment.query
.filter_by(commission_id=id)
.order_by(StudentEnrollment.enrolled_at.asc())
.all()
)
teachers = User.query.filter(
or_(User.role.ilike('%DOCENTE%'), User.role.ilike('%docent%'))
).filter_by(is_active=True).order_by(User.name.asc()).all()
students = User.query.filter(
or_(User.role.ilike('%ALUMNO%'), User.role.ilike('%alumn%'), User.role.ilike('%estud%'))
).filter_by(is_active=True).order_by(User.name.asc()).all()
enrolled_ids = {e.student_id for e in enrollments}
can_edit = (
current_user.has_permission('academic', 'read_write') or
current_user.is_admin() or
current_user.is_bedelia()
)
return render_template(
'admin/commissions/detail.html',
commission=commission,
enrollments=enrollments,
teachers=teachers,
students=students,
enrolled_ids=enrolled_ids,
can_edit=can_edit
)
@admin_bp.route('/commissions/<int:id>/assign-teacher', methods=['POST'])
@login_required
def commission_assign_teacher(id):
"""Assign or change the teacher of a commission"""
if not (
current_user.has_permission('academic', 'read_write') or
current_user.is_admin() or
current_user.is_bedelia()
):
abort(403)
from app.models.subject import Commission
from app.models.user import User
commission = Commission.query.get_or_404(id)
teacher_id = request.form.get('teacher_id', type=int)
if teacher_id:
teacher = User.query.get(teacher_id)
if not teacher or not teacher.is_active:
flash(_('El docente seleccionado no existe o esta inactivo.'), 'error')
return redirect(url_for('admin.commission_detail', id=id))
commission.teacher_id = teacher_id
db.session.commit()
flash(_('Docente asignado exitosamente a la comision.'), 'success')
else:
commission.teacher_id = None
db.session.commit()
flash(_('Se removio la asignacion de docente.'), 'warning')
return redirect(url_for('admin.commission_detail', id=id))
@admin_bp.route('/commissions/<int:id>/enroll-student', methods=['POST'])
@login_required
def commission_enroll_student(id):
"""Enroll a student into a commission"""
if not (
current_user.has_permission('academic', 'read_write') or
current_user.is_admin() or
current_user.is_bedelia()
):
abort(403)
from app.models.subject import Commission
from app.models.enrollment import StudentEnrollment
from app.models.user import User
commission = Commission.query.get_or_404(id)
student_id = request.form.get('student_id', type=int)
notes = request.form.get('notes', '').strip()
if not student_id:
flash(_('Debes seleccionar un alumno.'), 'error')
return redirect(url_for('admin.commission_detail', id=id))
student = User.query.get(student_id)
if not student or not student.is_active:
flash(_('El alumno seleccionado no existe o esta inactivo.'), 'error')
return redirect(url_for('admin.commission_detail', id=id))
existing = StudentEnrollment.query.filter_by(student_id=student_id, commission_id=id).first()
if existing:
flash(_('El alumno ya esta inscripto en esta comision.'), 'warning')
return redirect(url_for('admin.commission_detail', id=id))
enrollment = StudentEnrollment(
student_id=student_id,
commission_id=id,
status='activo',
notes=notes or None
)
db.session.add(enrollment)
commission.current_students = (commission.current_students or 0) + 1
db.session.commit()
flash(_('Alumno inscripto exitosamente en la comision.'), 'success')
return redirect(url_for('admin.commission_detail', id=id))
@admin_bp.route('/commissions/<int:id>/unenroll-student/<int:student_id>', methods=['POST'])
@login_required
def commission_unenroll_student(id, student_id):
"""Remove a student from a commission"""
if not (
current_user.has_permission('academic', 'read_write') or
current_user.is_admin() or
current_user.is_bedelia()
):
abort(403)
from app.models.subject import Commission
from app.models.enrollment import StudentEnrollment
commission = Commission.query.get_or_404(id)
enrollment = StudentEnrollment.query.filter_by(
student_id=student_id, commission_id=id
).first_or_404()
db.session.delete(enrollment)
if commission.current_students and commission.current_students > 0:
commission.current_students -= 1
db.session.commit()
flash(_('Alumno dado de baja de la comision.'), 'success')
return redirect(url_for('admin.commission_detail', id=id))
@admin_bp.route('/commissions/<int:id>/update-enrollment/<int:student_id>', methods=['POST'])
@login_required
def commission_update_enrollment(id, student_id):
"""Update enrollment status for a student"""
if not (
current_user.has_permission('academic', 'read_write') or
current_user.is_admin() or
current_user.is_bedelia()
):
abort(403)
from app.models.enrollment import StudentEnrollment
enrollment = StudentEnrollment.query.filter_by(
student_id=student_id, commission_id=id
).first_or_404()
new_status = request.form.get('status', 'activo')
if new_status not in ['activo', 'retirado', 'condicional']:
new_status = 'activo'
enrollment.status = new_status
enrollment.notes = request.form.get('notes', enrollment.notes or '')
db.session.commit()
flash(_('Estado de inscripcion actualizado.'), 'success')
return redirect(url_for('admin.commission_detail', id=id))
+126
View File
@@ -0,0 +1,126 @@
from flask import Blueprint, render_template, abort
from flask_login import login_required, current_user
from app import db
from app.models.subject import Commission
from app.models.enrollment import StudentEnrollment
from app.models.classroom import Classroom
my_subjects_bp = Blueprint('my_subjects', __name__, url_prefix='/mis-materias')
@my_subjects_bp.route('/')
@login_required
def my_subjects_list():
"""Show the commissions assigned to the current user (teacher or student)."""
role = current_user.get_institutional_role()
commissions = []
if role in ['admin', 'bedelia']:
# Admin/Bedelia: see all active commissions
commissions = (
Commission.query
.filter_by(active=True)
.order_by(Commission.year.desc(), Commission.semester.asc(), Commission.code.asc())
.all()
)
view_mode = 'all'
elif role == 'docente':
# Teacher: only commissions where they are assigned as teacher
commissions = (
Commission.query
.filter_by(teacher_id=current_user.id, active=True)
.order_by(Commission.year.desc(), Commission.semester.asc(), Commission.code.asc())
.all()
)
view_mode = 'teacher'
elif role == 'alumno':
# Student: only commissions where they are enrolled
enrollments = (
StudentEnrollment.query
.filter_by(student_id=current_user.id, status='activo')
.all()
)
commission_ids = [e.commission_id for e in enrollments]
commissions = (
Commission.query
.filter(Commission.id.in_(commission_ids), Commission.active == True)
.order_by(Commission.year.desc(), Commission.semester.asc(), Commission.code.asc())
.all()
)
view_mode = 'student'
else:
commissions = []
view_mode = 'none'
return render_template(
'my_subjects/list.html',
commissions=commissions,
view_mode=view_mode,
role=role
)
@my_subjects_bp.route('/<int:commission_id>')
@login_required
def my_subject_detail(commission_id):
"""Detail view of a single commission for the current user."""
role = current_user.get_institutional_role()
commission = Commission.query.get_or_404(commission_id)
# Access control: make sure the user is allowed to see this commission
if role in ['admin', 'bedelia']:
pass # full access
elif role == 'docente':
if commission.teacher_id != current_user.id:
abort(403)
elif role == 'alumno':
enrollment = StudentEnrollment.query.filter_by(
student_id=current_user.id,
commission_id=commission_id
).first()
if not enrollment:
abort(403)
else:
abort(403)
# Get enrolled students (visible to teachers and admin/bedelia)
enrollments = []
if role in ['admin', 'bedelia', 'docente']:
enrollments = (
StudentEnrollment.query
.filter_by(commission_id=commission_id)
.order_by(StudentEnrollment.enrolled_at.asc())
.all()
)
# Try to get classroom info from reservations linked to this commission
from app.models.reservation import Reservation
from sqlalchemy import func
from datetime import datetime
# Recent or upcoming reservation linked to this commission
recent_reservation = (
Reservation.query
.filter(
Reservation.commission_id == commission_id,
Reservation.start_time >= datetime.utcnow()
)
.order_by(Reservation.start_time.asc())
.first()
)
classroom = None
if recent_reservation and recent_reservation.classroom_id:
classroom = Classroom.query.get(recent_reservation.classroom_id)
return render_template(
'my_subjects/detail.html',
commission=commission,
enrollments=enrollments,
classroom=classroom,
recent_reservation=recent_reservation,
role=role
)