""" Attendance Code Module for Attendr Implements KR Rule #5: Auto-Refresh Code Confirmation ∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x)) """ import random import string from datetime import datetime, timedelta from models import AttendanceSession, db from config import Config import logging # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class AttendanceCodeModule: """Handles attendance code generation, validation, and auto-refresh""" def __init__(self, code_length=None, refresh_interval=None): """ Initialize attendance code module Args: code_length: Length of the code (default from config) refresh_interval: Code refresh interval in seconds (default from config) """ self.code_length = code_length or Config.CODE_LENGTH self.refresh_interval = refresh_interval or Config.CODE_REFRESH_INTERVAL logger.info(f"Attendance Code Module initialized: length={self.code_length}, refresh={self.refresh_interval}s") def generate_code(self): """ Generate a random attendance code Returns: String code (e.g., "A3X9K2") """ # Use uppercase letters and digits for clarity characters = string.ascii_uppercase + string.digits code = ''.join(random.choice(characters) for _ in range(self.code_length)) logger.info(f"Generated new code: {code}") return code def create_session_code(self, session_id): """ Create and store a new code for a session Args: session_id: ID of the attendance session Returns: tuple: (success, code or error_message) """ try: session = AttendanceSession.query.get(session_id) if not session: return False, "Session not found" if not session.is_active: return False, "Session is not active" # Generate new code new_code = self.generate_code() # Update session session.current_code = new_code session.code_generated_at = datetime.utcnow() db.session.commit() logger.info(f"Session {session_id} code updated to: {new_code}") return True, new_code except Exception as e: db.session.rollback() logger.error(f"Error creating session code: {str(e)}") return False, f"Failed to create code: {str(e)}" def is_code_valid(self, session_id, entered_code): """ Validate if entered code matches current session code and is within refresh cycle Implements: ValidCodeEntry(User,x) ∧ WithinCycle(Code,x) Args: session_id: ID of the attendance session entered_code: Code entered by student Returns: tuple: (is_valid: bool, message: str) """ try: session = AttendanceSession.query.get(session_id) if not session: return False, "Session not found" if not session.is_active: return False, "Session is no longer active" if not session.current_code: return False, "No active code for this session" # Check if code matches (case-insensitive) if session.current_code.upper() != entered_code.upper(): logger.warning(f"Code mismatch: expected {session.current_code}, got {entered_code}") return False, "Invalid code. Please check and try again." # Check if code is within refresh cycle if not session.code_generated_at: return False, "Code timestamp not found" time_elapsed = (datetime.utcnow() - session.code_generated_at).total_seconds() if time_elapsed > self.refresh_interval: logger.warning(f"Code expired: {time_elapsed:.0f}s elapsed (max: {self.refresh_interval}s)") return False, f"Code has expired. Please use the current code displayed by your lecturer." # Code is valid time_remaining = self.refresh_interval - time_elapsed logger.info(f"Code validated successfully. Time remaining: {time_remaining:.0f}s") return True, "Code verified successfully" except Exception as e: logger.error(f"Code validation error: {str(e)}") return False, f"Validation failed: {str(e)}" def should_refresh_code(self, session_id): """ Check if code should be refreshed based on time elapsed Args: session_id: ID of the attendance session Returns: bool: True if code should be refreshed """ try: session = AttendanceSession.query.get(session_id) if not session or not session.is_active: return False if not session.code_generated_at: return True # No code generated yet time_elapsed = (datetime.utcnow() - session.code_generated_at).total_seconds() return time_elapsed >= self.refresh_interval except Exception as e: logger.error(f"Error checking refresh status: {str(e)}") return False def auto_refresh_code(self, session_id): """ Automatically refresh code if needed Args: session_id: ID of the attendance session Returns: tuple: (refreshed: bool, code or None) """ try: if self.should_refresh_code(session_id): success, code = self.create_session_code(session_id) if success: logger.info(f"Code auto-refreshed for session {session_id}") return True, code else: return False, None else: # Return current code session = AttendanceSession.query.get(session_id) if session and session.current_code: return False, session.current_code return False, None except Exception as e: logger.error(f"Auto-refresh error: {str(e)}") return False, None def get_code_status(self, session_id): """ Get current code status including time remaining Args: session_id: ID of the attendance session Returns: dict: Code status information """ try: session = AttendanceSession.query.get(session_id) if not session: return {'error': 'Session not found'} if not session.is_active: return {'error': 'Session is not active'} if not session.current_code or not session.code_generated_at: return { 'code': None, 'time_remaining': 0, 'needs_refresh': True } time_elapsed = (datetime.utcnow() - session.code_generated_at).total_seconds() time_remaining = max(0, self.refresh_interval - time_elapsed) needs_refresh = time_elapsed >= self.refresh_interval return { 'code': session.current_code, 'time_remaining': int(time_remaining), 'time_elapsed': int(time_elapsed), 'refresh_interval': self.refresh_interval, 'needs_refresh': needs_refresh, 'generated_at': session.code_generated_at.isoformat() } except Exception as e: logger.error(f"Error getting code status: {str(e)}") return {'error': str(e)} # Singleton instance attendance_code_module = AttendanceCodeModule()