Files
admin-edu-space/backend/app/models/grade.py
T

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}>'