From 4248917d17b23a08a08fb7985dd376032631ca35 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Tue, 19 Aug 2025 13:15:07 -0400 Subject: [PATCH] Restructurign code --- app.py | 231 +------------------------------------------ models/__init__.py | 21 ++++ models/attendance.py | 58 +++++++++++ models/base.py | 7 ++ models/project.py | 40 ++++++++ models/qrcode.py | 81 +++++++++++++++ models/user.py | 68 +++++++++++++ 7 files changed, 279 insertions(+), 227 deletions(-) create mode 100644 models/__init__.py create mode 100644 models/attendance.py create mode 100644 models/base.py create mode 100644 models/project.py create mode 100644 models/qrcode.py create mode 100644 models/user.py diff --git a/app.py b/app.py index 5e5b716..089cfa5 100644 --- a/app.py +++ b/app.py @@ -32,236 +32,13 @@ VALID_ROLES = ['admin', 'staff', 'payroll', 'project_manager'] # Roles that have staff-level permissions (non-admin roles) STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager'] +# Import and initialize models +from models import set_db +User, QRCode, QRCodeStyle, Project, AttendanceData = set_db(db) + # Initialize the logging system logger_handler = AppLogger(app, db) -# User Model -class User(db.Model): - """ - User model to manage system users with role-based access control - """ - __tablename__ = 'users' - - id = db.Column(db.Integer, primary_key=True) - full_name = db.Column(db.String(100), nullable=False) - email = db.Column(db.String(120), unique=True, nullable=False) - username = db.Column(db.String(80), unique=True, nullable=False) - password_hash = db.Column(db.String(255), nullable=False) - role = db.Column(db.String(20), nullable=False, default='staff') # admin or staff - created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) - created_date = db.Column(db.DateTime, default=datetime.utcnow) - active_status = db.Column(db.Boolean, default=True) - last_login_date = db.Column(db.DateTime, nullable=True) - - # Relationships - created_users = db.relationship('User', backref=db.backref('creator', remote_side=[id])) - created_qr_codes = db.relationship('QRCode', backref='creator', lazy='dynamic') - - def set_password(self, password): - """Hash and set user password""" - self.password_hash = generate_password_hash(password) - - def check_password(self, password): - """Verify user password""" - return check_password_hash(self.password_hash, password) - - def is_admin(self): - """Check if user has admin privileges""" - return self.role == 'admin' - - def has_staff_permissions(self): - """Check if user has staff-level permissions (includes new roles)""" - return self.role in STAFF_LEVEL_ROLES - - def has_export_permissions(user_role): - """Check if user role has export permissions""" - return user_role in ['admin', 'payroll'] - - def get_role_display_name(self): - """Get user-friendly role name""" - role_names = { - 'admin': 'Administrator', - 'staff': 'Staff User', - 'payroll': 'Payroll Specialist', - 'project_manager': 'Project Manager' - } - return role_names.get(self.role, self.role.title()) - -# QR Code Model -class QRCode(db.Model): - """ - Enhanced QR Code model to manage QR code records and metadata with address coordinates - """ - __tablename__ = 'qr_codes' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - location = db.Column(db.String(100), nullable=False) - location_address = db.Column(db.Text, nullable=False) - location_event = db.Column(db.String(200), nullable=False) - qr_code_image = db.Column(db.Text, nullable=False) # Base64 encoded image - created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) - created_date = db.Column(db.DateTime, default=datetime.utcnow) - active_status = db.Column(db.Boolean, default=True) - qr_url = db.Column(db.String(255), unique=True, nullable=True) - # Address Coordinates Fields - address_latitude = db.Column(db.Float, nullable=True) - address_longitude = db.Column(db.Float, nullable=True) - coordinate_accuracy = db.Column(db.String(50), nullable=True, default='geocoded') - coordinates_updated_date = db.Column(db.DateTime, nullable=True) - project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=True) - # The project_id field: - project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=True) - # QR Code Customization fields - fill_color = db.Column(db.String(7), default="#000000") # Hex color - back_color = db.Column(db.String(7), default="#FFFFFF") # Background color - box_size = db.Column(db.Integer, default=10) - border = db.Column(db.Integer, default=4) - error_correction = db.Column(db.String(1), default='L') - style_id = db.Column(db.Integer, db.ForeignKey('qr_code_styles.id'), nullable=True) - - # Relationship to style - style = db.relationship('QRCodeStyle', backref='qr_codes') - - @property - def has_coordinates(self): - """Check if this QR code has address coordinates""" - return self.address_latitude is not None and self.address_longitude is not None - - @property - def coordinates_display(self): - """Get formatted coordinates for display""" - if self.has_coordinates: - return f"{self.address_latitude:.10f}, {self.address_longitude:.10f}" - return "Coordinates not available" - - def update_coordinates(self, latitude, longitude, accuracy='geocoded'): - """Update the address coordinates for this QR code""" - self.address_latitude = latitude - self.address_longitude = longitude - self.coordinate_accuracy = accuracy - self.coordinates_updated_date = datetime.utcnow() - -class QRCodeStyle(db.Model): - """QR Code customization styles""" - __tablename__ = 'qr_code_styles' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) # Style name - fill_color = db.Column(db.String(7), default="#000000") # Hex color for QR modules - back_color = db.Column(db.String(7), default="#FFFFFF") # Hex color for background - box_size = db.Column(db.Integer, default=10) # Size of each QR module - border = db.Column(db.Integer, default=4) # Border size - error_correction = db.Column(db.String(1), default='L') # L, M, Q, H - is_default = db.Column(db.Boolean, default=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - created_by = db.Column(db.Integer, db.ForeignKey('users.id')) - - def __repr__(self): - return f'' - -class Project(db.Model): - """ - Project model to organize QR codes by projects - """ - __tablename__ = 'projects' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - description = db.Column(db.Text, nullable=True) - created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) - created_date = db.Column(db.DateTime, default=datetime.utcnow) - active_status = db.Column(db.Boolean, default=True) - - # Relationships - qr_codes = db.relationship('QRCode', backref='project', lazy='dynamic') - creator = db.relationship('User', backref='created_projects') - - def __repr__(self): - return f'' - - @property - def qr_count(self): - """Get count of QR codes in this project""" - return self.qr_codes.filter_by(active_status=True).count() - - @property - def total_qr_count(self): - """Get total count of QR codes (including inactive) in this project""" - return self.qr_codes.count() - -# Attendance Data Model -class AttendanceData(db.Model): - """Enhanced attendance tracking model with location support""" - __tablename__ = 'attendance_data' - - # Existing fields - id = db.Column(db.Integer, primary_key=True) - qr_code_id = db.Column(db.Integer, db.ForeignKey('qr_codes.id', ondelete='CASCADE'), nullable=False) - employee_id = db.Column(db.String(50), nullable=False) - check_in_date = db.Column(db.Date, nullable=False, default=datetime.today) - check_in_time = db.Column(db.Time, nullable=False, default=datetime.now().time) - device_info = db.Column(db.String(200)) - user_agent = db.Column(db.Text) - ip_address = db.Column(db.String(45)) - location_name = db.Column(db.String(100), nullable=False) - status = db.Column(db.String(20), default='present') - created_timestamp = db.Column(db.DateTime, default=datetime.utcnow) - updated_timestamp = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - latitude = db.Column(db.Float, nullable=True) - longitude = db.Column(db.Float, nullable=True) - accuracy = db.Column(db.Float, nullable=True) - location_accuracy = db.Column(db.Float, nullable=True) - altitude = db.Column(db.Float, nullable=True) - location_source = db.Column(db.String(50), default='manual') - address = db.Column(db.String(500), nullable=True) - - # Relationships - qr_code = db.relationship('QRCode', backref=db.backref('attendance_records', lazy='dynamic')) - - def __repr__(self): - return f'' - - @property - def has_location_data(self): - """Check if this record has GPS coordinates""" - return self.latitude is not None and self.longitude is not None - - @property - def location_accuracy_level(self): - """Get human-readable accuracy level""" - if not self.accuracy: - return 'unknown' - elif self.accuracy <= 50: - return 'high' - elif self.accuracy <= 100: - return 'medium' - else: - return 'low' - - @property - def coordinates_display(self): - """Get formatted coordinates for display""" - if self.has_location_data: - return f"{self.latitude:.10f}, {self.longitude:.10f}" - return "No GPS data" - - def to_dict(self): - """Convert to dictionary for JSON responses""" - return { - 'id': self.id, - 'employee_id': self.employee_id, - 'check_in_date': self.check_in_date.isoformat(), - 'check_in_time': self.check_in_time.isoformat(), - 'location_name': self.location_name, - 'status': self.status, - 'has_location': self.has_location_data, - 'coordinates': self.coordinates_display, - 'accuracy': self.accuracy, - 'address': self.address, - 'location_source': self.location_source - } - # Utility functions def is_valid_role(role): """Check if role is valid""" diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e8688d2 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,21 @@ +""" +Models package for QR Attendance Management System +================================================== + +This package contains all SQLAlchemy models split from app.py for better organization. +All models maintain backward compatibility and existing functionality. +""" + +from . import base + +def set_db(database): + """Set the database instance for all models""" + base.db = database + + # Now import all models (they will use base.db) + from .user import User + from .qrcode import QRCode, QRCodeStyle + from .project import Project + from .attendance import AttendanceData + + return User, QRCode, QRCodeStyle, Project, AttendanceData \ No newline at end of file diff --git a/models/attendance.py b/models/attendance.py new file mode 100644 index 0000000..2350ecb --- /dev/null +++ b/models/attendance.py @@ -0,0 +1,58 @@ +""" +Attendance Model for QR Attendance Management System +=================================================== + +AttendanceData model for tracking attendance records with location support. +Extracted from app.py for better code organization. +""" + +from datetime import datetime +from . import base + +class AttendanceData(base.db.Model): + """Enhanced attendance tracking model with location support""" + __tablename__ = 'attendance_data' + + # Existing fields + id = base.db.Column(base.db.Integer, primary_key=True) + qr_code_id = base.db.Column(base.db.Integer, base.db.ForeignKey('qr_codes.id', ondelete='CASCADE'), nullable=False) + employee_id = base.db.Column(base.db.String(50), nullable=False) + check_in_date = base.db.Column(base.db.Date, nullable=False, default=datetime.today) + check_in_time = base.db.Column(base.db.Time, nullable=False, default=datetime.now().time) + device_info = base.db.Column(base.db.String(200)) + user_agent = base.db.Column(base.db.Text) + ip_address = base.db.Column(base.db.String(45)) + location_name = base.db.Column(base.db.String(100), nullable=False) + status = base.db.Column(base.db.String(20), default='present') + created_timestamp = base.db.Column(base.db.DateTime, default=datetime.utcnow) + updated_timestamp = base.db.Column(base.db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + latitude = base.db.Column(base.db.Float, nullable=True) + longitude = base.db.Column(base.db.Float, nullable=True) + accuracy = base.db.Column(base.db.Float, nullable=True) + location_accuracy = base.db.Column(base.db.Float, nullable=True) + altitude = base.db.Column(base.db.Float, nullable=True) + location_source = base.db.Column(base.db.String(50), default='manual') + address = base.db.Column(base.db.String(500), nullable=True) + + # Relationships + qr_code = base.db.relationship('QRCode', backref=base.db.backref('attendance_records', lazy='dynamic')) + + def __repr__(self): + return f'' + + @property + def has_location_data(self): + """Check if this record has GPS coordinates""" + return self.latitude is not None and self.longitude is not None + + @property + def location_accuracy_level(self): + """Get human-readable accuracy level""" + if not self.accuracy: + return 'unknown' + elif self.accuracy <= 5: + return 'high' + elif self.accuracy <= 20: + return 'medium' + else: + return 'low' \ No newline at end of file diff --git a/models/base.py b/models/base.py new file mode 100644 index 0000000..05c42d3 --- /dev/null +++ b/models/base.py @@ -0,0 +1,7 @@ +# models/base.py +""" +Base module to hold the database instance for all models +""" + +# This will be set by app.py +db = None \ No newline at end of file diff --git a/models/project.py b/models/project.py new file mode 100644 index 0000000..9975993 --- /dev/null +++ b/models/project.py @@ -0,0 +1,40 @@ +""" +Project Model for QR Attendance Management System +================================================ + +Project model to organize QR codes by projects. +Extracted from app.py for better code organization. +""" + +from datetime import datetime +from . import base + +class Project(base.db.Model): + """ + Project model to organize QR codes by projects + """ + __tablename__ = 'projects' + + id = base.db.Column(base.db.Integer, primary_key=True) + name = base.db.Column(base.db.String(100), nullable=False) + description = base.db.Column(base.db.Text, nullable=True) + created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'), nullable=True) + created_date = base.db.Column(base.db.DateTime, default=datetime.utcnow) + active_status = base.db.Column(base.db.Boolean, default=True) + + # Relationships + qr_codes = base.db.relationship('QRCode', backref='project', lazy='dynamic') + creator = base.db.relationship('User', backref='created_projects') + + def __repr__(self): + return f'' + + @property + def qr_count(self): + """Get count of QR codes in this project""" + return self.qr_codes.filter_by(active_status=True).count() + + @property + def total_qr_count(self): + """Get total count of QR codes (including inactive) in this project""" + return self.qr_codes.count() \ No newline at end of file diff --git a/models/qrcode.py b/models/qrcode.py new file mode 100644 index 0000000..d794f16 --- /dev/null +++ b/models/qrcode.py @@ -0,0 +1,81 @@ +""" +QRCode and QRCodeStyle Models for QR Attendance Management System +================================================================ + +QRCode models to manage QR code records and metadata with customization options. +Extracted from app.py for better code organization. +""" + +from datetime import datetime +from . import base + +class QRCode(base.db.Model): + """ + Enhanced QR Code model to manage QR code records and metadata with address coordinates + """ + __tablename__ = 'qr_codes' + + id = base.db.Column(base.db.Integer, primary_key=True) + name = base.db.Column(base.db.String(100), nullable=False) + location = base.db.Column(base.db.String(100), nullable=False) + location_address = base.db.Column(base.db.Text, nullable=False) + location_event = base.db.Column(base.db.String(200), nullable=False) + qr_code_image = base.db.Column(base.db.Text, nullable=False) # Base64 encoded image + created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'), nullable=True) + created_date = base.db.Column(base.db.DateTime, default=datetime.utcnow) + active_status = base.db.Column(base.db.Boolean, default=True) + qr_url = base.db.Column(base.db.String(255), unique=True, nullable=True) + # Address Coordinates Fields + address_latitude = base.db.Column(base.db.Float, nullable=True) + address_longitude = base.db.Column(base.db.Float, nullable=True) + coordinate_accuracy = base.db.Column(base.db.String(50), nullable=True, default='geocoded') + coordinates_updated_date = base.db.Column(base.db.DateTime, nullable=True) + project_id = base.db.Column(base.db.Integer, base.db.ForeignKey('projects.id'), nullable=True) + # QR Code Customization fields + fill_color = base.db.Column(base.db.String(7), default="#000000") # Hex color + back_color = base.db.Column(base.db.String(7), default="#FFFFFF") # Background color + box_size = base.db.Column(base.db.Integer, default=10) + border = base.db.Column(base.db.Integer, default=4) + error_correction = base.db.Column(base.db.String(1), default='L') + style_id = base.db.Column(base.db.Integer, base.db.ForeignKey('qr_code_styles.id'), nullable=True) + + # Relationship to style + style = base.db.relationship('QRCodeStyle', backref='qr_codes') + + @property + def has_coordinates(self): + """Check if this QR code has address coordinates""" + return self.address_latitude is not None and self.address_longitude is not None + + @property + def coordinates_display(self): + """Get formatted coordinates for display""" + if self.has_coordinates: + return f"{self.address_latitude:.10f}, {self.address_longitude:.10f}" + return "Coordinates not available" + + def update_coordinates(self, latitude, longitude, accuracy='geocoded'): + """Update the address coordinates for this QR code""" + self.address_latitude = latitude + self.address_longitude = longitude + self.coordinate_accuracy = accuracy + self.coordinates_updated_date = datetime.utcnow() + + +class QRCodeStyle(base.db.Model): + """QR Code customization styles""" + __tablename__ = 'qr_code_styles' + + id = base.db.Column(base.db.Integer, primary_key=True) + name = base.db.Column(base.db.String(100), nullable=False) # Style name + fill_color = base.db.Column(base.db.String(7), default="#000000") # Hex color for QR modules + back_color = base.db.Column(base.db.String(7), default="#FFFFFF") # Hex color for background + box_size = base.db.Column(base.db.Integer, default=10) # Size of each QR module + border = base.db.Column(base.db.Integer, default=4) # Border size + error_correction = base.db.Column(base.db.String(1), default='L') # L, M, Q, H + is_default = base.db.Column(base.db.Boolean, default=False) + created_at = base.db.Column(base.db.DateTime, default=datetime.utcnow) + created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id')) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..dbe8298 --- /dev/null +++ b/models/user.py @@ -0,0 +1,68 @@ +""" +User Model for QR Attendance Management System +============================================== + +User model to manage system users with role-based access control. +Extracted from app.py for better code organization. +""" + +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime + +# Valid user roles (kept in sync with app.py) +STAFF_LEVEL_ROLES = ['staff', 'payroll', 'project_manager'] + +# Import db from app - this works because app.py imports this file after db is created +import sys +from . import base + +class User(base.db.Model): + """ + User model to manage system users with role-based access control + """ + __tablename__ = 'users' + + id = base.db.Column(base.db.Integer, primary_key=True) + full_name = base.db.Column(base.db.String(100), nullable=False) + email = base.db.Column(base.db.String(120), unique=True, nullable=False) + username = base.db.Column(base.db.String(80), unique=True, nullable=False) + password_hash = base.db.Column(base.db.String(255), nullable=False) + role = base.db.Column(base.db.String(20), nullable=False, default='staff') # admin or staff + created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'), nullable=True) + created_date = base.db.Column(base.db.DateTime, default=datetime.utcnow) + active_status = base.db.Column(base.db.Boolean, default=True) + last_login_date = base.db.Column(base.db.DateTime, nullable=True) + + # Relationships + created_users = base.db.relationship('User', backref=base.db.backref('creator', remote_side=[id])) + created_qr_codes = base.db.relationship('QRCode', backref='creator', lazy='dynamic') + + def set_password(self, password): + """Hash and set user password""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + """Verify user password""" + return check_password_hash(self.password_hash, password) + + def is_admin(self): + """Check if user has admin privileges""" + return self.role == 'admin' + + def has_staff_permissions(self): + """Check if user has staff-level permissions (includes new roles)""" + return self.role in STAFF_LEVEL_ROLES + + def has_export_permissions(user_role): + """Check if user role has export permissions""" + return user_role in ['admin', 'payroll'] + + def get_role_display_name(self): + """Get user-friendly role name""" + role_names = { + 'admin': 'Administrator', + 'staff': 'Staff User', + 'payroll': 'Payroll Specialist', + 'project_manager': 'Project Manager' + } + return role_names.get(self.role, self.role.title()) \ No newline at end of file