feat: implementar ABM completo de comisiones, carreras y asignaturas con proteccion de integridad y corregir edificios y campus virtual

This commit is contained in:
2026-09-23 19:53:27 -03:00
parent 195bfd915e
commit 165a0b65b8
13 changed files with 1033 additions and 154 deletions
+236 -1
View File
@@ -131,7 +131,9 @@ def get_subjects():
'name': s.name,
'career_id': s.career_id,
'career_name': s.career_obj.name if s.career_obj else (s.department or 'General'),
'career_obj': {'id': s.career_obj.id, 'name': s.career_obj.name} if s.career_obj else None,
'department': s.department or 'General',
'description': getattr(s, 'description', '') or '',
'credits': getattr(s, 'credits', 4),
'is_active': s.is_active,
'commissions_count': len(s.commissions),
@@ -162,6 +164,7 @@ def get_careers():
'id': c.id,
'name': c.name,
'code': getattr(c, 'code', f'CAR-{c.id}'),
'description': getattr(c, 'description', '') or '',
'degree_level': getattr(c, 'degree_level', 'Grado / Tecnicatura'),
'is_active': getattr(c, 'is_active', True),
'subjects_count': len(c.subjects),
@@ -217,6 +220,7 @@ def get_commissions():
'career_name': career_name,
'semester': getattr(c, 'semester', '1C') or '1C',
'year': getattr(c, 'year', 2026) or 2026,
'academic_term_id': getattr(c, 'academic_term_id', None),
'teacher_id': c.teacher_id,
'teacher_name': c.teacher_name if hasattr(c, 'teacher_name') else (c.teacher.name if c.teacher else None),
'teachers': c.teachers if hasattr(c, 'teachers') else [],
@@ -926,6 +930,11 @@ def update_commission(id):
comm = Commission.query.get_or_404(id)
data = request.get_json(silent=True) or request.form.to_dict() or {}
if 'subject_id' in data and data['subject_id']:
comm.subject_id = int(data['subject_id'])
if 'academic_term_id' in data:
t_id = data['academic_term_id']
comm.academic_term_id = int(t_id) if (t_id and str(t_id).strip()) else None
if 'code' in data and data['code']:
comm.code = data['code'].strip()
if 'semester' in data and data['semester']:
@@ -934,7 +943,7 @@ def update_commission(id):
comm.year = int(data['year'])
if 'teacher_id' in data:
t_id = data['teacher_id']
comm.teacher_id = int(t_id) if t_id else None
comm.teacher_id = int(t_id) if (t_id and str(t_id).strip()) else None
if comm.teacher_id:
ct = CommissionTeacher.query.filter_by(commission_id=comm.id, user_id=comm.teacher_id).first()
if not ct:
@@ -971,6 +980,33 @@ def toggle_commission(id):
'message': f'Comisión {"activada" if comm.active else "desactivada"} correctamente.'
}), 200
@api_admin_bp.route('/commissions/<int:id>', methods=['DELETE'])
@jwt_required
def delete_commission(id):
comm = Commission.query.get_or_404(id)
reservations_count = len(comm.reservations) if comm.reservations else 0
enrollments_count = len(comm.enrollments) if hasattr(comm, 'enrollments') and comm.enrollments else 0
if reservations_count > 0 or enrollments_count > 0:
comm.active = False
db.session.commit()
return jsonify({
'status': 'deactivated',
'action': 'deactivated',
'message': f'La comisión tiene {reservations_count} reserva(s) y {enrollments_count} alumno(s) inscriptos. Se ha desactivado en su lugar para mantener la consistencia histórica.'
}), 200
from app.models.commission_teacher import CommissionTeacher
CommissionTeacher.query.filter_by(commission_id=comm.id).delete()
code = comm.code
db.session.delete(comm)
db.session.commit()
return jsonify({
'status': 'deleted',
'action': 'deleted',
'message': f'Comisión {code} eliminada exitosamente.'
}), 200
@api_admin_bp.route('/commissions/<int:id>/assign-teacher', methods=['POST'])
@jwt_required
def assign_commission_teacher(id):
@@ -2237,3 +2273,202 @@ def retry_all_failed_moodle_tasks():
'status': 'success',
'message': f'Se reiniciaron {count} tareas fallidas a estado PENDIENTE.'
}), 200
# ---------------------------------------------------------
# BUILDINGS CRUD
# ---------------------------------------------------------
@api_admin_bp.route('/buildings', methods=['GET'])
@jwt_required
def get_buildings():
from app.models.building import Building
from app.models.classroom import Classroom
search = request.args.get('search', '').strip().lower()
status_filter = request.args.get('status', '').strip().lower()
query = Building.query
if search:
query = query.filter(
db.or_(
Building.name.ilike(f'%{search}%'),
Building.code.ilike(f'%{search}%'),
Building.address.ilike(f'%{search}%')
)
)
if status_filter == 'active':
query = query.filter(Building.is_active == True)
elif status_filter == 'inactive':
query = query.filter(Building.is_active == False)
buildings = query.order_by(Building.name.asc()).all()
buildings_data = [b.to_dict() for b in buildings]
# Estadísticas globales: Capacidad física real sin sumar 9999 de aulas virtuales
all_classrooms = Classroom.query.filter_by(is_active=True).all()
physical_classrooms = [c for c in all_classrooms if not getattr(c, 'is_virtual', False) and (c.capacity or 0) < 999 and 'VIRTUAL' not in (c.code or '').upper()]
total_physical_capacity = sum(c.capacity for c in physical_classrooms)
return jsonify({
'status': 'success',
'total': len(buildings_data),
'buildings': buildings_data,
'stats': {
'total_buildings': len(buildings),
'total_classrooms': len(all_classrooms),
'physical_classrooms_count': len(physical_classrooms),
'total_physical_capacity': total_physical_capacity,
'total_floors': sum(b.floors for b in buildings if not b.is_virtual)
}
}), 200
@api_admin_bp.route('/buildings/<int:id>', methods=['GET'])
@jwt_required
def get_building(id):
from app.models.building import Building
building = Building.query.get(id)
if not building:
return jsonify({'error': 'NotFound', 'message': f'Edificio con ID {id} no encontrado.'}), 404
return jsonify({
'status': 'success',
'building': building.to_dict(),
'classrooms': [c.to_dict() for c in building.classrooms]
}), 200
@api_admin_bp.route('/buildings', methods=['POST'])
@jwt_required
def create_building():
from app.models.building import Building
data = request.get_json(silent=True) or request.form.to_dict() or {}
name = (data.get('name') or '').strip()
code = (data.get('code') or '').strip().upper() or None
address = (data.get('address') or '').strip() or None
floors = int(data.get('floors', 1) or 1)
description = (data.get('description') or '').strip() or None
is_active = data.get('is_active', True)
if isinstance(is_active, str):
is_active = is_active.lower() in ['true', '1', 'on']
if not name:
return jsonify({'error': 'ValidationError', 'message': 'El nombre del edificio es obligatorio.'}), 400
existing = Building.query.filter(db.func.lower(Building.name) == name.lower()).first()
if existing:
return jsonify({'error': 'Conflict', 'message': f'Ya existe un edificio con el nombre "{name}".'}), 409
building = Building(
name=name,
code=code,
address=address,
floors=floors,
description=description,
is_active=bool(is_active)
)
db.session.add(building)
db.session.commit()
return jsonify({
'status': 'success',
'message': f'Edificio "{name}" creado con éxito.',
'building': building.to_dict()
}), 201
@api_admin_bp.route('/buildings/<int:id>', methods=['PUT', 'POST'])
@jwt_required
def update_building(id):
from app.models.building import Building
from app.models.classroom import Classroom
building = Building.query.get(id)
if not building:
return jsonify({'error': 'NotFound', 'message': f'Edificio con ID {id} no encontrado.'}), 404
data = request.get_json(silent=True) or request.form.to_dict() or {}
name = (data.get('name') or '').strip()
if not name:
return jsonify({'error': 'ValidationError', 'message': 'El nombre del edificio es obligatorio.'}), 400
existing = Building.query.filter(
db.func.lower(Building.name) == name.lower(),
Building.id != id
).first()
if existing:
return jsonify({'error': 'Conflict', 'message': f'Ya existe otro edificio con el nombre "{name}".'}), 409
old_name = building.name
building.name = name
if 'code' in data:
building.code = (data.get('code') or '').strip().upper() or None
if 'address' in data:
building.address = (data.get('address') or '').strip() or None
if 'floors' in data:
building.floors = int(data.get('floors', 0) or 0)
if 'description' in data:
building.description = (data.get('description') or '').strip() or None
if 'is_active' in data:
is_active = data.get('is_active')
if isinstance(is_active, str):
building.is_active = is_active.lower() in ['true', '1', 'on']
else:
building.is_active = bool(is_active)
building.updated_at = datetime.utcnow()
if old_name != name:
Classroom.query.filter_by(building_id=building.id).update({'building': name})
db.session.commit()
return jsonify({
'status': 'success',
'message': f'Edificio "{building.name}" actualizado con éxito.',
'building': building.to_dict()
}), 200
@api_admin_bp.route('/buildings/<int:id>/toggle', methods=['POST'])
@jwt_required
def toggle_building(id):
from app.models.building import Building
building = Building.query.get(id)
if not building:
return jsonify({'error': 'NotFound', 'message': f'Edificio con ID {id} no encontrado.'}), 404
building.is_active = not building.is_active
building.updated_at = datetime.utcnow()
db.session.commit()
return jsonify({
'status': 'success',
'message': f'Edificio "{building.name}" {"activado" if building.is_active else "desactivado"} exitosamente.',
'building': building.to_dict()
}), 200
@api_admin_bp.route('/buildings/<int:id>', methods=['DELETE'])
@jwt_required
def delete_building(id):
from app.models.building import Building
from app.models.classroom import Classroom
building = Building.query.get(id)
if not building:
return jsonify({'error': 'NotFound', 'message': f'Edificio con ID {id} no encontrado.'}), 404
count = Classroom.query.filter(
db.or_(Classroom.building_id == building.id, Classroom.building == building.name)
).count()
if count > 0:
return jsonify({
'error': 'Conflict',
'message': f'No se puede eliminar el edificio "{building.name}" porque tiene {count} aulas asociadas.'
}), 409
db.session.delete(building)
db.session.commit()
return jsonify({
'status': 'success',
'message': f'Edificio "{building.name}" eliminado correctamente.'
}), 200
+5 -2
View File
@@ -32,15 +32,18 @@ def list_buildings():
buildings = query.order_by(Building.name.asc()).all()
# Calculate global building stats
# Calculate global building stats (excluye aulas virtuales con 9999)
total_classrooms = Classroom.query.filter_by(is_active=True).count()
total_capacity = sum(c.capacity for c in Classroom.query.filter_by(is_active=True).all())
physical_classrooms = [c for c in Classroom.query.filter_by(is_active=True).all() if not getattr(c, 'is_virtual', False) and (c.capacity or 0) < 999 and 'VIRTUAL' not in (c.code or '').upper()]
total_physical_capacity = sum(c.capacity for c in physical_classrooms)
total_capacity = total_physical_capacity
return render_template(
'buildings/list.html',
buildings=buildings,
total_classrooms=total_classrooms,
total_capacity=total_capacity,
total_physical_capacity=total_physical_capacity,
search=search,
status_filter=status_filter,
title='Building Management'