File size: 7,778 Bytes
ecd52b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e57781a
 
 
ecd52b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e57781a
ecd52b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
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
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
56
57
58
59
60
61
62
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
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
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import json
from werkzeug.security import generate_password_hash, check_password_hash

db = SQLAlchemy()

class User(db.Model):
    """User model for authentication - supports both students and lecturers"""
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    matric_no = db.Column(db.String(50), unique=True, nullable=False)  # A/B for students, L for lecturers
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(100), unique=True)
    password_hash = db.Column(db.String(255), nullable=False)
    role = db.Column(db.String(20), nullable=False)  # 'student', 'lecturer', or 'admin'
    is_active = db.Column(db.Boolean, default=False)
    status = db.Column(db.String(20), default='pending')  # 'pending', 'approved', 'rejected'
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def set_password(self, password):
        """Hash and set password"""
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        """Verify password"""
        return check_password_hash(self.password_hash, password)
    
    @staticmethod
    def validate_matric_no(matric_no, role):
        """Validate matric number format based on role"""
        if not matric_no:
            return False, "Matric number is required"
        
        matric_no = matric_no.upper().strip()
        
        if role == 'student':
            # Students must start with A or B
            if not (matric_no.startswith('A') or matric_no.startswith('B')):
                return False, "Student matric number must start with 'A' or 'B'"
        elif role == 'lecturer':
            # Lecturers must start with L
            if not matric_no.startswith('L'):
                return False, "Lecturer matric number must start with 'L'"
        else:
            return False, "Invalid role"
        
        return True, None
    
    def to_dict(self):
        return {
            'id': self.id,
            'matric_no': self.matric_no,
            'name': self.name,
            'email': self.email,
            'role': self.role,
            'is_active': self.is_active,
            'status': self.status,
            'created_at': self.created_at.isoformat() + 'Z' if self.created_at else None
        }


class Student(db.Model):
    """Student model - stores student information and face encodings"""
    __tablename__ = 'students'
    
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True)  # Link to User
    student_id = db.Column(db.String(50), unique=True, nullable=False)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(100), unique=True)
    face_encoding = db.Column(db.Text)  # Stored as JSON string
    registered_at = db.Column(db.DateTime, default=datetime.utcnow)
    is_active = db.Column(db.Boolean, default=True)
    
    # Relationships
    attendance_records = db.relationship('AttendanceRecord', backref='student', lazy=True)
    user = db.relationship('User', backref='student_profile', uselist=False)
    
    def get_face_encoding(self):
        """Deserialize face encoding from JSON"""
        if self.face_encoding:
            return json.loads(self.face_encoding)
        return None
    
    def set_face_encoding(self, encoding):
        """Serialize face encoding to JSON"""
        if encoding is not None:
            self.face_encoding = json.dumps(encoding.tolist())
    
    def to_dict(self):
        return {
            'id': self.id,
            'student_id': self.student_id,
            'name': self.name,
            'email': self.email,
            'registered_at': self.registered_at.isoformat() + 'Z' if self.registered_at else None,
            'is_active': self.is_active
        }


class Classroom(db.Model):
    """Classroom model - stores classroom locations with GPS coordinates"""
    __tablename__ = 'classrooms'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    building = db.Column(db.String(100))
    latitude = db.Column(db.Float, nullable=False)
    longitude = db.Column(db.Float, nullable=False)
    radius_meters = db.Column(db.Integer, default=50)  # Geofencing radius
    
    # Relationships
    sessions = db.relationship('AttendanceSession', backref='classroom', lazy=True)
    
    def to_dict(self):
        return {
            'id': self.id,
            'name': self.name,
            'building': self.building,
            'latitude': self.latitude,
            'longitude': self.longitude,
            'radius_meters': self.radius_meters
        }


class AttendanceSession(db.Model):
    """Attendance session - represents a class session with auto-refresh codes"""
    __tablename__ = 'attendance_sessions'
    
    id = db.Column(db.Integer, primary_key=True)
    course_name = db.Column(db.String(100), nullable=False)
    classroom_id = db.Column(db.Integer, db.ForeignKey('classrooms.id'), nullable=False)
    lecturer_name = db.Column(db.String(100), nullable=False)
    
    # Session timing
    start_time = db.Column(db.DateTime, default=datetime.utcnow)
    end_time = db.Column(db.DateTime)
    is_active = db.Column(db.Boolean, default=True)
    
    # Auto-refresh attendance code
    current_code = db.Column(db.String(10))
    code_generated_at = db.Column(db.DateTime)
    
    # Relationships
    attendance_records = db.relationship('AttendanceRecord', backref='session', lazy=True)
    
    def to_dict(self):
        return {
            'id': self.id,
            'course_name': self.course_name,
            'classroom_id': self.classroom_id,
            'classroom': self.classroom.to_dict() if self.classroom else None,
            'lecturer_name': self.lecturer_name,
            'start_time': self.start_time.isoformat() + 'Z' if self.start_time else None,
            'end_time': self.end_time.isoformat() + 'Z' if self.end_time else None,
            'is_active': self.is_active,
            'current_code': self.current_code,
            'code_generated_at': self.code_generated_at.isoformat() + 'Z' if self.code_generated_at else None
        }


class AttendanceRecord(db.Model):
    """Attendance record - individual student attendance for a session"""
    __tablename__ = 'attendance_records'
    
    id = db.Column(db.Integer, primary_key=True)
    session_id = db.Column(db.Integer, db.ForeignKey('attendance_sessions.id'), nullable=False)
    student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False)
    
    # Verification details
    marked_at = db.Column(db.DateTime, default=datetime.utcnow)
    face_verified = db.Column(db.Boolean, default=False)
    location_verified = db.Column(db.Boolean, default=False)
    code_verified = db.Column(db.Boolean, default=False)
    
    # Location data
    student_latitude = db.Column(db.Float)
    student_longitude = db.Column(db.Float)
    distance_from_classroom = db.Column(db.Float)  # In meters
    
    # Status
    status = db.Column(db.String(20), default='present')  # present, late, absent
    
    def to_dict(self):
        return {
            'id': self.id,
            'session_id': self.session_id,
            'student': self.student.to_dict() if self.student else None,
            'marked_at': self.marked_at.isoformat() + 'Z' if self.marked_at else None,
            'face_verified': self.face_verified,
            'location_verified': self.location_verified,
            'code_verified': self.code_verified,
            'distance_from_classroom': self.distance_from_classroom,
            'status': self.status
        }