diff --git a/app.py b/app.py index d96ede8..cf108ce 100644 --- a/app.py +++ b/app.py @@ -100,6 +100,7 @@ class QRCode(db.Model): self.coordinate_accuracy = accuracy self.coordinates_updated_date = datetime.utcnow() +# Attendance Data Model class AttendanceData(db.Model): """Enhanced attendance tracking model with location support""" __tablename__ = 'attendance_data' @@ -212,6 +213,110 @@ def get_coordinates_from_address(address): print(f"❌ Error geocoding address '{address}': {e}") return None, None +''' +def get_coordinates_from_address(address): + """ + Enhanced geocoding function to get latitude and longitude from address + Returns (lat, lng, accuracy) tuple or (None, None, None) if failed + """ + if not address or address.strip() == '': + return None, None, None + + try: + # Using Nominatim (OpenStreetMap) geocoding service + url = "https://nominatim.openstreetmap.org/search" + params = { + 'q': address, + 'format': 'json', + 'limit': 1, + 'addressdetails': 1 + } + + headers = { + 'User-Agent': 'QR-Attendance-System/1.0' + } + + response = requests.get(url, params=params, headers=headers, timeout=10) + + if response.status_code == 200: + data = response.json() + + if data and len(data) > 0: + result = data[0] + lat = float(result['lat']) + lng = float(result['lon']) + + # Determine accuracy based on result type + place_type = result.get('type', 'unknown') + osm_type = result.get('osm_type', 'unknown') + + if place_type in ['house', 'building'] or osm_type == 'way': + accuracy = 'high' + elif place_type in ['neighbourhood', 'suburb', 'quarter']: + accuracy = 'medium' + else: + accuracy = 'low' + + print(f"✅ Geocoded address: {address}") + print(f" Coordinates: {lat:.6f}, {lng:.6f}") + print(f" Accuracy: {accuracy} ({place_type})") + + return lat, lng, accuracy + + print(f"⚠️ No geocoding results for address: {address}") + return None, None, None + + except requests.RequestException as e: + print(f"❌ Geocoding request error: {e}") + return None, None, None + except (ValueError, KeyError) as e: + print(f"❌ Geocoding parsing error: {e}") + return None, None, None + except Exception as e: + print(f"❌ Unexpected geocoding error: {e}") + return None, None, None +''' +''' +def get_coordinates_from_address(address): + """ + Get latitude and longitude from address using geocoding service + Returns (lat, lng) tuple or (None, None) if failed + """ + if not address or address.strip() == '': + return None, None + + try: + # Using Nominatim/OpenStreetMap for free geocoding + url = "https://nominatim.openstreetmap.org/search" + params = { + 'q': address, + 'format': 'json', + 'limit': 1, + 'addressdetails': 1 + } + + headers = { + 'User-Agent': 'QR-Attendance-System/1.0' + } + + response = requests.get(url, params=params, headers=headers, timeout=10) + + if response.status_code == 200: + data = response.json() + if data and len(data) > 0: + lat = float(data[0]['lat']) + lng = float(data[0]['lon']) + print(f"✅ Geocoded address '{address[:50]}...' to coordinates: {lat}, {lng}") + return lat, lng + + print(f"⚠️ Could not geocode address: {address}") + return None, None + + except Exception as e: + print(f"❌ Error geocoding address '{address}': {e}") + return None, None +''' + def get_coordinates_from_address_enhanced(address): """ Enhanced geocoding function with better error handling @@ -545,68 +650,473 @@ def get_client_ip(): else: return request.environ['HTTP_X_FORWARDED_FOR'] -def get_coordinates_from_address(address): +def calculate_distance_miles(lat1, lng1, lat2, lng2): """ - Enhanced geocoding function to get latitude and longitude from address - Returns (lat, lng, accuracy) tuple or (None, None, None) if failed + Calculate the great circle distance between two points on Earth in miles + Using the Haversine formula """ - if not address or address.strip() == '': - return None, None, None + if any(coord is None for coord in [lat1, lng1, lat2, lng2]): + return None try: - # Using Nominatim (OpenStreetMap) geocoding service - url = "https://nominatim.openstreetmap.org/search" - params = { - 'q': address, - 'format': 'json', - 'limit': 1, - 'addressdetails': 1 - } + # Convert decimal degrees to radians + lat1, lng1, lat2, lng2 = map(radians, [lat1, lng1, lat2, lng2]) - headers = { - 'User-Agent': 'QR-Attendance-System/1.0' - } + # Haversine formula + dlng = lng2 - lng1 + dlat = lat2 - lat1 + a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlng/2)**2 + c = 2 * asin(sqrt(a)) - response = requests.get(url, params=params, headers=headers, timeout=10) + # Radius of Earth in miles + r_miles = 3959 - if response.status_code == 200: - data = response.json() - - if data and len(data) > 0: - result = data[0] - lat = float(result['lat']) - lng = float(result['lon']) - - # Determine accuracy based on result type - place_type = result.get('type', 'unknown') - osm_type = result.get('osm_type', 'unknown') - - if place_type in ['house', 'building'] or osm_type == 'way': - accuracy = 'high' - elif place_type in ['neighbourhood', 'suburb', 'quarter']: - accuracy = 'medium' - else: - accuracy = 'low' - - print(f"✅ Geocoded address: {address}") - print(f" Coordinates: {lat:.6f}, {lng:.6f}") - print(f" Accuracy: {accuracy} ({place_type})") - - return lat, lng, accuracy - - print(f"⚠️ No geocoding results for address: {address}") - return None, None, None + # Calculate the result + distance = c * r_miles + + print(f"📏 Calculated distance: {distance:.3f} miles") + return round(distance, 3) - except requests.RequestException as e: - print(f"❌ Geocoding request error: {e}") - return None, None, None - except (ValueError, KeyError) as e: - print(f"❌ Geocoding parsing error: {e}") - return None, None, None except Exception as e: - print(f"❌ Unexpected geocoding error: {e}") - return None, None, None + print(f"❌ Error calculating distance: {e}") + return None +def calculate_location_accuracy(qr_address, checkin_address, checkin_lat=None, checkin_lng=None): + """ + Calculate location accuracy by comparing QR code address with check-in location + Returns distance in miles between the two locations + """ + print(f"\n📍 CALCULATING LOCATION ACCURACY:") + print(f" QR Address: {qr_address}") + print(f" Check-in Address: {checkin_address}") + print(f" Check-in Coordinates: {checkin_lat}, {checkin_lng}") + + # Get QR code coordinates from address + qr_lat, qr_lng = get_coordinates_from_address(qr_address) + + if qr_lat is None or qr_lng is None: + print(f"⚠️ Could not geocode QR address, cannot calculate accuracy") + return None + + # Use check-in coordinates if available, otherwise geocode check-in address + if checkin_lat is not None and checkin_lng is not None: + checkin_coords_lat, checkin_coords_lng = checkin_lat, checkin_lng + print(f"✅ Using GPS coordinates for check-in location") + else: + checkin_coords_lat, checkin_coords_lng = get_coordinates_from_address(checkin_address) + if checkin_coords_lat is None or checkin_coords_lng is None: + print(f"⚠️ Could not geocode check-in address, cannot calculate accuracy") + return None + print(f"✅ Using geocoded coordinates for check-in address") + + # Calculate distance + distance = calculate_distance_miles(qr_lat, qr_lng, checkin_coords_lat, checkin_coords_lng) + + if distance is not None: + print(f"✅ Location accuracy calculated: {distance} miles") + + return distance + +def get_location_accuracy_level(location_accuracy): + """Get human-readable location accuracy level based on distance""" + if not location_accuracy: + return 'unknown' + elif location_accuracy <= 0.1: # Within 0.1 mile (528 feet) + return 'excellent' + elif location_accuracy <= 0.5: # Within 0.5 mile + return 'good' + elif location_accuracy <= 1.0: # Within 1 mile + return 'fair' + else: + return 'poor' + +def get_location_accuracy_level_enhanced(location_accuracy): + """ + Enhanced function to categorize location accuracy with more granular levels + """ + if not location_accuracy or location_accuracy is None: + return 'unknown' + + # More precise accuracy thresholds + if location_accuracy <= 0.05: # Within 264 feet (50 meters) + return 'excellent' + elif location_accuracy <= 0.1: # Within 528 feet (100 meters) + return 'very_good' + elif location_accuracy <= 0.25: # Within 0.25 mile (1320 feet) + return 'good' + elif location_accuracy <= 0.5: # Within 0.5 mile + return 'fair' + elif location_accuracy <= 1.0: # Within 1 mile + return 'poor' + else: # Greater than 1 mile + return 'very_poor' +''' +def process_location_data(location_data): + """ + Process and validate location data from form + Returns clean location data or None values for invalid data + """ + processed = { + 'latitude': None, + 'longitude': None, + 'accuracy': None, + 'altitude': None, + 'source': location_data.get('location_source', 'manual'), + 'address': location_data.get('address', '')[:500] if location_data.get('address') else None + } + + try: + # Process latitude + if location_data.get('latitude') and location_data['latitude'] not in ['null', '']: + lat = float(location_data['latitude']) + if -90 <= lat <= 90: # Valid latitude range + processed['latitude'] = lat + else: + print(f"⚠️ Invalid latitude: {lat}") + + # Process longitude + if location_data.get('longitude') and location_data['longitude'] not in ['null', '']: + lng = float(location_data['longitude']) + if -180 <= lng <= 180: # Valid longitude range + processed['longitude'] = lng + else: + print(f"⚠️ Invalid longitude: {lng}") + + # Process accuracy + if location_data.get('accuracy') and location_data['accuracy'] not in ['null', '']: + acc = float(location_data['accuracy']) + if acc >= 0: # Accuracy should be positive + processed['accuracy'] = acc + else: + print(f"⚠️ Invalid accuracy: {acc}") + + # Process altitude + if location_data.get('altitude') and location_data['altitude'] not in ['null', '']: + alt = float(location_data['altitude']) + # Altitude can be negative (below sea level) + processed['altitude'] = alt + + except (ValueError, TypeError) as e: + print(f"⚠️ Error processing location data: {e}") + + return processed +''' +def process_location_data(location_data): + """ + Process and validate location data from form + Returns clean location data or None values for invalid data + """ + processed = { + 'latitude': None, + 'longitude': None, + 'accuracy': None, + 'altitude': None, + 'source': location_data.get('location_source', 'manual'), + 'address': location_data.get('address', '')[:500] if location_data.get('address') else None + } + + try: + # Process latitude + if location_data.get('latitude') and location_data['latitude'] not in ['null', '']: + lat = float(location_data['latitude']) + if -90 <= lat <= 90: # Valid latitude range + processed['latitude'] = lat + else: + print(f"⚠️ Invalid latitude: {lat}") + + # Process longitude + if location_data.get('longitude') and location_data['longitude'] not in ['null', '']: + lng = float(location_data['longitude']) + if -180 <= lng <= 180: # Valid longitude range + processed['longitude'] = lng + else: + print(f"⚠️ Invalid longitude: {lng}") + + # Process accuracy + if location_data.get('accuracy') and location_data['accuracy'] not in ['null', '']: + acc = float(location_data['accuracy']) + if acc >= 0: # Accuracy should be positive + processed['accuracy'] = acc + else: + print(f"⚠️ Invalid accuracy: {acc}") + + # Process altitude + if location_data.get('altitude') and location_data['altitude'] not in ['null', '']: + alt = float(location_data['altitude']) + # Altitude can be negative (below sea level) + processed['altitude'] = alt + + except (ValueError, TypeError) as e: + print(f"⚠️ Error processing location data: {e}") + + return processed +''' +def process_location_data_enhanced(form_data): + """ + Enhanced location data processing with better validation and error handling + """ + processed = { + 'latitude': None, + 'longitude': None, + 'accuracy': None, + 'altitude': None, + 'source': form_data.get('location_source', 'manual'), + 'address': None + } + + try: + # Enhanced latitude validation + if form_data.get('latitude') and form_data['latitude'] not in ['null', '', 'undefined']: + lat = float(form_data['latitude']) + if -90 <= lat <= 90: + processed['latitude'] = round(lat, 6) # 6 decimal precision + else: + print(f"⚠️ Invalid latitude range: {lat}") + + # Enhanced longitude validation + if form_data.get('longitude') and form_data['longitude'] not in ['null', '', 'undefined']: + lng = float(form_data['longitude']) + if -180 <= lng <= 180: + processed['longitude'] = round(lng, 6) # 6 decimal precision + else: + print(f"⚠️ Invalid longitude range: {lng}") + + # Enhanced accuracy validation + if form_data.get('accuracy') and form_data['accuracy'] not in ['null', '', 'undefined']: + acc = float(form_data['accuracy']) + if 0 <= acc <= 50000: # Reasonable accuracy range in meters + processed['accuracy'] = round(acc, 1) + else: + print(f"⚠️ Invalid accuracy value: {acc}") + + # Enhanced altitude validation + if form_data.get('altitude') and form_data['altitude'] not in ['null', '', 'undefined']: + alt = float(form_data['altitude']) + if -1000 <= alt <= 10000: # Reasonable altitude range in meters + processed['altitude'] = round(alt, 1) + else: + print(f"⚠️ Invalid altitude value: {alt}") + + # Enhanced address processing + if form_data.get('address'): + address = form_data['address'].strip() + if len(address) > 0: + processed['address'] = address[:500] # Limit to 500 characters + + print(f"✅ Enhanced location data processed: {processed}") + + except (ValueError, TypeError) as e: + print(f"⚠️ Error in enhanced location data processing: {e}") + + return processed +''' +def process_location_data_enhanced(form_data): + """ + Enhanced location data processing with better validation and error handling + """ + processed = { + 'latitude': None, + 'longitude': None, + 'accuracy': None, + 'altitude': None, + 'source': form_data.get('location_source', 'manual'), + 'address': None + } + + try: + # Enhanced latitude validation + if form_data.get('latitude') and form_data['latitude'] not in ['null', '', 'undefined']: + lat = float(form_data['latitude']) + if -90 <= lat <= 90: + processed['latitude'] = round(lat, 6) # 6 decimal precision + else: + print(f"⚠️ Invalid latitude range: {lat}") + + # Enhanced longitude validation + if form_data.get('longitude') and form_data['longitude'] not in ['null', '', 'undefined']: + lng = float(form_data['longitude']) + if -180 <= lng <= 180: + processed['longitude'] = round(lng, 6) # 6 decimal precision + else: + print(f"⚠️ Invalid longitude range: {lng}") + + # Enhanced accuracy validation + if form_data.get('accuracy') and form_data['accuracy'] not in ['null', '', 'undefined']: + acc = float(form_data['accuracy']) + if 0 <= acc <= 50000: # Reasonable accuracy range in meters + processed['accuracy'] = round(acc, 1) + else: + print(f"⚠️ Invalid accuracy value: {acc}") + + # Enhanced altitude validation + if form_data.get('altitude') and form_data['altitude'] not in ['null', '', 'undefined']: + alt = float(form_data['altitude']) + if -1000 <= alt <= 10000: # Reasonable altitude range in meters + processed['altitude'] = round(alt, 1) + else: + print(f"⚠️ Invalid altitude value: {alt}") + + # Enhanced address processing + if form_data.get('address'): + address = form_data['address'].strip() + if len(address) > 0: + processed['address'] = address[:500] # Limit to 500 characters + + print(f"✅ Enhanced location data processed: {processed}") + + except (ValueError, TypeError) as e: + print(f"⚠️ Error in enhanced location data processing: {e}") + + return processed +''' +def migrate_to_enhanced_location_accuracy(): + """ + Migration function to recalculate all existing records with enhanced accuracy + """ + try: + print("🔄 Starting enhanced location accuracy migration...") + + # Get all records that need recalculation + records = db.session.execute(text(""" + SELECT ad.id, qc.location_address, ad.address, ad.latitude, ad.longitude, ad.location_accuracy + FROM attendance_data ad + LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE qc.location_address IS NOT NULL + """)).fetchall() + + print(f"📊 Found {len(records)} records to process") + + updated_count = 0 + improved_count = 0 + + for record in records: + try: + # Calculate enhanced location accuracy + new_accuracy = calculate_location_accuracy_enhanced( + qr_address=record.location_address, + checkin_address=record.address, + checkin_lat=record.latitude, + checkin_lng=record.longitude + ) + + if new_accuracy is not None: + # Update the record + db.session.execute(text(""" + UPDATE attendance_data + SET location_accuracy = :accuracy + WHERE id = :record_id + """), { + 'accuracy': new_accuracy, + 'record_id': record.id + }) + + updated_count += 1 + + # Check if this is an improvement + if record.location_accuracy is None or abs(new_accuracy - (record.location_accuracy or 0)) > 0.001: + improved_count += 1 + print(f" ✅ Updated record {record.id}: {record.location_accuracy} → {new_accuracy:.4f} miles") + + except Exception as e: + print(f" ⚠️ Error processing record {record.id}: {e}") + + # Commit all changes + db.session.commit() + + print(f"✅ Enhanced migration completed!") + print(f" 📊 Records processed: {len(records)}") + print(f" ✅ Records updated: {updated_count}") + print(f" 📈 Records improved: {improved_count}") + + return True + + except Exception as e: + print(f"❌ Enhanced migration failed: {e}") + db.session.rollback() + return False +''' +def migrate_to_enhanced_location_accuracy(): + """ + Migration function to recalculate all existing records with enhanced accuracy + """ + try: + print("🔄 Starting enhanced location accuracy migration...") + + # Get all records that need recalculation + records = db.session.execute(text(""" + SELECT ad.id, qc.location_address, ad.address, ad.latitude, ad.longitude, ad.location_accuracy + FROM attendance_data ad + LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id + WHERE qc.location_address IS NOT NULL + """)).fetchall() + + print(f"📊 Found {len(records)} records to process") + + updated_count = 0 + improved_count = 0 + + for record in records: + try: + # Calculate enhanced location accuracy + new_accuracy = calculate_location_accuracy_enhanced( + qr_address=record.location_address, + checkin_address=record.address, + checkin_lat=record.latitude, + checkin_lng=record.longitude + ) + + if new_accuracy is not None: + # Update the record + db.session.execute(text(""" + UPDATE attendance_data + SET location_accuracy = :accuracy + WHERE id = :record_id + """), { + 'accuracy': new_accuracy, + 'record_id': record.id + }) + + updated_count += 1 + + # Check if this is an improvement + if record.location_accuracy is None or abs(new_accuracy - (record.location_accuracy or 0)) > 0.001: + improved_count += 1 + print(f" ✅ Updated record {record.id}: {record.location_accuracy} → {new_accuracy:.4f} miles") + + except Exception as e: + print(f" ⚠️ Error processing record {record.id}: {e}") + + # Commit all changes + db.session.commit() + + print(f"✅ Enhanced migration completed!") + print(f" 📊 Records processed: {len(records)}") + print(f" ✅ Records updated: {updated_count}") + print(f" 📈 Records improved: {improved_count}") + + return True + + except Exception as e: + print(f"❌ Enhanced migration failed: {e}") + db.session.rollback() + return False + +def check_location_accuracy_column_exists(): + """Check if the location_accuracy column exists in the attendance_data table""" + try: + result = db.session.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name='attendance_data' AND column_name='location_accuracy' + """)) + + column_exists = result.fetchone() is not None + return column_exists + + except Exception as e: + print(f"⚠️ Error checking location_accuracy column: {e}") + return False + # Authentication decorator def login_required(f): """Decorator to ensure user is logged in""" @@ -1274,7 +1784,6 @@ def bulk_activate_users(): print(f"Error in bulk activate: {e}") return jsonify({'error': 'Failed to activate users'}), 500 - @app.route('/users/bulk/permanently-delete', methods=['POST']) @admin_required def bulk_permanently_delete_users(): @@ -1446,146 +1955,6 @@ def is_admin_user(user_id): except: return False -def get_coordinates_from_address(address): - """ - Get latitude and longitude from address using geocoding service - Returns (lat, lng) tuple or (None, None) if failed - """ - if not address or address.strip() == '': - return None, None - - try: - # Using Nominatim/OpenStreetMap for free geocoding - url = "https://nominatim.openstreetmap.org/search" - params = { - 'q': address, - 'format': 'json', - 'limit': 1, - 'addressdetails': 1 - } - - headers = { - 'User-Agent': 'QR-Attendance-System/1.0' - } - - response = requests.get(url, params=params, headers=headers, timeout=10) - - if response.status_code == 200: - data = response.json() - if data and len(data) > 0: - lat = float(data[0]['lat']) - lng = float(data[0]['lon']) - print(f"✅ Geocoded address '{address[:50]}...' to coordinates: {lat}, {lng}") - return lat, lng - - print(f"⚠️ Could not geocode address: {address}") - return None, None - - except Exception as e: - print(f"❌ Error geocoding address '{address}': {e}") - return None, None - -def calculate_distance_miles(lat1, lng1, lat2, lng2): - """ - Calculate the great circle distance between two points on Earth in miles - Using the Haversine formula - """ - if any(coord is None for coord in [lat1, lng1, lat2, lng2]): - return None - - try: - # Convert decimal degrees to radians - lat1, lng1, lat2, lng2 = map(radians, [lat1, lng1, lat2, lng2]) - - # Haversine formula - dlng = lng2 - lng1 - dlat = lat2 - lat1 - a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlng/2)**2 - c = 2 * asin(sqrt(a)) - - # Radius of Earth in miles - r_miles = 3959 - - # Calculate the result - distance = c * r_miles - - print(f"📏 Calculated distance: {distance:.3f} miles") - return round(distance, 3) - - except Exception as e: - print(f"❌ Error calculating distance: {e}") - return None - -def calculate_location_accuracy(qr_address, checkin_address, checkin_lat=None, checkin_lng=None): - """ - Calculate location accuracy by comparing QR code address with check-in location - Returns distance in miles between the two locations - """ - print(f"\n📍 CALCULATING LOCATION ACCURACY:") - print(f" QR Address: {qr_address}") - print(f" Check-in Address: {checkin_address}") - print(f" Check-in Coordinates: {checkin_lat}, {checkin_lng}") - - # Get QR code coordinates from address - qr_lat, qr_lng = get_coordinates_from_address(qr_address) - - if qr_lat is None or qr_lng is None: - print(f"⚠️ Could not geocode QR address, cannot calculate accuracy") - return None - - # Use check-in coordinates if available, otherwise geocode check-in address - if checkin_lat is not None and checkin_lng is not None: - checkin_coords_lat, checkin_coords_lng = checkin_lat, checkin_lng - print(f"✅ Using GPS coordinates for check-in location") - else: - checkin_coords_lat, checkin_coords_lng = get_coordinates_from_address(checkin_address) - if checkin_coords_lat is None or checkin_coords_lng is None: - print(f"⚠️ Could not geocode check-in address, cannot calculate accuracy") - return None - print(f"✅ Using geocoded coordinates for check-in address") - - # Calculate distance - distance = calculate_distance_miles(qr_lat, qr_lng, checkin_coords_lat, checkin_coords_lng) - - if distance is not None: - print(f"✅ Location accuracy calculated: {distance} miles") - - return distance - -def get_location_accuracy_level(location_accuracy): - """Get human-readable location accuracy level based on distance""" - if not location_accuracy: - return 'unknown' - elif location_accuracy <= 0.1: # Within 0.1 mile (528 feet) - return 'excellent' - elif location_accuracy <= 0.5: # Within 0.5 mile - return 'good' - elif location_accuracy <= 1.0: # Within 1 mile - return 'fair' - else: - return 'poor' - -def get_location_accuracy_level_enhanced(location_accuracy): - """ - Enhanced function to categorize location accuracy with more granular levels - """ - if not location_accuracy or location_accuracy is None: - return 'unknown' - - # More precise accuracy thresholds - if location_accuracy <= 0.05: # Within 264 feet (50 meters) - return 'excellent' - elif location_accuracy <= 0.1: # Within 528 feet (100 meters) - return 'very_good' - elif location_accuracy <= 0.25: # Within 0.25 mile (1320 feet) - return 'good' - elif location_accuracy <= 0.5: # Within 0.5 mile - return 'fair' - elif location_accuracy <= 1.0: # Within 1 mile - return 'poor' - else: # Greater than 1 mile - return 'very_poor' - @app.route('/qr-codes/create', methods=['GET', 'POST']) @login_required def create_qr_code(): @@ -1952,181 +2321,6 @@ def qr_checkin(qr_url): 'success': False, 'message': 'An unexpected error occurred. Please try again.' }), 500 - -def process_location_data(location_data): - """ - Process and validate location data from form - Returns clean location data or None values for invalid data - """ - processed = { - 'latitude': None, - 'longitude': None, - 'accuracy': None, - 'altitude': None, - 'source': location_data.get('location_source', 'manual'), - 'address': location_data.get('address', '')[:500] if location_data.get('address') else None - } - - try: - # Process latitude - if location_data.get('latitude') and location_data['latitude'] not in ['null', '']: - lat = float(location_data['latitude']) - if -90 <= lat <= 90: # Valid latitude range - processed['latitude'] = lat - else: - print(f"⚠️ Invalid latitude: {lat}") - - # Process longitude - if location_data.get('longitude') and location_data['longitude'] not in ['null', '']: - lng = float(location_data['longitude']) - if -180 <= lng <= 180: # Valid longitude range - processed['longitude'] = lng - else: - print(f"⚠️ Invalid longitude: {lng}") - - # Process accuracy - if location_data.get('accuracy') and location_data['accuracy'] not in ['null', '']: - acc = float(location_data['accuracy']) - if acc >= 0: # Accuracy should be positive - processed['accuracy'] = acc - else: - print(f"⚠️ Invalid accuracy: {acc}") - - # Process altitude - if location_data.get('altitude') and location_data['altitude'] not in ['null', '']: - alt = float(location_data['altitude']) - # Altitude can be negative (below sea level) - processed['altitude'] = alt - - except (ValueError, TypeError) as e: - print(f"⚠️ Error processing location data: {e}") - - return processed - -def process_location_data_enhanced(form_data): - """ - Enhanced location data processing with better validation and error handling - """ - processed = { - 'latitude': None, - 'longitude': None, - 'accuracy': None, - 'altitude': None, - 'source': form_data.get('location_source', 'manual'), - 'address': None - } - - try: - # Enhanced latitude validation - if form_data.get('latitude') and form_data['latitude'] not in ['null', '', 'undefined']: - lat = float(form_data['latitude']) - if -90 <= lat <= 90: - processed['latitude'] = round(lat, 6) # 6 decimal precision - else: - print(f"⚠️ Invalid latitude range: {lat}") - - # Enhanced longitude validation - if form_data.get('longitude') and form_data['longitude'] not in ['null', '', 'undefined']: - lng = float(form_data['longitude']) - if -180 <= lng <= 180: - processed['longitude'] = round(lng, 6) # 6 decimal precision - else: - print(f"⚠️ Invalid longitude range: {lng}") - - # Enhanced accuracy validation - if form_data.get('accuracy') and form_data['accuracy'] not in ['null', '', 'undefined']: - acc = float(form_data['accuracy']) - if 0 <= acc <= 50000: # Reasonable accuracy range in meters - processed['accuracy'] = round(acc, 1) - else: - print(f"⚠️ Invalid accuracy value: {acc}") - - # Enhanced altitude validation - if form_data.get('altitude') and form_data['altitude'] not in ['null', '', 'undefined']: - alt = float(form_data['altitude']) - if -1000 <= alt <= 10000: # Reasonable altitude range in meters - processed['altitude'] = round(alt, 1) - else: - print(f"⚠️ Invalid altitude value: {alt}") - - # Enhanced address processing - if form_data.get('address'): - address = form_data['address'].strip() - if len(address) > 0: - processed['address'] = address[:500] # Limit to 500 characters - - print(f"✅ Enhanced location data processed: {processed}") - - except (ValueError, TypeError) as e: - print(f"⚠️ Error in enhanced location data processing: {e}") - - return processed - -def migrate_to_enhanced_location_accuracy(): - """ - Migration function to recalculate all existing records with enhanced accuracy - """ - try: - print("🔄 Starting enhanced location accuracy migration...") - - # Get all records that need recalculation - records = db.session.execute(text(""" - SELECT ad.id, qc.location_address, ad.address, ad.latitude, ad.longitude, ad.location_accuracy - FROM attendance_data ad - LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id - WHERE qc.location_address IS NOT NULL - """)).fetchall() - - print(f"📊 Found {len(records)} records to process") - - updated_count = 0 - improved_count = 0 - - for record in records: - try: - # Calculate enhanced location accuracy - new_accuracy = calculate_location_accuracy_enhanced( - qr_address=record.location_address, - checkin_address=record.address, - checkin_lat=record.latitude, - checkin_lng=record.longitude - ) - - if new_accuracy is not None: - # Update the record - db.session.execute(text(""" - UPDATE attendance_data - SET location_accuracy = :accuracy - WHERE id = :record_id - """), { - 'accuracy': new_accuracy, - 'record_id': record.id - }) - - updated_count += 1 - - # Check if this is an improvement - if record.location_accuracy is None or abs(new_accuracy - (record.location_accuracy or 0)) > 0.001: - improved_count += 1 - print(f" ✅ Updated record {record.id}: {record.location_accuracy} → {new_accuracy:.4f} miles") - - except Exception as e: - print(f" ⚠️ Error processing record {record.id}: {e}") - - # Commit all changes - db.session.commit() - - print(f"✅ Enhanced migration completed!") - print(f" 📊 Records processed: {len(records)}") - print(f" ✅ Records updated: {updated_count}") - print(f" 📈 Records improved: {improved_count}") - - return True - - except Exception as e: - print(f"❌ Enhanced migration failed: {e}") - db.session.rollback() - return False @app.route('/qr-codes//toggle-status', methods=['POST']) @login_required @@ -2450,171 +2644,6 @@ def attendance_report(): flash('Error loading attendance report. Please check the server logs for details.', 'error') return redirect(url_for('dashboard')) -def check_location_accuracy_column_exists(): - """Check if the location_accuracy column exists in the attendance_data table""" - try: - result = db.session.execute(text(""" - SELECT column_name - FROM information_schema.columns - WHERE table_name='attendance_data' AND column_name='location_accuracy' - """)) - - column_exists = result.fetchone() is not None - return column_exists - - except Exception as e: - print(f"⚠️ Error checking location_accuracy column: {e}") - return False - -def get_location_accuracy_level(location_accuracy): - """Get human-readable location accuracy level based on distance""" - if not location_accuracy: - return 'unknown' - elif location_accuracy <= 0.1: # Within 0.1 mile (528 feet) - return 'excellent' - elif location_accuracy <= 0.5: # Within 0.5 mile - return 'good' - elif location_accuracy <= 1.0: # Within 1 mile - return 'fair' - else: - return 'poor' - -# Safe migration function -def safely_add_location_accuracy_column(): - """Safely add location_accuracy column if it doesn't exist""" - try: - # Check if column already exists - if not check_location_accuracy_column_exists(): - # Add the column - db.session.execute(text(""" - ALTER TABLE attendance_data - ADD COLUMN location_accuracy FLOAT - """)) - db.session.commit() - print("✅ Added location_accuracy column to attendance_data table") - return True - else: - print("✅ location_accuracy column already exists") - return True - - except Exception as e: - print(f"❌ Error adding location_accuracy column: {e}") - db.session.rollback() - return False - -# Test database connection -def test_database_connection(): - """Test if database connection is working""" - try: - result = db.session.execute(text("SELECT 1")) - test = result.fetchone() - print("✅ Database connection successful") - return True - except Exception as e: - print(f"❌ Database connection failed: {e}") - return False - -# Test attendance table structure -def test_attendance_table(): - """Test if attendance_data table exists and get its structure""" - try: - result = db.session.execute(text(""" - SELECT column_name, data_type - FROM information_schema.columns - WHERE table_name='attendance_data' - ORDER BY ordinal_position - """)) - - columns = result.fetchall() - print(f"✅ attendance_data table has {len(columns)} columns:") - for col in columns: - print(f" - {col.column_name}: {col.data_type}") - - return True - - except Exception as e: - print(f"❌ Error checking attendance_data table: {e}") - return False - -def get_location_accuracy_level(location_accuracy): - """Get human-readable location accuracy level based on distance""" - if not location_accuracy: - return 'unknown' - elif location_accuracy <= 0.1: # Within 0.1 mile (528 feet) - return 'excellent' - elif location_accuracy <= 0.5: # Within 0.5 mile - return 'good' - elif location_accuracy <= 1.0: # Within 1 mile - return 'fair' - else: - return 'poor' - -# Database migration function to add location_accuracy column -def add_location_accuracy_column(): - """Add location_accuracy column to existing attendance_data table""" - try: - # Check if column already exists - result = db.session.execute(text(""" - SELECT column_name - FROM information_schema.columns - WHERE table_name='attendance_data' AND column_name='location_accuracy' - """)) - - if not result.fetchone(): - # Add the column - db.session.execute(text(""" - ALTER TABLE attendance_data - ADD COLUMN location_accuracy FLOAT - """)) - db.session.commit() - print("✅ Added location_accuracy column to attendance_data table") - else: - print("✅ location_accuracy column already exists") - - except Exception as e: - print(f"❌ Error adding location_accuracy column: {e}") - db.session.rollback() - -# Function to recalculate location accuracy for existing records -def recalculate_existing_location_accuracy(): - """Recalculate location accuracy for all existing attendance records""" - try: - records = db.session.execute(text(""" - SELECT ad.id, qc.location_address, ad.address, ad.latitude, ad.longitude - FROM attendance_data ad - LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id - WHERE ad.location_accuracy IS NULL - AND qc.location_address IS NOT NULL - """)).fetchall() - - updated_count = 0 - - for record in records: - location_accuracy = calculate_location_accuracy( - qr_address=record.location_address, - checkin_address=record.address, - checkin_lat=record.latitude, - checkin_lng=record.longitude - ) - - if location_accuracy is not None: - db.session.execute(text(""" - UPDATE attendance_data - SET location_accuracy = :accuracy - WHERE id = :record_id - """), { - 'accuracy': location_accuracy, - 'record_id': record.id - }) - updated_count += 1 - - db.session.commit() - print(f"✅ Updated location accuracy for {updated_count} records") - - except Exception as e: - print(f"❌ Error recalculating location accuracy: {e}") - db.session.rollback() - @app.route('/api/attendance/stats') @admin_required def attendance_stats_api():