"""
Attendance data models for LMS MCP Server
"""
from typing import List, Optional, Dict, Any
from datetime import datetime, date as Date
from pydantic import BaseModel, Field, validator
from enum import Enum
class AttendanceStatus(str, Enum):
"""Enumeration for attendance status"""
PRESENT = "present"
ABSENT = "absent"
LATE = "late"
EXCUSED = "excused"
UNKNOWN = "unknown"
class AttendanceRecord(BaseModel):
"""Individual attendance record for a specific date and subject"""
date: Date = Field(..., description="Date of the class")
subject_code: str = Field(..., description="Subject/course code")
subject_name: Optional[str] = Field(None, description="Subject/course name")
status: AttendanceStatus = Field(..., description="Attendance status")
time_slot: Optional[str] = Field(None, description="Time slot of the class")
instructor: Optional[str] = Field(None, description="Instructor name")
room: Optional[str] = Field(None, description="Room/venue")
duration: Optional[int] = Field(None, description="Class duration in minutes")
notes: Optional[str] = Field(None, description="Additional notes")
class Config:
use_enum_values = True
json_encoders = {Date: lambda v: v.isoformat()}
class SubjectAttendance(BaseModel):
"""Attendance summary for a specific subject"""
subject_code: str = Field(..., description="Subject/course code")
subject_name: Optional[str] = Field(None, description="Subject/course name")
total_classes: int = Field(0, description="Total number of classes")
present_count: int = Field(0, description="Number of classes attended")
absent_count: int = Field(0, description="Number of classes missed")
late_count: int = Field(0, description="Number of late arrivals")
excused_count: int = Field(0, description="Number of excused absences")
attendance_percentage: float = Field(0.0, description="Attendance percentage")
required_percentage: Optional[float] = Field(
None, description="Required minimum attendance"
)
status: str = Field("unknown", description="Overall attendance status")
records: List[AttendanceRecord] = Field(
default_factory=list, description="Individual attendance records"
)
@validator("attendance_percentage", pre=True)
def calculate_percentage(cls, v, values):
"""Calculate attendance percentage if not provided"""
if v == 0.0 and values.get("total_classes", 0) > 0:
present = values.get("present_count", 0)
total = values.get("total_classes", 0)
return round((present / total) * 100, 2) if total > 0 else 0.0
return v
@validator("status", pre=True, always=True)
def determine_status(cls, v, values):
"""Determine attendance status based on percentage"""
percentage = values.get("attendance_percentage", 0.0)
required = values.get("required_percentage", 75.0)
if percentage >= required:
return "good"
elif percentage >= (required * 0.8): # 80% of required
return "warning"
else:
return "critical"
def add_record(self, record: AttendanceRecord):
"""Add an attendance record and update counts"""
self.records.append(record)
self.total_classes = len(self.records)
# Recalculate counts
self.present_count = sum(
1 for r in self.records if r.status == AttendanceStatus.PRESENT
)
self.absent_count = sum(
1 for r in self.records if r.status == AttendanceStatus.ABSENT
)
self.late_count = sum(
1 for r in self.records if r.status == AttendanceStatus.LATE
)
self.excused_count = sum(
1 for r in self.records if r.status == AttendanceStatus.EXCUSED
)
# Recalculate percentage
if self.total_classes > 0:
self.attendance_percentage = round(
(self.present_count / self.total_classes) * 100, 2
)
class SemesterAttendance(BaseModel):
"""Complete attendance data for a semester"""
semester: str = Field(..., description="Semester identifier")
academic_year: str = Field(..., description="Academic year")
student_id: Optional[str] = Field(None, description="Student ID")
student_name: Optional[str] = Field(None, description="Student name")
subjects: List[SubjectAttendance] = Field(
default_factory=list, description="Subject-wise attendance"
)
overall_percentage: float = Field(0.0, description="Overall attendance percentage")
total_classes_all_subjects: int = Field(
0, description="Total classes across all subjects"
)
total_present_all_subjects: int = Field(
0, description="Total present across all subjects"
)
last_updated: datetime = Field(
default_factory=datetime.now, description="Last update timestamp"
)
@validator("overall_percentage", pre=True, always=True)
def calculate_overall_percentage(cls, v, values):
"""Calculate overall attendance percentage"""
subjects = values.get("subjects", [])
if not subjects:
return 0.0
total_classes = sum(s.total_classes for s in subjects)
total_present = sum(s.present_count for s in subjects)
if total_classes > 0:
return round((total_present / total_classes) * 100, 2)
return 0.0
def add_subject(self, subject_attendance: SubjectAttendance):
"""Add subject attendance data"""
# Remove existing subject with same code
self.subjects = [
s
for s in self.subjects
if s.subject_code != subject_attendance.subject_code
]
# Add new subject
self.subjects.append(subject_attendance)
# Update totals
self.total_classes_all_subjects = sum(s.total_classes for s in self.subjects)
self.total_present_all_subjects = sum(s.present_count for s in self.subjects)
self.last_updated = datetime.now()
def get_subject_attendance(self, subject_code: str) -> Optional[SubjectAttendance]:
"""Get attendance data for a specific subject"""
for subject in self.subjects:
if subject.subject_code == subject_code:
return subject
return None
def get_critical_subjects(self, threshold: float = 75.0) -> List[SubjectAttendance]:
"""Get subjects with attendance below threshold"""
return [s for s in self.subjects if s.attendance_percentage < threshold]
def to_summary(self) -> Dict[str, Any]:
"""Convert to summary format"""
return {
"semester": self.semester,
"academic_year": self.academic_year,
"overall_percentage": self.overall_percentage,
"total_subjects": len(self.subjects),
"critical_subjects": len(self.get_critical_subjects()),
"total_classes": self.total_classes_all_subjects,
"total_present": self.total_present_all_subjects,
"last_updated": self.last_updated.isoformat(),
}
class AttendanceReport(BaseModel):
"""Complete attendance report for multiple semesters"""
student_id: str = Field(..., description="Student ID")
student_name: Optional[str] = Field(None, description="Student name")
semesters: List[SemesterAttendance] = Field(
default_factory=list, description="Semester-wise attendance"
)
generated_at: datetime = Field(
default_factory=datetime.now, description="Report generation timestamp"
)
def add_semester(self, semester_attendance: SemesterAttendance):
"""Add semester attendance data"""
# Remove existing semester
self.semesters = [
s for s in self.semesters if s.semester != semester_attendance.semester
]
# Add new semester
self.semesters.append(semester_attendance)
self.generated_at = datetime.now()
def get_current_semester(self) -> Optional[SemesterAttendance]:
"""Get most recent semester data"""
if not self.semesters:
return None
# Sort by last updated and return the most recent
sorted_semesters = sorted(
self.semesters, key=lambda x: x.last_updated, reverse=True
)
return sorted_semesters[0]
def get_semester(self, semester: str) -> Optional[SemesterAttendance]:
"""Get specific semester data"""
for sem in self.semesters:
if sem.semester == semester:
return sem
return None