198 lines
8.7 KiB
Python
198 lines
8.7 KiB
Python
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'<Subject {self.code}>'
|
|
|
|
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'<Commission {self.get_full_code()}>'
|
|
|
|
|
|
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'<CommissionTeacher Commission:{self.commission_id} User:{self.user_id} Role:{self.role}>'
|