mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-27 15:08:57 -06:00
- Remove redundant documentation files (DATABASE_INIT_FIX_*.md, TIMEZONE_FIX_README.md) - Delete unused Docker files (Dockerfile.test, Dockerfile.combined, docker-compose.yml) - Remove obsolete deployment scripts (deploy.sh) and unused files (index.html, _config.yml) - Clean up logs directory (remove 2MB timetracker.log, keep .gitkeep) - Remove .pytest_cache directory - Consolidate Docker setup to two main container types: * Simple container (recommended for production) * Public container (for development/testing) - Enhance timezone support in admin settings: * Add 100+ timezone options organized by region * Implement real-time timezone preview with current time display * Add timezone offset calculation and display * Remove search functionality for cleaner interface * Update timezone utility functions for database-driven configuration - Update documentation: * Revise README.md to reflect current project state * Add comprehensive timezone features documentation * Update Docker deployment instructions * Create PROJECT_STRUCTURE.md for project overview * Remove references to deleted files - Improve project structure: * Streamlined file organization * Better maintainability and focus * Preserved all essential functionality * Cleaner deployment options
212 lines
7.1 KiB
Python
212 lines
7.1 KiB
Python
import os
|
|
import logging
|
|
from datetime import timedelta
|
|
from flask import Flask
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from flask_migrate import Migrate
|
|
from flask_login import LoginManager
|
|
from flask_socketio import SocketIO
|
|
from dotenv import load_dotenv
|
|
import re
|
|
from jinja2 import ChoiceLoader, FileSystemLoader
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# Initialize extensions
|
|
db = SQLAlchemy()
|
|
migrate = Migrate()
|
|
login_manager = LoginManager()
|
|
socketio = SocketIO()
|
|
|
|
def create_app(config=None):
|
|
"""Application factory pattern"""
|
|
app = Flask(__name__)
|
|
|
|
# Configuration
|
|
app.config.from_object('app.config.Config')
|
|
if config:
|
|
app.config.update(config)
|
|
|
|
# Add top-level templates directory in addition to app/templates
|
|
extra_templates_path = os.path.abspath(
|
|
os.path.join(app.root_path, '..', 'templates')
|
|
)
|
|
app.jinja_loader = ChoiceLoader([
|
|
app.jinja_loader,
|
|
FileSystemLoader(extra_templates_path)
|
|
])
|
|
|
|
# Prefer Postgres if POSTGRES_* envs are present but URL points to SQLite
|
|
current_url = app.config.get('SQLALCHEMY_DATABASE_URI', '')
|
|
if (
|
|
not app.config.get('TESTING')
|
|
and isinstance(current_url, str)
|
|
and current_url.startswith('sqlite')
|
|
and (
|
|
os.getenv('POSTGRES_DB')
|
|
or os.getenv('POSTGRES_USER')
|
|
or os.getenv('POSTGRES_PASSWORD')
|
|
)
|
|
):
|
|
pg_user = os.getenv('POSTGRES_USER', 'timetracker')
|
|
pg_pass = os.getenv('POSTGRES_PASSWORD', 'timetracker')
|
|
pg_db = os.getenv('POSTGRES_DB', 'timetracker')
|
|
pg_host = os.getenv('POSTGRES_HOST', 'db')
|
|
app.config['SQLALCHEMY_DATABASE_URI'] = (
|
|
f'postgresql+psycopg2://{pg_user}:{pg_pass}@{pg_host}:5432/{pg_db}'
|
|
)
|
|
|
|
# Initialize extensions
|
|
db.init_app(app)
|
|
migrate.init_app(app, db)
|
|
login_manager.init_app(app)
|
|
socketio.init_app(app, cors_allowed_origins="*")
|
|
|
|
# Log effective database URL (mask password)
|
|
db_url = app.config.get('SQLALCHEMY_DATABASE_URI', '')
|
|
try:
|
|
masked_db_url = re.sub(r"//([^:]+):[^@]+@", r"//\\1:***@", db_url)
|
|
except Exception:
|
|
masked_db_url = db_url
|
|
app.logger.info(f"Using database URL: {masked_db_url}")
|
|
|
|
# Configure login manager
|
|
login_manager.login_view = 'auth.login'
|
|
login_manager.login_message = 'Please log in to access this page.'
|
|
login_manager.login_message_category = 'info'
|
|
|
|
# Configure session
|
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(
|
|
seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME', 86400))
|
|
)
|
|
|
|
# Setup logging
|
|
setup_logging(app)
|
|
|
|
# Register blueprints
|
|
from app.routes.auth import auth_bp
|
|
from app.routes.main import main_bp
|
|
from app.routes.projects import projects_bp
|
|
from app.routes.timer import timer_bp
|
|
from app.routes.reports import reports_bp
|
|
from app.routes.admin import admin_bp
|
|
from app.routes.api import api_bp
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(main_bp)
|
|
app.register_blueprint(projects_bp)
|
|
app.register_blueprint(timer_bp)
|
|
app.register_blueprint(reports_bp)
|
|
app.register_blueprint(admin_bp)
|
|
app.register_blueprint(api_bp)
|
|
|
|
# Register error handlers
|
|
from app.utils.error_handlers import register_error_handlers
|
|
register_error_handlers(app)
|
|
|
|
# Register context processors
|
|
from app.utils.context_processors import register_context_processors
|
|
register_context_processors(app)
|
|
|
|
# Register template filters
|
|
from app.utils.template_filters import register_template_filters
|
|
register_template_filters(app)
|
|
|
|
# Register CLI commands
|
|
from app.utils.cli import register_cli_commands
|
|
register_cli_commands(app)
|
|
|
|
# Initialize database on first request
|
|
def initialize_database():
|
|
try:
|
|
# Import models to ensure they are registered
|
|
from app.models import User, Project, TimeEntry, Settings
|
|
|
|
# Create database tables
|
|
db.create_all()
|
|
|
|
# Create default admin user if it doesn't exist
|
|
admin_username = app.config.get('ADMIN_USERNAMES', ['admin'])[0]
|
|
if not User.query.filter_by(username=admin_username).first():
|
|
admin_user = User(
|
|
username=admin_username,
|
|
role='admin'
|
|
)
|
|
admin_user.is_active = True
|
|
db.session.add(admin_user)
|
|
db.session.commit()
|
|
print(f"Created default admin user: {admin_username}")
|
|
|
|
print("Database initialized successfully")
|
|
except Exception as e:
|
|
print(f"Error initializing database: {e}")
|
|
# Don't raise the exception, just log it
|
|
|
|
# Store the initialization function for later use
|
|
app.initialize_database = initialize_database
|
|
|
|
return app
|
|
|
|
def setup_logging(app):
|
|
"""Setup application logging"""
|
|
log_level = os.getenv('LOG_LEVEL', 'INFO')
|
|
log_file = os.getenv('LOG_FILE')
|
|
|
|
# Prepare handlers
|
|
handlers = [logging.StreamHandler()]
|
|
|
|
# Add file handler if log_file is specified
|
|
if log_file:
|
|
try:
|
|
# Ensure log directory exists
|
|
log_dir = os.path.dirname(log_file)
|
|
if log_dir and not os.path.exists(log_dir):
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
# Test if we can write to the log file
|
|
file_handler = logging.FileHandler(log_file)
|
|
handlers.append(file_handler)
|
|
except (PermissionError, OSError) as e:
|
|
print(f"Warning: Could not create log file '{log_file}': {e}")
|
|
print("Logging to console only")
|
|
# Don't add file handler, just use console logging
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=getattr(logging, log_level.upper()),
|
|
format='%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]',
|
|
handlers=handlers
|
|
)
|
|
|
|
# Suppress Werkzeug logs in production
|
|
if not app.debug:
|
|
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
|
|
|
def init_database(app):
|
|
"""Initialize database tables and create default admin user"""
|
|
with app.app_context():
|
|
try:
|
|
# Import models to ensure they are registered
|
|
from app.models import User, Project, TimeEntry, Settings
|
|
|
|
# Create database tables
|
|
db.create_all()
|
|
|
|
# Create default admin user if it doesn't exist
|
|
admin_username = app.config.get('ADMIN_USERNAMES', ['admin'])[0]
|
|
if not User.query.filter_by(username=admin_username).first():
|
|
admin_user = User(
|
|
username=admin_username,
|
|
role='admin'
|
|
)
|
|
admin_user.is_active = True
|
|
db.session.add(admin_user)
|
|
db.session.commit()
|
|
print(f"Created default admin user: {admin_username}")
|
|
|
|
print("Database initialized successfully")
|
|
except Exception as e:
|
|
print(f"Error initializing database: {e}")
|
|
raise
|