| 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) |
| 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) |
| is_active = db.Column(db.Boolean, default=False) |
| status = db.Column(db.String(20), default='pending') |
| 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': |
| |
| 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': |
| |
| 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) |
| 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) |
| registered_at = db.Column(db.DateTime, default=datetime.utcnow) |
| is_active = db.Column(db.Boolean, default=True) |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| start_time = db.Column(db.DateTime, default=datetime.utcnow) |
| end_time = db.Column(db.DateTime) |
| is_active = db.Column(db.Boolean, default=True) |
| |
| |
| current_code = db.Column(db.String(10)) |
| code_generated_at = db.Column(db.DateTime) |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| student_latitude = db.Column(db.Float) |
| student_longitude = db.Column(db.Float) |
| distance_from_classroom = db.Column(db.Float) |
| |
| |
| status = db.Column(db.String(20), default='present') |
| |
| 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 |
| } |
|
|