Mar 21 2026: code refactory 3
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user