from datetime import datetime from app import db class MilestoneGrade(db.Model): """ Calificación y seguimiento pedagógico individual de un alumno en un hito evaluativo (Fase 3 MVP). Vincula un AcademicMilestone con un estudiante matriculado (StudentEnrollment / User). """ __tablename__ = 'milestone_grades' id = db.Column(db.Integer, primary_key=True) milestone_id = db.Column( db.Integer, db.ForeignKey('academic_milestones.id', ondelete='CASCADE'), nullable=False, index=True ) enrollment_id = db.Column( db.Integer, db.ForeignKey('student_enrollments.id', ondelete='CASCADE'), nullable=False, index=True ) student_id = db.Column( db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True ) score = db.Column(db.Float, nullable=True) # Calificación numérica de 1.0 a 10.0 is_absent = db.Column(db.Boolean, default=False) # Flag de Ausente feedback = db.Column(db.Text, nullable=True) # Devolución formativa/pedagógica graded_by_user_id = db.Column( db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'), nullable=True ) graded_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Restricción de unicidad: un estudiante tiene una única nota por hito __table_args__ = ( db.UniqueConstraint('milestone_id', 'student_id', name='uq_milestone_student'), ) # Relaciones milestone = db.relationship('AcademicMilestone', backref=db.backref('grades', lazy='dynamic', cascade='all, delete-orphan')) enrollment = db.relationship('StudentEnrollment', backref=db.backref('milestone_grades', lazy='dynamic', cascade='all, delete-orphan')) student = db.relationship('User', foreign_keys=[student_id], backref=db.backref('received_grades', lazy='dynamic')) graded_by = db.relationship('User', foreign_keys=[graded_by_user_id]) @property def is_passed(self) -> bool: """Determina si la nota está aprobada (>= 4.00 y no ausente).""" if self.is_absent or self.score is None: return False return self.score >= 4.0 @property def display_score(self) -> str: """Representación textual estándar: 'Ausente', '7.5' o '-'.""" if self.is_absent: return 'Ausente' if self.score is not None: return f'{self.score:.1f}' if self.score % 1 != 0 else f'{int(self.score)}' return '-' def to_dict(self): return { 'id': self.id, 'milestone_id': self.milestone_id, 'milestone_title': self.milestone.title if self.milestone else None, 'milestone_type': self.milestone.tipo if self.milestone else None, 'milestone_color': self.milestone.color if self.milestone else None, 'enrollment_id': self.enrollment_id, 'student_id': self.student_id, 'student_name': self.student.name if self.student else None, 'student_email': self.student.email if self.student else None, 'score': self.score, 'display_score': self.display_score, 'is_absent': bool(self.is_absent), 'is_passed': self.is_passed, 'feedback': self.feedback or '', 'graded_by_user_id': self.graded_by_user_id, 'graded_by_name': self.graded_by.name if self.graded_by else None, 'graded_at': self.graded_at.isoformat() if self.graded_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, } def __repr__(self): return f''