Skip to main content

Carnet de liaison — détails techniques

Fiche fonctionnelle

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.

Journal unifié — vérifié (manuel + Playwright), captures produites

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.

SurfaceTraitement 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ésexcerpt() retire les balises avant de tronquer

Détection HTML. Un corps est traité comme HTML s'il commence par une balise de bloc (p, h1h6, 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.

Éditeur avancé — vérifié (Playwright), captures produites (2026-09-15)

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 par GET/PUT /schoolbook/api/reminder-settings (workflow schoolbook.message.create).
  • Snapshot par message : à l'envoi, les colonnes auto_reminder, reminder_delay_hours, max_reminders sont figées sur la ligne schoolbook.messages depuis les préférences de l'enseignant (une valeur explicite dans la requête prime). reminder_count et last_reminder_at tracent les relances effectuées.
  • Worker planifié : ReminderWorker déclenché par un CronTrigger (config reminder-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 destinataires PENDING, puis renotifie ces destinataires (timeline schoolbook.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 un recipientId optionnel : fourni → relance ce seul destinataire (parent par parent) ; absent → relance tous les destinataires PENDING.
  • Le suivi des réponses (GET /schoolbook/api/messages/:id/recipients) résout les identifiants de destinataires en noms via l'annuaire Neo4j (champ displayName, 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.listForRelative résout désormais aussi la structureId de chaque enfant (requête Cypher étendue), nécessaire pour interroger les modules presences/ incidents qui exigent ce paramètre.
    • GET /schoolbook/api/messages accepte un paramètre childId optionnel (MessageService.listForChild) pour filtrer les mots d'un seul enfant dans la vue Journal.
    • SchoolbookViewController expose la route serveur /journal (miroir obligatoire de la route client react-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ées GET /presences/students/:id/events et GET /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é) si presences/incidents ne 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. FicheServiceBusImpl interroge l'adresse clusterisée workflowhub.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é un childId. 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é : withPayload n'est vrai que pour la famille. Pour un personnel, le contrôleur retire en plus le payload de autorisation-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 /fiche et /equipe dans SchoolbookViewController — sans elles, un accès direct ou un F5 renvoie 404 avant d'atteindre la SPA.
  • Année scolaire : FicheService.anneeScolaireCourante bascule 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 (childId obligatoire 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 de ChildrenService). 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.

Web uniquement — l'application mobile n'expose pas ces trois vues

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 externalId de la classe présent dans u.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 les User.
  • Professeur principal : ce n'est pas une relation du graphe mais deux propriétés du compte, headTeacher (AAF) et headTeacherManual (saisie annuaire), qui listent l'externalId des classes concernées — d'où la comparaison sur c.externalId et non sur l'id.
  • Personnels : HAS_FUNCTION (avec rf.scope, comparé à s.id et s.externalId selon les versions) pour la direction (DIR, ADMIN_LOCAL), la documentation (ADMIN_DOCUMENTALISTE) et l'orientation (ADMIN_ORIENTATION) ; HAS_POSITIONUserPosition pour 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 attenduesfiche fonctionnelle
🧪 Tests réaliséscouverture e2e
✅ Tests de conformitétableau de conformité