Version initiale — carte des établissements scolaires

Carte Leaflet (annuaire Éducation nationale) + API PHP/MySQL :
fiche établissement (effectifs, IPS, population INSEE, DPE ADEME),
GED documents/photos, commentaires par groupe, gestion utilisateurs
et politique de mots de passe. Imports opendata (effectifs, IPS,
population, DPE, IRIS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 21:35:41 +02:00
commit 5633969d98
40 changed files with 7242 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
# Carte des établissements scolaires (France)
Carte interactive OpenStreetMap / Leaflet des ~68 500 établissements de
l'annuaire de l'Éducation nationale, avec filtres côté client.
Source des données : [data.education.gouv.fr — annuaire-education](https://data.education.gouv.fr/explore/dataset/fr-en-annuaire-education/).
## Lancer
L'application nécessite **PHP + MySQL** (la carte et la GED sont servies par une
API PHP qui lit la base ; pas de fichier `file://`).
### 1. Initialiser la base (une seule fois)
MySQL doit tourner (Laragon le démarre). Puis :
```sh
php setup.php # crée la base « ecoles », les tables, importe les établissements
php setup.php --reset # réimporte les établissements depuis data/etablissements.json
```
> Identifiants par défaut (Laragon : `root` / mot de passe vide) dans `api/db.php`.
### 2. Servir l'application
**Avec Laragon** : place le projet dans `www/`, puis ouvre `http://ecoles.test/`
(ou `http://localhost/ecoles/`).
**Sans Laragon**, serveur PHP intégré :
```sh
php -S 127.0.0.1:8000
```
puis ouvre http://127.0.0.1:8000/.
## Fiche établissement
Un clic sur un point → **« Voir la fiche complète »** ouvre une fiche plein écran
(overlay superposé à la carte, donc le retour ne recharge pas la carte) avec
quatre onglets :
- **Informations** — toutes les données de l'établissement + bouton « Localiser sur la carte »,
les effectifs, et le **fil de commentaires** de l'établissement (voir *Groupes & commentaires*)
- **Documents** — arborescence de dossiers + fichiers / liens / photos **validés**
- **Photothèque** — galerie des photos validées
- **Proposer un élément** — tout internaute peut soumettre un fichier, une photo ou un lien
Les éléments sont stockés **sur disque** dans `uploads/`, leurs métadonnées en base
(table `items`, type `file` / `photo` / `link`), indexés par UAI et rangés dans
l'arborescence globale (`folders`).
Chaque document/photo de l'onglet *Documents* affiche, en plus de son libellé, sa
**date de dépôt** et une **description** (texte explicatif). La description est saisie par
le proposant au dépôt et **modifiable** ensuite par les **valideurs** (bouton ✏️ visible
sur la fiche quand on est connecté avec un droit de validation sur l'établissement). Chaque
document a son **fil de commentaires** dépliable (bouton 💬), soumis aux **mêmes règles**
que les commentaires d'établissement (visibilité par groupe, dépôt réservé aux membres d'un
groupe, suppression par l'auteur ou le super-admin) — voir *Groupes & commentaires*. Les
commentaires d'un document et ceux de l'établissement restent distincts
(`comments.item_id`).
## Droits & modération
Trois niveaux :
| Rôle | Droits |
|---|---|
| **Internaute** (anonyme) | Proposer des éléments (statut « en attente ») |
| **Commentateur** | Compte sans droit de modération : participe aux **commentaires** des fiches via ses groupes |
| **Admin de périmètre** | Valider/rejeter les éléments des communes de son périmètre (commune + département) ou de ses groupes valideurs ; commente aussi |
| **Super-admin (plateforme)** | Tout valider, gérer l'arborescence, créer les utilisateurs et les groupes, importer l'opendata |
- Seuls les éléments **validés** sont visibles du public.
- Espace d'administration : **`admin.html`** (lien en bas de la barre latérale).
- Super-admin par défaut créé par `setup.php` : **`admin` / `admin`** — *à changer*.
- L'import opendata (bouton dans l'admin) télécharge la dernière version depuis
data.education.gouv.fr et met la base à jour (accès Internet requis côté serveur).
## Groupes d'utilisateurs & commentaires
Le super-admin organise les utilisateurs en **groupes** (section *Groupes
d'utilisateurs* de l'admin) :
- Un groupe marqué **valideur** donne à ses membres le droit de **valider les
documents** des établissements de **ses communes** (même modèle de périmètre que les
admins de périmètre, mais porté au niveau du groupe). Un utilisateur peut appartenir à
plusieurs groupes ; ses droits de validation sont l'union de ceux de ses groupes
valideurs.
- Tables : `user_groups` (nom + `can_validate`), `group_members` (appartenance n..n),
`group_scopes` (communes du groupe valideur). Le contrôle est centralisé dans
`can_validate_uai()` (`api/db.php`), qui accepte super-admin, périmètre personnel
*(legacy)* **ou** groupe valideur. API : `api/groups.php` (super-admin).
**Commentaires de fiche** — affichés directement dans l'onglet *Informations* :
- Un commentaire est **horodaté** (date + heure), porte un **texte** et le **nom** de son
auteur (table `comments`).
- Il n'est **visible qu'entre membres d'un même groupe** que l'auteur ; le super-admin
voit tout. Déposer un commentaire nécessite d'**appartenir à au moins un groupe**.
- L'**auteur** (et le super-admin) peuvent supprimer un commentaire ; pas d'édition
(horodatage figé). API : `api/comments.php`.
- La connexion se fait **directement dans la carte commentaires** (mini-formulaire), la session étant
partagée avec l'espace d'administration.
- Pour les participants qui ne doivent **pas** modérer, le super-admin crée des comptes
de rôle **Commentateur** (section *Utilisateurs* de l'admin) : ils se connectent et
commentent via leurs groupes, sans accès à la file de validation.
## Sécurité des mots de passe
- **Hachage** : bcrypt (`password_hash` / `PASSWORD_DEFAULT`), vérifié par
`password_verify`. Aucun mot de passe n'est stocké ni transmis en clair.
- **Politique de complexité** (`password_policy_check`, `api/db.php`) : **≥ 8 caractères**
et **au moins 3 types** parmi minuscule / majuscule / chiffre / caractère spécial.
Appliquée à la création, à la réinitialisation et au changement. Côté navigateur, une
**jauge de robustesse** (`pwpolicy.js`) guide la saisie.
- **Changement forcé** : les comptes créés par un admin (et `admin`/`admin` par défaut)
sont marqués `must_change` → l'utilisateur doit choisir un nouveau mot de passe à sa
première connexion (`account.html?forced=1`).
- **Self-service** : tout utilisateur connecté peut changer son mot de passe via
**`account.html`** (saisie du mot de passe actuel).
- **Mot de passe oublié** : depuis l'écran de connexion → **`forgot.html`** (identifiant
ou e-mail). Un lien à **usage unique** (valable 1 h) est envoyé par e-mail et consommé
sur **`reset.html`**. Table `password_resets` (jeton stocké **haché** en SHA-256).
- **Anti-bruteforce** : après **5 échecs** de connexion, le compte est **verrouillé 15 min**
(`users.failed_attempts` / `locked_until`).
- **Envoi d'e-mails** — transport configurable dans `api/db.php` (`MAIL_TRANSPORT`) :
- `log` (par défaut) : écrit l'e-mail (dont le lien) dans `private/mail_outbox.log`
(dossier protégé par `.htaccess`) — pratique en local sans serveur de mail ;
- `mail` : fonction PHP `mail()` ;
- `smtp` : client SMTP intégré (renseigner `SMTP_HOST/PORT/USER/PASS/SECURE`).
Pensez à ajuster `APP_BASE_URL` pour que les liens de réinitialisation soient corrects.
## Couche IRIS & population
- Fond clair (CartoDB Positron) par défaut ; sélecteur de couches en haut à droite.
- Couche **Contours IRIS** (WFS Géoplateforme `STATISTICALUNITS.IRIS:contours_iris`), chargée par zone visible (zoom ≥ 11).
- **IRIS cliquables** : un clic sur une zone affiche son nom, sa commune, son type, et la
**population du recensement INSEE 2022** (de l'IRIS et de la commune).
- Populations importées via `php import_pop.php` dans la table `iris_pop`
(population communale = somme des IRIS de la commune).
- Bouton **« 📍 Me localiser »** + centrage automatique sur la position de l'internaute
(nécessite HTTPS ou localhost).
- **Coloration par densité** : chaque IRIS est colorié selon sa densité (population ÷ surface),
avec une légende ; un clic affiche population de l'IRIS, densité, et population de la commune.
## Effectifs des écoles (1er degré)
- Dataset `fr-en-ecoles-effectifs-nb_classes` importé via `php import_effectifs.php`
dans la table `effectifs` (uai, année → élèves, classes ; ~809 000 lignes, 55 360 écoles).
- La fiche d'une **école / collège / lycée** affiche, dans l'onglet *Informations*, ses
**derniers effectifs** (élèves ; classes pour le 1er degré) et un **graphique d'évolution**
sur tout l'historique connu. (1er degré depuis 2009 ; collèges/lycées via leurs datasets dédiés.)
- Une carte voisine montre les **effectifs cumulés des écoles publiques de la commune**
(somme des élèves des établissements **publics** de la commune, par année) pour situer
l'établissement dans son contexte local. API : `api/effectifs.php?action=commune&uai=…`.
- **À côté**, un graphique de l'**IPS** (indice de position sociale) de l'établissement.
Trois datasets importés via `php import_ips.php` dans la table `ips` (clé `uai` + année) :
`fr-en-ips-ecoles-ap2022` (écoles, rentrées 2022→2024, **+ référence départementale**),
`fr-en-ips-colleges-ap2022` (collèges) et `fr-en-ips-lycees-ap2022` (lycées, IPS
ensemble GT+Pro). Pour les écoles, la courbe de l'établissement est tracée avec la
**référence départementale** (même échelle) ; collèges et lycées n'ont qu'une rentrée
(2022-2023) pour l'instant, donc un point. API : `api/ips.php?uai=…`. Un IPS élevé reflète
un milieu social plus favorisé (référence nationale ≈ 100).
`php import_ips.php --niveau=ecoles|colleges|lycees` permet d'importer un seul niveau.
- À côté de cette courbe, un graphique d'**évolution de la population** (INSEE) sur les
recensements successifs (**1968 → 2022**, 9 dates) au niveau de la **commune** — ou de
l'**arrondissement** pour Paris/Lyon/Marseille (déduit du code INSEE `comc`). Pour un
arrondissement, une **2ᵉ courbe « ville entière »** donne en plus l'évolution de la ville
complète. Source : INSEE, *base communale séries historiques du recensement*
(<https://www.insee.fr/fr/statistiques/fichier/8582555/base-cc-serie-historique-2022_csv.zip>),
qui contient le **vrai historique par arrondissement** (Paris 75101…, Lyon 69381…,
Marseille 13201…) ainsi que les totaux communes, importée via `php import_pop_insee.php`
dans la table `pop_commune`. API : `api/population.php?uai=…`
`{zone, zone_estimated, ville, ville_nom}`.
- L'importeur lit le CSV dans le zip sans l'extension PHP `zip` (mini-lecteur ZIP +
`gzinflate`). `zone_estimated` n'est utilisé qu'en **repli** (zone sans historique
propre) : population de la ville × part de l'arrondissement dans les IRIS 2022.
- `import_pop_hist.php` reste une **alternative** (CSV Opendatasoft, s'arrête ~2014, sans
arrondissements).
## Performance énergétique (DPE — ADEME)
- Dataset `dpe01tertiaire` (DPE des **bâtiments tertiaires** depuis juillet 2021,
~532 000 diagnostics), importé via l'API data-fair de l'ADEME.
- ⚠ Ce jeu de données **n'a pas d'identifiant école (UAI)** : les bâtiments sont
repérés par adresse BAN + géolocalisation. Le rattachement école↔DPE est donc
**géographique** (plus proche bâtiment de la même commune, sous **5 m** par
défaut — soit le même bâtiment / la même adresse), comme le rattachement
école→IRIS. La distance affichée mesure la fiabilité du lien ; `--radius=N`
permet d'élargir.
- Import en deux temps :
```sh
php import_dpe.php # remplit la table « dpe » (long ; --dep=75 pour tester)
php import_comc.php # (si pas déjà fait) code INSEE commune des écoles
php match_dpe.php # rattache les DPE aux écoles (table « etab_dpe » ; --radius=N)
```
- La fiche d'un établissement affiche, dans l'onglet *Informations*, le ou les
**DPE rattachés** : étiquettes **Énergie** et **GES** (A→G, palette officielle),
consommation, émissions, surface, dates et adresse du bâtiment.
- **Coloration de la carte** : un sélecteur dans la légende (en bas à droite)
permet de colorer les marqueurs par **type d'établissement** (défaut) ou par
**étiquette DPE** (A→G ; gris = aucun DPE rattaché). Les étiquettes du
sous-ensemble affiché sont chargées à la demande via `api/dpe.php?action=latest`.
- **Import depuis l'admin** : un super-admin peut lancer l'import + le rattachement
depuis `admin.html` (section *Performance énergétique (DPE)*), avec un département
optionnel et un rayon réglable. Opération longue ⇒ pour le dataset complet,
préférez les scripts CLI ci-dessus.
- API : `api/dpe.php?uai=…` (liste triée par distance) et `?action=latest` (batch,
meilleur match par UAI). Logique d'import/rattachement partagée dans `api/dpe_lib.php`.
## Coloration des IRIS (2 modes)
Quand la couche IRIS est active, un sélecteur (en bas à gauche) permet de choisir :
1. **Densité de population** — population 2022 ÷ surface de l'IRIS (palette jaune→rouge).
2. **Taux d'écoliers publics** — **à l'échelle de l'IRIS** : élèves des écoles publiques
(1er degré, dernière rentrée) **situées dans l'IRIS** ÷ population de l'IRIS,
en palette **verte** (vert pâle = faible taux → vert foncé = fort taux). Chaque école
est rattachée à l'IRIS qui la contient (point-dans-polygone, côté navigateur).
Chaque mode a sa propre légende. Données : `api/iris.php` (population), `api/effectifs.php?action=latest`
(effectifs des écoles). Le rattachement école→IRIS et le calcul du taux se font côté client sur la zone visible.
## Filtres disponibles
- Recherche texte (nom d'établissement / commune), insensible aux accents
- Type d'établissement (École, Collège, Lycée, Médico-social, EREA…)
- Statut (Public / Privé)
- État (OUVERT / À FERMER)
- Région → Académie → Département → Circonscription (sélecteurs en cascade)
- Options proposées : Restauration, Internat, ULIS, SEGPA (logique ET ; coché = établissements proposant l'option)
> Note : la colonne `apprentissage` existe dans la source mais n'y est jamais renseignée (0 établissement), elle n'est donc pas proposée comme filtre.
Les points sont regroupés (clustering) pour rester fluides à l'échelle nationale
et colorés par type d'établissement (voir la légende).
## Mettre à jour les données
1. Télécharger l'export à jour (champs réduits) :
```sh
curl "https://data.education.gouv.fr/api/explore/v2.1/catalog/datasets/fr-en-annuaire-education/exports/json?select=identifiant_de_l_etablissement,nom_etablissement,type_etablissement,statut_public_prive,libelle_region,libelle_academie,libelle_departement,code_departement,nom_commune,code_postal,adresse_1,telephone,mail,web,etat,nom_circonscription,restauration,hebergement,ulis,segpa,apprentissage,latitude,longitude" -o raw_export.json
```
2. Recompacter vers `data/etablissements.json` :
```sh
php build_data.php
```
## Fichiers
| Fichier | Rôle |
|---|---|
| `index.html` | Page carte (sidebar + overlay fiche) |
| `admin.html` · `admin.js` | Espace d'administration (connexion, modération, arbre, admins, import) |
| `style.css` | Mise en forme (carte, fiche, galeries, admin, pages d'auth) |
| `app.js` | Chargement (API), filtres en cascade, clustering, popups |
| `account.html` · `forgot.html` · `reset.html` | Parcours mot de passe (changer / oublié / réinitialiser) |
| `pwpolicy.js` | Politique de mot de passe côté client (validation + jauge) |
| `fiche.js` | Fiche plein écran : infos, documents (arbre), photothèque, proposition |
| `setup.php` | Crée base + tables, super-admin, arbre d'exemple, importe les établissements |
| `api/db.php` | PDO MySQL + JSON + session/auth + import partagé |
| `api/auth.php` | Connexion / déconnexion / utilisateur courant |
| `api/etablissements.php` | GET : tous les établissements (sert la carte) |
| `api/items.php` | Éléments (fichiers/photos/liens) : liste, soumission, validation, suppression |
| `api/folders.php` | Arborescence globale (CRUD, super-admin) |
| `api/users.php` | Admins de périmètre + leurs communes (super-admin) |
| `api/groups.php` | Groupes d'utilisateurs : membres, communes, droit de validation (super-admin) |
| `api/comments.php` | Commentaires de fiche (horodatés, visibles par groupe) |
| `api/communes.php` | Autocomplétion des communes (affectation de périmètre) |
| `api/import.php` | Import de la dernière version de l'opendata (super-admin) |
| `api/iris.php` | Population INSEE 2022 d'un IRIS et de sa commune (+ densité par batch) |
| `api/effectifs.php` | Historique des effectifs d'un établissement + `?action=latest` (batch) + `?action=commune` (cumul commune) |
| `api/ips.php` | Historique de l'IPS (indice de position sociale) d'une école |
| `api/population.php` | Historique de la population municipale (INSEE) de la commune |
| `api/dpe.php` | DPE rattachés à un établissement (`?uai=`) + `?action=latest` (batch, meilleur match) |
| `api/dpe_lib.php` | Logique partagée DPE (création tables, import data-fair, rattachement géo) |
| `api/import_dpe.php` | Import DPE + rattachement déclenché depuis l'admin (super-admin) |
| `import_dpe.php` | Importe les DPE tertiaires de l'ADEME dans `dpe` (CLI ; appelle `dpe_lib`) |
| `match_dpe.php` | Rattache les DPE aux écoles par proximité géographique (CLI ; table `etab_dpe`) |
| `import_pop.php` | Importe les populations INSEE 2022 dans `iris_pop` (voir en-tête du script) |
| `import_effectifs.php` | Importe les effectifs des écoles (1er degré) dans `effectifs` |
| `import_ips.php` | Importe l'IPS (indice de position sociale) des écoles, collèges et lycées dans `ips` |
| `import_pop_insee.php` | Importe l'historique des populations INSEE (base séries historiques, communes **et arrondissements**, 1968→2022) dans `pop_commune` |
| `import_pop_hist.php` | Variante : historique des populations via CSV Opendatasoft (~2014) → `pop_commune` |
| `import_effectifs_2d.php` | Importe les effectifs des collèges et lycées dans `effectifs` |
| `import_comc.php` | Renseigne le code INSEE commune (`comc`) des établissements |
| `uploads/files/` · `uploads/photos/` | Fichiers et photos stockés sur disque |
| `build_data.php` | Convertit `raw_export.json` → `data/etablissements.json` (source du premier import) |
| `data/etablissements.json` | Jeu de données importé en base par `setup.php` |
| `raw_export.json` | Export brut temporaire (supprimable après build) |