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:
@@ -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']
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user