gmao/app_new/lib_ext/vacances_scolaires.py
root 6b56817dbb
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Utiliser la zone C et synchroniser les vacances
2026-08-20 17:51:36 +00:00

267 lines
8.8 KiB
Python

"""Service d'import des vacances scolaires depuis l'API gouvernementale."""
import requests
from datetime import datetime
from app_new.extensions import db
from app_new.core.models import CollegeClosure
# API du gouvernement pour les vacances scolaires
API_URL = "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-calendrier-scolaire-scolaires/exports/json"
def fetch_vacances_scolaires(zone="A", annee=None):
"""
Récupère les vacances scolaires depuis l'API gouvernementale.
Args:
zone: Zone académique (A, B ou C)
annee: Année scolaire (ex: 2024 pour 2024-2025). Si None, année actuelle.
Returns:
Liste de dictionnaires avec les vacances
"""
if annee is None:
annee = datetime.now().year
# L'API du gouvernement filtre par zone et année
params = {
"where": f"zones='{zone}'",
"order_by": "start_date",
"limit": 50
}
try:
response = requests.get(API_URL, params=params, timeout=30)
response.raise_for_status()
data = response.json()
vacances = []
for item in data:
start = item.get("start_date")
end = item.get("end_date")
description = item.get("description", "Vacances scolaires")
if start and end:
# Filtrer pour l'année scolaire demandée
start_date = datetime.fromisoformat(start.replace("Z", "+00:00"))
if start_date.year >= annee - 1:
vacances.append({
"start_date": start_date.date(),
"end_date": datetime.fromisoformat(end.replace("Z", "+00:00")).date(),
"description": description,
"zone": zone
})
return vacances
except Exception as e:
print(f"Erreur lors de la récupération des vacances: {e}")
return []
def import_vacances_scolaires(zone="A", annee=None, user_id=None):
"""
Importe les vacances scolaires dans la base de données.
Args:
zone: Zone académique (A, B ou C)
annee: Année scolaire
user_id: ID de l'utilisateur qui importe
Returns:
(nombre_ajoutees, nombre_ignorees, erreurs)
"""
vacances = fetch_vacances_scolaires(zone, annee)
if not vacances:
# Données de fallback si l'API n'est pas accessible
vacances = get_vacances_fallback(zone, annee)
# Les périodes importées représentent une seule zone de référence.
# Lors d'un changement de zone, retirer uniquement les anciennes
# périodes issues d'un import officiel (jamais les périodes manuelles).
imported_closures = CollegeClosure.query.filter(
CollegeClosure.closure_type == "vacances_scolaires",
CollegeClosure.notes.like("Zone académique % — import officiel"),
).all()
for existing in imported_closures:
if not (existing.notes or "").startswith(f"Zone académique {zone} "):
db.session.delete(existing)
if imported_closures:
db.session.flush()
ajoutees = 0
ignorees = 0
erreurs = []
for vac in vacances:
# Vérifier si cette fermeture existe déjà
existing = CollegeClosure.query.filter(
CollegeClosure.closure_type == "vacances_scolaires",
CollegeClosure.start_date == vac["start_date"],
CollegeClosure.end_date == vac["end_date"]
).first()
if existing:
ignorees += 1
continue
try:
closure = CollegeClosure(
name=vac["description"],
start_date=vac["start_date"],
end_date=vac["end_date"],
closure_type="vacances_scolaires",
work_hours_type="none",
notes=f"Zone académique {zone} — import officiel",
)
db.session.add(closure)
ajoutees += 1
except Exception as e:
erreurs.append(f"{vac['description']}: {str(e)}")
if ajoutees > 0:
db.session.commit()
return ajoutees, ignorees, erreurs
def get_vacances_fallback(zone, annee):
"""
Retourne les vacances scolaires par défaut si l'API n'est pas accessible.
Basé sur le calendrier officiel français.
"""
from datetime import timedelta
if annee is None:
annee = datetime.now().year
# Calculer l'année scolaire (2024-2025 pour annee=2024)
annee_scolaire = f"{annee}-{annee + 1}"
# Vacances standardisées (approximatives, à ajuster selon la zone)
vacances = [
# Toussaint
{
"start_date": datetime(annee, 10, 19).date(),
"end_date": datetime(annee, 11, 3).date(),
"description": f"Vacances de la Toussaint {annee_scolaire}",
"zone": zone
},
# Noël
{
"start_date": datetime(annee, 12, 21).date(),
"end_date": datetime(annee + 1, 1, 6).date(),
"description": f"Vacances de Noël {annee_scolaire}",
"zone": zone
},
# Hiver (diffère selon la zone)
{
"start_date": datetime(annee + 1, 2, 15).date() if zone != "C" else datetime(annee + 1, 2, 22).date(),
"end_date": datetime(annee + 1, 3, 2).date() if zone != "C" else datetime(annee + 1, 3, 9).date(),
"description": f"Vacances d'hiver {annee_scolaire}",
"zone": zone
},
# Printemps (diffère selon la zone) - CORRIGÉ: annee + 1 au lieu de annee + 2
{
"start_date": datetime(annee + 1, 4, 12).date() if zone == "A" else (datetime(annee + 1, 4, 19).date() if zone == "B" else datetime(annee + 1, 4, 26).date()),
"end_date": datetime(annee + 1, 4, 27).date() if zone == "A" else (datetime(annee + 1, 5, 4).date() if zone == "B" else datetime(annee + 1, 5, 11).date()),
"description": f"Vacances de printemps {annee_scolaire}",
"zone": zone
},
# Grandes vacances
{
"start_date": datetime(annee + 1, 7, 5).date(),
"end_date": datetime(annee + 1, 8, 31).date(),
"description": f"Grandes vacances {annee_scolaire}",
"zone": zone
},
]
return vacances
def get_jours_feries(annee=None):
"""
Retourne les jours fériés français pour une année donnée.
Calculés dynamiquement (Pâques est calculé).
"""
from datetime import timedelta
if annee is None:
annee = datetime.now().year
# Calcul de Pâques (algorithme de Butcher-Meeus)
a = annee % 19
b = annee // 100
c = annee % 100
d = b // 4
e = b % 4
f = (b + 8) // 25
g = (b - f + 1) // 3
h = (19 * a + b - d - g + 15) % 30
i = c // 4
k = c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) // 451
mois = (h + l - 7 * m + 114) // 31
jour = ((h + l - 7 * m + 114) % 31) + 1
paques = datetime(annee, mois, jour)
# Utiliser timedelta au lieu de replace pour éviter les jours invalides
jours_feries = [
(datetime(annee, 1, 1), "Jour de l'an"),
(paques, "Pâques"),
(paques + timedelta(days=1), "Lundi de Pâques"),
(datetime(annee, 5, 1), "Fête du travail"),
(datetime(annee, 5, 8), "Victoire 1945"),
(paques + timedelta(days=39), "Ascension"),
(paques + timedelta(days=49), "Pentecôte"),
(paques + timedelta(days=50), "Lundi de Pentecôte"),
(datetime(annee, 7, 14), "Fête nationale"),
(datetime(annee, 8, 15), "Assomption"),
(datetime(annee, 11, 1), "Toussaint"),
(datetime(annee, 11, 11), "Armistice 1918"),
(datetime(annee, 12, 25), "Noël"),
]
return [(d.date(), n) for d, n in jours_feries]
def import_jours_feries(annee=None, user_id=None):
"""
Importe les jours fériés dans la base de données.
Returns:
(nombre_ajoutees, nombre_ignorees)
"""
jours = get_jours_feries(annee)
ajoutees = 0
ignorees = 0
for date, nom in jours:
existing = CollegeClosure.query.filter(
CollegeClosure.closure_type == "jour_ferie",
CollegeClosure.start_date == date,
CollegeClosure.end_date == date
).first()
if existing:
ignorees += 1
continue
closure = CollegeClosure(
name=nom,
start_date=date,
end_date=date,
closure_type="jour_ferie",
work_hours_type="none",
notes="Import automatique des jours fériés français",
)
db.session.add(closure)
ajoutees += 1
if ajoutees > 0:
db.session.commit()
return ajoutees, ignorees