93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
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'<MilestoneGrade milestone={self.milestone_id} student={self.student_id} score={self.display_score}>'
|