smart-attendance-api / backend /app /services /session_service.py
Pratham200Rajbhar
first commit
78013c4
Raw
History Blame Contribute Delete
4.94 kB
from datetime import datetime, timedelta, timezone
from typing import Optional
from app.core.logging_config import get_logger
from app.db.redis import get_redis
from app.repositories.session_repo import SessionRepository
from app.repositories.class_repo import ClassRepository
from app.schemas.teacher import SessionResponse, SessionStart
logger = get_logger("app.session")
class SessionService:
def __init__(self) -> None:
self.session_repo = SessionRepository()
self.class_repo = ClassRepository()
async def start_session(self, data: SessionStart, teacher_id: str) -> Optional[SessionResponse]:
subject_class = await self.class_repo.get_by_id(data.academic_class_id)
if not subject_class or subject_class.teacherId != teacher_id:
return None
active = await self.session_repo.get_active_session_by_class(data.academic_class_id)
if active:
await self.close_session(active.id)
now = datetime.now(timezone.utc)
session = await self.session_repo.create(
class_id=data.academic_class_id, start_time=now, end_time=now + timedelta(minutes=data.duration_minutes)
)
return SessionResponse.model_validate(session)
async def stop_session(self, session_id: str, teacher_id: str) -> bool:
session = await self.session_repo.get_by_id(session_id)
if not session or not session.isActive:
return False
subject_class = await self.class_repo.get_by_id(session.academicClassId)
if not subject_class or subject_class.teacherId != teacher_id:
return False
await self.close_session(session_id)
return True
async def close_session(self, session_id: str) -> None:
"""Mark session as inactive, delete from Redis, and mark all unsubmitted students as Absent or Excused."""
session = await self.session_repo.get_by_id(session_id)
if not session or not session.isActive:
return
# 1. Deactivate in DB
await self.session_repo.deactivate(session_id)
# 2. Delete from Redis
try:
await get_redis().delete(f"session:{session_id}")
except Exception as e:
logger.warning("Failed to delete session from Redis: %s", e)
# 3. Get enrollments and existing attendance
from app.db.client import db
from app.services.gamification_service import GamificationService
enrollments = await db.enrollment.find_many(
where={"academicClassId": session.academicClassId},
include={"student": {"include": {"leaveRequests": True}}}
)
attendance_records = await db.attendance.find_many(where={"sessionId": session_id})
submitted_student_ids = {a.studentId for a in attendance_records}
gamification_service = GamificationService()
sess_start = session.startTime.replace(tzinfo=timezone.utc) if session.startTime.tzinfo is None else session.startTime
for enrollment in enrollments:
student = enrollment.student
if not student or student.id in submitted_student_ids:
continue
# Check if student was on approved leave during the session
on_leave = False
for leave in student.leaveRequests:
if leave.status == "APPROVED":
l_start = leave.startDate.replace(tzinfo=timezone.utc) if leave.startDate.tzinfo is None else leave.startDate
l_end = leave.endDate.replace(tzinfo=timezone.utc) if leave.endDate.tzinfo is None else leave.endDate
if l_start <= sess_start <= l_end:
on_leave = True
break
# Create the record
status_val = "Excused" if on_leave else "Absent"
remarks_val = "Excused via approved leave request" if on_leave else "Session ended without student submission"
try:
await db.attendance.create(data={
"studentId": student.id,
"sessionId": session_id,
"status": status_val,
"faceScore": 0.0,
"livenessScore": 0.0,
"backgroundScore": 0.0,
"finalAiScore": 0.0,
"gpsLatitude": 0.0,
"gpsLongitude": 0.0,
"remarks": remarks_val,
})
except Exception as e:
logger.warning("Failed to create default attendance for student %s: %s", student.id, e)
continue
# Recalculate streak
try:
await gamification_service.recalculate_student_streak(student.id)
except Exception as e:
logger.warning("Failed to recalculate streak for student %s: %s", student.id, e)