feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
"""Módulo de Repositorios para persistencia y consultas desacopladas."""
|
||||
from app.repositories.base_repository import BaseRepository
|
||||
from app.repositories.classroom_repository import ClassroomRepository
|
||||
from app.repositories.reservation_repository import ReservationRepository
|
||||
from app.repositories.user_repository import UserRepository
|
||||
|
||||
__all__ = [
|
||||
'BaseRepository',
|
||||
'ClassroomRepository',
|
||||
'ReservationRepository',
|
||||
'UserRepository'
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import TypeVar, Generic, Type, Optional, List
|
||||
from app import db
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class BaseRepository(Generic[T]):
|
||||
"""Repositorio base con operaciones CRUD genéricas y seguras."""
|
||||
|
||||
def __init__(self, model_class: Type[T]):
|
||||
self.model_class = model_class
|
||||
|
||||
def get_by_id(self, entity_id: int) -> Optional[T]:
|
||||
"""Obtiene una entidad por su ID numérico."""
|
||||
if not entity_id:
|
||||
return None
|
||||
return db.session.get(self.model_class, entity_id)
|
||||
|
||||
def get_all(self, order_by=None) -> List[T]:
|
||||
"""Obtiene todas las entidades opcionalmente ordenadas."""
|
||||
query = self.model_class.query
|
||||
if order_by is not None:
|
||||
query = query.order_by(order_by)
|
||||
return query.all()
|
||||
|
||||
def save(self, entity: T, commit: bool = True) -> T:
|
||||
"""Agrega o actualiza una entidad en la sesión."""
|
||||
db.session.add(entity)
|
||||
if commit:
|
||||
db.session.commit()
|
||||
return entity
|
||||
|
||||
def delete(self, entity: T, commit: bool = True) -> bool:
|
||||
"""Elimina una entidad de la sesión."""
|
||||
if entity:
|
||||
db.session.delete(entity)
|
||||
if commit:
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def commit(self):
|
||||
"""Confirma la transacción actual."""
|
||||
db.session.commit()
|
||||
|
||||
def rollback(self):
|
||||
"""Revierte la transacción actual."""
|
||||
db.session.rollback()
|
||||
@@ -0,0 +1,59 @@
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import func
|
||||
from app.models.classroom import Classroom
|
||||
from app.repositories.base_repository import BaseRepository
|
||||
from app import db
|
||||
|
||||
class ClassroomRepository(BaseRepository[Classroom]):
|
||||
"""Repositorio especializado en operaciones de persistencia y filtrado de aulas."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(Classroom)
|
||||
|
||||
def get_by_code_and_building(self, building_id: int, code: str, floor: str) -> Optional[Classroom]:
|
||||
"""Busca un aula por edificio, código y piso para verificar duplicados."""
|
||||
return Classroom.query.filter_by(
|
||||
building_id=building_id,
|
||||
code=code.strip(),
|
||||
floor=str(floor).strip()
|
||||
).first()
|
||||
|
||||
def get_active_classrooms(self, building_id: Optional[int] = None, floor: Optional[str] = None,
|
||||
only_virtual: bool = False, only_physical: bool = False) -> List[Classroom]:
|
||||
"""Retorna aulas activas con filtros opcionales de edificio, piso y modalidad."""
|
||||
query = Classroom.query.filter_by(is_active=True)
|
||||
|
||||
if building_id:
|
||||
query = query.filter_by(building_id=building_id)
|
||||
|
||||
if floor:
|
||||
query = query.filter_by(floor=str(floor).strip())
|
||||
|
||||
classrooms = query.order_by(Classroom.floor.asc(), Classroom.code.asc()).all()
|
||||
|
||||
if only_virtual:
|
||||
return [c for c in classrooms if c.is_virtual]
|
||||
elif only_physical:
|
||||
return [c for c in classrooms if not c.is_virtual]
|
||||
|
||||
return classrooms
|
||||
|
||||
def get_virtual_classrooms(self) -> List[Classroom]:
|
||||
"""Obtiene todas las salas virtuales activas."""
|
||||
return [c for c in self.get_active_classrooms() if c.is_virtual]
|
||||
|
||||
def get_physical_classrooms(self) -> List[Classroom]:
|
||||
"""Obtiene todas las aulas físicas activas."""
|
||||
return [c for c in self.get_active_classrooms() if not c.is_virtual]
|
||||
|
||||
def search(self, term: str) -> List[Classroom]:
|
||||
"""Búsqueda de aulas por código, edificio o descripción."""
|
||||
if not term:
|
||||
return self.get_all(order_by=Classroom.code.asc())
|
||||
term_clean = f"%{term.strip()}%"
|
||||
return Classroom.query.filter(
|
||||
Classroom.is_active == True,
|
||||
(Classroom.code.ilike(term_clean)) |
|
||||
(Classroom.building.ilike(term_clean)) |
|
||||
(Classroom.description.ilike(term_clean))
|
||||
).order_by(Classroom.code.asc()).all()
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date, time
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy import and_, or_
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
from app.repositories.base_repository import BaseRepository
|
||||
from app import db
|
||||
|
||||
class ReservationRepository(BaseRepository[Reservation]):
|
||||
"""Repositorio para la persistencia y consulta de reservas de aulas."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(Reservation)
|
||||
|
||||
def find_conflicts(self, classroom_id: int, start_time: datetime, end_time: datetime,
|
||||
exclude_reservation_id: Optional[int] = None) -> List[Reservation]:
|
||||
"""
|
||||
Encuentra reservas activas que se solapan en el mismo aula y franja horaria.
|
||||
Condición de solapamiento: (start_time < existing.end_time) AND (end_time > existing.start_time).
|
||||
"""
|
||||
query = Reservation.query.filter(
|
||||
Reservation.classroom_id == classroom_id,
|
||||
Reservation.status != ReservationStatus.CANCELLED.value,
|
||||
Reservation.start_time < end_time,
|
||||
Reservation.end_time > start_time
|
||||
)
|
||||
|
||||
if exclude_reservation_id:
|
||||
query = query.filter(Reservation.id != exclude_reservation_id)
|
||||
|
||||
return query.all()
|
||||
|
||||
def get_by_date_range(self, start_dt: datetime, end_dt: datetime,
|
||||
classroom_id: Optional[int] = None) -> List[Reservation]:
|
||||
"""Obtiene reservas dentro de una ventana temporal con carga ansiosa de relaciones."""
|
||||
query = Reservation.query.options(
|
||||
joinedload(Reservation.classroom),
|
||||
joinedload(Reservation.commission),
|
||||
joinedload(Reservation.user)
|
||||
).filter(
|
||||
Reservation.start_time >= start_dt,
|
||||
Reservation.start_time <= end_dt,
|
||||
Reservation.status != ReservationStatus.CANCELLED.value
|
||||
)
|
||||
|
||||
if classroom_id:
|
||||
query = query.filter(Reservation.classroom_id == classroom_id)
|
||||
|
||||
return query.order_by(Reservation.start_time.asc()).all()
|
||||
|
||||
def get_by_teacher(self, teacher_id: int) -> List[Reservation]:
|
||||
"""Obtiene reservas asignadas a un docente."""
|
||||
return Reservation.query.filter_by(
|
||||
user_id=teacher_id
|
||||
).order_by(Reservation.start_time.desc()).all()
|
||||
|
||||
def get_by_commission(self, commission_id: int) -> List[Reservation]:
|
||||
"""Obtiene todas las reservas asociadas a una comisión."""
|
||||
return Reservation.query.filter_by(
|
||||
commission_id=commission_id
|
||||
).order_by(Reservation.start_time.asc()).all()
|
||||
@@ -0,0 +1,34 @@
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import func
|
||||
from app.models.user import User
|
||||
from app.repositories.base_repository import BaseRepository
|
||||
|
||||
class UserRepository(BaseRepository[User]):
|
||||
"""Repositorio para la gestión de usuarios, credenciales y roles."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(User)
|
||||
|
||||
def get_by_email(self, email: str) -> Optional[User]:
|
||||
"""Obtiene un usuario por su dirección de email normalizada."""
|
||||
if not email:
|
||||
return None
|
||||
return User.query.filter(func.lower(User.email) == email.strip().lower()).first()
|
||||
|
||||
def get_active_users(self) -> List[User]:
|
||||
"""Obtiene todos los usuarios con cuenta activa."""
|
||||
return User.query.filter_by(is_active=True).order_by(User.name.asc()).all()
|
||||
|
||||
def get_by_role(self, role_name: str) -> List[User]:
|
||||
"""Obtiene usuarios por nombre de rol."""
|
||||
return User.query.filter(
|
||||
User.is_active == True,
|
||||
func.lower(User.role) == role_name.strip().lower()
|
||||
).order_by(User.name.asc()).all()
|
||||
|
||||
def get_teachers(self) -> List[User]:
|
||||
"""Obtiene lista de docentes y profesores activos."""
|
||||
return User.query.filter(
|
||||
User.is_active == True,
|
||||
User.role.in_(['TEACHER', 'DOCENTE', 'PROFESOR', 'teacher', 'docente'])
|
||||
).order_by(User.name.asc()).all()
|
||||
Reference in New Issue
Block a user