| """
|
| Geolocation Verification Module for Attendr
|
| Implements KR Rule #2: Geolocation Verification
|
| ∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]
|
| """
|
|
|
| from utils import haversine_distance, validate_coordinates
|
| from models import Classroom, db
|
| from config import Config
|
| import logging
|
|
|
|
|
| logging.basicConfig(level=logging.INFO)
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| class GeolocationModule:
|
| """Handles all geolocation verification operations"""
|
|
|
| def __init__(self, default_radius=None):
|
| """
|
| Initialize geolocation module
|
|
|
| Args:
|
| default_radius: Default radius in meters for geofencing
|
| """
|
| self.default_radius = default_radius or Config.GEOLOCATION_RADIUS_METERS
|
| logger.info(f"Geolocation Module initialized with default radius={self.default_radius}m")
|
|
|
| def get_classroom_location(self, classroom_id):
|
| """
|
| Get classroom GPS coordinates and radius
|
|
|
| Args:
|
| classroom_id: ID of the classroom
|
|
|
| Returns:
|
| tuple: (latitude, longitude, radius) or (None, None, None) if not found
|
| """
|
| try:
|
| classroom = Classroom.query.get(classroom_id)
|
|
|
| if not classroom:
|
| logger.warning(f"Classroom {classroom_id} not found")
|
| return None, None, None
|
|
|
| radius = classroom.radius_meters or self.default_radius
|
| logger.info(f"Classroom {classroom.name}: lat={classroom.latitude}, lon={classroom.longitude}, radius={radius}m")
|
|
|
| return classroom.latitude, classroom.longitude, radius
|
|
|
| except Exception as e:
|
| logger.error(f"Error fetching classroom location: {str(e)}")
|
| return None, None, None
|
|
|
| def calculate_distance(self, lat1, lon1, lat2, lon2):
|
| """
|
| Calculate distance between two GPS coordinates
|
|
|
| Args:
|
| lat1, lon1: First coordinate
|
| lat2, lon2: Second coordinate
|
|
|
| Returns:
|
| Distance in meters
|
| """
|
| try:
|
| distance = haversine_distance(lat1, lon1, lat2, lon2)
|
| logger.info(f"Distance calculated: {distance:.2f} meters")
|
| return distance
|
| except Exception as e:
|
| logger.error(f"Distance calculation error: {str(e)}")
|
| return None
|
|
|
| def is_within_allowed_area(self, student_lat, student_lon, classroom_id):
|
| """
|
| Check if student is within allowed classroom area
|
| Implements: IsWithinAllowedArea(x)
|
|
|
| Args:
|
| student_lat: Student's latitude
|
| student_lon: Student's longitude
|
| classroom_id: ID of the classroom
|
|
|
| Returns:
|
| tuple: (is_within: bool, distance: float, error_message: str or None)
|
| """
|
| try:
|
|
|
| valid, error = validate_coordinates(student_lat, student_lon)
|
| if not valid:
|
| logger.warning(f"Invalid student coordinates: {error}")
|
| return False, None, error
|
|
|
|
|
| class_lat, class_lon, radius = self.get_classroom_location(classroom_id)
|
|
|
| if class_lat is None:
|
| return False, None, "Classroom location not found"
|
|
|
|
|
| distance = self.calculate_distance(
|
| float(student_lat),
|
| float(student_lon),
|
| class_lat,
|
| class_lon
|
| )
|
|
|
| if distance is None:
|
| return False, None, "Failed to calculate distance"
|
|
|
|
|
| is_within = distance <= radius
|
|
|
| logger.info(f"Location verification: within_area={is_within}, distance={distance:.2f}m, allowed_radius={radius}m")
|
|
|
| if is_within:
|
| return True, distance, None
|
| else:
|
| return False, distance, f"You are {distance:.0f}m away from the classroom (max allowed: {radius}m)"
|
|
|
| except Exception as e:
|
| logger.error(f"Location verification error: {str(e)}")
|
| return False, None, f"Location verification failed: {str(e)}"
|
|
|
| def verify_location(self, student_lat, student_lon, classroom_id):
|
| """
|
| Complete location verification process
|
| Implements: Student(x) ∧ IsWithinAllowedArea(x) → VerifiedLocation(x)
|
|
|
| Args:
|
| student_lat: Student's latitude
|
| student_lon: Student's longitude
|
| classroom_id: ID of the classroom
|
|
|
| Returns:
|
| dict: Verification result with status, distance, and message
|
| """
|
| is_within, distance, error = self.is_within_allowed_area(
|
| student_lat,
|
| student_lon,
|
| classroom_id
|
| )
|
|
|
| result = {
|
| 'verified': is_within,
|
| 'distance': distance,
|
| 'message': None,
|
| 'error': error
|
| }
|
|
|
| if is_within:
|
| result['message'] = f"Location verified! You are {distance:.0f}m from the classroom."
|
| elif error:
|
| result['message'] = error
|
|
|
| return result
|
|
|
| def get_all_classrooms(self):
|
| """
|
| Get all available classrooms
|
|
|
| Returns:
|
| List of classroom dictionaries
|
| """
|
| try:
|
| classrooms = Classroom.query.all()
|
| return [classroom.to_dict() for classroom in classrooms]
|
| except Exception as e:
|
| logger.error(f"Error fetching classrooms: {str(e)}")
|
| return []
|
|
|
| def add_classroom(self, name, building, latitude, longitude, radius_meters=None):
|
| """
|
| Add a new classroom to the system
|
|
|
| Args:
|
| name: Classroom name
|
| building: Building name
|
| latitude: GPS latitude
|
| longitude: GPS longitude
|
| radius_meters: Geofencing radius (optional)
|
|
|
| Returns:
|
| tuple: (success, classroom_dict or error_message)
|
| """
|
| try:
|
|
|
| valid, error = validate_coordinates(latitude, longitude)
|
| if not valid:
|
| return False, error
|
|
|
|
|
| classroom = Classroom(
|
| name=name,
|
| building=building,
|
| latitude=float(latitude),
|
| longitude=float(longitude),
|
| radius_meters=radius_meters or self.default_radius
|
| )
|
|
|
| db.session.add(classroom)
|
| db.session.commit()
|
|
|
| logger.info(f"Classroom added: {name} in {building}")
|
| return True, classroom.to_dict()
|
|
|
| except Exception as e:
|
| db.session.rollback()
|
| logger.error(f"Error adding classroom: {str(e)}")
|
| return False, f"Failed to add classroom: {str(e)}"
|
|
|
|
|
|
|
| geolocation_module = GeolocationModule()
|
|
|