from datetime import datetime from app import db class MilestoneType(db.Model): """ Catálogo dinámico y administrable de tipificaciones de hitos y evaluaciones académicas. (Parciales, Trabajos Prácticos, Recuperatorios, Finales, etc.) """ __tablename__ = 'milestone_types' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) # Ej: "Parcial 1", "Trabajo Práctico 1" code = db.Column(db.String(50), unique=True, nullable=False, index=True) # Ej: "parcial_1", "tp_1" color = db.Column(db.String(30), default='#0d6efd') # Hex color para badges y calendario description = db.Column(db.String(255), nullable=True) sort_order = db.Column(db.Integer, default=0) # Orden de aparición en selects is_active = db.Column(db.Boolean, default=True, nullable=False) # Activo / Inactivo created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relación uno-a-muchos con AcademicMilestone milestones = db.relationship('AcademicMilestone', backref='type_obj', lazy='dynamic', cascade='all, delete-orphan') def to_dict(self): return { 'id': self.id, 'name': self.name, 'code': self.code, 'color': self.color or '#0d6efd', 'description': self.description or '', 'sort_order': self.sort_order or 0, 'is_active': self.is_active, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f'' class AcademicMilestone(db.Model): """ Hito o evento académico programado para una materia/comisión (Examen, TP, Entrega). """ __tablename__ = 'academic_milestones' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(150), nullable=True) materia_codigo = db.Column(db.String(50), nullable=False, index=True) # Código de materia (ej: INGA0003) subject_id = db.Column(db.Integer, db.ForeignKey('subjects.id'), nullable=True) milestone_type_id = db.Column(db.Integer, db.ForeignKey('milestone_types.id'), nullable=False) date = db.Column(db.DateTime, nullable=False) # Fecha y hora del hito classroom = db.Column(db.String(100), nullable=True) # Aula o enlace virtual asignado link = db.Column(db.String(255), nullable=True) # Link Teams / Meet / Zoom si aplica notes = db.Column(db.Text, nullable=True) # Temario, consignas o notas is_active = db.Column(db.Boolean, default=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relación con Asignatura subject = db.relationship('Subject', backref=db.backref('milestones', lazy='dynamic')) @property def tipo(self): return self.type_obj.name if self.type_obj else 'Otro' @property def color(self): return self.type_obj.color if self.type_obj and self.type_obj.color else '#0d6efd' def to_dict(self): return { 'id': self.id, 'title': self.title or self.tipo, 'materiaCodigo': self.materia_codigo, 'tipo': self.tipo, 'milestone_type_id': self.milestone_type_id, 'tipo_info': self.type_obj.to_dict() if self.type_obj else None, 'color': self.color, 'fecha': self.date.isoformat() if self.date else None, 'aula': self.classroom, 'link': self.link, 'notas': self.notes, 'is_active': self.is_active } def __repr__(self): return f'' # Datos semilla estándar para inicialización BASE_MILESTONE_TYPES = [ { 'name': 'Parcial 1', 'code': 'parcial_1', 'color': '#0d6efd', 'description': 'Primer examen parcial de la cursada', 'sort_order': 1 }, { 'name': 'Parcial 2', 'code': 'parcial_2', 'color': '#0b5ed7', 'description': 'Segundo examen parcial de la cursada', 'sort_order': 2 }, { 'name': 'Trabajo Práctico 1', 'code': 'tp_1', 'color': '#198754', 'description': 'Primera entrega o evaluación práctica', 'sort_order': 3 }, { 'name': 'Trabajo Práctico 2', 'code': 'tp_2', 'color': '#157347', 'description': 'Segunda entrega o evaluación práctica', 'sort_order': 4 }, { 'name': 'Trabajo Práctico 3', 'code': 'tp_3', 'color': '#0f5132', 'description': 'Tercera entrega o proyecto integrador', 'sort_order': 5 }, { 'name': 'Recuperatorio Parcial 1', 'code': 'recu_p1', 'color': '#fd7e14', 'description': 'Instancia de recuperación para el Parcial 1', 'sort_order': 6 }, { 'name': 'Recuperatorio Parcial 2', 'code': 'recu_p2', 'color': '#e76b08', 'description': 'Instancia de recuperación para el Parcial 2', 'sort_order': 7 }, { 'name': 'Recuperatorio TP 1', 'code': 'recu_tp1', 'color': '#ffc107', 'description': 'Reentrega o recuperación de Trabajo Práctico 1', 'sort_order': 8 }, { 'name': 'Recuperatorio TP 2', 'code': 'recu_tp2', 'color': '#e0a800', 'description': 'Reentrega o recuperación de Trabajo Práctico 2', 'sort_order': 9 }, { 'name': 'Recuperatorio TP 3', 'code': 'recu_tp3', 'color': '#c69500', 'description': 'Reentrega o recuperación de Trabajo Práctico 3', 'sort_order': 10 }, { 'name': 'Final', 'code': 'final', 'color': '#dc3545', 'description': 'Examen final regular o libre', 'sort_order': 11 }, { 'name': 'Otro', 'code': 'otro', 'color': '#6c757d', 'description': 'Taller, seminario, clase especial o hito no tipificado', 'sort_order': 12 } ]