"""Service d'import des vacances scolaires depuis l'API gouvernementale.""" import requests from datetime import datetime, timedelta 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(), # L'API indique la date de rentrée comme borne de fin. # Elle ne doit pas être affichée comme un jour de vacances. "end_date": datetime.fromisoformat(end.replace("Z", "+00:00")).date() - timedelta(days=1), "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 [{**vac, "end_date": vac["end_date"] - timedelta(days=1)} for vac in 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