"""Package principal de l'application Flask. Ce package initialise l'application Flask en utilisant le "Factory Pattern" (via la fonction `create_app`). Il configure également la base de données SQLite (via Flask-SQLAlchemy), charge la configuration TOML, enregistre les blueprints de routes, applique les migrations de schéma manuelles, et définit les filtres Jinja2 personnalisés. Architecture et composants clés : 1. Factory Pattern : `create_app` permet d'instancier l'application de manière isolée, facilitant les tests. 2. Base de données : SQLite stockée dans `instance/worklog.db`. 3. Migration de schéma : Gérée manuellement par `_migrate_db` sans utiliser Alembic. 4. Filtres Jinja2 : - `date_fr` : Formate une date Python en chaîne lisible en français (ex: "mercredi 11 mars 2026"). - `day_type_fr` : Traduit les codes internes des types de jours (ex: "WORK" -> "Travail"). """ import os import tomllib import sqlalchemy as sa from flask import Flask from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() def _migrate_db(app): """Applique les migrations de schéma manquantes de manière incrémentale (sans Alembic). Cette fonction vérifie l'état actuel de la base de données SQLite avant d'exécuter des instructions DDL (comme `ALTER TABLE`). Elle permet d'éviter les erreurs si la table `work_entries` n'existe pas encore (auquel cas `db.create_all()` s'en charge) ou si la colonne `motor_vehicle_id` a déjà été ajoutée lors d'un démarrage précédent. Paramètres: app (Flask): L'instance de l'application Flask en cours d'initialisation. """ import sqlite3 db_path = os.path.join(app.instance_path, "worklog.db") if not os.path.exists(db_path): return # Nouvelle DB, create_all() s'en charge conn = sqlite3.connect(db_path) tables = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")} if "work_entries" not in tables: conn.close() return # Table pas encore créée, create_all() s'en charge columns = {row[1] for row in conn.execute("PRAGMA table_info(work_entries)").fetchall()} conn.close() with app.app_context(): engine = db.engine if "motor_vehicle_id" not in columns: with engine.connect() as conn: conn.execute( sa.text("ALTER TABLE work_entries ADD COLUMN motor_vehicle_id VARCHAR(64)") ) conn.commit() _JOURS_FR = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"] _MOIS_FR = [ "", "janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre", ] _DAY_TYPE_LABELS = { "WORK": "Travail", "TT": "Télétravail", "GARDE": "Garde", "ASTREINTE": "Astreinte", "FORMATION": "Formation", "RTT": "RTT", "CONGE": "Congé", "MALADE": "Maladie", "FERIE": "Férié", } def _day_type_fr(code): """Filtre Jinja2 pour traduire un code de type de jour en libellé français. Exemple: `{{ 'WORK' | day_type_fr }}` -> "Travail" Paramètres: code (str): Le code interne du type de jour (ex: "WORK", "TT", "GARDE"). Retourne: str: Le libellé en français correspondant, ou le code d'origine si aucune traduction n'est définie. """ return _DAY_TYPE_LABELS.get(code, code) def _date_fr(d): """Filtre Jinja2 pour formater une date en français lisible. Exemple: `{{ entry.date | date_fr }}` -> "mercredi 11 mars 2026" Paramètres: d (datetime.date): L'objet date à formater. Retourne: str: La date formatée en français (jour de la semaine, jour du mois, mois en toutes lettres, année). """ jour = _JOURS_FR[d.weekday()] mois = _MOIS_FR[d.month] return f"{jour} {d.day} {mois} {d.year}" def create_app(config_path=None): """Factory de création et de configuration de l'application Flask. Cette fonction réalise les étapes suivantes : 1. Instancie l'application Flask avec le support des configurations relatives à l'instance. 2. Crée le dossier d'instance s'il n'existe pas. 3. Configure l'URI de la base de données SQLite (`instance/worklog.db`). 4. Charge la configuration TOML depuis `config.toml` (ou le chemin spécifié). 5. Initialise l'extension Flask-SQLAlchemy (`db`). 6. Enregistre les filtres Jinja2 personnalisés (`date_fr` et `day_type_fr`). 7. Enregistre les blueprints de routes (`dashboard`, `entries`, `reports`). 8. Exécute la migration de schéma manuelle (`_migrate_db`) puis crée les tables manquantes (`db.create_all()`). Paramètres: config_path (str | None): Chemin optionnel vers le fichier de configuration TOML. Par défaut, cherche `config.toml` à la racine du projet. Retourne: Flask: L'instance de l'application Flask configurée et prête à l'emploi. """ app = Flask(__name__, instance_relative_config=True) os.makedirs(app.instance_path, exist_ok=True) app.config["SQLALCHEMY_DATABASE_URI"] = ( f"sqlite:///{os.path.join(app.instance_path, 'worklog.db')}" ) app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-secret-change-in-prod") # Load TOML config if config_path is None: config_path = os.path.join(os.path.dirname(app.root_path), "config.toml") if os.path.exists(config_path): with open(config_path, "rb") as f: app.config["TOML"] = tomllib.load(f) else: app.config["TOML"] = {} from app.config_loader import get_home_assistant_config with app.app_context(): app.config["HOME_ASSISTANT"] = get_home_assistant_config() db.init_app(app) app.jinja_env.filters["date_fr"] = _date_fr app.jinja_env.filters["day_type_fr"] = _day_type_fr from app.routes.dashboard import bp as dashboard_bp from app.routes.entries import bp as entries_bp from app.routes.reports import bp as reports_bp app.register_blueprint(dashboard_bp) app.register_blueprint(entries_bp) app.register_blueprint(reports_bp) with app.app_context(): _migrate_db(app) db.create_all() return app