chualinwei3 commited on
Commit
3f635ef
·
verified ·
1 Parent(s): 518ee45

Upload 33 files

Browse files
Files changed (12) hide show
  1. .dockerignore +30 -0
  2. Dockerfile +54 -27
  3. README.md +334 -14
  4. _navbar.html +41 -37
  5. add_custom_venue.py +37 -0
  6. app.py +813 -805
  7. attendr.db +0 -0
  8. attendr.sql +46 -45
  9. config.py +1 -1
  10. face_recognition_module.py +160 -120
  11. lecturer.html +4 -0
  12. requirements.txt +8 -5
.dockerignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .db
6
+ .sqlite
7
+ .env
8
+ .git
9
+ .gitignore
10
+ .vscode
11
+ .gemini
12
+ node_modules
13
+ venv
14
+ env
15
+ uploads/*
16
+ !uploads/.gitkeep
17
+ README.md
18
+ *.sql
19
+ *.ipynb
20
+ *.log
21
+ add_custom_venue.py
22
+ add_home_venue.py
23
+ add_utm_venues.py
24
+ clear_database.py
25
+ dump_db_to_sql.py
26
+ init_db.py
27
+ update_utm_coordinates.py
28
+ fix_sql_quotes.py
29
+ # Keep attendr.db if you want pre-loaded data, but usually better to start fresh
30
+ # attendr.db
Dockerfile CHANGED
@@ -1,27 +1,54 @@
1
- # Use a Conda-based image (much faster for AI libraries)
2
- FROM continuumio/miniconda3:latest
3
-
4
- # Install basic system dependencies
5
- RUN apt-get update && apt-get install -y \
6
- libgl1 \
7
- libglx-mesa0 \
8
- libglib2.0-0 \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- # Set working directory
12
- WORKDIR /app
13
-
14
- # Step 1: Install AI libraries via Conda (Downloads pre-built files)
15
- RUN conda install -c conda-forge dlib face_recognition numpy -y
16
-
17
- # Step 2: Install Web libraries via pip
18
- RUN pip install Flask==3.0.0 Flask-SQLAlchemy==3.1.1 opencv-python-headless Pillow python-dotenv werkzeug pymysql
19
-
20
- # Step 3: Copy your project files
21
- COPY . .
22
-
23
- # Hugging Face mandatory port
24
- EXPOSE 7860
25
-
26
- # Run the app
27
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.10-slim
3
+
4
+ # Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE 1
6
+ ENV PYTHONUNBUFFERED 1
7
+ ENV PORT 7860
8
+
9
+ # Install system dependencies for OpenCV and face_recognition
10
+ RUN apt-get update && apt-get install -y \
11
+ build-essential \
12
+ cmake \
13
+ libopenblas-dev \
14
+ liblapack-dev \
15
+ libx11-dev \
16
+ libgtk-3-dev \
17
+ libboost-python-dev \
18
+ python3-dev \
19
+ libgl1-mesa-glx \
20
+ libglib2.0-0 \
21
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
22
+
23
+ # Set the working directory in the container
24
+ WORKDIR /app
25
+
26
+ # Copy the requirements file into the container
27
+ COPY requirements.txt .
28
+
29
+ # Install gunicorn for production
30
+ RUN pip install --no-cache-dir gunicorn
31
+
32
+ # Install any needed packages specified in requirements.txt
33
+ RUN pip install --no-cache-dir -r requirements.txt
34
+
35
+ # Copy the rest of the application code into the container
36
+ COPY . .
37
+
38
+ # Ensure upload directory exists and has permissions
39
+ RUN mkdir -p uploads/face_encodings && chmod -R 777 uploads
40
+
41
+ # Create a non-root user (Hugging Face requirement)
42
+ RUN useradd -m -u 1000 user
43
+ USER user
44
+ ENV HOME=/home/user \
45
+ PATH=/home/user/.local/bin:$PATH
46
+
47
+ WORKDIR $HOME/app
48
+ COPY --chown=user . $HOME/app
49
+
50
+ # Expose the port the app runs on
51
+ EXPOSE 7860
52
+
53
+ # Define the command to run the application
54
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app", "--timeout", "120"]
README.md CHANGED
@@ -1,18 +1,338 @@
1
- ---
2
- title: Attendr
3
- emoji:
4
- colorFrom: red
5
- colorTo: yellow
6
- sdk: docker
7
- app_port: 7860
8
- pinned: false
9
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- # Attendr - Smart Attendance System
 
 
 
12
 
13
- AI-powered attendance verification using facial recognition, geolocation, and auto-refresh codes.
14
 
15
- ## Setup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- This project runs on Docker in Hugging Face Spaces.
18
- Port: 7860
 
1
+ # Attendr - Smart Attendance System 🎓
2
+
3
+ An AI-powered smart attendance system for Universiti Teknologi Malaysia (UTM) that combines **facial recognition**, **geolocation verification**, and **auto-refresh attendance codes** to eliminate proxy attendance and streamline the attendance process.
4
+
5
+ ![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)
6
+ ![Flask](https://img.shields.io/badge/Flask-3.0-green.svg)
7
+ ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
8
+
9
+ ## ✨ Features
10
+
11
+ ### For Students
12
+ - 🎭 **Biometric Face Recognition** - Secure identity verification using AI
13
+ - 📍 **GPS Location Validation** - Confirms physical presence in classroom
14
+ - 🔐 **Auto-Refresh Codes** - Time-limited codes prevent sharing
15
+ - ⚡ **Real-time Feedback** - Instant attendance confirmation
16
+ - 📱 **Mobile Friendly** - Works on smartphones and tablets
17
+
18
+ ### For Lecturers
19
+ - 🎯 **One-Click Session Creation** - Quick setup for any class
20
+ - 🔄 **Auto-Refreshing Codes** - New code every 2 minutes
21
+ - 📊 **Real-time Monitoring** - Live attendance updates
22
+ - 📥 **Export Reports** - Download CSV for record-keeping
23
+ - 📈 **Attendance Analytics** - View statistics at a glance
24
+
25
+ ### Security Features
26
+ - ✅ Prevents proxy attendance through face + location verification
27
+ - ✅ Time-limited codes expire automatically
28
+ - ✅ All verifications logged with timestamps
29
+ - ✅ Distance tracking from classroom center
30
+
31
+ ## 🧠 AI Logic & Knowledge Representation
32
+
33
+ Attendr implements **5 Knowledge Representation (KR) rules** using First-Order Logic:
34
+
35
+ 1. **Face Recognition**: `∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x))`
36
+ 2. **Location Verification**: `∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]`
37
+ 3. **Device Readiness**: `∀d ((CameraOn(d) ∧ GPSOn(d)) → StartVerification(d))`
38
+ 4. **Attendance Validation**: `∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x))`
39
+ 5. **Code Confirmation**: `∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x))`
40
+
41
+ ## 🚀 Installation
42
+
43
+ ### Prerequisites
44
+ - Python 3.8 or higher
45
+ - Webcam for face capture
46
+ - GPS-enabled device (or browser location services)
47
+
48
+ ### Step 1: Clone Repository
49
+ ```bash
50
+ git clone <repository-url>
51
+ cd smartAttandence
52
+ ```
53
+
54
+ ### Step 2: Install Dependencies
55
+
56
+ #### Windows (Recommended Method)
57
+ ```powershell
58
+ # Install Visual C++ Build Tools first (required for dlib)
59
+ # Download from: https://visualstudio.microsoft.com/visual-cpp-build-tools/
60
+
61
+ # Create virtual environment
62
+ python -m venv venv
63
+ venv\Scripts\activate
64
+
65
+ # Install dependencies
66
+ pip install -r requirements.txt
67
+ ```
68
+
69
+ #### Alternative: Use Pre-compiled Wheels
70
+ If you encounter issues installing `face_recognition`, use pre-compiled wheels:
71
+ ```powershell
72
+ pip install https://github.com/jloh02/dlib/releases/download/v19.22/dlib-19.22.99-cp38-cp38-win_amd64.whl
73
+ pip install face_recognition
74
+ ```
75
+
76
+ ### Step 3: Initialize Database
77
+ ```bash
78
+ python init_db.py
79
+ ```
80
+
81
+ This creates:
82
+ - SQLite database with all tables
83
+ - Sample UTM classrooms (N28, V01, C22)
84
+ - Test student account
85
+
86
+ ### Step 4: Run Application
87
+ ```bash
88
+ python app.py
89
+ ```
90
+
91
+ The application will be available at: **http://localhost:5000**
92
+
93
+ ## 📖 Usage Guide
94
+
95
+ ### For Students
96
+
97
+ 1. **Register Your Face**
98
+ - Visit `/register`
99
+ - Enter Student ID and Name
100
+ - Capture your face using webcam
101
+ - System stores your biometric encoding
102
+
103
+ 2. **Mark Attendance**
104
+ - Visit `/student`
105
+ - Enter Student ID and select active session
106
+ - Enable camera and GPS permissions
107
+ - System verifies your face and location
108
+ - Enter attendance code displayed by lecturer
109
+ - Attendance marked instantly!
110
+
111
+ ### For Lecturers
112
+
113
+ 1. **Create Session**
114
+ - Visit `/lecturer`
115
+ - Enter course name and your name
116
+ - Select classroom location
117
+ - Click "Create Session"
118
+
119
+ 2. **Display Code**
120
+ - Large attendance code appears on screen
121
+ - Code auto-refreshes every 2 minutes
122
+ - Show this code to students in class
123
+
124
+ 3. **Monitor Attendance**
125
+ - View real-time attendance list
126
+ - See verification details (distance, time)
127
+ - Export CSV report when done
128
+
129
+ ## 🗂️ Project Structure
130
+
131
+ ```
132
+ smartAttandence/
133
+ ├── app.py # Main Flask application
134
+ ├── config.py # Configuration settings
135
+ ├── models.py # Database models
136
+ ├── init_db.py # Database initialization
137
+ ├── requirements.txt # Python dependencies
138
+
139
+ ├── face_recognition_module.py # Face recognition logic (KR Rule #1)
140
+ ├── geolocation_module.py # GPS verification (KR Rule #2)
141
+ ├── attendance_code_module.py # Auto-refresh codes (KR Rule #5)
142
+ ├── utils.py # Helper functions
143
+
144
+ ├── static/
145
+ │ ├── css/
146
+ │ │ └── style.css # Premium design system
147
+ │ └── js/
148
+ │ ├── student.js # Student portal logic
149
+ │ └── lecturer.js # Lecturer dashboard logic
150
+
151
+ └── templates/
152
+ ├── index.html # Landing page
153
+ ├── student.html # Student portal
154
+ ├── lecturer.html # Lecturer dashboard
155
+ └── register.html # Face registration
156
+ ```
157
+
158
+ ## 🔧 Configuration
159
+
160
+ Edit `config.py` to customize:
161
+
162
+ ```python
163
+ # Face recognition settings
164
+ FACE_RECOGNITION_TOLERANCE = 0.6 # Lower = more strict (0.4-0.6 recommended)
165
+
166
+ # Geolocation settings
167
+ GEOLOCATION_RADIUS_METERS = 50 # Classroom detection radius
168
 
169
+ # Attendance code settings
170
+ CODE_REFRESH_INTERVAL_SECONDS = 120 # Code refresh interval (2 minutes)
171
+ CODE_LENGTH = 6 # Length of attendance code
172
+ ```
173
 
174
+ ## 🌐 API Endpoints
175
 
176
+ ### Student Endpoints
177
+ - `POST /api/register_face` - Register student face encoding
178
+ - `POST /api/verify_face` - Verify face against stored encoding
179
+ - `POST /api/verify_location` - Validate GPS location
180
+ - `POST /api/mark_attendance` - Mark attendance with code
181
+
182
+ ### Lecturer Endpoints
183
+ - `POST /api/create_session` - Create attendance session
184
+ - `GET /api/get_session/<id>` - Get session details with current code
185
+ - `GET /api/get_attendance/<id>` - Get attendance records
186
+ - `POST /api/end_session/<id>` - End session
187
+
188
+ ### General Endpoints
189
+ - `GET /api/get_active_sessions` - List all active sessions
190
+ - `GET /api/classrooms` - Get all classrooms
191
+
192
+ ## 🧪 Testing
193
+
194
+ ### Manual Testing Checklist
195
+
196
+ **Face Recognition:**
197
+ - [ ] Register new student face
198
+ - [ ] Verify with same person (should succeed)
199
+ - [ ] Verify with different person (should fail)
200
+ - [ ] Test with poor lighting
201
+ - [ ] Test with glasses/mask
202
+
203
+ **Geolocation:**
204
+ - [ ] Mark attendance from inside classroom (should succeed)
205
+ - [ ] Mark attendance from outside radius (should fail)
206
+ - [ ] Verify distance calculation accuracy
207
+
208
+ **Auto-Refresh Codes:**
209
+ - [ ] Code refreshes every 2 minutes
210
+ - [ ] Expired code rejected
211
+ - [ ] Valid code accepted
212
+ - [ ] Code sharing prevented
213
+
214
+ **End-to-End Flow:**
215
+ - [ ] Complete student registration
216
+ - [ ] Lecturer creates session
217
+ - [ ] Student marks attendance successfully
218
+ - [ ] Attendance appears in real-time
219
+ - [ ] Export CSV works
220
+
221
+ ## 🐛 Troubleshooting
222
+
223
+ ### Face Recognition Issues
224
+
225
+ **Problem:** `dlib` installation fails on Windows
226
+ **Solution:**
227
+ 1. Install Visual C++ Build Tools
228
+ 2. Or use pre-compiled wheel: `pip install dlib-19.22.99-cp38-cp38-win_amd64.whl`
229
+
230
+ **Problem:** "No face detected"
231
+ **Solution:**
232
+ - Ensure good lighting
233
+ - Face camera directly
234
+ - Remove glasses/mask if possible
235
+ - Move closer to camera
236
+
237
+ ### GPS Issues
238
+
239
+ **Problem:** Location permission denied
240
+ **Solution:**
241
+ - Enable location services in browser settings
242
+ - Use HTTPS (required for geolocation API)
243
+ - Check device GPS is enabled
244
+
245
+ **Problem:** "Location unavailable"
246
+ **Solution:**
247
+ - Ensure GPS is enabled on device
248
+ - Try outdoors for better signal
249
+ - Check browser location permissions
250
+
251
+ ### Code Validation Issues
252
+
253
+ **Problem:** "Code has expired"
254
+ **Solution:**
255
+ - Enter code within 2-minute window
256
+ - Check lecturer's displayed code
257
+ - Ensure system clocks are synchronized
258
+
259
+ ## 📊 Database Schema
260
+
261
+ ### Students Table
262
+ - `id` - Primary key
263
+ - `student_id` - Unique student identifier
264
+ - `name` - Student name
265
+ - `email` - Email address
266
+ - `face_encoding` - Biometric data (JSON)
267
+ - `registered_at` - Registration timestamp
268
+
269
+ ### Classrooms Table
270
+ - `id` - Primary key
271
+ - `name` - Classroom name (e.g., N28-01-01)
272
+ - `building` - Building name
273
+ - `latitude` - GPS latitude
274
+ - `longitude` - GPS longitude
275
+ - `radius_meters` - Geofencing radius
276
+
277
+ ### AttendanceSessions Table
278
+ - `id` - Primary key
279
+ - `course_name` - Course name
280
+ - `lecturer_name` - Lecturer name
281
+ - `classroom_id` - Foreign key to Classrooms
282
+ - `current_code` - Active attendance code
283
+ - `code_generated_at` - Code generation time
284
+ - `is_active` - Session status
285
+
286
+ ### AttendanceRecords Table
287
+ - `id` - Primary key
288
+ - `session_id` - Foreign key to AttendanceSessions
289
+ - `student_id` - Foreign key to Students
290
+ - `marked_at` - Attendance timestamp
291
+ - `face_verified` - Face verification status
292
+ - `location_verified` - Location verification status
293
+ - `code_verified` - Code verification status
294
+ - `distance_from_classroom` - Distance in meters
295
+
296
+ ## 🎨 Design Philosophy
297
+
298
+ Attendr features a **premium dark mode design** with:
299
+ - 🌈 Vibrant gradient accents
300
+ - ✨ Glassmorphism effects
301
+ - 🎭 Smooth micro-animations
302
+ - 📱 Fully responsive layout
303
+ - 🎯 Modern typography (Inter + Outfit fonts)
304
+
305
+ ## 🤝 Contributing
306
+
307
+ Contributions are welcome! Please follow these steps:
308
+ 1. Fork the repository
309
+ 2. Create a feature branch
310
+ 3. Make your changes
311
+ 4. Test thoroughly
312
+ 5. Submit a pull request
313
+
314
+ ## 📄 License
315
+
316
+ This project is licensed under the MIT License.
317
+
318
+ ## 👥 Authors
319
+
320
+ Developed for Universiti Teknologi Malaysia (UTM) as part of an AI Smart Attendance System project.
321
+
322
+ ## 🙏 Acknowledgments
323
+
324
+ - **OpenCV** - Computer vision library
325
+ - **face_recognition** - Face recognition library by Adam Geitgey
326
+ - **Flask** - Web framework
327
+ - **UTM** - Project inspiration and requirements
328
+
329
+ ## 📞 Support
330
+
331
+ For issues or questions:
332
+ 1. Check the Troubleshooting section
333
+ 2. Review API documentation
334
+ 3. Open an issue on GitHub
335
+
336
+ ---
337
 
338
+ **Made with ❤️ for UTM Students and Lecturers**
 
_navbar.html CHANGED
@@ -2,48 +2,52 @@
2
  <!-- Copy this to all pages: index.html, login.html, register.html, student.html, student_history.html, lecturer.html -->
3
 
4
  <nav class="navbar">
5
- <div class="container navbar-content">
6
- <a href="/" class="navbar-brand">✨ Attendr</a>
7
- <ul class="navbar-nav">
8
- <li><a href="/" class="navbar-link">Home</a></li>
9
- {% if current_user %}
10
- {% if current_user.role == 'student' %}
11
- <li><a href="/student" class="navbar-link">Mark Attendance</a></li>
12
- <li><a href="/student/history" class="navbar-link">History</a></li>
13
- {% elif current_user.role == 'lecturer' %}
14
- <li><a href="/lecturer" class="navbar-link">Dashboard</a></li>
15
- {% endif %}
16
- <li><span class="navbar-link" style="color: var(--color-primary);">👤 {{ current_user.name }}</span></li>
17
- <li><a href="#" onclick="logout()" class="navbar-link">Logout</a></li>
18
- {% else %}
19
- <li><a href="/login" class="navbar-link">Login</a></li>
20
- <li><a href="/register" class="navbar-link">Register</a></li>
21
- {% endif %}
22
- </ul>
23
- </div>
 
 
 
 
24
  </nav>
25
 
26
  <!-- Logout Script (add to pages that need it) -->
27
  <script>
28
- async function logout() {
29
- if (!confirm('Are you sure you want to logout?')) return;
30
 
31
- try {
32
- const response = await fetch('/api/auth/logout', {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' }
35
- });
36
 
37
- const data = await response.json();
38
 
39
- if (data.success) {
40
- alert(data.message);
41
- window.location.href = '/';
42
- } else {
43
- alert('Logout failed');
44
- }
45
- } catch (error) {
46
- alert('Logout failed');
47
- }
48
  }
49
- </script>
 
 
2
  <!-- Copy this to all pages: index.html, login.html, register.html, student.html, student_history.html, lecturer.html -->
3
 
4
  <nav class="navbar">
5
+ <div class="container navbar-content">
6
+ <a href="/" class="navbar-brand">✨ Attendr</a>
7
+ <ul class="navbar-nav">
8
+ <li><a href="/" class="navbar-link">Home</a></li>
9
+ {% if current_user %} {% if current_user.role == 'student' %}
10
+ <li><a href="/student" class="navbar-link">Mark Attendance</a></li>
11
+ <li><a href="/student/history" class="navbar-link">History</a></li>
12
+ {% elif current_user.role == 'lecturer' %}
13
+ <li><a href="/lecturer" class="navbar-link">Dashboard</a></li>
14
+ <li><a href="/lecturer/history" class="navbar-link">History</a></li>
15
+ {% endif %}
16
+ <li>
17
+ <span class="navbar-link" style="color: var(--color-primary)"
18
+ >👤 {{ current_user.name }}</span
19
+ >
20
+ </li>
21
+ <li><a href="#" onclick="logout()" class="navbar-link">Logout</a></li>
22
+ {% else %}
23
+ <li><a href="/login" class="navbar-link">Login</a></li>
24
+ <li><a href="/register" class="navbar-link">Register</a></li>
25
+ {% endif %}
26
+ </ul>
27
+ </div>
28
  </nav>
29
 
30
  <!-- Logout Script (add to pages that need it) -->
31
  <script>
32
+ async function logout() {
33
+ if (!confirm("Are you sure you want to logout?")) return;
34
 
35
+ try {
36
+ const response = await fetch("/api/auth/logout", {
37
+ method: "POST",
38
+ headers: { "Content-Type": "application/json" },
39
+ });
40
 
41
+ const data = await response.json();
42
 
43
+ if (data.success) {
44
+ alert(data.message);
45
+ window.location.href = "/";
46
+ } else {
47
+ alert("Logout failed");
48
+ }
49
+ } catch (error) {
50
+ alert("Logout failed");
 
51
  }
52
+ }
53
+ </script>
add_custom_venue.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import app, db
2
+ from models import Classroom
3
+
4
+ def add_custom_venue():
5
+ with app.app_context():
6
+ # Define the new venue
7
+ venue_name = "Pejabat Pos UTM (KDSE)"
8
+ venue_building = "W1, Kolej Datin Seri Endon, 81110 Johor Bahru"
9
+ lat = 1.560662
10
+ lon = 103.638421
11
+ radius = 2000 # Increased as requested to allow location verification
12
+
13
+ # Check if already exists (searching for "Pejabat Pos" to handle renaming)
14
+ existing = Classroom.query.filter(Classroom.name.like("%Pejabat Pos%")).first()
15
+ if existing:
16
+ print(f"Updating existing venue: {existing.name} -> {venue_name}")
17
+ existing.name = venue_name
18
+ existing.building = venue_building
19
+ existing.latitude = lat
20
+ existing.longitude = lon
21
+ existing.radius_meters = radius
22
+ else:
23
+ new_venue = Classroom(
24
+ name=venue_name,
25
+ building=venue_building,
26
+ latitude=lat,
27
+ longitude=lon,
28
+ radius_meters=radius
29
+ )
30
+ db.session.add(new_venue)
31
+ print(f"Adding new venue: {venue_name}")
32
+
33
+ db.session.commit()
34
+ print("Database updated successfully.")
35
+
36
+ if __name__ == "__main__":
37
+ add_custom_venue()
app.py CHANGED
@@ -1,805 +1,813 @@
1
- """
2
- Attendr - Smart Attendance System
3
- Main Flask Application
4
- """
5
-
6
- from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash, send_from_directory
7
- from flask_sqlalchemy import SQLAlchemy
8
- from config import Config
9
- from models import db, User, Student, Classroom, AttendanceSession, AttendanceRecord
10
- from face_recognition_module import face_recognition_module
11
- from geolocation_module import geolocation_module
12
- from attendance_code_module import attendance_code_module
13
- from utils import base64_to_image, image_to_numpy, format_error_response, format_success_response
14
- from auth import login_required, role_required, get_current_user
15
- from datetime import datetime
16
- import logging
17
-
18
- # Initialize Flask app
19
- app = Flask(__name__, template_folder='.')
20
- app.config.from_object(Config)
21
- Config.init_app(app)
22
-
23
- # Initialize database
24
- db.init_app(app)
25
-
26
- # Set up logging
27
- logging.basicConfig(level=logging.INFO)
28
- logger = logging.getLogger(__name__)
29
-
30
-
31
- # ==================== ROUTES ====================
32
-
33
- @app.route('/style.css')
34
- def style():
35
- return send_from_directory('.', 'style.css')
36
-
37
-
38
- @app.route('/')
39
- def index():
40
- """Landing page"""
41
- current_user = get_current_user()
42
- return render_template('index.html', current_user=current_user)
43
-
44
-
45
- @app.route('/student')
46
- @login_required
47
- @role_required('student')
48
- def student_page():
49
- """Student attendance interface - requires student login"""
50
- return render_template('student.html', current_user=get_current_user())
51
-
52
-
53
- @app.route('/student/history')
54
- @login_required
55
- @role_required('student')
56
- def student_history():
57
- """Student attendance history page"""
58
- return render_template('student_history.html', current_user=get_current_user())
59
-
60
-
61
- @app.route('/lecturer')
62
- @login_required
63
- @role_required('lecturer')
64
- def lecturer_page():
65
- """Lecturer dashboard - requires lecturer login"""
66
- return render_template('lecturer.html', current_user=get_current_user())
67
-
68
-
69
- @app.route('/lecturer/history')
70
- @login_required
71
- @role_required('lecturer')
72
- def lecturer_history():
73
- """Lecturer session history page"""
74
- return render_template('lecturer_history.html', current_user=get_current_user())
75
-
76
-
77
- @app.route('/register')
78
- def register_page():
79
- """User registration page"""
80
- return render_template('register.html')
81
-
82
-
83
- @app.route('/login')
84
- def login_page():
85
- """Login page"""
86
- return render_template('login.html')
87
-
88
-
89
- # ==================== AUTHENTICATION API ====================
90
-
91
- @app.route('/api/auth/register', methods=['POST'])
92
- def register_user():
93
- """
94
- Register new user (student or lecturer)
95
- Validates matric number format based on role
96
- """
97
- try:
98
- data = request.get_json()
99
-
100
- required_fields = ['matric_no', 'name', 'password', 'role']
101
- if not all(k in data for k in required_fields):
102
- return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
103
-
104
- matric_no = data['matric_no'].upper().strip()
105
- name = data['name']
106
- password = data['password']
107
- role = data['role'].lower()
108
- email = data.get('email')
109
-
110
- # Validate role
111
- if role not in ['student', 'lecturer']:
112
- return format_error_response("Role must be 'student' or 'lecturer'")
113
-
114
- # Validate matric number format
115
- is_valid, error_msg = User.validate_matric_no(matric_no, role)
116
- if not is_valid:
117
- return format_error_response(error_msg, 400)
118
-
119
- # Check if user already exists
120
- existing_user = User.query.filter_by(matric_no=matric_no).first()
121
- if existing_user:
122
- return format_error_response("Matric number already registered", 409)
123
-
124
- if email:
125
- existing_email = User.query.filter_by(email=email).first()
126
- if existing_email:
127
- return format_error_response("Email already registered", 409)
128
-
129
- # Create user
130
- user = User(
131
- matric_no=matric_no,
132
- name=name,
133
- email=email,
134
- role=role
135
- )
136
- user.set_password(password)
137
-
138
- db.session.add(user)
139
- db.session.commit()
140
-
141
- logger.info(f"User registered: {matric_no} ({role})")
142
-
143
- return format_success_response(
144
- data=user.to_dict(),
145
- message=f"{role.capitalize()} account created successfully!"
146
- )
147
-
148
- except Exception as e:
149
- db.session.rollback()
150
- logger.error(f"User registration error: {str(e)}")
151
- return format_error_response(f"Registration failed: {str(e)}", 500)
152
-
153
-
154
- @app.route('/api/auth/login', methods=['POST'])
155
- def login():
156
- """Login user and create session"""
157
- try:
158
- data = request.get_json()
159
-
160
- if not all(k in data for k in ['matric_no', 'password']):
161
- return format_error_response("Missing required fields: matric_no, password")
162
-
163
- matric_no = data['matric_no'].upper().strip()
164
- password = data['password']
165
-
166
- # Find user
167
- user = User.query.filter_by(matric_no=matric_no).first()
168
-
169
- if not user or not user.check_password(password):
170
- return format_error_response("Invalid matric number or password", 401)
171
-
172
- if not user.is_active:
173
- return format_error_response("Account is inactive. Please contact admin.", 403)
174
-
175
- # Create session
176
- session['user_id'] = user.id
177
- session['matric_no'] = user.matric_no
178
- session['role'] = user.role
179
- session['name'] = user.name
180
-
181
- logger.info(f"User logged in: {matric_no} ({user.role})")
182
-
183
- return format_success_response(
184
- data={
185
- 'user': user.to_dict(),
186
- 'redirect': '/student' if user.role == 'student' else '/lecturer'
187
- },
188
- message=f"Welcome back, {user.name}!"
189
- )
190
-
191
- except Exception as e:
192
- logger.error(f"Login error: {str(e)}")
193
- return format_error_response(f"Login failed: {str(e)}", 500)
194
-
195
-
196
- @app.route('/api/auth/logout', methods=['POST'])
197
- @login_required
198
- def logout():
199
- """Logout user and destroy session"""
200
- try:
201
- user_name = session.get('name', 'User')
202
- matric_no = session.get('matric_no', 'Unknown')
203
-
204
- # Clear all session data
205
- session.clear()
206
-
207
- logger.info(f"User logged out: {matric_no}")
208
-
209
- return format_success_response(
210
- message=f"Goodbye, {user_name}! You have been logged out successfully."
211
- )
212
-
213
- except Exception as e:
214
- logger.error(f"Logout error: {str(e)}")
215
- return format_error_response(f"Logout failed: {str(e)}", 500)
216
-
217
-
218
- @app.route('/api/auth/current_user', methods=['GET'])
219
- def get_current_user_api():
220
- """Get current logged in user"""
221
- user = get_current_user()
222
- if user:
223
- return format_success_response(data=user.to_dict())
224
- return format_error_response("Not logged in", 401)
225
-
226
-
227
- # ==================== API ENDPOINTS ====================
228
-
229
- @app.route('/api/register_face', methods=['POST'])
230
- def register_face():
231
- """
232
- Register student face encoding
233
- Implements: PB-1 (Register Student Face Data)
234
- """
235
- try:
236
- data = request.get_json()
237
-
238
- # Validate input
239
- if not all(k in data for k in ['student_id', 'name', 'image']):
240
- return format_error_response("Missing required fields: student_id, name, image")
241
-
242
- student_id = data['student_id']
243
- name = data['name']
244
- email = data.get('email')
245
- image_base64 = data['image']
246
-
247
- # Check if student already exists
248
- existing_student = Student.query.filter_by(student_id=student_id).first()
249
- if existing_student:
250
- return format_error_response("Student ID already registered", 409)
251
-
252
- # Get user account (if exists)
253
- user = User.query.filter_by(matric_no=student_id).first()
254
-
255
- # Convert base64 to image
256
- try:
257
- image = base64_to_image(image_base64)
258
- image_array = image_to_numpy(image)
259
- except Exception as e:
260
- return format_error_response(f"Invalid image data: {str(e)}")
261
-
262
- # Generate face encoding
263
- success, result, face_location = face_recognition_module.register_face(image_array)
264
-
265
- if not success:
266
- return format_error_response(result)
267
-
268
- # Create student record
269
- student = Student(
270
- student_id=student_id,
271
- name=name,
272
- email=email,
273
- user_id=user.id if user else None # Link to user account if exists
274
- )
275
- student.set_face_encoding(result)
276
-
277
- db.session.add(student)
278
- db.session.commit()
279
-
280
- logger.info(f"Student registered: {student_id} - {name}")
281
-
282
- return format_success_response(
283
- data=student.to_dict(),
284
- message="Face registered successfully!"
285
- )
286
-
287
- except Exception as e:
288
- db.session.rollback()
289
- logger.error(f"Registration error: {str(e)}")
290
- return format_error_response(f"Registration failed: {str(e)}", 500)
291
-
292
-
293
- @app.route('/api/verify_face', methods=['POST'])
294
- def verify_face():
295
- """
296
- Verify student face against stored encoding
297
- Implements: PB-3 (Face Recognition Matching)
298
- Implements KR Rule #1: ∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x))
299
- """
300
- try:
301
- data = request.get_json()
302
-
303
- if not all(k in data for k in ['student_id', 'image']):
304
- return format_error_response("Missing required fields: student_id, image")
305
-
306
- student_id = data['student_id']
307
- image_base64 = data['image']
308
-
309
- # Get student record
310
- student = Student.query.filter_by(student_id=student_id).first()
311
- if not student:
312
- return format_error_response("Student not found. Please register first.", 404)
313
-
314
- if not student.is_active:
315
- return format_error_response("Student account is inactive", 403)
316
-
317
- # Get stored encoding
318
- stored_encoding = student.get_face_encoding()
319
- if not stored_encoding:
320
- return format_error_response("No face encoding found. Please register your face.", 404)
321
-
322
- # Convert base64 to image
323
- try:
324
- image = base64_to_image(image_base64)
325
- image_array = image_to_numpy(image)
326
- except Exception as e:
327
- return format_error_response(f"Invalid image data: {str(e)}")
328
-
329
- # Generate encoding from captured image
330
- success, result = face_recognition_module.generate_face_encoding(image_array)
331
-
332
- if not success:
333
- return format_error_response(result)
334
-
335
- captured_encoding = result
336
-
337
- # Verify face match
338
- is_match, confidence = face_recognition_module.verify_face(captured_encoding, stored_encoding)
339
-
340
- if is_match:
341
- logger.info(f"Face verified for student {student_id}: confidence={confidence:.2f}%")
342
- return format_success_response(
343
- data={
344
- 'verified': True,
345
- 'confidence': round(confidence, 2),
346
- 'student': student.to_dict()
347
- },
348
- message=f"Face verified! Confidence: {confidence:.1f}%"
349
- )
350
- else:
351
- logger.warning(f"Face verification failed for {student_id}: confidence={confidence:.2f}%")
352
- return format_error_response(
353
- f"Face verification failed. Confidence too low: {confidence:.1f}%",
354
- 401
355
- )
356
-
357
- except Exception as e:
358
- logger.error(f"Face verification error: {str(e)}")
359
- return format_error_response(f"Verification failed: {str(e)}", 500)
360
-
361
-
362
- @app.route('/api/verify_location', methods=['POST'])
363
- def verify_location():
364
- """
365
- Verify student location against classroom boundaries
366
- Implements: PB-5 (Geofencing Logic – Classroom Detection)
367
- Implements KR Rule #2: ∀x[(Student(x) ∧ IsWithinAllowedArea(x)) → VerifiedLocation(x)]
368
- """
369
- try:
370
- data = request.get_json()
371
-
372
- if not all(k in data for k in ['latitude', 'longitude', 'session_id']):
373
- return format_error_response("Missing required fields: latitude, longitude, session_id")
374
-
375
- latitude = data['latitude']
376
- longitude = data['longitude']
377
- session_id = data['session_id']
378
-
379
- # Get session
380
- session_obj = AttendanceSession.query.get(session_id)
381
- if not session_obj:
382
- return format_error_response("Session not found", 404)
383
-
384
- if not session_obj.is_active:
385
- return format_error_response("Session is not active", 403)
386
-
387
- # Verify location
388
- result = geolocation_module.verify_location(
389
- latitude,
390
- longitude,
391
- session_obj.classroom_id
392
- )
393
-
394
- if result['verified']:
395
- logger.info(f"Location verified: distance={result['distance']:.2f}m")
396
- return format_success_response(
397
- data={
398
- 'verified': True,
399
- 'distance': round(result['distance'], 2),
400
- 'classroom': session_obj.classroom.to_dict()
401
- },
402
- message=result['message']
403
- )
404
- else:
405
- logger.warning(f"Location verification failed: {result['error']}")
406
- return format_error_response(result['error'] or result['message'], 403)
407
-
408
- except Exception as e:
409
- logger.error(f"Location verification error: {str(e)}")
410
- return format_error_response(f"Location verification failed: {str(e)}", 500)
411
-
412
-
413
- @app.route('/api/mark_attendance', methods=['POST'])
414
- def mark_attendance():
415
- """
416
- Mark student attendance after all verifications
417
- Implements: PB-12 (Mark Attendance in Database)
418
- Implements KR Rule #4: ∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x))
419
- Implements KR Rule #5: ∀x ((ValidCodeEntry(User,x) ∧ WithinCycle(Code,x)) → MarkPresent(System,x))
420
- """
421
- try:
422
- data = request.get_json()
423
-
424
- required_fields = ['student_id', 'session_id', 'code', 'latitude', 'longitude']
425
- if not all(k in data for k in required_fields):
426
- return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
427
-
428
- student_id = data['student_id']
429
- session_id = data['session_id']
430
- code = data['code']
431
- latitude = data['latitude']
432
- longitude = data['longitude']
433
-
434
- # Get student
435
- student = Student.query.filter_by(student_id=student_id).first()
436
- if not student:
437
- return format_error_response("Student not found", 404)
438
-
439
- # Get session
440
- session_obj = AttendanceSession.query.get(session_id)
441
- if not session_obj:
442
- return format_error_response("Session not found", 404)
443
-
444
- if not session_obj.is_active:
445
- return format_error_response("Session is not active", 403)
446
-
447
- # Check if already marked
448
- existing_record = AttendanceRecord.query.filter_by(
449
- session_id=session_id,
450
- student_id=student.id
451
- ).first()
452
-
453
- if existing_record:
454
- return format_error_response("Attendance already marked for this session", 409)
455
-
456
- # Validate code
457
- is_valid, message = attendance_code_module.is_code_valid(session_id, code)
458
-
459
- if not is_valid:
460
- return format_error_response(message, 401)
461
-
462
- # Verify location one more time
463
- location_result = geolocation_module.verify_location(
464
- latitude,
465
- longitude,
466
- session_obj.classroom_id
467
- )
468
-
469
- if not location_result['verified']:
470
- return format_error_response(location_result['error'] or "Location verification failed", 403)
471
-
472
- # Create attendance record
473
- attendance = AttendanceRecord(
474
- session_id=session_id,
475
- student_id=student.id,
476
- face_verified=True, # Assumed verified before reaching this endpoint
477
- location_verified=True,
478
- code_verified=True,
479
- student_latitude=float(latitude),
480
- student_longitude=float(longitude),
481
- distance_from_classroom=location_result['distance'],
482
- status='present'
483
- )
484
-
485
- db.session.add(attendance)
486
- db.session.commit()
487
-
488
- logger.info(f"Attendance marked: {student.name} for session {session_id}")
489
-
490
- return format_success_response(
491
- data=attendance.to_dict(),
492
- message="Attendance marked successfully! ✓"
493
- )
494
-
495
- except Exception as e:
496
- db.session.rollback()
497
- logger.error(f"Mark attendance error: {str(e)}")
498
- return format_error_response(f"Failed to mark attendance: {str(e)}", 500)
499
-
500
-
501
- @app.route('/api/create_session', methods=['POST'])
502
- def create_session():
503
- """
504
- Create new attendance session
505
- Implements: US-06 (Generate Auto-Refresh Codes)
506
- """
507
- try:
508
- data = request.get_json()
509
-
510
- if not all(k in data for k in ['course_name', 'classroom_id', 'lecturer_name']):
511
- return format_error_response("Missing required fields: course_name, classroom_id, lecturer_name")
512
-
513
- course_name = data['course_name']
514
- classroom_id = data['classroom_id']
515
- lecturer_name = data['lecturer_name']
516
-
517
- # Verify classroom exists
518
- classroom = Classroom.query.get(classroom_id)
519
- if not classroom:
520
- return format_error_response("Classroom not found", 404)
521
-
522
- # Create session
523
- session_obj = AttendanceSession(
524
- course_name=course_name,
525
- classroom_id=classroom_id,
526
- lecturer_name=lecturer_name,
527
- is_active=True
528
- )
529
-
530
- db.session.add(session_obj)
531
- db.session.commit()
532
-
533
- # Generate initial code
534
- success, code = attendance_code_module.create_session_code(session_obj.id)
535
-
536
- if not success:
537
- return format_error_response(f"Session created but code generation failed: {code}")
538
-
539
- logger.info(f"Session created: {course_name} by {lecturer_name}")
540
-
541
- return format_success_response(
542
- data=session_obj.to_dict(),
543
- message="Session created successfully!"
544
- )
545
-
546
- except Exception as e:
547
- db.session.rollback()
548
- logger.error(f"Create session error: {str(e)}")
549
- return format_error_response(f"Failed to create session: {str(e)}", 500)
550
-
551
-
552
- @app.route('/api/get_session/<int:session_id>', methods=['GET'])
553
- def get_session(session_id):
554
- """Get session details including current code"""
555
- try:
556
- session_obj = AttendanceSession.query.get(session_id)
557
-
558
- if not session_obj:
559
- return format_error_response("Session not found", 404)
560
-
561
- # Get code status
562
- code_status = attendance_code_module.get_code_status(session_id)
563
-
564
- # Auto-refresh if needed
565
- if code_status.get('needs_refresh'):
566
- attendance_code_module.auto_refresh_code(session_id)
567
- code_status = attendance_code_module.get_code_status(session_id)
568
-
569
- session_data = session_obj.to_dict()
570
- session_data['code_status'] = code_status
571
-
572
- return format_success_response(data=session_data)
573
-
574
- except Exception as e:
575
- logger.error(f"Get session error: {str(e)}")
576
- return format_error_response(f"Failed to get session: {str(e)}", 500)
577
-
578
-
579
- @app.route('/api/get_active_sessions', methods=['GET'])
580
- def get_active_sessions():
581
- """Get all active sessions"""
582
- try:
583
- sessions = AttendanceSession.query.filter_by(is_active=True).all()
584
- return format_success_response(
585
- data=[session.to_dict() for session in sessions]
586
- )
587
- except Exception as e:
588
- logger.error(f"Get active sessions error: {str(e)}")
589
- return format_error_response(f"Failed to get sessions: {str(e)}", 500)
590
-
591
-
592
- @app.route('/api/get_attendance/<int:session_id>', methods=['GET'])
593
- def get_attendance(session_id):
594
- """
595
- Get attendance records for a session
596
- Implements: US-07 (View Real-time Attendance)
597
- """
598
- try:
599
- session_obj = AttendanceSession.query.get(session_id)
600
-
601
- if not session_obj:
602
- return format_error_response("Session not found", 404)
603
-
604
- records = AttendanceRecord.query.filter_by(session_id=session_id).all()
605
-
606
- return format_success_response(
607
- data={
608
- 'session': session_obj.to_dict(),
609
- 'records': [record.to_dict() for record in records],
610
- 'total_present': len(records)
611
- }
612
- )
613
-
614
- except Exception as e:
615
- logger.error(f"Get attendance error: {str(e)}")
616
- return format_error_response(f"Failed to get attendance: {str(e)}", 500)
617
-
618
-
619
- @app.route('/api/end_session/<int:session_id>', methods=['POST'])
620
- def end_session(session_id):
621
- """End an attendance session"""
622
- try:
623
- session_obj = AttendanceSession.query.get(session_id)
624
-
625
- if not session_obj:
626
- return format_error_response("Session not found", 404)
627
-
628
- session_obj.is_active = False
629
- session_obj.end_time = datetime.utcnow()
630
-
631
- db.session.commit()
632
-
633
- logger.info(f"Session ended: {session_id}")
634
-
635
- return format_success_response(message="Session ended successfully")
636
-
637
- except Exception as e:
638
- db.session.rollback()
639
- logger.error(f"End session error: {str(e)}")
640
- return format_error_response(f"Failed to end session: {str(e)}", 500)
641
-
642
-
643
- @app.route('/api/student/history', methods=['GET'])
644
- @login_required
645
- @role_required('student')
646
- def get_student_history():
647
- """Get attendance history for logged-in student"""
648
- try:
649
- # Get current user's student profile
650
- current_user = get_current_user()
651
- student = Student.query.filter_by(student_id=current_user.matric_no).first()
652
-
653
- if not student:
654
- return format_error_response("Student profile not found", 404)
655
-
656
- # Get all attendance records for this student
657
- records = AttendanceRecord.query.filter_by(student_id=student.id)\
658
- .order_by(AttendanceRecord.marked_at.desc()).all()
659
-
660
- # Format response with session details
661
- history = []
662
- for record in records:
663
- session = record.session
664
- history.append({
665
- 'id': record.id,
666
- 'course_name': session.course_name,
667
- 'lecturer_name': session.lecturer_name,
668
- 'classroom': session.classroom.name,
669
- 'building': session.classroom.building,
670
- 'status': record.status,
671
- 'marked_at': record.marked_at.isoformat() if record.marked_at else None,
672
- 'session_date': session.start_time.isoformat() if session.start_time else None,
673
- 'face_verified': record.face_verified,
674
- 'location_verified': record.location_verified,
675
- 'code_verified': record.code_verified,
676
- 'distance_from_classroom': round(record.distance_from_classroom, 2) if record.distance_from_classroom else None
677
- })
678
-
679
- return format_success_response(
680
- data={
681
- 'student': student.to_dict(),
682
- 'total_records': len(history),
683
- 'history': history
684
- }
685
- )
686
-
687
- except Exception as e:
688
- logger.error(f"Get student history error: {str(e)}")
689
- return format_error_response(f"Failed to get history: {str(e)}", 500)
690
-
691
-
692
- @app.route('/api/lecturer/history', methods=['GET'])
693
- @login_required
694
- @role_required('lecturer')
695
- def get_lecturer_history():
696
- """Get session history for logged-in lecturer"""
697
- try:
698
- current_user = get_current_user()
699
-
700
- # Get all sessions created by this lecturer (match by name)
701
- sessions = AttendanceSession.query.filter_by(lecturer_name=current_user.name)\
702
- .order_by(AttendanceSession.start_time.desc()).all()
703
-
704
- # Format response with attendance statistics
705
- history = []
706
- for session in sessions:
707
- # Count attendance records
708
- total_attendance = len(session.attendance_records)
709
- present_count = sum(1 for r in session.attendance_records if r.status == 'present')
710
-
711
- # Calculate session duration
712
- if session.end_time and session.start_time:
713
- duration_minutes = int((session.end_time - session.start_time).total_seconds() / 60)
714
- elif session.is_active and session.start_time:
715
- duration_minutes = int((datetime.utcnow() - session.start_time).total_seconds() / 60)
716
- else:
717
- duration_minutes = 0
718
-
719
- history.append({
720
- 'id': session.id,
721
- 'course_name': session.course_name,
722
- 'lecturer_name': session.lecturer_name,
723
- 'classroom': session.classroom.name,
724
- 'building': session.classroom.building,
725
- 'start_time': session.start_time.isoformat() if session.start_time else None,
726
- 'end_time': session.end_time.isoformat() if session.end_time else None,
727
- 'is_active': session.is_active,
728
- 'duration_minutes': duration_minutes,
729
- 'total_attendance': total_attendance,
730
- 'present_count': present_count,
731
- 'attendance_records': [
732
- {
733
- 'student_id': r.student.student_id if r.student else 'Unknown',
734
- 'student_name': r.student.name if r.student else 'Unknown',
735
- 'status': r.status,
736
- 'marked_at': r.marked_at.isoformat() if r.marked_at else None,
737
- 'face_verified': r.face_verified,
738
- 'location_verified': r.location_verified,
739
- 'code_verified': r.code_verified
740
- } for r in session.attendance_records
741
- ]
742
- })
743
-
744
- return format_success_response(
745
- data={
746
- 'lecturer': {
747
- 'name': current_user.name,
748
- 'matric_no': current_user.matric_no
749
- },
750
- 'total_sessions': len(history),
751
- 'history': history
752
- }
753
- )
754
-
755
- except Exception as e:
756
- logger.error(f"Get lecturer history error: {str(e)}")
757
- return format_error_response(f"Failed to get history: {str(e)}", 500)
758
-
759
-
760
- @app.route('/api/classrooms', methods=['GET'])
761
- def get_classrooms():
762
- """Get all classrooms"""
763
- try:
764
- classrooms = geolocation_module.get_all_classrooms()
765
- return format_success_response(data=classrooms)
766
- except Exception as e:
767
- logger.error(f"Get classrooms error: {str(e)}")
768
- return format_error_response(f"Failed to get classrooms: {str(e)}", 500)
769
-
770
-
771
- # ==================== ERROR HANDLERS ====================
772
-
773
- @app.errorhandler(404)
774
- def not_found(error):
775
- return format_error_response("Resource not found", 404)
776
-
777
-
778
- @app.errorhandler(500)
779
- def internal_error(error):
780
- db.session.rollback()
781
- return format_error_response("Internal server error", 500)
782
-
783
-
784
- # ==================== MAIN ====================
785
-
786
- if __name__ == '__main__':
787
- with app.app_context():
788
- db.create_all()
789
- # Seed classrooms if empty
790
- if Classroom.query.count() == 0:
791
- logger.info("Seeding default classrooms...")
792
- default_classrooms = [
793
- Classroom(name="N28-01-01", building="N28 (Faculty of Computing)", latitude=1.5586, longitude=103.6376, radius_meters=50),
794
- Classroom(name="N28-02-03", building="N28 (Faculty of Computing)", latitude=1.5588, longitude=103.6378, radius_meters=50),
795
- Classroom(name="V01-LT1", building="V01 (Lecture Hall)", latitude=1.5590, longitude=103.6380, radius_meters=75),
796
- Classroom(name="C22-Lab A", building="C22 (Computer Lab)", latitude=1.5584, longitude=103.6374, radius_meters=40)
797
- ]
798
- for c in default_classrooms:
799
- db.session.add(c)
800
- db.session.commit()
801
- logger.info("Database seeded successfully")
802
-
803
- logger.info("Database tables verified")
804
-
805
- app.run(debug=True, host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Attendr - Smart Attendance System
3
+ Main Flask Application
4
+ """
5
+
6
+ from flask import Flask, render_template, request, jsonify, session, redirect, url_for, flash, send_from_directory
7
+ from flask_sqlalchemy import SQLAlchemy
8
+ from config import Config
9
+ from models import db, User, Student, Classroom, AttendanceSession, AttendanceRecord
10
+ from face_recognition_module import face_recognition_module
11
+ from geolocation_module import geolocation_module
12
+ from attendance_code_module import attendance_code_module
13
+ from utils import base64_to_image, image_to_numpy, format_error_response, format_success_response
14
+ from auth import login_required, role_required, get_current_user
15
+ from datetime import datetime
16
+ import logging
17
+
18
+ # Initialize Flask app
19
+ app = Flask(__name__, template_folder='.')
20
+ app.config.from_object(Config)
21
+ Config.init_app(app)
22
+
23
+ # Initialize database
24
+ db.init_app(app)
25
+
26
+ # Set up logging
27
+ logging.basicConfig(level=logging.INFO)
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ @app.after_request
32
+ def add_header(response):
33
+ """
34
+ Add headers to prevent caching of dynamic pages.
35
+ This ensures that when a user logs out, they cannot click 'Back'
36
+ to see protected content from the browser cache.
37
+ """
38
+ if 'Cache-Control' not in response.headers:
39
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
40
+ response.headers['Pragma'] = 'no-cache'
41
+ response.headers['Expires'] = '-1'
42
+ return response
43
+
44
+
45
+ # ==================== ROUTES ====================
46
+
47
+ @app.route('/style.css')
48
+ def style():
49
+ return send_from_directory('.', 'style.css')
50
+
51
+
52
+ @app.route('/')
53
+ def index():
54
+ """Landing page"""
55
+ current_user = get_current_user()
56
+ return render_template('index.html', current_user=current_user)
57
+
58
+
59
+ @app.route('/student')
60
+ @login_required
61
+ @role_required('student')
62
+ def student_page():
63
+ """Student attendance interface - requires student login"""
64
+ return render_template('student.html', current_user=get_current_user())
65
+
66
+
67
+ @app.route('/student/history')
68
+ @login_required
69
+ @role_required('student')
70
+ def student_history():
71
+ """Student attendance history page"""
72
+ return render_template('student_history.html', current_user=get_current_user())
73
+
74
+
75
+ @app.route('/lecturer')
76
+ @login_required
77
+ @role_required('lecturer')
78
+ def lecturer_page():
79
+ """Lecturer dashboard - requires lecturer login"""
80
+ return render_template('lecturer.html', current_user=get_current_user())
81
+
82
+
83
+ @app.route('/lecturer/history')
84
+ @login_required
85
+ @role_required('lecturer')
86
+ def lecturer_history():
87
+ """Lecturer session history page"""
88
+ return render_template('lecturer_history.html', current_user=get_current_user())
89
+
90
+
91
+ @app.route('/register')
92
+ def register_page():
93
+ """User registration page - redirect if already logged in"""
94
+ if 'user_id' in session:
95
+ return redirect(url_for('student_page' if session.get('role') == 'student' else 'lecturer_page'))
96
+ return render_template('register.html')
97
+
98
+
99
+ @app.route('/login')
100
+ def login_page():
101
+ """Login page - redirect if already logged in"""
102
+ if 'user_id' in session:
103
+ return redirect(url_for('student_page' if session.get('role') == 'student' else 'lecturer_page'))
104
+ return render_template('login.html')
105
+
106
+
107
+ # ==================== AUTHENTICATION API ====================
108
+
109
+ @app.route('/api/auth/register', methods=['POST'])
110
+ def register_user():
111
+ """
112
+ Register new user (student or lecturer)
113
+ Validates matric number format based on role
114
+ """
115
+ try:
116
+ data = request.get_json()
117
+
118
+ required_fields = ['matric_no', 'name', 'password', 'role']
119
+ if not all(k in data for k in required_fields):
120
+ return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
121
+
122
+ matric_no = data['matric_no'].upper().strip()
123
+ name = data['name']
124
+ password = data['password']
125
+ role = data['role'].lower()
126
+ email = data.get('email')
127
+
128
+ # Validate role
129
+ if role not in ['student', 'lecturer']:
130
+ return format_error_response("Role must be 'student' or 'lecturer'")
131
+
132
+ # Validate matric number format
133
+ is_valid, error_msg = User.validate_matric_no(matric_no, role)
134
+ if not is_valid:
135
+ return format_error_response(error_msg, 400)
136
+
137
+ # Check if user already exists
138
+ existing_user = User.query.filter_by(matric_no=matric_no).first()
139
+ if existing_user:
140
+ return format_error_response("Matric number already registered", 409)
141
+
142
+ if email:
143
+ existing_email = User.query.filter_by(email=email).first()
144
+ if existing_email:
145
+ return format_error_response("Email already registered", 409)
146
+
147
+ # Create user
148
+ user = User(
149
+ matric_no=matric_no,
150
+ name=name,
151
+ email=email,
152
+ role=role
153
+ )
154
+ user.set_password(password)
155
+
156
+ db.session.add(user)
157
+ db.session.commit()
158
+
159
+ logger.info(f"User registered: {matric_no} ({role})")
160
+
161
+ return format_success_response(
162
+ data=user.to_dict(),
163
+ message=f"{role.capitalize()} account created successfully!"
164
+ )
165
+
166
+ except Exception as e:
167
+ db.session.rollback()
168
+ logger.error(f"User registration error: {str(e)}")
169
+ return format_error_response(f"Registration failed: {str(e)}", 500)
170
+
171
+
172
+ @app.route('/api/auth/login', methods=['POST'])
173
+ def login():
174
+ """Login user and create session"""
175
+ try:
176
+ data = request.get_json()
177
+
178
+ if not all(k in data for k in ['matric_no', 'password']):
179
+ return format_error_response("Missing required fields: matric_no, password")
180
+
181
+ matric_no = data['matric_no'].upper().strip()
182
+ password = data['password']
183
+
184
+ # Find user
185
+ user = User.query.filter_by(matric_no=matric_no).first()
186
+
187
+ if not user or not user.check_password(password):
188
+ return format_error_response("Invalid matric number or password", 401)
189
+
190
+ if not user.is_active:
191
+ return format_error_response("Account is inactive. Please contact admin.", 403)
192
+
193
+ # Create session
194
+ session['user_id'] = user.id
195
+ session['matric_no'] = user.matric_no
196
+ session['role'] = user.role
197
+ session['name'] = user.name
198
+
199
+ logger.info(f"User logged in: {matric_no} ({user.role})")
200
+
201
+ return format_success_response(
202
+ data={
203
+ 'user': user.to_dict(),
204
+ 'redirect': '/student' if user.role == 'student' else '/lecturer'
205
+ },
206
+ message=f"Welcome back, {user.name}!"
207
+ )
208
+
209
+ except Exception as e:
210
+ logger.error(f"Login error: {str(e)}")
211
+ return format_error_response(f"Login failed: {str(e)}", 500)
212
+
213
+
214
+ @app.route('/api/auth/logout', methods=['POST'])
215
+ @login_required
216
+ def logout():
217
+ """Logout user and destroy session"""
218
+ try:
219
+ user_name = session.get('name', 'User')
220
+ matric_no = session.get('matric_no', 'Unknown')
221
+
222
+ # Clear all session data
223
+ session.clear()
224
+
225
+ logger.info(f"User logged out: {matric_no}")
226
+
227
+ return format_success_response(
228
+ message=f"Goodbye, {user_name}! You have been logged out successfully."
229
+ )
230
+
231
+ except Exception as e:
232
+ logger.error(f"Logout error: {str(e)}")
233
+ return format_error_response(f"Logout failed: {str(e)}", 500)
234
+
235
+
236
+ @app.route('/api/auth/current_user', methods=['GET'])
237
+ def get_current_user_api():
238
+ """Get current logged in user"""
239
+ user = get_current_user()
240
+ if user:
241
+ return format_success_response(data=user.to_dict())
242
+ return format_error_response("Not logged in", 401)
243
+
244
+
245
+ # ==================== API ENDPOINTS ====================
246
+
247
+ @app.route('/api/register_face', methods=['POST'])
248
+ def register_face():
249
+ """
250
+ Register student face encoding
251
+ Implements: PB-1 (Register Student Face Data)
252
+ """
253
+ try:
254
+ data = request.get_json()
255
+
256
+ # Validate input
257
+ if not all(k in data for k in ['student_id', 'name', 'image']):
258
+ return format_error_response("Missing required fields: student_id, name, image")
259
+
260
+ student_id = data['student_id']
261
+ name = data['name']
262
+ email = data.get('email')
263
+ image_base64 = data['image']
264
+
265
+ # Check if student already exists
266
+ existing_student = Student.query.filter_by(student_id=student_id).first()
267
+ if existing_student:
268
+ return format_error_response("Student ID already registered", 409)
269
+
270
+ # Get user account (if exists)
271
+ user = User.query.filter_by(matric_no=student_id).first()
272
+
273
+ # Convert base64 to image
274
+ try:
275
+ image = base64_to_image(image_base64)
276
+ image_array = image_to_numpy(image)
277
+ except Exception as e:
278
+ return format_error_response(f"Invalid image data: {str(e)}")
279
+
280
+ # Generate face encoding
281
+ success, result, face_location = face_recognition_module.register_face(image_array)
282
+
283
+ if not success:
284
+ return format_error_response(result)
285
+
286
+ # Create student record
287
+ student = Student(
288
+ student_id=student_id,
289
+ name=name,
290
+ email=email,
291
+ user_id=user.id if user else None # Link to user account if exists
292
+ )
293
+ student.set_face_encoding(result)
294
+
295
+ db.session.add(student)
296
+ db.session.commit()
297
+
298
+ logger.info(f"Student registered: {student_id} - {name}")
299
+
300
+ return format_success_response(
301
+ data=student.to_dict(),
302
+ message="Face registered successfully!"
303
+ )
304
+
305
+ except Exception as e:
306
+ db.session.rollback()
307
+ logger.error(f"Registration error: {str(e)}")
308
+ return format_error_response(f"Registration failed: {str(e)}", 500)
309
+
310
+
311
+ @app.route('/api/verify_face', methods=['POST'])
312
+ def verify_face():
313
+ """
314
+ Verify student face against stored encoding
315
+ Implements: PB-3 (Face Recognition Matching)
316
+ Implements KR Rule #1: ∀x ((CapturedFace(x) ∧ MatchStored(x)) → FaceMatch(x))
317
+ """
318
+ try:
319
+ data = request.get_json()
320
+
321
+ if not all(k in data for k in ['student_id', 'image']):
322
+ return format_error_response("Missing required fields: student_id, image")
323
+
324
+ student_id = data['student_id']
325
+ image_base64 = data['image']
326
+
327
+ # Get student record
328
+ student = Student.query.filter_by(student_id=student_id).first()
329
+ if not student:
330
+ return format_error_response("Student not found. Please register first.", 404)
331
+
332
+ if not student.is_active:
333
+ return format_error_response("Student account is inactive", 403)
334
+
335
+ # Get stored encoding
336
+ stored_encoding = student.get_face_encoding()
337
+ if not stored_encoding:
338
+ return format_error_response("No face encoding found. Please register your face.", 404)
339
+
340
+ # Convert base64 to image
341
+ try:
342
+ image = base64_to_image(image_base64)
343
+ image_array = image_to_numpy(image)
344
+ except Exception as e:
345
+ return format_error_response(f"Invalid image data: {str(e)}")
346
+
347
+ # Generate encoding from captured image
348
+ success, result = face_recognition_module.generate_face_encoding(image_array)
349
+
350
+ if not success:
351
+ return format_error_response(result)
352
+
353
+ captured_encoding = result
354
+
355
+ # Verify face match
356
+ is_match, confidence = face_recognition_module.verify_face(captured_encoding, stored_encoding)
357
+
358
+ if is_match:
359
+ logger.info(f"Face verified for student {student_id}: confidence={confidence:.2f}%")
360
+ return format_success_response(
361
+ data={
362
+ 'verified': True,
363
+ 'confidence': round(confidence, 2),
364
+ 'student': student.to_dict()
365
+ },
366
+ message=f"Face verified! Confidence: {confidence:.1f}%"
367
+ )
368
+ else:
369
+ logger.warning(f"Face verification failed for {student_id}: confidence={confidence:.2f}%")
370
+ return format_error_response(
371
+ f"Face verification failed. Confidence too low: {confidence:.1f}%",
372
+ 401
373
+ )
374
+
375
+ except Exception as e:
376
+ logger.error(f"Face verification error: {str(e)}")
377
+ return format_error_response(f"Verification failed: {str(e)}", 500)
378
+
379
+
380
+ @app.route('/api/verify_location', methods=['POST'])
381
+ def verify_location():
382
+ """
383
+ Verify student location against classroom boundaries
384
+ Implements: PB-5 (Geofencing Logic – Classroom Detection)
385
+ Implements KR Rule #2: ∀x[(Student(x) IsWithinAllowedArea(x)) VerifiedLocation(x)]
386
+ """
387
+ try:
388
+ data = request.get_json()
389
+
390
+ if not all(k in data for k in ['latitude', 'longitude', 'session_id']):
391
+ return format_error_response("Missing required fields: latitude, longitude, session_id")
392
+
393
+ latitude = data['latitude']
394
+ longitude = data['longitude']
395
+ session_id = data['session_id']
396
+
397
+ # Get session
398
+ session_obj = AttendanceSession.query.get(session_id)
399
+ if not session_obj:
400
+ return format_error_response("Session not found", 404)
401
+
402
+ if not session_obj.is_active:
403
+ return format_error_response("Session is not active", 403)
404
+
405
+ # Verify location
406
+ result = geolocation_module.verify_location(
407
+ latitude,
408
+ longitude,
409
+ session_obj.classroom_id
410
+ )
411
+
412
+ if result['verified']:
413
+ logger.info(f"Location verified: distance={result['distance']:.2f}m")
414
+ return format_success_response(
415
+ data={
416
+ 'verified': True,
417
+ 'distance': round(result['distance'], 2),
418
+ 'classroom': session_obj.classroom.to_dict()
419
+ },
420
+ message=result['message']
421
+ )
422
+ else:
423
+ logger.warning(f"Location verification failed: {result['error']}")
424
+ return format_error_response(result['error'] or result['message'], 403)
425
+
426
+ except Exception as e:
427
+ logger.error(f"Location verification error: {str(e)}")
428
+ return format_error_response(f"Location verification failed: {str(e)}", 500)
429
+
430
+
431
+ @app.route('/api/mark_attendance', methods=['POST'])
432
+ def mark_attendance():
433
+ """
434
+ Mark student attendance after all verifications
435
+ Implements: PB-12 (Mark Attendance in Database)
436
+ Implements KR Rule #4: ∀x ((FaceMatch(x) ∧ LocationValid(x)) → GrantCodeAccess(x))
437
+ Implements KR Rule #5: ∀x ((ValidCodeEntry(User,x) WithinCycle(Code,x)) → MarkPresent(System,x))
438
+ """
439
+ try:
440
+ data = request.get_json()
441
+
442
+ required_fields = ['student_id', 'session_id', 'code', 'latitude', 'longitude']
443
+ if not all(k in data for k in required_fields):
444
+ return format_error_response(f"Missing required fields: {', '.join(required_fields)}")
445
+
446
+ student_id = data['student_id']
447
+ session_id = data['session_id']
448
+ code = data['code']
449
+ latitude = data['latitude']
450
+ longitude = data['longitude']
451
+
452
+ # Get student
453
+ student = Student.query.filter_by(student_id=student_id).first()
454
+ if not student:
455
+ return format_error_response("Student not found", 404)
456
+
457
+ # Get session
458
+ session_obj = AttendanceSession.query.get(session_id)
459
+ if not session_obj:
460
+ return format_error_response("Session not found", 404)
461
+
462
+ if not session_obj.is_active:
463
+ return format_error_response("Session is not active", 403)
464
+
465
+ # Check if already marked
466
+ existing_record = AttendanceRecord.query.filter_by(
467
+ session_id=session_id,
468
+ student_id=student.id
469
+ ).first()
470
+
471
+ if existing_record:
472
+ return format_error_response("Attendance already marked for this session", 409)
473
+
474
+ # Validate code
475
+ is_valid, message = attendance_code_module.is_code_valid(session_id, code)
476
+
477
+ if not is_valid:
478
+ return format_error_response(message, 401)
479
+
480
+ # Verify location one more time
481
+ location_result = geolocation_module.verify_location(
482
+ latitude,
483
+ longitude,
484
+ session_obj.classroom_id
485
+ )
486
+
487
+ if not location_result['verified']:
488
+ return format_error_response(location_result['error'] or "Location verification failed", 403)
489
+
490
+ # Create attendance record
491
+ attendance = AttendanceRecord(
492
+ session_id=session_id,
493
+ student_id=student.id,
494
+ face_verified=True, # Assumed verified before reaching this endpoint
495
+ location_verified=True,
496
+ code_verified=True,
497
+ student_latitude=float(latitude),
498
+ student_longitude=float(longitude),
499
+ distance_from_classroom=location_result['distance'],
500
+ status='present'
501
+ )
502
+
503
+ db.session.add(attendance)
504
+ db.session.commit()
505
+
506
+ logger.info(f"Attendance marked: {student.name} for session {session_id}")
507
+
508
+ return format_success_response(
509
+ data=attendance.to_dict(),
510
+ message="Attendance marked successfully! ✓"
511
+ )
512
+
513
+ except Exception as e:
514
+ db.session.rollback()
515
+ logger.error(f"Mark attendance error: {str(e)}")
516
+ return format_error_response(f"Failed to mark attendance: {str(e)}", 500)
517
+
518
+
519
+ @app.route('/api/create_session', methods=['POST'])
520
+ def create_session():
521
+ """
522
+ Create new attendance session
523
+ Implements: US-06 (Generate Auto-Refresh Codes)
524
+ """
525
+ try:
526
+ data = request.get_json()
527
+
528
+ if not all(k in data for k in ['course_name', 'classroom_id', 'lecturer_name']):
529
+ return format_error_response("Missing required fields: course_name, classroom_id, lecturer_name")
530
+
531
+ course_name = data['course_name']
532
+ classroom_id = data['classroom_id']
533
+ lecturer_name = data['lecturer_name']
534
+
535
+ # Verify classroom exists
536
+ classroom = Classroom.query.get(classroom_id)
537
+ if not classroom:
538
+ return format_error_response("Classroom not found", 404)
539
+
540
+ # Create session
541
+ session_obj = AttendanceSession(
542
+ course_name=course_name,
543
+ classroom_id=classroom_id,
544
+ lecturer_name=lecturer_name,
545
+ is_active=True
546
+ )
547
+
548
+ db.session.add(session_obj)
549
+ db.session.commit()
550
+
551
+ # Generate initial code
552
+ success, code = attendance_code_module.create_session_code(session_obj.id)
553
+
554
+ if not success:
555
+ return format_error_response(f"Session created but code generation failed: {code}")
556
+
557
+ logger.info(f"Session created: {course_name} by {lecturer_name}")
558
+
559
+ return format_success_response(
560
+ data=session_obj.to_dict(),
561
+ message="Session created successfully!"
562
+ )
563
+
564
+ except Exception as e:
565
+ db.session.rollback()
566
+ logger.error(f"Create session error: {str(e)}")
567
+ return format_error_response(f"Failed to create session: {str(e)}", 500)
568
+
569
+
570
+ @app.route('/api/get_session/<int:session_id>', methods=['GET'])
571
+ def get_session(session_id):
572
+ """Get session details including current code"""
573
+ try:
574
+ session_obj = AttendanceSession.query.get(session_id)
575
+
576
+ if not session_obj:
577
+ return format_error_response("Session not found", 404)
578
+
579
+ # Get code status
580
+ code_status = attendance_code_module.get_code_status(session_id)
581
+
582
+ # Auto-refresh if needed
583
+ if code_status.get('needs_refresh'):
584
+ attendance_code_module.auto_refresh_code(session_id)
585
+ code_status = attendance_code_module.get_code_status(session_id)
586
+
587
+ session_data = session_obj.to_dict()
588
+ session_data['code_status'] = code_status
589
+
590
+ return format_success_response(data=session_data)
591
+
592
+ except Exception as e:
593
+ logger.error(f"Get session error: {str(e)}")
594
+ return format_error_response(f"Failed to get session: {str(e)}", 500)
595
+
596
+
597
+ @app.route('/api/get_active_sessions', methods=['GET'])
598
+ def get_active_sessions():
599
+ """Get all active sessions"""
600
+ try:
601
+ sessions = AttendanceSession.query.filter_by(is_active=True).all()
602
+ return format_success_response(
603
+ data=[session.to_dict() for session in sessions]
604
+ )
605
+ except Exception as e:
606
+ logger.error(f"Get active sessions error: {str(e)}")
607
+ return format_error_response(f"Failed to get sessions: {str(e)}", 500)
608
+
609
+
610
+ @app.route('/api/get_attendance/<int:session_id>', methods=['GET'])
611
+ def get_attendance(session_id):
612
+ """
613
+ Get attendance records for a session
614
+ Implements: US-07 (View Real-time Attendance)
615
+ """
616
+ try:
617
+ session_obj = AttendanceSession.query.get(session_id)
618
+
619
+ if not session_obj:
620
+ return format_error_response("Session not found", 404)
621
+
622
+ records = AttendanceRecord.query.filter_by(session_id=session_id).all()
623
+
624
+ return format_success_response(
625
+ data={
626
+ 'session': session_obj.to_dict(),
627
+ 'records': [record.to_dict() for record in records],
628
+ 'total_present': len(records)
629
+ }
630
+ )
631
+
632
+ except Exception as e:
633
+ logger.error(f"Get attendance error: {str(e)}")
634
+ return format_error_response(f"Failed to get attendance: {str(e)}", 500)
635
+
636
+
637
+ @app.route('/api/end_session/<int:session_id>', methods=['POST'])
638
+ def end_session(session_id):
639
+ """End an attendance session"""
640
+ try:
641
+ session_obj = AttendanceSession.query.get(session_id)
642
+
643
+ if not session_obj:
644
+ return format_error_response("Session not found", 404)
645
+
646
+ session_obj.is_active = False
647
+ session_obj.end_time = datetime.utcnow()
648
+
649
+ db.session.commit()
650
+
651
+ logger.info(f"Session ended: {session_id}")
652
+
653
+ return format_success_response(message="Session ended successfully")
654
+
655
+ except Exception as e:
656
+ db.session.rollback()
657
+ logger.error(f"End session error: {str(e)}")
658
+ return format_error_response(f"Failed to end session: {str(e)}", 500)
659
+
660
+
661
+ @app.route('/api/student/history', methods=['GET'])
662
+ @login_required
663
+ @role_required('student')
664
+ def get_student_history():
665
+ """Get attendance history for logged-in student"""
666
+ try:
667
+ # Get current user's student profile
668
+ current_user = get_current_user()
669
+ student = Student.query.filter_by(student_id=current_user.matric_no).first()
670
+
671
+ if not student:
672
+ return format_error_response("Student profile not found", 404)
673
+
674
+ # Get all attendance records for this student
675
+ records = AttendanceRecord.query.filter_by(student_id=student.id)\
676
+ .order_by(AttendanceRecord.marked_at.desc()).all()
677
+
678
+ # Format response with session details
679
+ history = []
680
+ for record in records:
681
+ session = record.session
682
+ history.append({
683
+ 'id': record.id,
684
+ 'course_name': session.course_name,
685
+ 'lecturer_name': session.lecturer_name,
686
+ 'classroom': session.classroom.name,
687
+ 'building': session.classroom.building,
688
+ 'status': record.status,
689
+ 'marked_at': record.marked_at.isoformat() if record.marked_at else None,
690
+ 'session_date': session.start_time.isoformat() if session.start_time else None,
691
+ 'face_verified': record.face_verified,
692
+ 'location_verified': record.location_verified,
693
+ 'code_verified': record.code_verified,
694
+ 'distance_from_classroom': round(record.distance_from_classroom, 2) if record.distance_from_classroom else None
695
+ })
696
+
697
+ return format_success_response(
698
+ data={
699
+ 'student': student.to_dict(),
700
+ 'total_records': len(history),
701
+ 'history': history
702
+ }
703
+ )
704
+
705
+ except Exception as e:
706
+ logger.error(f"Get student history error: {str(e)}")
707
+ return format_error_response(f"Failed to get history: {str(e)}", 500)
708
+
709
+
710
+ @app.route('/api/lecturer/history', methods=['GET'])
711
+ @login_required
712
+ @role_required('lecturer')
713
+ def get_lecturer_history():
714
+ """Get session history for logged-in lecturer"""
715
+ try:
716
+ current_user = get_current_user()
717
+
718
+ # Get all sessions created by this lecturer (case-insensitive match by name)
719
+ from sqlalchemy import func
720
+ sessions = AttendanceSession.query.filter(
721
+ func.lower(AttendanceSession.lecturer_name) == func.lower(current_user.name)
722
+ ).order_by(AttendanceSession.start_time.desc()).all()
723
+
724
+ logger.info(f"Fetched {len(sessions)} sessions for lecturer: {current_user.name}")
725
+
726
+ # Format response with attendance statistics
727
+ history = []
728
+ for session in sessions:
729
+ # Count attendance records
730
+ total_attendance = len(session.attendance_records)
731
+ present_count = sum(1 for r in session.attendance_records if r.status == 'present')
732
+
733
+ # Calculate session duration
734
+ if session.end_time and session.start_time:
735
+ duration_minutes = int((session.end_time - session.start_time).total_seconds() / 60)
736
+ elif session.is_active and session.start_time:
737
+ duration_minutes = int((datetime.utcnow() - session.start_time).total_seconds() / 60)
738
+ else:
739
+ duration_minutes = 0
740
+
741
+ history.append({
742
+ 'id': session.id,
743
+ 'course_name': session.course_name,
744
+ 'lecturer_name': session.lecturer_name,
745
+ 'classroom': session.classroom.name,
746
+ 'building': session.classroom.building,
747
+ 'start_time': session.start_time.isoformat() if session.start_time else None,
748
+ 'end_time': session.end_time.isoformat() if session.end_time else None,
749
+ 'is_active': session.is_active,
750
+ 'duration_minutes': duration_minutes,
751
+ 'total_attendance': total_attendance,
752
+ 'present_count': present_count,
753
+ 'attendance_records': [
754
+ {
755
+ 'student_id': r.student.student_id if r.student else 'Unknown',
756
+ 'student_name': r.student.name if r.student else 'Unknown',
757
+ 'status': r.status,
758
+ 'marked_at': r.marked_at.isoformat() if r.marked_at else None,
759
+ 'face_verified': r.face_verified,
760
+ 'location_verified': r.location_verified,
761
+ 'code_verified': r.code_verified
762
+ } for r in session.attendance_records
763
+ ]
764
+ })
765
+
766
+ return format_success_response(
767
+ data={
768
+ 'lecturer': {
769
+ 'name': current_user.name,
770
+ 'matric_no': current_user.matric_no
771
+ },
772
+ 'total_sessions': len(history),
773
+ 'history': history
774
+ }
775
+ )
776
+
777
+ except Exception as e:
778
+ logger.error(f"Get lecturer history error: {str(e)}")
779
+ return format_error_response(f"Failed to get history: {str(e)}", 500)
780
+
781
+
782
+ @app.route('/api/classrooms', methods=['GET'])
783
+ def get_classrooms():
784
+ """Get all classrooms"""
785
+ try:
786
+ classrooms = geolocation_module.get_all_classrooms()
787
+ return format_success_response(data=classrooms)
788
+ except Exception as e:
789
+ logger.error(f"Get classrooms error: {str(e)}")
790
+ return format_error_response(f"Failed to get classrooms: {str(e)}", 500)
791
+
792
+
793
+ # ==================== ERROR HANDLERS ====================
794
+
795
+ @app.errorhandler(404)
796
+ def not_found(error):
797
+ return format_error_response("Resource not found", 404)
798
+
799
+
800
+ @app.errorhandler(500)
801
+ def internal_error(error):
802
+ db.session.rollback()
803
+ return format_error_response("Internal server error", 500)
804
+
805
+
806
+ # ==================== MAIN ====================
807
+
808
+ if __name__ == '__main__':
809
+ with app.app_context():
810
+ db.create_all()
811
+ logger.info("Database tables created")
812
+
813
+ app.run(debug=True, host='0.0.0.0', port=5000)
attendr.db CHANGED
Binary files a/attendr.db and b/attendr.db differ
 
attendr.sql CHANGED
@@ -53,52 +53,53 @@ CREATE TABLE classrooms (
53
  radius_meters INTEGER,
54
  PRIMARY KEY (id)
55
  );
56
- INSERT INTO `classrooms` VALUES(1,'N28-105-01','N28 - Faculty of Computing',1.5638,103.6388,50);
57
- INSERT INTO `classrooms` VALUES(2,'N28-116-02','N28 - Faculty of Computing',1.5639,103.6388,50);
58
- INSERT INTO `classrooms` VALUES(3,'N28-112-02','N28 - Faculty of Computing',1.564,103.6388,50);
59
- INSERT INTO `classrooms` VALUES(4,'N28-111-01','N28 - Faculty of Computing',1.5641,103.6388,50);
60
- INSERT INTO `classrooms` VALUES(5,'N28-106-01','N28 - Faculty of Computing',1.5642,103.6388,50);
61
- INSERT INTO `classrooms` VALUES(6,'N28-107-01','N28 - Faculty of Computing',1.5643,103.6388,50);
62
- INSERT INTO `classrooms` VALUES(7,'N28-108-01','N28 - Faculty of Computing',1.5644,103.6388,50);
63
- INSERT INTO `classrooms` VALUES(8,'N28-351-02','N28 - Level 3',1.5645,103.6388,50);
64
- INSERT INTO `classrooms` VALUES(9,'N28-301-01','N28 - Level 3',1.5646,103.6388,50);
65
- INSERT INTO `classrooms` VALUES(10,'N28-301-01B','N28 - Level 3',1.5647,103.6388,50);
66
- INSERT INTO `classrooms` VALUES(11,'N28A-BT1','N28A - Tutorial Block',1.5638,103.6389,50);
67
- INSERT INTO `classrooms` VALUES(12,'N28A-BT2','N28A - Tutorial Block',1.5639,103.6389,50);
68
- INSERT INTO `classrooms` VALUES(13,'N28A-BT3','N28A - Tutorial Block',1.564,103.6389,50);
69
- INSERT INTO `classrooms` VALUES(14,'N28A-BT4','N28A - Tutorial Block',1.5641,103.6389,50);
70
- INSERT INTO `classrooms` VALUES(15,'N28A-BK1','N28A - Tutorial Block',1.5642,103.6389,50);
71
- INSERT INTO `classrooms` VALUES(16,'N28A-BK2','N28A - Tutorial Block',1.5643,103.6389,50);
72
- INSERT INTO `classrooms` VALUES(17,'N28A-BK5','N28A - Tutorial Block',1.5644,103.6389,50);
73
- INSERT INTO `classrooms` VALUES(18,'N28A-BK6','N28A - Tutorial Block',1.5645,103.6389,50);
74
- INSERT INTO `classrooms` VALUES(19,'N28-502-01','N28 - Level 5',1.5646,103.6389,50);
75
- INSERT INTO `classrooms` VALUES(20,'N28-505-01','N28 - Level 5',1.5647,103.6389,50);
76
- INSERT INTO `classrooms` VALUES(21,'N28-506-01','N28 - Level 5',1.5638,1.036390000000000101e+02,50);
77
- INSERT INTO `classrooms` VALUES(22,'N28-512-01','N28 - Level 5',1.5639,1.036390000000000101e+02,50);
78
- INSERT INTO `classrooms` VALUES(23,'N28-516-01','N28 - Level 5',1.564,1.036390000000000101e+02,50);
79
- INSERT INTO `classrooms` VALUES(24,'N28-517-02','N28 - Level 5',1.5641,1.036390000000000101e+02,50);
80
- INSERT INTO `classrooms` VALUES(25,'N28-518-02','N28 - Level 5',1.5642,1.036390000000000101e+02,50);
81
- INSERT INTO `classrooms` VALUES(26,'N28-524-02','N28 - Level 5',1.5643,1.036390000000000101e+02,50);
82
- INSERT INTO `classrooms` VALUES(27,'N28-525-01','N28 - Level 5',1.5644,1.036390000000000101e+02,50);
83
- INSERT INTO `classrooms` VALUES(28,'N28-527-02','N28 - Level 5',1.5645,1.036390000000000101e+02,50);
84
- INSERT INTO `classrooms` VALUES(29,'N28A-02-33','N28A - Level 2',1.5646,1.036390000000000101e+02,50);
85
- INSERT INTO `classrooms` VALUES(30,'N28A-02-34','N28A - Level 2',1.5647,1.036390000000000101e+02,50);
86
- INSERT INTO `classrooms` VALUES(31,'N28-223-02','N28 - Level 2',1.5638,103.6391,50);
87
- INSERT INTO `classrooms` VALUES(32,'N28-204-01','N28 - Level 2',1.5639,103.6391,50);
88
- INSERT INTO `classrooms` VALUES(33,'N28-203-01','N28 - Level 2',1.564,103.6391,50);
89
- INSERT INTO `classrooms` VALUES(34,'N28-202-01','N28 - Level 2',1.5641,103.6391,50);
90
- INSERT INTO `classrooms` VALUES(35,'N28-330-01','N28 - Level 3',1.5642,103.6391,50);
91
- INSERT INTO `classrooms` VALUES(36,'N28-352-01','N28 - Level 3',1.5643,103.6391,50);
92
- INSERT INTO `classrooms` VALUES(37,'N28-321-02','N28 - Level 3',1.5644,103.6391,50);
93
- INSERT INTO `classrooms` VALUES(38,'N28-329-01','N28 - Level 3',1.5645,103.6391,50);
94
- INSERT INTO `classrooms` VALUES(39,'N28-350-03','N28 - Level 3',1.5646,103.6391,50);
95
- INSERT INTO `classrooms` VALUES(40,'N28-422-01','N28 - Level 4',1.5647,103.6391,50);
96
- INSERT INTO `classrooms` VALUES(41,'N28-423-01','N28 - Level 4',1.5638,103.6392,50);
97
- INSERT INTO `classrooms` VALUES(42,'L50-DK3','Block L50 - Centre Point',1.5639,103.6392,50);
98
- INSERT INTO `classrooms` VALUES(43,'P19-DK4','Block P19 - FKE',1.564,103.6392,50);
99
- INSERT INTO `classrooms` VALUES(44,'P19-DK5','Block P19 - FKE',1.5641,103.6392,50);
100
- INSERT INTO `classrooms` VALUES(45,'P19-DK6','Block P19 - FKE',1.5642,103.6392,50);
101
  INSERT INTO `classrooms` VALUES(46,'PUTERI-COURT','Puteri Court Condominium (Home Demo)',3.128024,101.765479,1000);
 
102
  CREATE TABLE students (
103
  id INTEGER NOT NULL,
104
  user_id INTEGER,
 
53
  radius_meters INTEGER,
54
  PRIMARY KEY (id)
55
  );
56
+ INSERT INTO `classrooms` VALUES(1,'N28-105-01','N28 - Faculty of Computing',1.5638,103.6388,2000);
57
+ INSERT INTO `classrooms` VALUES(2,'N28-116-02','N28 - Faculty of Computing',1.5639,103.6388,2000);
58
+ INSERT INTO `classrooms` VALUES(3,'N28-112-02','N28 - Faculty of Computing',1.564,103.6388,2000);
59
+ INSERT INTO `classrooms` VALUES(4,'N28-111-01','N28 - Faculty of Computing',1.5641,103.6388,2000);
60
+ INSERT INTO `classrooms` VALUES(5,'N28-106-01','N28 - Faculty of Computing',1.5642,103.6388,2000);
61
+ INSERT INTO `classrooms` VALUES(6,'N28-107-01','N28 - Faculty of Computing',1.5643,103.6388,2000);
62
+ INSERT INTO `classrooms` VALUES(7,'N28-108-01','N28 - Faculty of Computing',1.5644,103.6388,2000);
63
+ INSERT INTO `classrooms` VALUES(8,'N28-351-02','N28 - Level 3',1.5645,103.6388,2000);
64
+ INSERT INTO `classrooms` VALUES(9,'N28-301-01','N28 - Level 3',1.5646,103.6388,2000);
65
+ INSERT INTO `classrooms` VALUES(10,'N28-301-01B','N28 - Level 3',1.5647,103.6388,2000);
66
+ INSERT INTO `classrooms` VALUES(11,'N28A-BT1','N28A - Tutorial Block',1.5638,103.6389,2000);
67
+ INSERT INTO `classrooms` VALUES(12,'N28A-BT2','N28A - Tutorial Block',1.5639,103.6389,2000);
68
+ INSERT INTO `classrooms` VALUES(13,'N28A-BT3','N28A - Tutorial Block',1.564,103.6389,2000);
69
+ INSERT INTO `classrooms` VALUES(14,'N28A-BT4','N28A - Tutorial Block',1.5641,103.6389,2000);
70
+ INSERT INTO `classrooms` VALUES(15,'N28A-BK1','N28A - Tutorial Block',1.5642,103.6389,2000);
71
+ INSERT INTO `classrooms` VALUES(16,'N28A-BK2','N28A - Tutorial Block',1.5643,103.6389,2000);
72
+ INSERT INTO `classrooms` VALUES(17,'N28A-BK5','N28A - Tutorial Block',1.5644,103.6389,2000);
73
+ INSERT INTO `classrooms` VALUES(18,'N28A-BK6','N28A - Tutorial Block',1.5645,103.6389,2000);
74
+ INSERT INTO `classrooms` VALUES(19,'N28-502-01','N28 - Level 5',1.5646,103.6389,2000);
75
+ INSERT INTO `classrooms` VALUES(20,'N28-505-01','N28 - Level 5',1.5647,103.6389,2000);
76
+ INSERT INTO `classrooms` VALUES(21,'N28-506-01','N28 - Level 5',1.5638,1.036390000000000101e+02,2000);
77
+ INSERT INTO `classrooms` VALUES(22,'N28-512-01','N28 - Level 5',1.5639,1.036390000000000101e+02,2000);
78
+ INSERT INTO `classrooms` VALUES(23,'N28-516-01','N28 - Level 5',1.564,1.036390000000000101e+02,2000);
79
+ INSERT INTO `classrooms` VALUES(24,'N28-517-02','N28 - Level 5',1.5641,1.036390000000000101e+02,2000);
80
+ INSERT INTO `classrooms` VALUES(25,'N28-518-02','N28 - Level 5',1.5642,1.036390000000000101e+02,2000);
81
+ INSERT INTO `classrooms` VALUES(26,'N28-524-02','N28 - Level 5',1.5643,1.036390000000000101e+02,2000);
82
+ INSERT INTO `classrooms` VALUES(27,'N28-525-01','N28 - Level 5',1.5644,1.036390000000000101e+02,2000);
83
+ INSERT INTO `classrooms` VALUES(28,'N28-527-02','N28 - Level 5',1.5645,1.036390000000000101e+02,2000);
84
+ INSERT INTO `classrooms` VALUES(29,'N28A-02-33','N28A - Level 2',1.5646,1.036390000000000101e+02,2000);
85
+ INSERT INTO `classrooms` VALUES(30,'N28A-02-34','N28A - Level 2',1.5647,1.036390000000000101e+02,2000);
86
+ INSERT INTO `classrooms` VALUES(31,'N28-223-02','N28 - Level 2',1.5638,103.6391,2000);
87
+ INSERT INTO `classrooms` VALUES(32,'N28-204-01','N28 - Level 2',1.5639,103.6391,2000);
88
+ INSERT INTO `classrooms` VALUES(33,'N28-203-01','N28 - Level 2',1.564,103.6391,2000);
89
+ INSERT INTO `classrooms` VALUES(34,'N28-202-01','N28 - Level 2',1.5641,103.6391,2000);
90
+ INSERT INTO `classrooms` VALUES(35,'N28-330-01','N28 - Level 3',1.5642,103.6391,2000);
91
+ INSERT INTO `classrooms` VALUES(36,'N28-352-01','N28 - Level 3',1.5643,103.6391,2000);
92
+ INSERT INTO `classrooms` VALUES(37,'N28-321-02','N28 - Level 3',1.5644,103.6391,2000);
93
+ INSERT INTO `classrooms` VALUES(38,'N28-329-01','N28 - Level 3',1.5645,103.6391,2000);
94
+ INSERT INTO `classrooms` VALUES(39,'N28-350-03','N28 - Level 3',1.5646,103.6391,2000);
95
+ INSERT INTO `classrooms` VALUES(40,'N28-422-01','N28 - Level 4',1.5647,103.6391,2000);
96
+ INSERT INTO `classrooms` VALUES(41,'N28-423-01','N28 - Level 4',1.5638,103.6392,2000);
97
+ INSERT INTO `classrooms` VALUES(42,'L50-DK3','Block L50 - Centre Point',1.5639,103.6392,2000);
98
+ INSERT INTO `classrooms` VALUES(43,'P19-DK4','Block P19 - FKE',1.564,103.6392,2000);
99
+ INSERT INTO `classrooms` VALUES(44,'P19-DK5','Block P19 - FKE',1.5641,103.6392,2000);
100
+ INSERT INTO `classrooms` VALUES(45,'P19-DK6','Block P19 - FKE',1.5642,103.6392,2000);
101
  INSERT INTO `classrooms` VALUES(46,'PUTERI-COURT','Puteri Court Condominium (Home Demo)',3.128024,101.765479,1000);
102
+ INSERT INTO `classrooms` VALUES(47,'Pejabat Pos UTM (KDSE)','W1, Kolej Datin Seri Endon, 81110 Johor Bahru',1.560662,103.638421,2000);
103
  CREATE TABLE students (
104
  id INTEGER NOT NULL,
105
  user_id INTEGER,
config.py CHANGED
@@ -22,7 +22,7 @@ class Config:
22
  FACE_DETECTION_MODEL = 'hog' # 'hog' is faster, 'cnn' is more accurate
23
 
24
  # Geolocation settings
25
- GEOLOCATION_RADIUS_METERS = 50 # Classroom detection radius
26
 
27
  # Attendance Code Settings
28
  CODE_LENGTH = 6 # Length of attendance code
 
22
  FACE_DETECTION_MODEL = 'hog' # 'hog' is faster, 'cnn' is more accurate
23
 
24
  # Geolocation settings
25
+ GEOLOCATION_RADIUS_METERS = 2000 # Increased to 2000m as requested for testing
26
 
27
  # Attendance Code Settings
28
  CODE_LENGTH = 6 # Length of attendance code
face_recognition_module.py CHANGED
@@ -1,120 +1,160 @@
1
- """
2
- Face Recognition Module for Attendr
3
- Automatically switches between production (face_recognition) and mock mode.
4
- """
5
-
6
- import numpy as np
7
- import cv2
8
- from config import Config
9
- import logging
10
- import hashlib
11
-
12
- # Set up logging
13
- logging.basicConfig(level=logging.INFO)
14
- logger = logging.getLogger(__name__)
15
-
16
- # Try to import face_recognition
17
- try:
18
- import face_recognition
19
- HAS_FACE_RECOGNITION = True
20
- logger.info("PRODUCTION face_recognition library loaded successfully")
21
- except ImportError:
22
- HAS_FACE_RECOGNITION = False
23
- logger.warning("Using MOCK face recognition module - for testing only!")
24
- logger.warning("Install face_recognition library for production use")
25
-
26
-
27
- class FaceRecognitionModule:
28
- """Handles all face recognition operations"""
29
-
30
- def __init__(self, tolerance=None, model='hog'):
31
- self.tolerance = tolerance or Config.FACE_RECOGNITION_TOLERANCE
32
- self.model = model or Config.FACE_DETECTION_MODEL
33
- if HAS_FACE_RECOGNITION:
34
- logger.info(f"PRODUCTION Face Detection initialized (model={self.model})")
35
- else:
36
- logger.info("MOCK Face Recognition initialized")
37
-
38
- def detect_faces(self, image_array):
39
- """Detect faces in an image"""
40
- try:
41
- if HAS_FACE_RECOGNITION:
42
- return face_recognition.face_locations(image_array, model=self.model)
43
- else:
44
- # Mock face detection using OpenCV Haar Cascades
45
- gray = cv2.cvtColor(image_array, cv2.COLOR_RGB2GRAY)
46
- face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
47
- faces = face_cascade.detectMultiScale(gray, 1.3, 5)
48
- return [(y, x+w, y+h, x) for (x, y, w, h) in faces]
49
- except Exception as e:
50
- logger.error(f"Face detection error: {str(e)}")
51
- return []
52
-
53
- def generate_face_encoding(self, image_array):
54
- """Generate face encoding from an image"""
55
- try:
56
- face_locations = self.detect_faces(image_array)
57
-
58
- if len(face_locations) == 0:
59
- return False, "No face detected in the image. Please ensure your face is clearly visible."
60
-
61
- if len(face_locations) > 1:
62
- return False, "Multiple faces detected. Please ensure only one person is in the frame."
63
-
64
- if HAS_FACE_RECOGNITION:
65
- encodings = face_recognition.face_encodings(image_array, face_locations)
66
- if len(encodings) == 0:
67
- return False, "Failed to generate face encoding. Try better lighting."
68
- return True, encodings[0]
69
- else:
70
- # Mock encoding generation
71
- top, right, bottom, left = face_locations[0]
72
- face_region = image_array[top:bottom, left:right]
73
- face_resized = cv2.resize(face_region, (100, 100))
74
- face_bytes = face_resized.tobytes()
75
- hash_digest = hashlib.sha256(face_bytes).digest()
76
- encoding = np.array([float(b) for b in hash_digest[:128]] + [0.0] * (128 - len(hash_digest[:128])))
77
- return True, encoding
78
-
79
- except Exception as e:
80
- logger.error(f"Face encoding error: {str(e)}")
81
- return False, f"Face encoding failed: {str(e)}"
82
-
83
- def verify_face(self, captured_encoding, stored_encoding):
84
- """Verify if captured face matches stored encoding"""
85
- try:
86
- if captured_encoding is None or stored_encoding is None:
87
- return False, 0.0
88
-
89
- if not isinstance(captured_encoding, np.ndarray):
90
- captured_encoding = np.array(captured_encoding)
91
- if not isinstance(stored_encoding, np.ndarray):
92
- stored_encoding = np.array(stored_encoding)
93
-
94
- if HAS_FACE_RECOGNITION:
95
- face_distance = face_recognition.face_distance([stored_encoding], captured_encoding)[0]
96
- confidence = (1 - face_distance) * 100
97
- is_match = face_distance <= self.tolerance
98
- else:
99
- # Mock similarity check
100
- correlation = np.corrcoef(captured_encoding, stored_encoding)[0, 1]
101
- distance = 1 - abs(correlation)
102
- confidence = (1 - distance) * 100
103
- is_match = distance <= self.tolerance
104
-
105
- return is_match, confidence
106
-
107
- except Exception as e:
108
- logger.error(f"Face verification error: {str(e)}")
109
- return False, 0.0
110
-
111
- def register_face(self, image_array):
112
- """Complete face registration process"""
113
- success, result = self.generate_face_encoding(image_array)
114
- if success:
115
- face_locations = self.detect_faces(image_array)
116
- return True, result, face_locations[0]
117
- return False, result, None
118
-
119
- # Singleton instance
120
- face_recognition_module = FaceRecognitionModule()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mock Face Recognition Module for Testing (Windows-friendly)
3
+ This is a simplified version that doesn't require dlib/face_recognition
4
+ For production, install the full face_recognition library
5
+ """
6
+
7
+ import numpy as np
8
+ import cv2
9
+ from config import Config
10
+ import logging
11
+ import hashlib
12
+
13
+ # Set up logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ logger.warning("Using MOCK face recognition module - for testing only!")
18
+ logger.warning("Install face_recognition library for production use")
19
+
20
+
21
+ class FaceRecognitionModule:
22
+ """Mock face recognition for testing without dlib dependencies"""
23
+
24
+ def __init__(self, tolerance=None, model='hog'):
25
+ self.tolerance = tolerance or Config.FACE_RECOGNITION_TOLERANCE
26
+ self.model = model
27
+ logger.info(f"Mock Face Recognition Module initialized")
28
+
29
+ def detect_faces(self, image_array):
30
+ """Mock face detection using OpenCV Haar Cascades"""
31
+ try:
32
+ # Convert to grayscale
33
+ gray = cv2.cvtColor(image_array, cv2.COLOR_RGB2GRAY)
34
+
35
+ # Load Haar cascade for face detection
36
+ face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
37
+
38
+ # Detect faces
39
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
40
+
41
+ # Convert to format similar to face_recognition library
42
+ face_locations = [(y, x+w, y+h, x) for (x, y, w, h) in faces]
43
+
44
+ logger.info(f"Detected {len(face_locations)} face(s)")
45
+ return face_locations
46
+ except Exception as e:
47
+ logger.error(f"Face detection error: {str(e)}")
48
+ return []
49
+
50
+ def generate_face_encoding(self, image_array):
51
+ """
52
+ Mock encoding generation using image hash
53
+ In production, this would use deep learning-based face encodings
54
+ """
55
+ try:
56
+ face_locations = self.detect_faces(image_array)
57
+
58
+ if len(face_locations) == 0:
59
+ return False, "No face detected in the image. Please ensure your face is clearly visible."
60
+
61
+ if len(face_locations) > 1:
62
+ return False, "Multiple faces detected. Please ensure only one person is in the frame."
63
+
64
+ # Create a simple "encoding" using image hash (for testing only)
65
+ # In production, this would be a 128-dimensional face encoding
66
+ top, right, bottom, left = face_locations[0]
67
+ face_region = image_array[top:bottom, left:right]
68
+
69
+ # Resize to standard size
70
+ face_resized = cv2.resize(face_region, (100, 100))
71
+
72
+ # Create hash-based encoding (mock)
73
+ face_bytes = face_resized.tobytes()
74
+ hash_obj = hashlib.sha256(face_bytes)
75
+ hash_digest = hash_obj.digest() # Get bytes directly
76
+
77
+ # Convert to numpy array (128 dimensions to match real encodings)
78
+ # Use hash bytes to create 128-dimensional vector
79
+ encoding = np.array([float(b) for b in hash_digest[:128]] + [0.0] * (128 - len(hash_digest[:128])))
80
+
81
+ logger.info("Mock face encoding generated")
82
+ return True, encoding
83
+
84
+ except Exception as e:
85
+ logger.error(f"Face encoding error: {str(e)}")
86
+ return False, f"Face encoding failed: {str(e)}"
87
+
88
+ def verify_face(self, captured_encoding, stored_encoding):
89
+ """
90
+ Mock face verification using encoding similarity
91
+ In production, this would use Euclidean distance between face encodings
92
+ """
93
+ try:
94
+ if captured_encoding is None or stored_encoding is None:
95
+ return False, 0.0
96
+
97
+ # Convert to numpy arrays
98
+ if not isinstance(captured_encoding, np.ndarray):
99
+ captured_encoding = np.array(captured_encoding)
100
+ if not isinstance(stored_encoding, np.ndarray):
101
+ stored_encoding = np.array(stored_encoding)
102
+
103
+ # Calculate similarity (mock - using correlation)
104
+ # In production, this would be face_recognition.face_distance()
105
+ correlation = np.corrcoef(captured_encoding, stored_encoding)[0, 1]
106
+
107
+ # Convert to distance (0 = identical, 1 = completely different)
108
+ distance = 1 - abs(correlation)
109
+
110
+ # Calculate confidence
111
+ confidence = (1 - distance) * 100
112
+
113
+ # Check if match
114
+ is_match = distance <= self.tolerance
115
+
116
+ logger.info(f"Mock verification: match={is_match}, confidence={confidence:.2f}%")
117
+
118
+ return is_match, confidence
119
+
120
+ except Exception as e:
121
+ logger.error(f"Face verification error: {str(e)}")
122
+ return False, 0.0
123
+
124
+ def register_face(self, image_array):
125
+ """Complete face registration"""
126
+ try:
127
+ face_locations = self.detect_faces(image_array)
128
+
129
+ if len(face_locations) == 0:
130
+ return False, "No face detected. Please ensure your face is clearly visible and well-lit.", None
131
+
132
+ if len(face_locations) > 1:
133
+ return False, "Multiple faces detected. Please ensure only one person is in the frame.", None
134
+
135
+ success, result = self.generate_face_encoding(image_array)
136
+
137
+ if success:
138
+ return True, result, face_locations[0]
139
+ else:
140
+ return False, result, None
141
+
142
+ except Exception as e:
143
+ logger.error(f"Face registration error: {str(e)}")
144
+ return False, f"Registration failed: {str(e)}", None
145
+
146
+ def compare_faces_batch(self, known_encodings, face_encoding_to_check):
147
+ """Compare against multiple encodings"""
148
+ try:
149
+ matches = []
150
+ for known_encoding in known_encodings:
151
+ is_match, _ = self.verify_face(face_encoding_to_check, known_encoding)
152
+ matches.append(is_match)
153
+ return matches
154
+ except Exception as e:
155
+ logger.error(f"Batch comparison error: {str(e)}")
156
+ return []
157
+
158
+
159
+ # Singleton instance
160
+ face_recognition_module = FaceRecognitionModule()
lecturer.html CHANGED
@@ -206,6 +206,10 @@
206
  <div id="sessionsList" class="mt-2">
207
  <p class="text-muted">No active sessions</p>
208
  </div>
 
 
 
 
209
  </div>
210
  </div>
211
 
 
206
  <div id="sessionsList" class="mt-2">
207
  <p class="text-muted">No active sessions</p>
208
  </div>
209
+ <div class="mt-3" style="border-top: 1px solid var(--color-border); padding-top: 1rem;">
210
+ <a href="/lecturer/history" class="btn btn-outline" style="width: 100%;">📊 View All Session
211
+ History</a>
212
+ </div>
213
  </div>
214
  </div>
215
 
requirements.txt CHANGED
@@ -1,5 +1,8 @@
1
- Flask==3.0.0
2
- Flask-SQLAlchemy==3.1.1
3
- Pillow==10.1.0
4
- python-dotenv==1.0.0
5
- werkzeug==3.0.1
 
 
 
 
1
+ Flask==3.0.0
2
+ Flask-SQLAlchemy==3.1.1
3
+ opencv-python==4.8.1.78
4
+ face-recognition==1.3.0
5
+ numpy==1.24.3
6
+ Pillow==10.1.0
7
+ python-dotenv==1.0.0
8
+ werkzeug==3.0.1