import os from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager from flask_bcrypt import Bcrypt from app.config import config # Initialize extensions db = SQLAlchemy() migrate = Migrate() login_manager = LoginManager() bcrypt = Bcrypt() def create_app(config_name='default'): """Application factory pattern.""" app = Flask(__name__) # Load configuration app.config.from_object(config[config_name]) # Logging first, so anything below can report problems. from app.services.logging_setup import configure_logging configure_logging(app) # Initialize extensions db.init_app(app) migrate.init_app(app, db) bcrypt.init_app(app) # Configure Flask-Login login_manager.init_app(app) login_manager.login_view = 'auth.login' login_manager.login_message = 'Please log in to access this page.' login_manager.login_message_category = 'info' # User loader for Flask-Login from app.models.user import User @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) # Register blueprints from app.routes.auth import auth_bp from app.routes.main import main_bp from app.routes.sets import sets_bp from app.routes.instructions import instructions_bp from app.routes.admin import admin_bp from app.routes.extra_files import extra_files_bp from app.routes.moc import bp as moc_bp from app.routes.backup import backup_bp from app.routes.instruction_fetch import instruction_fetch_bp from app.routes.logs import logs_bp app.register_blueprint(auth_bp) app.register_blueprint(main_bp) app.register_blueprint(sets_bp) app.register_blueprint(instructions_bp) app.register_blueprint(admin_bp) app.register_blueprint(extra_files_bp) app.register_blueprint(moc_bp) app.register_blueprint(backup_bp) app.register_blueprint(instruction_fetch_bp) app.register_blueprint(logs_bp) # Import models to ensure they're registered with SQLAlchemy from app.models.user import User from app.models.set import Set from app.models.instruction import Instruction from app.models.extra_file import ExtraFile from app.models.set_share import SetShare from app.models.site_setting import SiteSetting from app.models.instruction_fetch import InstructionFetchJob from app.models.api_call import BricksetApiCall # Add any columns missing from an older database (additive only). if app.config.get('AUTO_SCHEMA_UPGRADE', True): from app.services.schema_upgrade import ensure_schema ensure_schema(app) # One place that decides who may see or change a set, covering every # set-scoped route in the app. from app.services.access import install_access_guard install_access_guard(app) # Create upload directories os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'pdfs'), exist_ok=True) os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'images'), exist_ok=True) os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'covers'), exist_ok=True) os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'thumbnails'), exist_ok=True) os.makedirs(os.path.join(app.config['UPLOAD_FOLDER'], 'extra_files'), exist_ok=True) # Create backup directory os.makedirs(app.config['BACKUP_FOLDER'], exist_ok=True) # Serve uploads from UPLOAD_FOLDER wherever it lives. # Stored paths are rendered as /static/uploads/, which only works # by accident when UPLOAD_FOLDER sits inside app/static. This explicit # route takes precedence over Flask's static handler and keeps those # URLs valid when the folder is a mounted volume (e.g. /data/uploads). from flask import send_from_directory @app.route('/static/uploads/') def uploaded_file(filename): return send_from_directory(app.config['UPLOAD_FOLDER'], filename) # Register CLI commands (flask backup ... / flask instructions ...) from app.cli import register_cli register_cli(app) # Templates need to know whether to show owner-only links. @app.context_processor def inject_ownership(): from app.services.ownership import is_owner try: return {'current_user_is_owner': is_owner()} except Exception: return {'current_user_is_owner': False} # Background ticker for the automatic instruction fetcher. It does # nothing at all until an admin enables it on the fetcher page. from app.services import fetch_worker fetch_worker.start(app) # Context processor for global template variables @app.context_processor def inject_global_vars(): from app.services.brickset_api import BricksetAPI from datetime import datetime from app.models.site_setting import SiteSetting return { 'app_name': 'LEGO Instructions Manager', 'brickset_available': BricksetAPI.is_configured(), 'current_year': datetime.now().year, 'allow_registration': SiteSetting.get_bool('allow_registration', True) } return app