| """ |
| Attendr - Smart Attendance System |
| Main Flask Application |
| """ |
|
|
| from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash, send_from_directory |
| from werkzeug.middleware.proxy_fix import ProxyFix |
| from flask_sqlalchemy import SQLAlchemy |
| from config import Config |
| from models import db, User, Student, Classroom, AttendanceSession, AttendanceRecord |
| from face_recognition_module import face_recognition_module |
| from geolocation_module import geolocation_module |
| from attendance_code_module import attendance_code_module |
| from utils import base64_to_image, image_to_numpy, format_error_response, format_success_response |
| from auth import login_required, role_required, get_current_user |
| from datetime import datetime |
| import logging |
|
|
| |
| app = Flask(__name__, template_folder='.') |
| app.config.from_object(Config) |
| Config.init_app(app) |
|
|
| |
| app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) |
|
|
| |
| db.init_app(app) |
|
|
| |
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
|
|
| def migrate_database(): |
| """Manually add missing columns to the database if they don't exist""" |
| try: |
| from sqlalchemy import text |
| with db.engine.connect() as conn: |
| |
| result = conn.execute(text("PRAGMA table_info(users)")) |
| columns = [row[1] for row in result] |
| |
| if 'status' not in columns: |
| logger.info("Adding 'status' column to users table...") |
| conn.execute(text("ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'pending'")) |
| conn.commit() |
| logger.info("Column 'status' added successfully.") |
| except Exception as e: |
| logger.error(f"Migration error: {str(e)}") |
|
|
| |
| with app.app_context(): |
| db.create_all() |
| migrate_database() |
| |
| |
| try: |
| admin = User.query.filter_by(role='admin').first() |
| if not admin: |
| admin = User( |
| matric_no='ADMIN', |
| name='System Admin', |
| email='admin@attendr.com', |
| role='admin', |
| is_active=True, |
| status='approved' |
| ) |
| admin.set_password('admin123') |
| db.session.add(admin) |
| db.session.commit() |
| logger.info("Default Admin user created (ADMIN / admin123)") |
| except Exception as init_err: |
| logger.error(f"Admin initialization error: {str(init_err)}") |
| |
| logger.info("Database initialized") |
| |
| |
| try: |
| inconsistent_users = User.query.filter_by(status='pending', is_active=True).all() |
| if inconsistent_users: |
| for u in inconsistent_users: |
| u.is_active = False |
| db.session.commit() |
| logger.info(f"Fixed {len(inconsistent_users)} inconsistent pending accounts.") |
| except Exception as e: |
| logger.error(f"Cleanup error: {str(e)}") |
|
|
|
|
| @app.after_request |
| def add_header(response): |
| """ |
| Add headers to prevent caching of dynamic pages. |
| This ensures that when a user logs out, they cannot click 'Back' |
| to see protected content from the browser cache. |
| """ |
| if 'Cache-Control' not in response.headers: |
| response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' |
| response.headers['Pragma'] = 'no-cache' |
| response.headers['Expires'] = '-1' |
| return response |
|
|
|
|
| |
|
|
| @app.route('/style.css') |
| def style(): |
| return send_from_directory('.', 'style.css') |
|
|
|
|
| @app.route('/') |
| def index(): |
| """Landing page""" |
| current_user = get_current_user() |
| return render_template('index.html', current_user=current_user) |
|
|
|
|
| @app.route('/student') |
| @login_required |
| @role_required('student') |
| def student_page(): |
| """Student attendance interface - requires student login""" |
| return render_template('student.html', current_user=get_current_user()) |
|
|
|
|
| @app.route('/student/history') |
| @login_required |
| @role_required('student') |
| def student_history(): |
| """Student attendance history page""" |
| return render_template('student_history.html', current_user=get_current_user()) |
|
|
|
|
| @app.route('/lecturer') |
| @login_required |
| @role_required('lecturer') |
| def lecturer_page(): |
| """Lecturer dashboard - requires lecturer login""" |
| return render_template('lecturer.html', current_user=get_current_user()) |
|
|
|
|
| @app.route('/lecturer/history') |
| @login_required |
| @role_required('lecturer') |
| def lecturer_history(): |
| """Lecturer session history page""" |
| return render_template('lecturer_history.html', current_user=get_current_user()) |
|
|
|
|
| @app.route('/admin') |
| @login_required |
| @role_required('admin') |
| def admin_page(): |
| """Admin dashboard - approve/reject users""" |
| return render_template('admin.html', current_user=get_current_user()) |
|
|
|
|
| @app.route('/register') |
| def register_page(): |
| """User registration page - redirect if already logged in""" |
| current_user = get_current_user() |
| if current_user: |
| role = current_user.role |
| if role == 'admin': return redirect(url_for('admin_page')) |
| return redirect(url_for('student_page' if role == 'student' else 'lecturer_page')) |
| |
| |
| if 'user_id' in session: |
| session.clear() |
| |
| return render_template('register.html') |
|
|
|
|
| @app.route('/login') |
| def login_page(): |
| """Login page - redirect if already logged in""" |
| current_user = get_current_user() |
| if current_user: |
| role = current_user.role |
| if role == 'admin': return redirect(url_for('admin_page')) |
| return redirect(url_for('student_page' if role == 'student' else 'lecturer_page')) |
| |
| |
| if 'user_id' in session: |
| session.clear() |
|
|
| return render_template('login.html') |
|
|
|
|
| |
|
|
| @app.route('/api/auth/register', methods=['POST']) |
| def register_user(): |
| """ |
| Register new user (student or lecturer) |
| Validates matric number format based on role |
| """ |
| try: |
| data = request.get_json() |
| |
| required_fields = ['matric_no', 'name', 'password', 'role'] |
| if not all(k in data for k in required_fields): |
| return format_error_response(f"Missing required fields: {', '.join(required_fields)}") |
| |
| matric_no = data['matric_no'].upper().strip() |
| name = data['name'] |
| password = data['password'] |
| role = data['role'].lower() |
| email = data.get('email') |
| |
| |
| if role not in ['student', 'lecturer']: |
| return format_error_response("Role must be 'student' or 'lecturer'") |
| |
| |
| is_valid, error_msg = User.validate_matric_no(matric_no, role) |
| if not is_valid: |
| return format_error_response(error_msg, 400) |
| |
| |
| existing_user = User.query.filter_by(matric_no=matric_no).first() |
| if existing_user: |
| return format_error_response("Matric number already registered", 409) |
| |
| if email: |
| existing_email = User.query.filter_by(email=email).first() |
| if existing_email: |
| return format_error_response("Email already registered", 409) |
| |
| |
| user = User( |
| matric_no=matric_no, |
| name=name, |
| email=email, |
| role=role, |
| is_active=False, |
| status='pending' |
| ) |
| user.set_password(password) |
| |
| db.session.add(user) |
| db.session.commit() |
| |
| logger.info(f"User registered: {matric_no} ({role})") |
| |
| return format_success_response( |
| data=user.to_dict(), |
| message=f"{role.capitalize()} account created successfully! Please wait for Admin approval before logging in." |
| ) |
| |
| except Exception as e: |
| db.session.rollback() |
| logger.error(f"User registration error: {str(e)}") |
| return format_error_response(f"Registration failed: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/auth/login', methods=['POST']) |
| def login(): |
| """Login user and create session""" |
| try: |
| data = request.get_json() |
| |
| if not all(k in data for k in ['matric_no', 'password']): |
| return format_error_response("Missing required fields: matric_no, password") |
| |
| matric_no = data['matric_no'].upper().strip() |
| password = data['password'] |
| |
| |
| user = User.query.filter_by(matric_no=matric_no).first() |
| |
| if not user or not user.check_password(password): |
| return format_error_response("Invalid matric number or password", 401) |
| |
| if not user.is_active: |
| if user.status == 'pending': |
| return format_error_response("Your account is pending approval by an Admin.", 403) |
| elif user.status == 'rejected': |
| return format_error_response("Your account registration has been rejected.", 403) |
| else: |
| return format_error_response("Account is inactive. Please contact admin.", 403) |
| |
| |
| session['user_id'] = user.id |
| session['matric_no'] = user.matric_no |
| session['role'] = user.role |
| session['name'] = user.name |
| |
| logger.info(f"User logged in: {matric_no} ({user.role})") |
| |
| return format_success_response( |
| data={ |
| 'user': user.to_dict(), |
| 'redirect': '/admin' if user.role == 'admin' else ('/student' if user.role == 'student' else '/lecturer') |
| }, |
| message=f"Welcome back, {user.name}!" |
| ) |
| |
| except Exception as e: |
| logger.error(f"Login error: {str(e)}") |
| return format_error_response(f"Login failed: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/auth/logout', methods=['POST']) |
| @login_required |
| def logout(): |
| """Logout user and destroy session""" |
| try: |
| user_name = session.get('name', 'User') |
| matric_no = session.get('matric_no', 'Unknown') |
| |
| |
| session.clear() |
| |
| logger.info(f"User logged out: {matric_no}") |
| |
| return format_success_response( |
| message=f"Goodbye, {user_name}! You have been logged out successfully." |
| ) |
| |
| except Exception as e: |
| logger.error(f"Logout error: {str(e)}") |
| return format_error_response(f"Logout failed: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/auth/current_user', methods=['GET']) |
| def get_current_user_api(): |
| """Get current logged in user""" |
| user = get_current_user() |
| if user: |
| return format_success_response(data=user.to_dict()) |
| return format_error_response("Not logged in", 401) |
|
|
|
|
| |
|
|
| @app.route('/api/register_face', methods=['POST']) |
| def register_face(): |
| """ |
| Register student face encoding |
| Implements: PB-1 (Register Student Face Data) |
| """ |
| try: |
| data = request.get_json() |
| |
| |
| if not all(k in data for k in ['student_id', 'name', 'image']): |
| return format_error_response("Missing required fields: student_id, name, image") |
| |
| student_id = data['student_id'] |
| name = data['name'] |
| email = data.get('email') |
| image_base64 = data['image'] |
| |
| |
| existing_student = Student.query.filter_by(student_id=student_id).first() |
| if existing_student: |
| return format_error_response("Student ID already registered", 409) |
| |
| |
| user = User.query.filter_by(matric_no=student_id).first() |
| |
| |
| try: |
| image = base64_to_image(image_base64) |
| image_array = image_to_numpy(image) |
| except Exception as e: |
| return format_error_response(f"Invalid image data: {str(e)}") |
| |
| |
| success, result, face_location = face_recognition_module.register_face(image_array) |
| |
| if not success: |
| return format_error_response(result) |
| |
| |
| student = Student( |
| student_id=student_id, |
| name=name, |
| email=email, |
| user_id=user.id if user else None |
| ) |
| student.set_face_encoding(result) |
| |
| db.session.add(student) |
| db.session.commit() |
| |
| logger.info(f"Student registered: {student_id} - {name}") |
| |
| return format_success_response( |
| data=student.to_dict(), |
| message="Face registered successfully!" |
| ) |
| |
| except Exception as e: |
| db.session.rollback() |
| logger.error(f"Registration error: {str(e)}") |
| return format_error_response(f"Registration failed: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/verify_face', methods=['POST']) |
| def verify_face(): |
| """ |
| Verify student face against stored encoding |
| Implements: PB-3 (Face Recognition Matching) |
| Implements KR Rule #1: ∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x)) |
| """ |
| try: |
| data = request.get_json() |
| |
| if not all(k in data for k in ['student_id', 'image']): |
| return format_error_response("Missing required fields: student_id, image") |
| |
| student_id = data['student_id'] |
| image_base64 = data['image'] |
| |
| |
| student = Student.query.filter_by(student_id=student_id).first() |
| if not student: |
| return format_error_response("Student not found. Please register first.", 404) |
| |
| if not student.is_active: |
| return format_error_response("Student account is inactive", 403) |
| |
| |
| stored_encoding = student.get_face_encoding() |
| if not stored_encoding: |
| return format_error_response("No face encoding found. Please register your face.", 404) |
| |
| |
| try: |
| image = base64_to_image(image_base64) |
| image_array = image_to_numpy(image) |
| except Exception as e: |
| return format_error_response(f"Invalid image data: {str(e)}") |
| |
| |
| success, result = face_recognition_module.generate_face_encoding(image_array) |
| |
| if not success: |
| return format_error_response(result) |
| |
| captured_encoding = result |
| |
| |
| is_match, confidence = face_recognition_module.verify_face(captured_encoding, stored_encoding) |
| |
| if is_match: |
| logger.info(f"Face verified for student {student_id}: confidence={confidence:.2f}%") |
| return format_success_response( |
| data={ |
| 'verified': True, |
| 'confidence': round(confidence, 2), |
| 'student': student.to_dict() |
| }, |
| message=f"Face verified! Confidence: {confidence:.1f}%" |
| ) |
| else: |
| logger.warning(f"Face verification failed for {student_id}: confidence={confidence:.2f}%") |
| return format_error_response( |
| f"Face verification failed. Confidence too low: {confidence:.1f}%", |
| 401 |
| ) |
| |
| except Exception as e: |
| logger.error(f"Face verification error: {str(e)}") |
| return format_error_response(f"Verification failed: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/verify_location', methods=['POST']) |
| def verify_location(): |
| """ |
| Verify student location against classroom boundaries |
| Implements: PB-5 (Geofencing Logic – Classroom Detection) |
| Implements KR Rule #2: ∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)] |
| """ |
| try: |
| data = request.get_json() |
| |
| if not all(k in data for k in ['latitude', 'longitude', 'session_id']): |
| return format_error_response("Missing required fields: latitude, longitude, session_id") |
| |
| latitude = data['latitude'] |
| longitude = data['longitude'] |
| session_id = data['session_id'] |
| |
| |
| session_obj = AttendanceSession.query.get(session_id) |
| if not session_obj: |
| return format_error_response("Session not found", 404) |
| |
| if not session_obj.is_active: |
| return format_error_response("Session is not active", 403) |
| |
| |
| result = geolocation_module.verify_location( |
| latitude, |
| longitude, |
| session_obj.classroom_id |
| ) |
| |
| if result['verified']: |
| logger.info(f"Location verified: distance={result['distance']:.2f}m") |
| return format_success_response( |
| data={ |
| 'verified': True, |
| 'distance': round(result['distance'], 2), |
| 'classroom': session_obj.classroom.to_dict() |
| }, |
| message=result['message'] |
| ) |
| else: |
| logger.warning(f"Location verification failed: {result['error']}") |
| return format_error_response(result['error'] or result['message'], 403) |
| |
| except Exception as e: |
| logger.error(f"Location verification error: {str(e)}") |
| return format_error_response(f"Location verification failed: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/mark_attendance', methods=['POST']) |
| def mark_attendance(): |
| """ |
| Mark student attendance after all verifications |
| Implements: PB-12 (Mark Attendance in Database) |
| Implements KR Rule #4: ∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x)) |
| Implements KR Rule #5: ∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x)) |
| """ |
| try: |
| data = request.get_json() |
| |
| required_fields = ['student_id', 'session_id', 'code', 'latitude', 'longitude'] |
| if not all(k in data for k in required_fields): |
| return format_error_response(f"Missing required fields: {', '.join(required_fields)}") |
| |
| student_id = data['student_id'] |
| session_id = data['session_id'] |
| code = data['code'] |
| latitude = data['latitude'] |
| longitude = data['longitude'] |
| |
| |
| student = Student.query.filter_by(student_id=student_id).first() |
| if not student: |
| return format_error_response("Student not found", 404) |
| |
| |
| session_obj = AttendanceSession.query.get(session_id) |
| if not session_obj: |
| return format_error_response("Session not found", 404) |
| |
| if not session_obj.is_active: |
| return format_error_response("Session is not active", 403) |
| |
| |
| existing_record = AttendanceRecord.query.filter_by( |
| session_id=session_id, |
| student_id=student.id |
| ).first() |
| |
| if existing_record: |
| return format_error_response("Attendance already marked for this session", 409) |
| |
| |
| is_valid, message = attendance_code_module.is_code_valid(session_id, code) |
| |
| if not is_valid: |
| return format_error_response(message, 401) |
| |
| |
| location_result = geolocation_module.verify_location( |
| latitude, |
| longitude, |
| session_obj.classroom_id |
| ) |
| |
| if not location_result['verified']: |
| return format_error_response(location_result['error'] or "Location verification failed", 403) |
| |
| |
| attendance = AttendanceRecord( |
| session_id=session_id, |
| student_id=student.id, |
| face_verified=True, |
| location_verified=True, |
| code_verified=True, |
| student_latitude=float(latitude), |
| student_longitude=float(longitude), |
| distance_from_classroom=location_result['distance'], |
| status='present' |
| ) |
| |
| db.session.add(attendance) |
| db.session.commit() |
| |
| logger.info(f"Attendance marked: {student.name} for session {session_id}") |
| |
| return format_success_response( |
| data=attendance.to_dict(), |
| message="Attendance marked successfully! ✓" |
| ) |
| |
| except Exception as e: |
| db.session.rollback() |
| logger.error(f"Mark attendance error: {str(e)}") |
| return format_error_response(f"Failed to mark attendance: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/create_session', methods=['POST']) |
| def create_session(): |
| """ |
| Create new attendance session |
| Implements: US-06 (Generate Auto-Refresh Codes) |
| """ |
| try: |
| data = request.get_json() |
| |
| if not all(k in data for k in ['course_name', 'classroom_id', 'lecturer_name']): |
| return format_error_response("Missing required fields: course_name, classroom_id, lecturer_name") |
| |
| course_name = data['course_name'] |
| classroom_id = data['classroom_id'] |
| lecturer_name = data['lecturer_name'] |
| |
| |
| classroom = Classroom.query.get(classroom_id) |
| if not classroom: |
| return format_error_response("Classroom not found", 404) |
| |
| |
| session_obj = AttendanceSession( |
| course_name=course_name, |
| classroom_id=classroom_id, |
| lecturer_name=lecturer_name, |
| is_active=True |
| ) |
| |
| db.session.add(session_obj) |
| db.session.commit() |
| |
| |
| success, code = attendance_code_module.create_session_code(session_obj.id) |
| |
| if not success: |
| return format_error_response(f"Session created but code generation failed: {code}") |
| |
| logger.info(f"Session created: {course_name} by {lecturer_name}") |
| |
| return format_success_response( |
| data=session_obj.to_dict(), |
| message="Session created successfully!" |
| ) |
| |
| except Exception as e: |
| db.session.rollback() |
| logger.error(f"Create session error: {str(e)}") |
| return format_error_response(f"Failed to create session: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/get_session/<int:session_id>', methods=['GET']) |
| def get_session(session_id): |
| """Get session details including current code""" |
| try: |
| session_obj = AttendanceSession.query.get(session_id) |
| |
| if not session_obj: |
| return format_error_response("Session not found", 404) |
| |
| |
| code_status = attendance_code_module.get_code_status(session_id) |
| |
| |
| if code_status.get('needs_refresh'): |
| attendance_code_module.auto_refresh_code(session_id) |
| code_status = attendance_code_module.get_code_status(session_id) |
| |
| session_data = session_obj.to_dict() |
| session_data['code_status'] = code_status |
| |
| return format_success_response(data=session_data) |
| |
| except Exception as e: |
| logger.error(f"Get session error: {str(e)}") |
| return format_error_response(f"Failed to get session: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/get_active_sessions', methods=['GET']) |
| def get_active_sessions(): |
| """Get all active sessions""" |
| try: |
| sessions = AttendanceSession.query.filter_by(is_active=True).all() |
| return format_success_response( |
| data=[session.to_dict() for session in sessions] |
| ) |
| except Exception as e: |
| logger.error(f"Get active sessions error: {str(e)}") |
| return format_error_response(f"Failed to get sessions: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/get_attendance/<int:session_id>', methods=['GET']) |
| def get_attendance(session_id): |
| """ |
| Get attendance records for a session |
| Implements: US-07 (View Real-time Attendance) |
| """ |
| try: |
| session_obj = AttendanceSession.query.get(session_id) |
| |
| if not session_obj: |
| return format_error_response("Session not found", 404) |
| |
| records = AttendanceRecord.query.filter_by(session_id=session_id).all() |
| |
| return format_success_response( |
| data={ |
| 'session': session_obj.to_dict(), |
| 'records': [record.to_dict() for record in records], |
| 'total_present': len(records) |
| } |
| ) |
| |
| except Exception as e: |
| logger.error(f"Get attendance error: {str(e)}") |
| return format_error_response(f"Failed to get attendance: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/end_session/<int:session_id>', methods=['POST']) |
| def end_session(session_id): |
| """End an attendance session""" |
| try: |
| session_obj = AttendanceSession.query.get(session_id) |
| |
| if not session_obj: |
| return format_error_response("Session not found", 404) |
| |
| session_obj.is_active = False |
| session_obj.end_time = datetime.utcnow() |
| |
| db.session.commit() |
| |
| logger.info(f"Session ended: {session_id}") |
| |
| return format_success_response(message="Session ended successfully") |
| |
| except Exception as e: |
| db.session.rollback() |
| logger.error(f"End session error: {str(e)}") |
| return format_error_response(f"Failed to end session: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/student/history', methods=['GET']) |
| @login_required |
| @role_required('student') |
| def get_student_history(): |
| """Get attendance history for logged-in student""" |
| try: |
| |
| current_user = get_current_user() |
| student = Student.query.filter_by(student_id=current_user.matric_no).first() |
| |
| if not student: |
| return format_error_response("Student profile not found", 404) |
| |
| |
| records = AttendanceRecord.query.filter_by(student_id=student.id)\ |
| .order_by(AttendanceRecord.marked_at.desc()).all() |
| |
| |
| history = [] |
| for record in records: |
| session = record.session |
| history.append({ |
| 'id': record.id, |
| 'course_name': session.course_name, |
| 'lecturer_name': session.lecturer_name, |
| 'classroom': session.classroom.name, |
| 'building': session.classroom.building, |
| 'status': record.status, |
| 'marked_at': record.marked_at.isoformat() if record.marked_at else None, |
| 'session_date': session.start_time.isoformat() if session.start_time else None, |
| 'face_verified': record.face_verified, |
| 'location_verified': record.location_verified, |
| 'code_verified': record.code_verified, |
| 'distance_from_classroom': round(record.distance_from_classroom, 2) if record.distance_from_classroom else None |
| }) |
| |
| return format_success_response( |
| data={ |
| 'student': student.to_dict(), |
| 'total_records': len(history), |
| 'history': history |
| } |
| ) |
| |
| except Exception as e: |
| logger.error(f"Get student history error: {str(e)}") |
| return format_error_response(f"Failed to get history: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/lecturer/history', methods=['GET']) |
| @login_required |
| @role_required('lecturer') |
| def get_lecturer_history(): |
| """Get session history for logged-in lecturer""" |
| try: |
| current_user = get_current_user() |
| |
| |
| from sqlalchemy import func |
| sessions = AttendanceSession.query.filter( |
| func.lower(AttendanceSession.lecturer_name) == func.lower(current_user.name) |
| ).order_by(AttendanceSession.start_time.desc()).all() |
| |
| logger.info(f"Fetched {len(sessions)} sessions for lecturer: {current_user.name}") |
| |
| |
| history = [] |
| for session in sessions: |
| |
| total_attendance = len(session.attendance_records) |
| present_count = sum(1 for r in session.attendance_records if r.status == 'present') |
| |
| |
| if session.end_time and session.start_time: |
| duration_minutes = int((session.end_time - session.start_time).total_seconds() / 60) |
| elif session.is_active and session.start_time: |
| duration_minutes = int((datetime.utcnow() - session.start_time).total_seconds() / 60) |
| else: |
| duration_minutes = 0 |
| |
| history.append({ |
| 'id': session.id, |
| 'course_name': session.course_name, |
| 'lecturer_name': session.lecturer_name, |
| 'classroom': session.classroom.name, |
| 'building': session.classroom.building, |
| 'start_time': session.start_time.isoformat() if session.start_time else None, |
| 'end_time': session.end_time.isoformat() if session.end_time else None, |
| 'is_active': session.is_active, |
| 'duration_minutes': duration_minutes, |
| 'total_attendance': total_attendance, |
| 'present_count': present_count, |
| 'attendance_records': [ |
| { |
| 'student_id': r.student.student_id if r.student else 'Unknown', |
| 'student_name': r.student.name if r.student else 'Unknown', |
| 'status': r.status, |
| 'marked_at': r.marked_at.isoformat() if r.marked_at else None, |
| 'face_verified': r.face_verified, |
| 'location_verified': r.location_verified, |
| 'code_verified': r.code_verified |
| } for r in session.attendance_records |
| ] |
| }) |
| |
| return format_success_response( |
| data={ |
| 'lecturer': { |
| 'name': current_user.name, |
| 'matric_no': current_user.matric_no |
| }, |
| 'total_sessions': len(history), |
| 'history': history |
| } |
| ) |
| |
| except Exception as e: |
| logger.error(f"Get lecturer history error: {str(e)}") |
| return format_error_response(f"Failed to get history: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/classrooms', methods=['GET']) |
| def get_classrooms(): |
| """Get all classrooms""" |
| try: |
| classrooms = geolocation_module.get_all_classrooms() |
| return format_success_response(data=classrooms) |
| except Exception as e: |
| logger.error(f"Get classrooms error: {str(e)}") |
| return format_error_response(f"Failed to get classrooms: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/admin/users', methods=['GET']) |
| @login_required |
| @role_required('admin') |
| def admin_get_users(): |
| """Get list of users for admin management""" |
| try: |
| status = request.args.get('status', 'pending') |
| query = User.query |
| if status != 'all': |
| query = query.filter_by(status=status) |
| |
| users = query.order_by(User.created_at.desc()).all() |
| return format_success_response(data=[u.to_dict() for u in users]) |
| except Exception as e: |
| logger.error(f"Admin get users error: {str(e)}") |
| return format_error_response(f"Failed to get users: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/admin/users/<int:user_id>/approve', methods=['POST']) |
| @login_required |
| @role_required('admin') |
| def admin_approve_user(user_id): |
| """Approve a user registration""" |
| try: |
| user = User.query.get(user_id) |
| if not user: |
| return format_error_response("User not found", 404) |
| |
| user.status = 'approved' |
| user.is_active = True |
| db.session.commit() |
| |
| logger.info(f"Admin approved user: {user.matric_no}") |
| return format_success_response(message=f"User {user.matric_no} approved successfully.") |
| except Exception as e: |
| db.session.rollback() |
| logger.error(f"Admin approve user error: {str(e)}") |
| return format_error_response(f"Failed to approve user: {str(e)}", 500) |
|
|
|
|
| @app.route('/api/admin/users/<int:user_id>/reject', methods=['POST']) |
| @login_required |
| @role_required('admin') |
| def admin_reject_user(user_id): |
| """Reject a user registration""" |
| try: |
| user = User.query.get(user_id) |
| if not user: |
| return format_error_response("User not found", 404) |
| |
| user.status = 'rejected' |
| user.is_active = False |
| db.session.commit() |
| |
| logger.info(f"Admin rejected user: {user.matric_no}") |
| return format_success_response(message=f"User {user.matric_no} rejected.") |
| except Exception as e: |
| db.session.rollback() |
| logger.error(f"Admin reject user error: {str(e)}") |
| return format_error_response(f"Failed to reject user: {str(e)}", 500) |
|
|
|
|
| |
|
|
| @app.errorhandler(404) |
| def not_found(error): |
| return format_error_response("Resource not found", 404) |
|
|
|
|
| @app.errorhandler(500) |
| def internal_error(error): |
| db.session.rollback() |
| return format_error_response("Internal server error", 500) |
|
|
|
|
| |
|
|
| if __name__ == '__main__': |
| with app.app_context(): |
| db.create_all() |
| logger.info("Database tables created") |
| |
| app.run(debug=True, host='0.0.0.0', port=5000) |
|
|