Carnet de liaison — détails techniques
Description et fonctionnalités de ce module : Carnet de liaison.
Couverture de tests
5 scénarios e2e couvrent ce module — dossier apps/open-ent-e2e/src/modules/42_carnet_de_liaison.
Le premier sert aussi de seed : il crée de vrais mots de liaison que l'espace parent
(application mobile) consomme.
18 test(s) e2e dans 5 fichiers — voir le détail
Carnet de liaison — enseignant 01_carnet_de_liaison.spec.ts
- accès au module
- création de messages de liaison (seed)
- liste des messages envoyés
Carnet de liaison — renvoi automatique 02_renvoi_automatique.spec.ts
- paramétrage du renvoi automatique
- composer un message avec renvoi automatique
- suivi du renvoi automatique sur un message envoyé
- relance manuelle : tous, ou parent par parent
- suivi des accusés : noms des destinataires
Carnet de liaison — journal (responsable) 03_journal.spec.ts
- accès au journal
- sélection d'un enfant et chronologie
Carnet de correspondance — autorisations, régime et équipe (responsable) 04_autorisations_et_equipe.spec.ts
- autorisations & régime : les trois démarches du carnet sont listées
- autorisations & régime : chaque démarche porte un état lisible
- équipe éducative : rubriques de l'établissement
- les deux onglets sont accessibles depuis la navigation du module
Carnet de correspondance — règlement intérieur (élève) 04_autorisations_et_equipe.spec.ts
- l'élève voit son propre règlement et l'état des deux paraphes
Carnet de liaison — éditeur avancé 05_editeur_avance.spec.ts
- rédaction d'un mot mis en forme avec une pièce jointe (enseignant/chef)
- la famille lit le mot mis en forme (parent — pièce jointe servie en 200)
Le cycle complet de ces démarches (création depuis le modèle, diffusion à une classe, réponse
de la famille, décision, puis lecture depuis le carnet par le bus) est couvert côté WorkflowHub :
apps/open-ent-e2e/src/modules/45_workflowhub/07_autorisations_permanentes.spec.ts, et celui du
double paraphe du règlement — élève puis responsable, avec trois comptes réels — par
08_reglement_double_paraphe.spec.ts. Voir Démarches — détails techniques.
Carnet de liaison — journal (responsable) 03_journal.spec.ts (profil parent, compte démo
BFC sophie.bernard cf. demo-openent/README-BFC.md) : 3/3 tests passés contre l'instance
locale (npx playwright test --project=parent 03_journal.spec.ts). Captures produites par ce run
et publiées dans docs/static/img/test-shots/42/9.png et 10.png (chronologie vide pour un enfant,
mot réel affiché pour l'autre).
Contenu riche (éditeur avancé)
Le champ body d'un mot de liaison (schoolbook.messages.body, TEXT) contient désormais le
HTML produit par l'éditeur tiptap de @open-ent/react/editor (composant Editor). Aucune
migration SQL : la colonne n'avait pas de limite de taille. Les mots antérieurs, en texte brut,
restent valides.
| Surface | Traitement du body |
|---|---|
Rédaction web (ComposePage) | <Editor mode="edit" visibility="protected"> → editor.getHTML() (vide si editor.isEmpty) |
Détail web (MessageDetailPage) | <Editor mode="read" variant="ghost"> ; toEditorContent() convertit un ancien mot brut en paragraphes échappés |
Cartes de liste (MessageCard) | toPlainText() (DOMParser, document inerte) ; « Contenu multimédia » si le mot n'a que des médias |
| Application mobile (espace parent) | liaisonBodyToText() dans experiences/primaire/services/mapping.ts : texte + mention des médias à ouvrir sur l'ENT |
| Tableau de bord — mots signalés | excerpt() retire les balises avant de tronquer |
Détection HTML. Un corps est traité comme HTML s'il commence par une balise de bloc
(p, h1…h6, ul, ol, img, video, audio, div…) — c'est toujours le cas de la sortie
tiptap. Un mot brut contenant « <demain> » reste donc du texte.
Sûreté du rendu. Le HTML n'est jamais injecté tel quel (dangerouslySetInnerHTML absent) :
en lecture, l'éditeur re-parse le contenu à travers le schéma ProseMirror, qui n'instancie que les
nœuds et attributs connus de ses extensions. Le serveur ne filtre pas le HTML ; tout nouveau
consommateur du body doit soit passer par Editor mode="read", soit en extraire le texte.
Médias du workspace. La médiathèque de l'éditeur verse les fichiers déposés en visibilité
protected, et recopie en protected (POST /workspace/documents/transfer) les documents
personnels choisis dans l'espace documentaire. Un document protected est lisible par tout
utilisateur authentifié : les responsables destinataires ouvrent la pièce jointe sans partage
explicite. Les onglets de la médiathèque dépendent des droits de l'auteur (dépôt :
WorkspaceController|addDocument ; capture vidéo : application Vidéo).
Hors périmètre. Les mots rapides (quick_messages.body, contrainte char_length <= 1000,
aperçu de notification, fil de conversation mobile) restent en texte simple, avec leur photo jointe.
05_editeur_avance.spec.ts : 4/4 passés contre l'instance locale — profil chef
(emael.shafy001, élève E2E_SCHOOLBOOK_STUDENT='Abd-Samad') puis profil parent
(E2E_PARENT_USER=karim.corbett001), dans un même run pour que le parent lise le mot créé.
Le parent télécharge la pièce jointe (GET /workspace/document/:id → 200) sans partage explicite.
Non-régression 02_renvoi_automatique.spec.ts : 6/6. Captures publiées dans
static/img/test-shots/42/ (editeur-mise-en-forme, editeur-piece-jointe,
detail-contenu-riche, lecture-famille-contenu-riche). Non simulable en headless :
enregistrement micro (dictaphone) et caméra.
Renvoi automatique des mots non consultés
Fonctionnalité ajoutée au module schoolbook (migration SQL 003-auto-reminders.sql).
- Préférences enseignant : table
schoolbook.reminder_settings(par expéditeur + établissement) —enabled,delay_hours(1–720),max_reminders(1–10). Exposées parGET/PUT /schoolbook/api/reminder-settings(workflowschoolbook.message.create). - Snapshot par message : à l'envoi, les colonnes
auto_reminder,reminder_delay_hours,max_reminderssont figées sur la ligneschoolbook.messagesdepuis les préférences de l'enseignant (une valeur explicite dans la requête prime).reminder_countetlast_reminder_attracent les relances effectuées. - Worker planifié :
ReminderWorkerdéclenché par unCronTrigger(configreminder-cron, par défaut horaire). Il balaie les messages dont le délai est dépassé, qui n'ont pas atteint le plafond de relances et qui ont encore des destinatairesPENDING, puis renotifie ces destinataires (timelineschoolbook.liaison_reminder+ push) et incrémente le compteur. - La relance manuelle (
POST /schoolbook/api/messages/:id/remind) réutilise le même chemin de notification. Le corps accepte unrecipientIdoptionnel : fourni → relance ce seul destinataire (parent par parent) ; absent → relance tous les destinatairesPENDING. - Le suivi des réponses (
GET /schoolbook/api/messages/:id/recipients) résout les identifiants de destinataires en noms via l'annuaire Neo4j (champdisplayName, repli sur l'id) — y compris pour des parents qui ne se sont jamais connectés.
Les scénarios de rédaction/envoi s'exécutent sur l'établissement de référence cd16-primaire (E.P.PU-JULES FERRY-PONTIVY, classe de CE2) : ils requièrent les données primaire (élève + familles) absentes des autres environnements. Les captures de cette page en sont issues.
➡️ Statut dans le dashboard E2E · Résultats détaillés
Journal unifié (carnet de correspondance)
Agrège, pour un élève donné, les mots du carnet de liaison avec les événements d'autres modules de vie scolaire — sans dupliquer leur stockage : chaque module reste la source de vérité de ses propres événements.
- Backend
schoolbook:ChildrenServiceNeo4jImpl.listForRelativerésout désormais aussi lastructureIdde chaque enfant (requête Cypher étendue), nécessaire pour interroger les modulespresences/incidentsqui exigent ce paramètre.GET /schoolbook/api/messagesaccepte un paramètrechildIdoptionnel (MessageService.listForChild) pour filtrer les mots d'un seul enfant dans la vue Journal.SchoolbookViewControllerexpose la route serveur/journal(miroir obligatoire de la route clientreact-router-dom— ce contrôleur n'a pas de catch-all/*, une route cliente sans son miroir serveur renvoie un 404 direct).
- Frontend
schoolbook(JournalPage.tsx, route/journal) : appelle directement, avec la session du navigateur (même domaine, reverse-proxy), les routes déjà sécuriséesGET /presences/students/:id/eventsetGET /incidents/students/:id/events, puis fusionne le résultat avec les mots du carnet dans une chronologie triée par date. L'extraction des champs bruts (start_date,end_date,reason.label…) est défensive (schéma hétérogène selon le type d'événement) et dégrade proprement (alerte d'indisponibilité) sipresences/incidentsne sont pas installés sur l'établissement. - Limite connue : réservé aux responsables (
GET /children) ; un élève consultant son propre journal nécessiterait une route dédiée, non développée. La vue « Autorisations & régime » ci-dessous l'ouvre en revanche à l'élève (ChildrenService.getStudent).
Autorisations permanentes & régime scolaire
Vue en lecture seule, par enfant, des trois démarches WorkflowHub reprises du carnet papier
(autorisation-sorties, autorisation-familiale, regime-scolaire). Le module schoolbook
ne stocke rien de ces démarches : aucune table, aucune migration SQL — WorkflowHub en reste la
source de vérité, exactement comme presences/incidents le sont pour le journal.
- Transport : bus d'événements, pas HTTP.
FicheServiceBusImplinterroge l'adresse clusteriséeworkflowhub.autorisations({action: "autorisations-eleves" | "demarches-ouvertes", …}). Passer par HTTP ferait dépendre la décision de la session du navigateur, alors que le contrôle d'accès est déjà fait côtéschoolbook. Toute erreur de bus — y compris l'absence de WorkflowHub sur la plateforme — est traduite en réponse vide (timeout 5 s) : la page s'affiche sans les démarches plutôt que de renvoyer une erreur. - Contrôle d'accès (
FicheController+Perimetre) : le périmètre est recalculé depuis l'annuaire, jamais celui demandé par le client. Responsable → ses enfants (RELATED) ; élève → lui-même (ChildrenService.getStudent, requête Cypher ajoutée) ; personnel → les élèves de ses établissements, et seulement après avoir désigné unchildId. Un identifiant hors périmètre renvoie une réponse vide, pas une erreur — répondre « cet élève existe mais vous n'y avez pas droit » renseignerait déjà l'appelant. - Données de santé :
withPayloadn'est vrai que pour la famille. Pour un personnel, le contrôleur retire en plus lepayloaddeautorisation-familiale(sansDonneesDeSante) : l'état de la fiche suffit à la réclamer, son contenu reste au back-office des démarches. - Routes :
GET /schoolbook/api/fiches(+?childId=,?anneeScolaire=) et les routes serveur miroir/ficheet/equipedansSchoolbookViewController— sans elles, un accès direct ou un F5 renvoie 404 avant d'atteindre la SPA. - Année scolaire :
FicheService.anneeScolaireCourantebascule au 1er août (une autorisation signée en juin vaut pour l'année qui s'achève). Testé unitairement (FicheServiceTest, 4 cas). Côté WorkflowHub, une campagne sans année scolaire n'est pas filtrée : le concepteur ne renseigne pas ce champ par défaut, et l'exclure rendait la démarche invisible du carnet sans explication.
Règlement intérieur : le paraphe passe par le carnet
POST /schoolbook/api/reglement/parapher (corps {childId}) est le seul point d'écriture du
carnet vers WorkflowHub. Trois choix y sont structurants :
- Le rôle du paraphe est déduit du profil du compte connecté (
FicheController.roleDeParaphe), jamais de ce que demande le client. Sans cela un responsable pourrait poser le paraphe « élève » et signer à la place de son enfant — précisément ce que la double signature vise à empêcher. Un personnel est refusé (403) : le règlement engage la famille. - Un responsable de plusieurs enfants doit désigner lequel (
childIdobligatoire dès que le périmètre en contient plus d'un) : parapher le premier de la liste serait une signature à l'aveugle. - La majorité de l'élève est calculée depuis
birthDate(ajouté aux requêtes Cypher deChildrenService). Date absente ou illisible ⇒ non majeur : se tromper dans ce sens fait attendre un paraphe de trop, l'inverse validerait un règlement qu'aucun responsable n'a signé.
Contrairement aux lectures, l'échec n'est pas masqué par FicheServiceBusImpl : une famille
qui croit avoir signé alors que rien n'a été enregistré est le pire des cas.
L'application mobile consomme schoolbook/api/messages et /quick-messages (expérience
« primaire », 1er degré). Elle n'appelle ni /fiches, ni /equipe, ni
/reglement/parapher : les onglets « Autorisations & régime » et « Équipe éducative », et donc le
paraphe du règlement, ne sont accessibles que depuis le web. Une famille qui n'utilise que
l'application ne peut pas parapher — à prendre en compte avant d'ouvrir la démarche, et à traiter
si l'usage mobile devient majoritaire au 2nd degré.
Équipe éducative & administration
Lecture pure de l'annuaire (EquipeServiceNeo4jImpl), sans stockage ni ressaisie — une liste
recopiée serait fausse dès la première mutation. Deux requêtes Cypher, exposées par
GET /schoolbook/api/equipe (+ ?childId=), sous le même périmètre que ci-dessus.
- Enseignants de la classe : union des deux rattachements existants — membre du groupe de
profil de la classe et
externalIdde la classe présent dansu.classes(alimenté par l'AAF). Ils ne coïncident pas toujours : sur une classe réelle de l'instance locale, l'un donnait 8 enseignants et l'autre 7. La seconde branche est restreinte à l'établissement, pour éviter un balayage de tous lesUser. - Professeur principal : ce n'est pas une relation du graphe mais deux propriétés du compte,
headTeacher(AAF) etheadTeacherManual(saisie annuaire), qui listent l'externalIddes classes concernées — d'où la comparaison surc.externalIdet non sur l'id. - Personnels :
HAS_FUNCTION(avecrf.scope, comparé às.idets.externalIdselon les versions) pour la direction (DIR,ADMIN_LOCAL), la documentation (ADMIN_DOCUMENTALISTE) et l'orientation (ADMIN_ORIENTATION) ;HAS_POSITION→UserPositionpour les autres rubriques (secrétariat, comptabilité, vie scolaire…), que l'ENT ne code pas en dur. - Aucune adresse e-mail n'est renvoyée : la page renvoie vers
/userbook/annuaire#/<id>, qui porte déjà les règles de visibilité des coordonnées.
Conformité
Évaluation au référentiel Open ENT NG (module schoolbook).
| Maillon de la chaîne qualité | Référence |
|---|---|
| 🎯 Fonctionnalités attendues | fiche fonctionnelle |
| 🧪 Tests réalisés | couverture e2e |
| ✅ Tests de conformité | tableau de conformité |