| """
|
| 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
|
|
|
|
|
| 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")
|
| """
|
|
|
| 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"
|
|
|
|
|
| new_code = self.generate_code()
|
|
|
|
|
| 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"
|
|
|
|
|
| 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."
|
|
|
|
|
| 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."
|
|
|
|
|
| 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
|
|
|
| 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:
|
|
|
| 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)}
|
|
|
|
|
|
|
| attendance_code_module = AttendanceCodeModule()
|
|
|