Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me> Co-authored-by: Google/Gemini-3.5-Flash <vibecoder@antoineve.me> Co-authored-by: DeepSeek/DeepSeek-v4-Flash <vibecoder@antoineve.me>
179 lines
6.1 KiB
Python
179 lines
6.1 KiB
Python
"""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 sqlalchemy as sa
|
|
import tomllib
|
|
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"] = {}
|
|
|
|
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
|