# File: app/__init__.py import os import json import importlib import time from flask import Flask, request from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect from datetime import datetime from dotenv import load_dotenv, find_dotenv # ─── Load .env ──────────────────────────────────────────────────────────────── dotenv_path = find_dotenv() if dotenv_path: load_dotenv(dotenv_path, override=True) # ─── Core extensions ─────────────────────────────────────────────────────────── db = SQLAlchemy() migrate = Migrate() login_manager = LoginManager() csrf = CSRFProtect() # ─── Template helper (still in core) ────────────────────────────────────────── from plugins.media.routes import generate_image_url # noqa: E402 def create_app(): # ─── Configure Flask ──────────────────────────────────────────────────────── project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) app = Flask( __name__, static_folder=os.path.join(project_root, 'static'), static_url_path='/static' ) app.config.from_object('app.config.Config') # ─── Init extensions ─────────────────────────────────────────────────────── csrf.init_app(app) db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) login_manager.login_view = 'auth.login' # ─── Core routes & errors ─────────────────────────────────────────────────── from .errors import bp as errors_bp # noqa: E402 app.register_blueprint(errors_bp) from .routes import init_app as register_core_routes # noqa: E402 register_core_routes(app) app.logger.info("✔️ Registered core routes") # ─── JSON‐driven plugin loader ────────────────────────────────────────────── plugins_dir = os.path.join(project_root, 'plugins') for name in sorted(os.listdir(plugins_dir)): plugin_path = os.path.join(plugins_dir, name) manifest = os.path.join(plugin_path, 'plugin.json') if not os.path.isfile(manifest): continue errors = [] try: meta = json.load(open(manifest)) except Exception as e: print(f"Plugin '{name}' 🛑 manifest load failed: {e}") continue # 1) Import models for model_path in meta.get('models', []): try: importlib.import_module(model_path) except Exception as e: errors.append(f"model import ({model_path}): {e}") # 1.b) user_loader hook ul = meta.get('user_loader') if ul: try: mod = importlib.import_module(ul['module']) fn = getattr(mod, ul['callable']) fn(app) except Exception as e: errors.append(f"user_loader ({ul['module']}:{ul['callable']}): {e}") # 2) Register routes routes_cfg = meta.get('routes') if routes_cfg: try: mod = importlib.import_module(routes_cfg['module']) bp_obj = getattr(mod, routes_cfg['blueprint']) prefix = routes_cfg.get('url_prefix') app.register_blueprint(bp_obj, url_prefix=prefix, strict_slashes=False) except Exception as e: errors.append(f"routes ({routes_cfg['module']}): {e}") # 3) Register CLI commands cli_cfg = meta.get('cli') if cli_cfg: try: mod = importlib.import_module(cli_cfg['module']) fn = getattr(mod, cli_cfg['callable']) app.cli.add_command(fn) except Exception as e: errors.append(f"cli ({cli_cfg['module']}:{cli_cfg['callable']}): {e}") # 4) Template globals for tg in meta.get('template_globals', []): try: mod_name, fn_name = tg['callable'].rsplit('.', 1) mod = importlib.import_module(mod_name) fn = getattr(mod, fn_name) app.jinja_env.globals[tg['name']] = fn except Exception as e: errors.append(f"template_global ({tg}): {e}") # 5) Subplugins (models + routes) for sp in meta.get('subplugins', []): for mp in sp.get('models', []): try: importlib.import_module(mp) except Exception as e: errors.append(f"subplugin model ({mp}): {e}") sp_rt = sp.get('routes') if sp_rt: try: mod = importlib.import_module(sp_rt['module']) bp_obj = getattr(mod, sp_rt['blueprint']) prefix = sp_rt.get('url_prefix') app.register_blueprint(bp_obj, url_prefix=prefix, strict_slashes=False) except Exception as e: errors.append(f"subplugin routes ({sp_rt['module']}): {e}") # Final status if errors: print(f"Plugin '{name}' 🛑 failed to load: {'; '.join(errors)}") else: print(f"Plugin '{name}' ✔️ Loaded Successfully.") # ─── Context processors, analytics, teardown ─────────────────────────────── @app.context_processor def inject_current_year(): return {'current_year': datetime.now().year} @app.context_processor def inject_now(): return {'utcnow': datetime.utcnow()} @app.before_request def start_timer(): request._start_time = time.time() @app.after_request def log_analytics(response): from plugins.admin.models import AnalyticsEvent # noqa: E402 try: duration = time.time() - getattr(request, '_start_time', time.time()) ev = AnalyticsEvent( method = request.method, path = request.path, status_code = response.status_code, response_time = duration, user_agent = request.headers.get('User-Agent'), referer = request.headers.get('Referer'), accept_language=request.headers.get('Accept-Language'), ) db.session.add(ev) db.session.commit() except Exception: db.session.rollback() return response @app.teardown_appcontext def shutdown_session(exception=None): db.session.remove() # ─── Keep the template helper exposed ────────────────────────────────────── app.jinja_env.globals['generate_image_url'] = generate_image_url return app