feat(roadmap): implementar Sprints 1, 2 y 3 (MVC, Stateless JWT API y Spring Security)

This commit is contained in:
Carlos Tello
2026-09-05 01:46:57 -03:00
parent fa878cb0d5
commit 69b8b7ccef
34 changed files with 1750 additions and 21 deletions
+16
View File
@@ -0,0 +1,16 @@
"""Módulo de Data Transfer Objects (DTOs) y esquemas tipados con Pydantic."""
from app.schemas.classroom_dto import ClassroomCreateDTO, ClassroomUpdateDTO, ClassroomResponseDTO
from app.schemas.reservation_dto import ReservationCreateDTO, ReservationUpdateDTO, ReservationResponseDTO
from app.schemas.auth_dto import LoginDTO, TokenResponseDTO, RefreshTokenDTO
__all__ = [
'ClassroomCreateDTO',
'ClassroomUpdateDTO',
'ClassroomResponseDTO',
'ReservationCreateDTO',
'ReservationUpdateDTO',
'ReservationResponseDTO',
'LoginDTO',
'TokenResponseDTO',
'RefreshTokenDTO'
]
+16
View File
@@ -0,0 +1,16 @@
from typing import Optional, Dict, Any, List
from pydantic import BaseModel, Field, EmailStr
class LoginDTO(BaseModel):
email: str = Field(..., description="Correo electrónico institucional")
password: str = Field(..., min_length=1, description="Contraseña de acceso")
class TokenResponseDTO(BaseModel):
access_token: str
refresh_token: str
token_type: str = "Bearer"
expires_in: int = 900 # 15 minutos en segundos
user: Dict[str, Any]
class RefreshTokenDTO(BaseModel):
refresh_token: str = Field(..., description="Token de refresco válido")
+44
View File
@@ -0,0 +1,44 @@
from typing import Optional
from pydantic import BaseModel, Field, field_validator
class ClassroomBaseDTO(BaseModel):
code: str = Field(..., min_length=1, max_length=50, description="Código o nombre del aula (ej: Aula 101, VIRTUAL-A)")
floor: str = Field(default="PB", max_length=50, description="Piso o nivel (PB, Piso 1, etc.)")
capacity: int = Field(default=30, ge=1, le=10000, description="Capacidad máxima de estudiantes")
description: Optional[str] = Field(default=None, max_length=500, description="Detalles o equipamiento")
is_active: bool = Field(default=True, description="Estado operativo del aula")
@field_validator('code')
@classmethod
def clean_code(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("El código del aula no puede estar vacío.")
return v
@field_validator('floor')
@classmethod
def clean_floor(cls, v: str) -> str:
return str(v).strip() or "PB"
class ClassroomCreateDTO(ClassroomBaseDTO):
building_id: Optional[int] = Field(default=None, description="ID del edificio asignado")
building_name: Optional[str] = Field(default=None, description="Nombre de edificio (para creación dinámica)")
class ClassroomUpdateDTO(BaseModel):
building_id: Optional[int] = None
code: Optional[str] = None
floor: Optional[str] = None
capacity: Optional[int] = None
description: Optional[str] = None
is_active: Optional[bool] = None
class ClassroomResponseDTO(ClassroomBaseDTO):
id: int
building_id: Optional[int] = None
building_name: str
is_virtual: bool
location_display: str
class Config:
from_attributes = True
+43
View File
@@ -0,0 +1,43 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field, model_validator
class ReservationBaseDTO(BaseModel):
classroom_id: int = Field(..., description="ID del aula asignada")
commission_id: int = Field(..., description="ID de la comisión académica")
user_id: int = Field(..., description="ID del docente responsable")
start_time: datetime = Field(..., description="Fecha y hora de inicio")
end_time: datetime = Field(..., description="Fecha y hora de finalización")
purpose: str = Field(default="Clase regular", max_length=500, description="Motivo o título de la reserva")
expected_attendees: int = Field(default=20, ge=1, description="Cantidad estimada de asistentes")
shift: Optional[str] = Field(default=None, max_length=30, description="Turno (Mañana, Tarde, Vespertino, Noche)")
virtual_link: Optional[str] = Field(default=None, max_length=500, description="Enlace a sala virtual (Meet/Zoom)")
notes: Optional[str] = Field(default=None, description="Observaciones adicionales")
status: Optional[str] = Field(default="PENDING", description="Estado de la reserva")
@model_validator(mode='after')
def validate_time_range(self):
if self.end_time <= self.start_time:
raise ValueError("La hora de finalización debe ser posterior a la hora de inicio.")
return self
class ReservationCreateDTO(ReservationBaseDTO):
pass
class ReservationUpdateDTO(BaseModel):
classroom_id: Optional[int] = None
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
purpose: Optional[str] = None
expected_attendees: Optional[int] = None
shift: Optional[str] = None
virtual_link: Optional[str] = None
notes: Optional[str] = None
status: Optional[str] = None
class ReservationResponseDTO(ReservationBaseDTO):
id: int
created_at: Optional[datetime] = None
class Config:
from_attributes = True