from app import db from datetime import datetime class Subject(db.Model): __tablename__ = 'subjects' id = db.Column(db.Integer, primary_key=True) code = db.Column(db.String(50), unique=True, nullable=False, index=True) name = db.Column(db.String(200), nullable=False) description = db.Column(db.Text) department = db.Column(db.String(100), nullable=False, default='') credits = db.Column(db.Integer, nullable=False, default=4) career_id = db.Column(db.Integer, db.ForeignKey('careers.id', ondelete='SET NULL'), nullable=True) is_active = db.Column('active', db.Boolean, default=True, nullable=False) # Maps to active column in DB is_short_course = db.Column(db.Boolean, default=False, nullable=True) # True para talleres/cursos cortos created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships commissions = db.relationship('Commission', backref='subject', lazy=True, cascade='all, delete-orphan') def to_dict(self): """Convert subject to dictionary""" return { 'id': self.id, 'code': self.code, 'name': self.name, 'description': self.description, 'department': self.department, 'credits': self.credits, 'career_id': self.career_id, 'career_name': self.career_obj.name if self.career_obj else None, 'is_active': self.is_active, 'is_short_course': bool(self.is_short_course), 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None } def __repr__(self): return f'' class Commission(db.Model): __tablename__ = 'commissions' id = db.Column(db.Integer, primary_key=True) subject_id = db.Column(db.Integer, db.ForeignKey('subjects.id'), nullable=False) code = db.Column(db.String(50), nullable=False) semester = db.Column(db.String(20), nullable=False) year = db.Column(db.Integer, nullable=False) teacher_id = db.Column(db.Integer, db.ForeignKey('users.id')) max_students = db.Column(db.Integer, nullable=False) current_students = db.Column(db.Integer, default=0) schedule = db.Column(db.String(255)) shift = db.Column(db.String(30), nullable=True) # Mañana, Tarde, Vespertino, Noche virtual_link = db.Column(db.String(500), nullable=True) # Link Zoom, Meet, Teams, Moodle active = db.Column(db.Boolean, default=True) # Changed from is_active to active # Cierre de Actas y Libro de Calificaciones (Fase 3 MVP) grades_closed = db.Column(db.Boolean, default=False, nullable=True) grades_closed_at = db.Column(db.DateTime, nullable=True) grades_closed_by = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'), nullable=True) acta_number = db.Column(db.String(50), nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Virtual property for compatibility @property def is_active(self): return self.active @is_active.setter def is_active(self, value): self.active = value # Relationships pass # Reservation already defines the relationship back to commission teacher = db.relationship('User', backref='teaching_commissions', foreign_keys=[teacher_id]) closed_by_user = db.relationship('User', foreign_keys=[grades_closed_by]) # Virtual field for teacher_name @property def teacher_name(self): return f"{self.teacher.first_name} {self.teacher.last_name}" if self.teacher else "TBD" @property def teachers(self): """Retorna la lista de docentes asignados. Si teachers_association tiene registros, los retorna ordenados (Titular primero). Si está vacía pero teacher_id está configurado, devuelve el docente titular. """ if hasattr(self, 'teachers_association') and self.teachers_association and len(self.teachers_association) > 0: res = [ta.to_dict() for ta in self.teachers_association] res.sort(key=lambda x: (not x.get('is_primary', False), x.get('role', '') != 'Titular')) return res if self.teacher: return [{ 'id': 0, 'commission_id': self.id, 'user_id': self.teacher.id, 'name': self.teacher.name, 'email': self.teacher.email, 'role': 'Titular', 'is_primary': True }] return [] @property def teacher_names(self): t_list = self.teachers if not t_list: return "Sin asignar" return ", ".join([f"{t['name']} ({t['role']})" for t in t_list]) # Unique constraint __table_args__ = (db.UniqueConstraint('subject_id', 'code', 'semester', 'year'),) def to_dict(self): """Convert commission to dictionary""" return { 'id': self.id, 'subject_id': self.subject_id, 'code': self.code, 'semester': self.semester, 'year': self.year, 'teacher_id': self.teacher_id, 'teacher_name': self.teacher_name, 'teachers': self.teachers, 'teacher_names': self.teacher_names, 'max_students': self.max_students, 'current_students': self.current_students, 'schedule': self.schedule, 'shift': self.shift, 'virtual_link': self.virtual_link, 'active': self.active, 'grades_closed': bool(self.grades_closed), 'grades_closed_at': self.grades_closed_at.isoformat() if self.grades_closed_at else None, 'grades_closed_by': self.grades_closed_by, 'closed_by_name': self.closed_by_user.name if self.closed_by_user else None, 'acta_number': self.acta_number, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 'full_code': self.get_full_code(), 'subject': self.subject.to_dict() if self.subject else None } def get_full_code(self): """Get full commission code""" subj = self.subject.code if self.subject else 'N/A' code = self.code or '' sem = str(self.semester or '').strip() yr = str(self.year or '').strip() if yr and yr in sem: return f"{subj}-{code}-{sem}" elif sem and yr: return f"{subj}-{code}-{sem}{yr}" elif sem: return f"{subj}-{code}-{sem}" elif yr: return f"{subj}-{code}-{yr}" return f"{subj}-{code}" def __repr__(self): return f'' class CommissionTeacher(db.Model): """Modelo de asignación de múltiples docentes a una comisión (Co-Docencia / Cátedra).""" __tablename__ = 'commission_teachers' id = db.Column(db.Integer, primary_key=True) commission_id = db.Column(db.Integer, db.ForeignKey('commissions.id', ondelete='CASCADE'), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) role = db.Column(db.String(50), default='Titular', nullable=False) # Titular, Adjunto, JTP, Ayudante is_primary = db.Column(db.Boolean, default=False, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # Relationships commission = db.relationship('Commission', backref=db.backref('teachers_association', cascade='all, delete-orphan', lazy='joined')) teacher = db.relationship('User', backref=db.backref('commission_assignments', cascade='all, delete-orphan', lazy='joined')) __table_args__ = (db.UniqueConstraint('commission_id', 'user_id'),) def to_dict(self): return { 'id': self.id, 'commission_id': self.commission_id, 'user_id': self.user_id, 'name': self.teacher.name if self.teacher else 'Docente', 'email': self.teacher.email if self.teacher else '', 'role': self.role or 'Titular', 'is_primary': self.is_primary, 'created_at': self.created_at.isoformat() if self.created_at else None } def __repr__(self): return f''