Upload 33 files
Browse files- .dockerignore +30 -0
- Dockerfile +54 -27
- README.md +334 -14
- _navbar.html +41 -37
- add_custom_venue.py +37 -0
- app.py +813 -805
- attendr.db +0 -0
- attendr.sql +46 -45
- config.py +1 -1
- face_recognition_module.py +160 -120
- lecturer.html +4 -0
- 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
|
| 2 |
-
FROM
|
| 3 |
-
|
| 4 |
-
#
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
#
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
#
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
#
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
|
| 15 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 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 |
+

|
| 6 |
+

|
| 7 |
+

|
| 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 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
</nav>
|
| 25 |
|
| 26 |
<!-- Logout Script (add to pages that need it) -->
|
| 27 |
<script>
|
| 28 |
-
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
}
|
| 48 |
}
|
| 49 |
-
|
|
|
|
|
|
| 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 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
return
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
@
|
| 48 |
-
def
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
@
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
@
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
@
|
| 78 |
-
def
|
| 79 |
-
"""
|
| 80 |
-
return render_template('
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
@app.route('/
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
@app.route('/
|
| 92 |
-
def
|
| 93 |
-
"""
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
)
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
if
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
if not
|
| 333 |
-
return format_error_response(
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
)
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
if not
|
| 460 |
-
return format_error_response(
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
)
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
logger.
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
'
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
'
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
'
|
| 751 |
-
'
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 57 |
-
INSERT INTO `classrooms` VALUES(2,'N28-116-02','N28 - Faculty of Computing',1.5639,103.6388,
|
| 58 |
-
INSERT INTO `classrooms` VALUES(3,'N28-112-02','N28 - Faculty of Computing',1.564,103.6388,
|
| 59 |
-
INSERT INTO `classrooms` VALUES(4,'N28-111-01','N28 - Faculty of Computing',1.5641,103.6388,
|
| 60 |
-
INSERT INTO `classrooms` VALUES(5,'N28-106-01','N28 - Faculty of Computing',1.5642,103.6388,
|
| 61 |
-
INSERT INTO `classrooms` VALUES(6,'N28-107-01','N28 - Faculty of Computing',1.5643,103.6388,
|
| 62 |
-
INSERT INTO `classrooms` VALUES(7,'N28-108-01','N28 - Faculty of Computing',1.5644,103.6388,
|
| 63 |
-
INSERT INTO `classrooms` VALUES(8,'N28-351-02','N28 - Level 3',1.5645,103.6388,
|
| 64 |
-
INSERT INTO `classrooms` VALUES(9,'N28-301-01','N28 - Level 3',1.5646,103.6388,
|
| 65 |
-
INSERT INTO `classrooms` VALUES(10,'N28-301-01B','N28 - Level 3',1.5647,103.6388,
|
| 66 |
-
INSERT INTO `classrooms` VALUES(11,'N28A-BT1','N28A - Tutorial Block',1.5638,103.6389,
|
| 67 |
-
INSERT INTO `classrooms` VALUES(12,'N28A-BT2','N28A - Tutorial Block',1.5639,103.6389,
|
| 68 |
-
INSERT INTO `classrooms` VALUES(13,'N28A-BT3','N28A - Tutorial Block',1.564,103.6389,
|
| 69 |
-
INSERT INTO `classrooms` VALUES(14,'N28A-BT4','N28A - Tutorial Block',1.5641,103.6389,
|
| 70 |
-
INSERT INTO `classrooms` VALUES(15,'N28A-BK1','N28A - Tutorial Block',1.5642,103.6389,
|
| 71 |
-
INSERT INTO `classrooms` VALUES(16,'N28A-BK2','N28A - Tutorial Block',1.5643,103.6389,
|
| 72 |
-
INSERT INTO `classrooms` VALUES(17,'N28A-BK5','N28A - Tutorial Block',1.5644,103.6389,
|
| 73 |
-
INSERT INTO `classrooms` VALUES(18,'N28A-BK6','N28A - Tutorial Block',1.5645,103.6389,
|
| 74 |
-
INSERT INTO `classrooms` VALUES(19,'N28-502-01','N28 - Level 5',1.5646,103.6389,
|
| 75 |
-
INSERT INTO `classrooms` VALUES(20,'N28-505-01','N28 - Level 5',1.5647,103.6389,
|
| 76 |
-
INSERT INTO `classrooms` VALUES(21,'N28-506-01','N28 - Level 5',1.5638,1.036390000000000101e+02,
|
| 77 |
-
INSERT INTO `classrooms` VALUES(22,'N28-512-01','N28 - Level 5',1.5639,1.036390000000000101e+02,
|
| 78 |
-
INSERT INTO `classrooms` VALUES(23,'N28-516-01','N28 - Level 5',1.564,1.036390000000000101e+02,
|
| 79 |
-
INSERT INTO `classrooms` VALUES(24,'N28-517-02','N28 - Level 5',1.5641,1.036390000000000101e+02,
|
| 80 |
-
INSERT INTO `classrooms` VALUES(25,'N28-518-02','N28 - Level 5',1.5642,1.036390000000000101e+02,
|
| 81 |
-
INSERT INTO `classrooms` VALUES(26,'N28-524-02','N28 - Level 5',1.5643,1.036390000000000101e+02,
|
| 82 |
-
INSERT INTO `classrooms` VALUES(27,'N28-525-01','N28 - Level 5',1.5644,1.036390000000000101e+02,
|
| 83 |
-
INSERT INTO `classrooms` VALUES(28,'N28-527-02','N28 - Level 5',1.5645,1.036390000000000101e+02,
|
| 84 |
-
INSERT INTO `classrooms` VALUES(29,'N28A-02-33','N28A - Level 2',1.5646,1.036390000000000101e+02,
|
| 85 |
-
INSERT INTO `classrooms` VALUES(30,'N28A-02-34','N28A - Level 2',1.5647,1.036390000000000101e+02,
|
| 86 |
-
INSERT INTO `classrooms` VALUES(31,'N28-223-02','N28 - Level 2',1.5638,103.6391,
|
| 87 |
-
INSERT INTO `classrooms` VALUES(32,'N28-204-01','N28 - Level 2',1.5639,103.6391,
|
| 88 |
-
INSERT INTO `classrooms` VALUES(33,'N28-203-01','N28 - Level 2',1.564,103.6391,
|
| 89 |
-
INSERT INTO `classrooms` VALUES(34,'N28-202-01','N28 - Level 2',1.5641,103.6391,
|
| 90 |
-
INSERT INTO `classrooms` VALUES(35,'N28-330-01','N28 - Level 3',1.5642,103.6391,
|
| 91 |
-
INSERT INTO `classrooms` VALUES(36,'N28-352-01','N28 - Level 3',1.5643,103.6391,
|
| 92 |
-
INSERT INTO `classrooms` VALUES(37,'N28-321-02','N28 - Level 3',1.5644,103.6391,
|
| 93 |
-
INSERT INTO `classrooms` VALUES(38,'N28-329-01','N28 - Level 3',1.5645,103.6391,
|
| 94 |
-
INSERT INTO `classrooms` VALUES(39,'N28-350-03','N28 - Level 3',1.5646,103.6391,
|
| 95 |
-
INSERT INTO `classrooms` VALUES(40,'N28-422-01','N28 - Level 4',1.5647,103.6391,
|
| 96 |
-
INSERT INTO `classrooms` VALUES(41,'N28-423-01','N28 - Level 4',1.5638,103.6392,
|
| 97 |
-
INSERT INTO `classrooms` VALUES(42,'L50-DK3','Block L50 - Centre Point',1.5639,103.6392,
|
| 98 |
-
INSERT INTO `classrooms` VALUES(43,'P19-DK4','Block P19 - FKE',1.564,103.6392,
|
| 99 |
-
INSERT INTO `classrooms` VALUES(44,'P19-DK5','Block P19 - FKE',1.5641,103.6392,
|
| 100 |
-
INSERT INTO `classrooms` VALUES(45,'P19-DK6','Block P19 - FKE',1.5642,103.6392,
|
| 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 =
|
| 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
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
import
|
| 8 |
-
|
| 9 |
-
import
|
| 10 |
-
import
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
logging
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
if
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|