Mar 21 2026: code refactory 3

This commit is contained in:
2026-03-21 08:07:37 -04:00
parent f6ad3bd930
commit 503d9e18c7
7 changed files with 2253 additions and 2058 deletions
+14 -65
View File
@@ -21,6 +21,8 @@ import time as _time
load_dotenv()
from extensions import db, init_logger
from config import get_config
from utils.template_helpers import register_template_helpers
from logger_handler import log_database_operations
from models import set_db
from turnstile_utils import turnstile_utils
@@ -43,21 +45,9 @@ def create_app() -> Flask:
# ------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = os.environ.get('SQLALCHEMY_TRACK_MODIFICATIONS')
app.config['TEMPLATES_AUTO_RELOAD'] = os.environ.get('TEMPLATES_AUTO_RELOAD')
# Session / cookie configuration
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=30)
app.config['SESSION_COOKIE_SECURE'] = os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
app.config['SESSION_COOKIE_HTTPONLY'] = os.environ.get('SESSION_COOKIE_HTTPONLY', 'true').lower() == 'true'
app.config['SESSION_COOKIE_SAMESITE'] = os.environ.get('SESSION_COOKIE_SAMESITE')
# Photo verification
app.config['PHOTO_VERIFICATION_ENABLED'] = os.environ.get('ENABLE_PHOTO_VERIFICATION', 'true').lower() == 'true'
app.config['DISTANCE_THRESHOLD_FOR_VERIFICATION'] = float(os.environ.get('PHOTO_VERIFICATION_DISTANCE_THRESHOLD', '0.3'))
app.config['VERIFICATION_PHOTO_MAX_SIZE'] = int(os.environ.get('VERIFICATION_PHOTO_MAX_SIZE', str(5 * 1024 * 1024)))
# Load configuration from config.py (single source of truth for env vars)
cfg = get_config()
app.config.from_object(cfg)
# ------------------------------------------------------------------
# Database initialization
@@ -129,51 +119,8 @@ def create_app() -> Flask:
'turnstile_site_key': turnstile_utils.get_site_key()
}
# Helper functions for context processors
def get_employee_name(employee_id):
"""Helper function to get employee full name by ID"""
from sqlalchemy import text as sa_text
try:
result = db.session.execute(sa_text("""
SELECT CONCAT(firstName, ' ', lastName) as full_name
FROM employee
WHERE id = :employee_id
"""), {'employee_id': employee_id})
row = result.fetchone()
return row[0] if row else f"Employee {employee_id}"
except Exception as e:
print(f"⚠️ Error getting employee name for ID {employee_id}: {e}")
return f"Employee {employee_id}"
def get_qr_code_checkin_count(qr_code_id):
"""Helper function to get total check-ins count for a QR code"""
from models.attendance import AttendanceData
try:
count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count()
return count
except Exception as e:
from extensions import logger_handler as _lh
_lh.logger.error(f"Error getting check-ins count for QR {qr_code_id}: {e}")
return 0
@app.context_processor
def inject_payroll_utils():
"""Inject payroll utility functions into templates"""
from working_hours_calculator import convert_minutes_to_base100, round_base100_hours
return {
'convert_minutes_to_base100': convert_minutes_to_base100,
'round_base100_hours': round_base100_hours,
'get_employee_name': get_employee_name,
'format_hours': lambda hours: f"{hours:.2f}" if hours else "0.00"
}
@app.context_processor
def inject_dashboard_utils():
"""Inject dashboard utility functions into templates"""
return {
'now': datetime.utcnow,
'get_qr_code_checkin_count': get_qr_code_checkin_count
}
# Register template helper context processors (from utils/template_helpers.py)
register_template_helpers(app)
@app.template_filter('strftime')
def strftime_filter(value, format='%m/%d/%Y'):
@@ -319,7 +266,8 @@ def create_tables():
from models.user import User
admin = User.query.filter_by(username='admin').first()
if not admin:
default_password = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123')
from config import Config as _Cfg
default_password = _Cfg.DEFAULT_ADMIN_PASSWORD
admin = User(
full_name='System Administrator',
email='admin@example.com',
@@ -436,9 +384,10 @@ if __name__ == '__main__':
print(f"❌ Application startup failed: {e}")
raise
from config import Config as _Cfg
app.run(
debug=os.environ.get('DEBUG'),
host=os.environ.get('FLASK_HOST'),
port=os.environ.get('FLASK_PORT'),
threaded=os.environ.get('THREADED')
debug=_Cfg.DEBUG,
host=_Cfg.FLASK_HOST,
port=_Cfg.FLASK_PORT,
threaded=_Cfg.THREADED
)
+110
View File
@@ -0,0 +1,110 @@
"""
config.py
=========
Centralised application configuration.
All environment variable reads happen here — once, at startup.
Blueprints and helpers that need a config value use:
from flask import current_app
value = current_app.config['KEY']
Or for values needed at module import time (before app context):
from config import Config
value = Config.COMPANY_NAME
"""
import os
from datetime import timedelta
class Config:
# ------------------------------------------------------------------ #
# Core Flask
# ------------------------------------------------------------------ #
SECRET_KEY = os.environ.get('SECRET_KEY', 'change-me-in-production')
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '')
SQLALCHEMY_TRACK_MODIFICATIONS = (
os.environ.get('SQLALCHEMY_TRACK_MODIFICATIONS', 'False').lower() == 'true'
)
TEMPLATES_AUTO_RELOAD = (
os.environ.get('TEMPLATES_AUTO_RELOAD', 'True').lower() == 'true'
)
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
# ------------------------------------------------------------------ #
# Session / cookies
# ------------------------------------------------------------------ #
PERMANENT_SESSION_LIFETIME = timedelta(days=30)
SESSION_COOKIE_SECURE = (
os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
)
SESSION_COOKIE_HTTPONLY = (
os.environ.get('SESSION_COOKIE_HTTPONLY', 'true').lower() == 'true'
)
SESSION_COOKIE_SAMESITE = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax')
# ------------------------------------------------------------------ #
# Application identity
# ------------------------------------------------------------------ #
COMPANY_NAME = os.environ.get('COMPANY_NAME', 'QR Code Management System')
CONTRACT_NAME = os.environ.get('CONTRACT_NAME', 'Default Contract')
# ------------------------------------------------------------------ #
# File uploads
# ------------------------------------------------------------------ #
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', '/tmp')
# ------------------------------------------------------------------ #
# Photo verification
# ------------------------------------------------------------------ #
PHOTO_VERIFICATION_ENABLED = (
os.environ.get('ENABLE_PHOTO_VERIFICATION', 'true').lower() == 'true'
)
DISTANCE_THRESHOLD_FOR_VERIFICATION = float(
os.environ.get('PHOTO_VERIFICATION_DISTANCE_THRESHOLD', '0.3')
)
VERIFICATION_PHOTO_MAX_SIZE = int(
os.environ.get('VERIFICATION_PHOTO_MAX_SIZE', str(5 * 1024 * 1024))
)
# ------------------------------------------------------------------ #
# Check-in interval
# ------------------------------------------------------------------ #
TIME_INTERVAL = int(os.environ.get('TIME_INTERVAL', '30'))
# ------------------------------------------------------------------ #
# Server
# ------------------------------------------------------------------ #
FLASK_HOST = os.environ.get('FLASK_HOST', '0.0.0.0')
FLASK_PORT = int(os.environ.get('FLASK_PORT', '5000'))
THREADED = os.environ.get('THREADED', 'True').lower() == 'true'
# ------------------------------------------------------------------ #
# Default admin (used only on first boot)
# ------------------------------------------------------------------ #
DEFAULT_ADMIN_PASSWORD = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123')
class DevelopmentConfig(Config):
DEBUG = True
SESSION_COOKIE_SECURE = False
class ProductionConfig(Config):
DEBUG = False
TEMPLATES_AUTO_RELOAD = False
# Active config selected by environment variable
_config_map = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': Config,
}
def get_config():
"""Return the active Config class based on FLASK_ENV."""
env = os.environ.get('FLASK_ENV', 'default').lower()
return _config_map.get(env, Config)
+13 -13
View File
@@ -6,7 +6,7 @@ Payroll dashboard and Excel export routes.
Routes: /payroll, /payroll/export-excel, /api/working-hours/calculate,
/api/employee/<id>/miss-punch-details
"""
from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, url_for
from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, url_for, current_app
from datetime import datetime, date, timedelta, time
import io, json, traceback, os
@@ -292,7 +292,7 @@ def export_payroll_excel():
print("📊 Creating enhanced payroll report with SP/PW support")
try:
from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
exporter = EnhancedPayrollExcelExporter(company_name=os.environ.get('COMPANY_NAME', 'Your Company'))
exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'))
excel_file = exporter.create_enhanced_payroll_report(
start_date, end_date, attendance_records, employee_names, project_name
)
@@ -302,8 +302,8 @@ def export_payroll_excel():
print("⚠️ Enhanced exporter not available, falling back to standard exporter")
# Fall back to standard exporter
exporter = PayrollExcelExporter(
company_name=os.environ.get('COMPANY_NAME', 'Your Company'),
contract_name=os.environ.get('CONTRACT_NAME', 'Default Contract')
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_payroll_report(
start_date, end_date, attendance_records, employee_names
@@ -313,8 +313,8 @@ def export_payroll_excel():
print(f"⚠️ Error with enhanced exporter: {e}, falling back to standard exporter")
# Fall back to standard exporter
exporter = PayrollExcelExporter(
company_name=os.environ.get('COMPANY_NAME', 'Your Company'),
contract_name=os.environ.get('CONTRACT_NAME', 'Default Contract')
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_payroll_report(
start_date, end_date, attendance_records, employee_names
@@ -326,7 +326,7 @@ def export_payroll_excel():
print("📊 Creating detailed SP/PW daily breakdown report")
try:
from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
exporter = EnhancedPayrollExcelExporter(company_name=os.environ.get('COMPANY_NAME', 'Your Company'))
exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'))
excel_file = exporter.create_detailed_sp_pw_report(
start_date, end_date, attendance_records, employee_names
)
@@ -336,8 +336,8 @@ def export_payroll_excel():
print("⚠️ Enhanced exporter not available, falling back to detailed hours report")
# Fall back to standard detailed report
exporter = PayrollExcelExporter(
company_name=os.environ.get('COMPANY_NAME', 'Your Company'),
contract_name=os.environ.get('CONTRACT_NAME', 'Default Contract')
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_detailed_hours_report(
start_date, end_date, attendance_records, employee_names
@@ -347,8 +347,8 @@ def export_payroll_excel():
print(f"⚠️ Error with enhanced exporter: {e}, falling back to detailed hours report")
# Fall back to standard detailed report
exporter = PayrollExcelExporter(
company_name=os.environ.get('COMPANY_NAME', 'Your Company'),
contract_name=os.environ.get('CONTRACT_NAME', 'Default Contract')
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
excel_file = exporter.create_detailed_hours_report(
start_date, end_date, attendance_records, employee_names
@@ -358,8 +358,8 @@ def export_payroll_excel():
else:
# Use standard exporter for existing report types
exporter = PayrollExcelExporter(
company_name=os.environ.get('COMPANY_NAME', 'Your Company'),
contract_name=os.environ.get('CONTRACT_NAME', 'Default Contract')
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
contract_name=current_app.config.get('CONTRACT_NAME', 'Default Contract')
)
if report_type == 'detailed':
+1 -1
View File
@@ -633,7 +633,7 @@ def qr_checkin(qr_url):
# Check for recent check-ins with 30-minute interval validation
today = date.today()
current_time = datetime.now()
time_interval = int(os.environ.get('TIME_INTERVAL'))
time_interval = current_app.config.get('TIME_INTERVAL', 30)
the_last_checkin_time = current_time - timedelta(minutes=time_interval)
# Find the most recent check-in for this employee at this location today
+7 -1979
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
"""
utils/template_helpers.py
=========================
Template utility functions injected into Jinja2 via context processors.
Extracted from create_app() in app.py so they can be independently
imported, tested, and reused.
"""
from datetime import datetime
from sqlalchemy import text as sa_text
from extensions import db, logger_handler
def get_employee_name(employee_id):
"""Return 'Lastname, Firstname' for a given employee ID.
Falls back to 'Employee <id>' if not found or on error.
"""
try:
result = db.session.execute(sa_text("""
SELECT CONCAT(firstName, ' ', lastName) as full_name
FROM employee
WHERE id = :employee_id
"""), {'employee_id': employee_id})
row = result.fetchone()
return row[0] if row else f"Employee {employee_id}"
except Exception as e:
print(f"⚠️ Error getting employee name for ID {employee_id}: {e}")
return f"Employee {employee_id}"
def get_qr_code_checkin_count(qr_code_id):
"""Return total number of check-ins for a given QR code ID."""
from models.attendance import AttendanceData
try:
return AttendanceData.query.filter_by(qr_code_id=qr_code_id).count()
except Exception as e:
logger_handler.logger.error(
f"Error getting check-ins count for QR {qr_code_id}: {e}"
)
return 0
def format_hours(hours):
"""Format a decimal hours value to 2 decimal places."""
return f"{hours:.2f}" if hours else "0.00"
def register_template_helpers(app):
"""
Register all template helper context processors on the given Flask app.
Call this once inside create_app() after the app is configured.
"""
from working_hours_calculator import (
convert_minutes_to_base100, round_base100_hours
)
@app.context_processor
def inject_payroll_utils():
"""Inject payroll utility functions into all templates."""
return {
'convert_minutes_to_base100': convert_minutes_to_base100,
'round_base100_hours': round_base100_hours,
'get_employee_name': get_employee_name,
'format_hours': format_hours,
}
@app.context_processor
def inject_dashboard_utils():
"""Inject dashboard utility functions into all templates."""
return {
'now': datetime.utcnow,
'get_qr_code_checkin_count': get_qr_code_checkin_count,
}