1. Contexte

Migration des données du webform Drupal "Espace de Dépôt" vers le système BDES.

Source

Drupal Webform (https://dce.presidence.bj)

Destination

BDES (PostgreSQL via API)

Période des données

21/11/2024 - 05/01/2026

2. Extraction des données

2.1. Contraintes techniques

  • Pas d’accès direct à la base de données Drupal

  • Export uniquement via l’interface web Webform

  • Erreur 500 lors du téléchargement des fichiers joints en masse

2.2. Stratégie d’extraction

L’extraction a été réalisée par lots en utilisant la date de création comme filtre pour contourner l’erreur 500.

2.3. Lots d’extraction

Période Statut Nombre de soumissions

21/11/2024 - 30/11/2024

OK

32

01/12/2024 - 05/12/2024

OK

76

06/12/2024 - 08/12/2024

OK

138

09/12/2024 - 09/12/2024

OK

105

10/12/2024 - 10/12/2024

OK

218

11/12/2024 - 11/12/2024

OK

25

12/12/2024 - 14/12/2024

OK

24

15/12/2024 - 30/12/2024

OK

27

31/12/2024 - 31/12/2025

OK

292

01/01/2026 - 05/01/2026

OK

1

Total soumissions extraites : 938

2.4. Statistiques des fichiers

Métrique Valeur

Fichiers JSON présents

938

Plage des IDs

264 → 4643

Plage théorique

4380

IDs manquants

3442

Les IDs manquants correspondent à des soumissions supprimées, d’autres webforms, ou des brouillons non exportés. L’ID de soumission (sid) est global à Drupal, pas au webform.

2.5. Grandes plages d’IDs manquants

Plage Nombre manquant

3352-4364

1013

2357-3343

987

1829-1889

61

4430-4487

58

1654-1703

50

3. Format des données

3.1. Choix du format : JSON

Critère CSV JSON YAML

Structure préservée

Non

Oui

Oui

Parsing programmatique

Difficile

Natif

Natif

Données imbriquées

Texte brut

Arrays

Arrays

Références fichiers

URLs

IDs

IDs

JSON retenu car il préserve la structure hiérarchique des données (enseignements, articles, recherches).

3.2. Limitation : Références des fichiers

Le format JSON exporte les IDs internes Drupal des fichiers, pas leurs URLs.

Exemple JSON
{
    "annee_academique": "2024-2025",
    "code_ec": "1MEH1503",
    "code_ue": "MEH1503",
    "intitule_ec": "Réseau et techniques d'irrigation",
    "joindre_support": "21360",  // <-- ID interne, pas l'URL
    "nature_support": "Syllabus"
}
Équivalent texte brut (avec URL)
Joindre le support: https://dce.presidence.bj/system/files/webform/espace_de_depot/277/formation_43_277_90401378.pdf

3.3. Stratégie de mapping fichiers

Pour associer les IDs aux URLs :

  1. Exporter le format texte brut ou CSV qui contient les URLs complètes

  2. Croiser avec les JSON via le submission_id (présent dans le chemin URL)

  3. Construire une table de correspondance file_id → URL

4. Structure des données

4.1. Schéma d’une soumission

{
  "serial": "344",
  "sid": "344",
  "uuid": "fd5c83fe-b6fa-4451-abb5-97675155db64",
  "created": "1733208750",
  "changed": "1734466474",
  "in_draft": "1",
  "uid": "114",
  "webform_id": "espace_de_depot",
  "data": {
    // Identification
    "nom": "...",
    "prenom": "...",
    "date_de_naissance": "...",
    "numero_matricule": "...",
    "grade": "...",
    "specialite": "...",

    // Établissement
    "nature_de_l_etablissement_de_rattachement": "...",
    "denomination_de_l_etablissement_de_rattachement_upb": "...",
    "denomination_de_l_ufr_ecole": "...",

    // Enseignements (array)
    "ajouter_un_element": [
      {
        "annee_academique": "...",
        "code_ue": "...",
        "code_ec": "...",
        "intitule_ue": "...",
        "intitule_ec": "...",
        "semestre": "...",
        "type_activite": "...",
        "nature_support": "...",
        "joindre_support": "..."  // file_id
      }
    ],

    // Articles (array)
    "article": [...],

    // Recherches (array)
    "travaux_de_recherche_2": [...],

    // Rapports de performance (array)
    "rapport_de_performance": [...]
  }
}

5. Règles de transformation et chargement

5.1. Légende

Symbole Signification

✅

Mapping direct depuis le JSON source

🔄

Transformation ou lookup sur une table de référence

❌

Donnée non disponible dans la source

⚠️

Valeur générée ou par défaut

5.2. Génération des codes

Les codes sont générés automatiquement via un hash SHA256 encodé en base62 :

  • Format : PREFIX_ + 8 caractères (ex: GRADE_iL8S4JVG)

  • Déterministe : même libellé produit toujours le même code

  • Unique : probabilité de collision ~1/218 billions

5.3. Tables de référence

5.3.1. reference.grade_enseignant

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

data.grade

✅ Direct, dédupliqué

niveau_hierarchique

—

❌ Non alimenté

corps_fonction_publique

—

❌ Non alimenté

grade_superieur_code

—

❌ Non alimenté

actif

—

⚠️ Défaut : true

5.3.2. reference.statut_enseignant

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

data.statut

✅ Direct, dédupliqué

description

—

❌ Non alimenté

actif

—

⚠️ Défaut : true

5.3.3. reference.domaine_resao

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

data.domaine_de_la_formation*

✅ Union de tous les champs domaine_de_la_formation_*, dédupliqué

niveau

—

⚠️ Défaut : 999

actif

—

⚠️ Défaut : true

5.3.4. reference.domaine_specialite

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

data.specialite_filiere_de_la_formation

✅ Direct, dédupliqué

domaine_resao_code

data.domaine_de_la_formation

🔄 Lookup sur reference.domaine_resao par libellé

actif

—

⚠️ Défaut : true

5.3.5. reference.type_diplome

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

data.licence, data.master, data.doctorat, data.ingenieur, data.diplome_autre, data.grade_de_la_formation_*

✅ Union de ces champs, dédupliqué

sigle

—

🔄 Extrait entre parenthèses (ex: "Licence (L)" → "L")

actif

—

⚠️ Défaut : true

5.3.6. reference.nature_support_cours

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

data.ajouter_un_element[].nature_support

✅ Direct, dédupliqué

categorie

—

❌ Non alimenté

description

—

❌ Non alimenté

actif

—

⚠️ Défaut : true

5.3.7. reference.type_structure

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

data.type_d_unite_de_formation_et_de_recherche_ufr et data.nature_de_l_etablissement_de_rattachement

✅ Direct, dédupliqué

niveau_hierarchique

—

🔄 Calculé : université=1, faculté/école=2, département=3

actif

—

⚠️ Défaut : true

5.4. Tables golden

5.4.1. golden.personne_physique

Attribut Source JSON Règle

matricule

data.numero_matricule

✅ Direct, clé de déduplication

npi

data.npi

✅ Direct

civilite

data.civilite

🔄 Normalisé : M./Monsieur → M, Mme/Madame → MME, Mlle → MLLE

nom_famille

data.nom

✅ Direct

prenoms

data.prenom

✅ Direct

nom_usage

data.nom

✅ Copie de nom_famille

sexe

data.civilite

🔄 Déduit : M → H, Mme/Mlle → F

genre_code

data.civilite

🔄 Déduit : M → HOMME, Mme/Mlle → FEMME

date_naissance

data.date_de_naissance

✅ Direct

ville_naissance

data.ville_de_naissance

✅ Direct

pays_naissance_code

data.lieu_de_naissance

🔄 Lookup sur reference.ville → reference.pays

nationalite_code

—

⚠️ Défaut : BJ (Bénin)

statut_matrimonial_code

data.situation_matrimoniale

🔄 Lookup sur reference.statut_civil

adresse

data.contact.address

✅ Direct

grade_code

data.grade

🔄 Lookup sur reference.grade_enseignant par libellé

statut_code

data.statut

🔄 Lookup sur reference.statut_enseignant par libellé

echelle_code

data.echelle

🔄 Lookup sur reference.echelle par code/libellé

echelon_code

data.echelon

🔄 Converti : "1" → ECH01, "12" → ECH12

statut

—

⚠️ Défaut : ACTIF

Les lookups sur reference.ville et reference.statut_civil nécessitent que ces tables soient préalablement alimentées.

5.4.2. golden.formation

Attribut Source JSON Règle

code

—

⚠️ Généré : hash de l’intitulé

intitule

data.intitule_de_la_formation*

✅ Direct, dédupliqué

domaine_code

data.domaine_de_la_formation*

🔄 Lookup sur reference.domaine_resao par libellé

type_diplome_code

data.grade_de_la_formation*

🔄 Lookup sur reference.type_diplome par libellé

niveau_diplome_code

—

⚠️ Défaut : N0

credits_totaux

—

⚠️ Défaut : 0 (NOT NULL)

duree_annees

—

⚠️ Défaut : 0 (NOT NULL)

date_creation

—

⚠️ Date d’insertion (NOT NULL)

statut

—

⚠️ Défaut : ACTIF

5.4.3. golden.unite_enseignement

Attribut Source JSON Règle

code

data.ajouter_un_element[].code_ue

✅ Direct, dédupliqué

intitule

data.ajouter_un_element[].intitule_ue

✅ Direct

formation_code

data.intitule_de_la_formation

🔄 Lookup sur golden.formation par intitulé

semestre_id

data.ajouter_un_element[].semestre

🔄 Lookup sur reference.semestre par numéro

credits

—

⚠️ Défaut : 0 (NOT NULL)

coefficient

—

⚠️ Défaut : 1.0 (NOT NULL)

type_ue

—

⚠️ Défaut : FONDAMENTALE (NOT NULL)

obligatoire

—

⚠️ Défaut : true (NOT NULL)

5.4.4. golden.element_constitutif

Attribut Source JSON Règle

code

data.ajouter_un_element[].code_ec

✅ Direct, dédupliqué

intitule

data.ajouter_un_element[].intitule_ec

✅ Direct

ue_code

data.ajouter_un_element[].code_ue

✅ Direct

credits

—

⚠️ Défaut : 0 (NOT NULL)

volume_horaire_cm

—

⚠️ Défaut : 0 (NOT NULL)

volume_horaire_td

—

⚠️ Défaut : 0 (NOT NULL)

volume_horaire_tp

—

⚠️ Défaut : 0 (NOT NULL)

5.4.5. golden.adresse_postale

Attribut Source JSON Règle

personne_matricule

data.numero_matricule

✅ FK vers golden.personne_physique

pays_code

data.contact.country

🔄 Lookup sur reference.pays par libellé, défaut BJ

type_adresse_code

—

⚠️ Défaut : PERSO (NOT NULL)

numero_voie

data.contact.address

✅ Direct (50 premiers caractères, NOT NULL)

nom_voie

—

⚠️ Défaut : N/A (NOT NULL)

complement

data.contact.address_2

✅ Direct

code_postal

data.contact.postal_code

✅ Direct (NOT NULL)

quartier

—

⚠️ Défaut : N/A (NOT NULL)

ville

data.contact.city

✅ Direct (NOT NULL)

principale

—

⚠️ Défaut : true (NOT NULL)

date_debut_validite

—

❌ Non alimenté

date_fin_validite

—

❌ Non alimenté

5.4.6. golden.coordonnees_contact

Attribut Source JSON Règle

personne_matricule

data.numero_matricule

✅ FK vers golden.personne_physique

type_contact_code

—

⚠️ EMAIL ou TEL_MOBILE selon le type (NOT NULL)

valeur

data.contact.email ou data.contact.phone

✅ Direct, 1 ligne par contact (NOT NULL)

principal

—

⚠️ Défaut : true (NOT NULL)

date_debut_validite

—

❌ Non alimenté

date_fin_validite

—

❌ Non alimenté

5.4.7. golden.structure_organisationnelle

Hiérarchie à 2 niveaux depuis les données source :

  1. Établissement : data.denomination_de_l_etablissement_de_rattachement_*

  2. UFR/École : data.denomination_de_l_ufr_*

Attribut Source JSON Règle

code

—

⚠️ Généré : hash du libellé

libelle

Champs hiérarchiques ci-dessus

✅ Direct, dédupliqué (NOT NULL)

sigle

—

🔄 Extrait entre parenthèses, défaut libellé tronqué (NOT NULL)

type_structure_code

data.nature_de_l_etablissement_de_rattachement ou data.type_d_unite_de_formation_et_de_recherche_ufr

🔄 Lookup sur reference.type_structure (NOT NULL)

parent_id

—

🔄 Lookup sur structure parente

adresse

—

⚠️ Défaut : N/A (NOT NULL)

email

—

⚠️ Défaut : contact@etablissement.bj (NOT NULL)

telephone

—

⚠️ Défaut : 00000000 (NOT NULL)

date_creation

—

❌ Non alimenté

date_fermeture

—

❌ Non alimenté

statut

—

⚠️ Défaut : ACTIF (NOT NULL)

L’ordre d’insertion est important : établissements d’abord, puis UFR/écoles.

5.5. Tables non migrées

Sur 21 tables golden, 7 sont alimentées par la migration. Les 14 suivantes ne sont pas migrées :

Entité Raison

golden.etat_civil

Redondante : données intégrées dans personne_physique

golden.affectation_role_personne

Pas de données dans la source Drupal

golden.affectation_fonction_personne

Pas de données dans la source Drupal

golden.piece_identite

Pas de données dans la source Drupal

golden.lien_personne

Pas de données dans la source Drupal

golden.diplome

Pas de données (formation ≠ diplôme obtenu par l’enseignant)

golden.fonction

Pas de données dans la source Drupal

golden.role_metier

Pas de données dans la source Drupal

golden.periode_validite_structure

Pas de données dans la source Drupal

golden.prerequis_formation

Pas de données dans la source Drupal

golden.programme_formation

Pas de données dans la source Drupal

golden.specialites_diplome

Pas de données dans la source Drupal

golden.structure_specialites

Pas de données dans la source Drupal

golden.type_parcours

Pas de données dans la source Drupal

5.6. API Évaluation

5.6.1. POST /api/v1/folders/{folderId}/courses

Attribut données Attribut API Commentaires

data.enseignement_*.code_ec

courseCode

✅ Direct

data.enseignement_*.joindre_support

files[].fileId

✅ Direct (ID interne Drupal)

—

entityId

⚠️ Valeur fixe : 1

data.enseignement_*.nature_support

nature_support

🔄 Lookup NatureSupport par libellé

data.enseignement_*.titre_support

❌ Manquant

data.enseignement_*.type_activite

❌ Manquant

data.enseignement_*.annee_academique

❌ Manquant

5.6.2. POST /api/v1/folders/{folderId}/articles

Attribut données Attribut API Commentaires

data.article.intitule

title

✅ Direct

data.article.journal

baseIndexationArticlesCode

🔄 Lookup par dénomination (à travailler)

data.article.statut

nature

✅ Direct

—

citationsNb

❌ Manquant (défaut : 1)

—

publicationDate

❌ Manquant

—

justification

❌ Manquant

data.article.joindre_article

files[].fileId

✅ Direct

—

entityId

⚠️ Valeur fixe : 1

5.6.3. POST /api/v1/folders/{folderId}/reviews

Attribut données Attribut API Commentaires

data.rapport_de_performance_*.reference

title

✅ Direct

data.rapport_de_performance_*.titre_rapport

references

✅ Direct

data.rapport_de_performance_*.joindre_rapport

files[].fileId

✅ Direct

—

entityId

⚠️ Valeur fixe : 1

5.6.4. POST /api/v1/folders/{folderId}/researches

Attribut données Attribut API Commentaires

data.travaux_de_recherche_*.titre_article

title

✅ Direct

—

organizationCode

❌ Manquant

data.travaux_de_recherche_*.auteurs

authors

✅ Direct

data.travaux_de_recherche_*.statut

status

✅ Direct

—

publicationDate

❌ Manquant

data.travaux_de_recherche_*.titre_revue

reviewTitle

✅ Direct

—

reviewNumber

❌ Manquant (défaut : 1)

data.travaux_de_recherche_*.doi

doi

✅ Direct

—

keywords

❌ Manquant

—

summary

❌ Manquant

data.travaux_de_recherche_*.pages

startPage

🔄 Parser le champ pages

data.travaux_de_recherche_*.pages

endPage

🔄 Parser le champ pages

data.travaux_de_recherche_*.issn

issn

✅ Direct

data.travaux_de_recherche_*.isbn

isbn

✅ Direct

data.travaux_de_recherche_*.telechargement

files[].fileId

✅ Direct

—

entityId

⚠️ Valeur fixe : 1

data.travaux_de_recherche_*.num_volume

❌ Manquant

data.travaux_de_recherche_*.annee

❌ Manquant

6. Remarques et questions ouvertes

6.1. Attributs manquants dans l’API

Entité API Attributs à ajouter

personnes

npi, echelle, echelon, civilite

articles

annee_academique

researches

num_volume, annee

courses

titre_support, type_activite

6.2. Entités potentiellement requises

Entité Justification

corpsEnseignant

Gestion des corps de la fonction publique enseignante

typeActivite

Types d’activités pédagogiques (Cours théorique, TD, TP, etc.)

titreSupport

Titres des supports pédagogiques

6.3. Questions de conception

Question

Pourquoi personne_matricule dans personnes ? Quelle est son utilité ?

annee_de_recrutement : doit-il être uniquement dans affectation-role-personnes ou aussi dans personnes ?

domaine et domaine_specialite ne sont pas liées dans le modèle actuel. Est-ce intentionnel ?

domaineSpecialiteCode ne devrait-il pas remplacer domaineCode dans formations ?

Renommer composants en unites_enseignement (UE) ? Le terme actuel est trompeur.

C’est quoi typeUe dans composants ? Valeurs possibles ?

Le format de validation des contacts (email, téléphone) doit-il être dans reference.type_contact ?

C’est quoi entityId dans les objets APIs évaluation ? Toujours 1 ?

7. Script de chargement MDM

Le chargement des données est orchestré par le script run_migration.py du projet bdes-migration.

7.1. Prérequis

  • Base de données referentiel-service existante

  • Migrations Flyway V1 à V5 appliquées (schémas staging, golden, reference, mdm)

  • Fonctions mdm.reconcile_*() disponibles

  • Fichiers JSON des soumissions Drupal

7.2. Composants

Composant Description

run_migration.py

Orchestrateur principal, gère l’ordre de chargement

mdm/db.py

Connexion PostgreSQL et exécution des requêtes

mdm/reconcile.py

Appels aux fonctions mdm.reconcile_*() SQL

mdm/utils.py

Gestion des triggers, utilitaires d’affichage

loaders/*.py

Un loader par entité (extraction et transformation)

loaders/base.py

Classe abstraite BaseLoader avec generate_code()

7.3. Flux d’exécution

JSON Drupal → Loader.extract() → staging.* (PENDING) → mdm.reconcile_*() → golden.*/reference.* (PROCESSED)
Documentation complète : bdes-migration/README.md

7.4. Statistiques d’extraction

Onglet Enregistrements Description

grades

13

Grades uniques

domaines_resao

25

Domaines RESAO uniques

domaines_specialites

443

Spécialités par domaine

personnes

590

Enseignants uniques (par matricule)

etat_civils

590

États civils

adresses_postales

590

Adresses postales

coordonnees_contacts

1155

Emails + téléphones (2 lignes max/personne)

affectations

590

Affectations établissements

formations

441

Formations uniques (par intitulé)

composants_ue

1301

Unités d’enseignement uniques

elements_ec

1511

Éléments constitutifs uniques

nature_supports

169

Types de supports uniques

courses

5758

Enseignements (API évaluation)

articles

4378

Articles (API évaluation)

reviews

1389

Rapports de performance (API évaluation)

researches

665

Travaux de recherche (API évaluation)

types_structure

9

Types de structure (Faculté, École, etc.)

structures_organisationnelles

46

Structures hiérarchiques

7.5. Statistiques MDM (dernière exécution)

Table Staging Golden Remarque

reference.grade_enseignant

12

12

✅ Complet

reference.statut_enseignant

15

15

✅ Complet

reference.domaine_resao

25

21

⚠️ 4 PENDING (doublons ou invalides)

reference.nature_support_cours

166

157

⚠️ 9 PENDING

reference.type_diplome

65

65

✅ Complet

reference.domaine_specialite

795

715

⚠️ 80 PENDING (domaine_resao_code manquant)

reference.type_structure

16

16

✅ Complet

golden.personne_physique

590

582

⚠️ 8 PENDING (données manquantes)

golden.formation

572

522

⚠️ 50 PENDING (domaine_code ou type_diplome manquant)

golden.unite_enseignement

1331

1171

⚠️ 160 PENDING (formation_code manquant)

golden.element_constitutif

1583

1405

⚠️ 178 PENDING (ue_code manquant)

golden.adresse_postale

555

547

⚠️ 8 PENDING (personne_matricule manquant)

golden.coordonnees_contact

1155

1139

⚠️ 16 PENDING (personne_matricule manquant)

golden.structure_organisationnelle

39

26

⚠️ 13 PENDING (type_structure_code manquant)

Les enregistrements PENDING n’ont pas été réconciliés car une FK est manquante ou invalide. Ils restent en staging pour correction manuelle ou enrichissement des tables de référence.

8. Historique

Date Action

05/01/2026

Extraction initiale des 938 soumissions JSON

06/01/2026

Ajout des règles de transformation (tableaux de mapping API)

07/01/2026

Refonte section "Règles de transformation" : architecture MDM staging → golden avec détail des attributs