From a41951d4e6a0d1454d017e4cebf7ce53a4e90434 Mon Sep 17 00:00:00 2001 From: Adam Belkacemi Date: Tue, 13 Oct 2026 16:14:36 +0200 Subject: [PATCH] =?UTF-8?q?peuple=20la=20banque=20de=20quiz=20avec=20du=20?= =?UTF-8?q?vrai=20contenu,=20rang=C3=A9=20par=20th=C3=A8me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un quiz.gift par séquence (01 à 07, le 08 existait déjà), lu par un nouveau service LecteurGift et synchronisé par quiz:synchroniser-banque (même esprit que labs:synchroniser) vers un propriétaire technique jamais archivé (BanqueOfficielle), pour ne pas reproduire la dette du quiz partagé qui disparaît à la purge RGPD. /banque-quiz regroupe maintenant par thème comme /bibliotheque. Co-Authored-By: Claude Sonnet 5 --- ansible/roles/portail/tasks/03-code.yml | 4 + .../roles/portail/tasks/08-banque-quiz.yml | 29 ++++++++ ansible/roles/portail/tasks/main.yml | 3 + docs/dette.md | 3 +- docs/journal.md | 24 ++++++ paillasse-contenus/01-shell-droits/quiz.gift | 29 ++++++++ .../02-reseau-paquets-ip/quiz.gift | 29 ++++++++ .../03-web-client-serveur/quiz.gift | 29 ++++++++ paillasse-contenus/04-processus/quiz.gift | 29 ++++++++ paillasse-contenus/05-routage-ospf/quiz.gift | 27 +++++++ .../06-chiffrement-https/quiz.gift | 31 ++++++++ paillasse-contenus/07-sql/quiz.gift | 31 ++++++++ .../Commands/SynchroniserBanqueQuiz.php | 74 +++++++++++++++++++ .../Http/Controllers/BanqueQuizController.php | 19 +++-- portail/app/Models/Quiz.php | 2 +- .../app/Services/Quiz/BanqueOfficielle.php | 37 ++++++++++ portail/app/Services/Quiz/LecteurGift.php | 72 ++++++++++++++++++ portail/config/contenus.php | 33 +++++++++ ...0_13_130000_add_theme_to_quizzes_table.php | 27 +++++++ portail/resources/views/quiz/banque.blade.php | 62 ++++++++++------ .../Feature/BanqueQuizSynchronisationTest.php | 66 +++++++++++++++++ 21 files changed, 628 insertions(+), 32 deletions(-) create mode 100644 ansible/roles/portail/tasks/08-banque-quiz.yml create mode 100644 paillasse-contenus/01-shell-droits/quiz.gift create mode 100644 paillasse-contenus/02-reseau-paquets-ip/quiz.gift create mode 100644 paillasse-contenus/03-web-client-serveur/quiz.gift create mode 100644 paillasse-contenus/04-processus/quiz.gift create mode 100644 paillasse-contenus/05-routage-ospf/quiz.gift create mode 100644 paillasse-contenus/06-chiffrement-https/quiz.gift create mode 100644 paillasse-contenus/07-sql/quiz.gift create mode 100644 portail/app/Console/Commands/SynchroniserBanqueQuiz.php create mode 100644 portail/app/Services/Quiz/BanqueOfficielle.php create mode 100644 portail/app/Services/Quiz/LecteurGift.php create mode 100644 portail/config/contenus.php create mode 100644 portail/database/migrations/2026_10_13_130000_add_theme_to_quizzes_table.php create mode 100644 portail/tests/Feature/BanqueQuizSynchronisationTest.php diff --git a/ansible/roles/portail/tasks/03-code.yml b/ansible/roles/portail/tasks/03-code.yml index 25c01f4..d68432d 100644 --- a/ansible/roles/portail/tasks/03-code.yml +++ b/ansible/roles/portail/tasks/03-code.yml @@ -123,6 +123,10 @@ - { cle: "OS_APPLICATION_CREDENTIAL_SECRET", valeur: "" } - { cle: "REDIS_PREFIX", valeur: "" } - { cle: "LABS_DOSSIER", valeur: "/opt/labs" } + # Banque de quiz officielle (quiz:synchroniser-banque) : contenu + # éditorial source (cours, TD, quiz), pas seulement les lab.yaml + # déployés — voir 08-banque-quiz.yml. + - { cle: "CONTENUS_DOSSIER", valeur: "/opt/paillasse-contenus" } # > timeout des superviseurs Horizon (300 s, config/horizon.php) - { cle: "REDIS_QUEUE_RETRY_AFTER", valeur: "330" } # Étape 12 : HTTPS. Cookies de session envoyés en HTTPS seulement. diff --git a/ansible/roles/portail/tasks/08-banque-quiz.yml b/ansible/roles/portail/tasks/08-banque-quiz.yml new file mode 100644 index 0000000..8443073 --- /dev/null +++ b/ansible/roles/portail/tasks/08-banque-quiz.yml @@ -0,0 +1,29 @@ +--- +# Banque de quiz officielle (demande d'Adam, 2026-10-13) : paillasse-contenus/ +# est le contenu éditorial source (cours, TD, quiz), synchronisé à part des +# lab.yaml déployés (voir 04-labs.yml), lu par +# App\Services\Quiz\LecteurGift pour peupler la banque. +- name: créer le dossier du contenu éditorial + ansible.builtin.file: + path: /opt/paillasse-contenus + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +- name: synchroniser le contenu éditorial + become: false + ansible.posix.synchronize: + src: "{{ playbook_dir }}/../paillasse-contenus/" + dest: /opt/paillasse-contenus/ + delete: true + +# Ne supprime jamais un quiz officiel : seules ses questions sont +# remplacées à chaque synchronisation (lui seul les écrit). +- name: synchroniser la banque de quiz officielle + ansible.builtin.command: + cmd: runuser -u www-data -- php artisan quiz:synchroniser-banque + args: + chdir: "{{ portail_dossier_app }}" + register: portail_banque_quiz_synchronisee + changed_when: "'créé' in portail_banque_quiz_synchronisee.stdout" diff --git a/ansible/roles/portail/tasks/main.yml b/ansible/roles/portail/tasks/main.yml index f8947e3..529f932 100644 --- a/ansible/roles/portail/tasks/main.yml +++ b/ansible/roles/portail/tasks/main.yml @@ -11,6 +11,9 @@ - name: labs (copie de travail des lab.yaml) ansible.builtin.import_tasks: 04-labs.yml +- name: banque de quiz officielle (contenu éditorial de paillasse-contenus/) + ansible.builtin.import_tasks: 08-banque-quiz.yml + - name: Nginx ansible.builtin.import_tasks: 05-nginx.yml diff --git a/docs/dette.md b/docs/dette.md index f98aa95..01056ee 100644 --- a/docs/dette.md +++ b/docs/dette.md @@ -29,4 +29,5 @@ retirer la ligne (et le noter dans le journal) quand c'est fait. | Image Jitsi figée sans procédure de mise à jour | `stable-11248` : comme pour Kuma, prévoir la montée de version par Ansible (variable `visio_version`) et rejouer `visio-e2e.mjs`. | Bloc B | Étape 15 | | Self-healing : pas d'alerte Prometheus dédiée | `syllabus_vm_panne_generale` et `syllabus_vm_irreparables` sont publiées mais sans règle d'alerte ; seul le prof voit une VM irréparable dans la salle d'attente. | Avant le pilote | 2026-10-12 | | Self-healing : ne couvre que les VM (pas le serveur, déjà en dette, ni les conteneurs/services, déjà `Restart=always`) | Documenté dans `docs/runbooks/self-healing.md`, pas une lacune à corriger — rappel pour ne pas le redemander. | — | 2026-10-12 | -| Banque de quiz : un quiz partagé disparaît si la classe d'origine est archivée puis purgée (RGPD, 90 jours) | La banque n'a pas de propriétaire indépendant d'une classe (pas de `user_id` ni de `classe_id` nullable sur `quizzes`, pour rester simple). À corriger si des profs partagent des quiz qu'ils veulent garder au-delà de la durée de vie d'une classe. | Si ça gêne en usage réel | 2026-10-13 | +| Banque de quiz : un quiz partagé disparaît si la classe d'origine est archivée puis purgée (RGPD, 90 jours) | La banque n'a pas de propriétaire indépendant d'une classe (pas de `user_id` ni de `classe_id` nullable sur `quizzes`, pour rester simple). À corriger si des profs partagent des quiz qu'ils veulent garder au-delà de la durée de vie d'une classe. N'affecte pas les quiz officiels (voir ligne suivante). | Si ça gêne en usage réel | 2026-10-13 | +| Banque de quiz officielle : établissement, prof et classe techniques invisibles (« Bibliothèque Paillasse » / « Banque officielle », `App\Services\Quiz\BanqueOfficielle`) | Simple propriétaire stable pour les quiz synchronisés depuis `paillasse-contenus/`, jamais archivé donc jamais purgé — mais apparaîtrait comme un établissement et un prof fictifs dans toute future liste d'administration globale. À filtrer explicitement si un tel écran est ajouté. | Si un écran d'admin globale est ajouté | 2026-10-13 | diff --git a/docs/journal.md b/docs/journal.md index 56f7bcc..f12e007 100644 --- a/docs/journal.md +++ b/docs/journal.md @@ -953,3 +953,27 @@ Deux décisions d'Adam avant de coder : rendu en texte libre (pas de dépôt de **Suite** Avec Quiz flash, la banque de quiz et les devoirs maison, trois des quatre fonctions fictives du tableau de bord démo sont maintenant réelles. Reste : Observations (puis Surveillance d'examen, la plus sensible — suivi de mineurs à cadrer avec Adam avant de coder). + +## 2026-10-13 — Peupler la banque de quiz avec du vrai contenu + +Avant de continuer le chantier « vrai cockpit », demande d'Adam : la banque de quiz ne doit pas rester vide, elle doit puiser dans le contenu pédagogique déjà écrit (cours, TD, labs), rangée par thème comme la bibliothèque (`/bibliotheque`). + +**Fait** + +- Un fichier `quiz.gift` (format GIFT, déjà utilisé par le lab 08) écrit pour chacune des séquences 01 à 07 (01-shell-droits, 02-reseau-paquets-ip, 03-web-client-serveur, 04-processus, 05-routage-ospf, 06-chiffrement-https, 07-sql) — 6 à 7 questions QCM ou vrai/faux chacun, relues pour être justes (pas de question piège ni de réponse approximative). +- `App\Services\Quiz\LecteurGift` : lit un `quiz.gift` en questions structurées. Ignore avec un avertissement le type numérique du GIFT (`{#N}`, présent dans le lab 08) : non supporté par le schéma de questions_quiz, pas question d'inventer une réponse pour le faire rentrer. +- `App\Services\Quiz\BanqueOfficielle` : un établissement, un prof et une classe « Banque officielle » créés une seule fois (`firstOrCreate`), jamais archivés — propriétaire stable des quiz officiels, pour ne pas reproduire la dette du quiz partagé qui disparaît si sa classe d'origine est purgée (RGPD, 90 jours). +- `quiz:synchroniser-banque` (même esprit que `labs:synchroniser`) : lit `paillasse-contenus/`, crée ou met à jour un quiz par séquence (jamais supprimé), remplace ses questions à chaque passage (lui seul les écrit, aucun prof ne peut les éditer). Colonne `theme` ajoutée à `quizzes` (LIN, NET, SRV, SEC, BDD, selon `config/contenus.php`) pour le groupement. +- `paillasse-contenus/` synchronisé sur le serveur (`/opt/paillasse-contenus`, nouvelle tâche Ansible), comme `labs/` mais pour le contenu éditorial source entier, pas seulement les lab.yaml déployés. +- `/banque-quiz` regroupée par thème (badge + nom, comme `/bibliotheque`), les quiz partagés par des profs (sans thème) à la fin. +- 2 nouveaux tests Pest (84 au total) : création idempotente depuis un `quiz.gift` de test, pas de doublon à une deuxième synchronisation. + +**Vérifié sur le serveur réel** : après déploiement, 8 quiz officiels créés (un par séquence, le lab 08 avec son avertissement attendu sur la question numérique ignorée). Scénario avec un prof jetable (supprimé ensuite) : `/banque-quiz` affiche bien 5 sections par thème (LIN ×2, NET ×3, SRV ×1, SEC ×1, BDD ×1) puis « Partagés par des profs » ; dupliqué le quiz officiel « Un serveur web et son client » (6 questions, 16 choix) dans la classe du prof jetable, copie identique et indépendante créée avec `theme` à nul. Le quiz réel d'Adam (« dfdffd ») et sa classe n'ont pas été touchés. + +**Comprendre** + +Le choix qui évite le plus de dette ici : ne pas réutiliser le mécanisme « partager » existant pour le contenu officiel (qui dépendrait d'une classe de prof, purgeable), mais lui donner un propriétaire à part qui n'est jamais archivé. Le prix à payer est une petite classe technique invisible dans l'usage normal — acceptable parce que documentée et jamais visible d'un vrai prof. + +**Suite** + +Reprendre le chantier « vrai cockpit » : Observations. diff --git a/paillasse-contenus/01-shell-droits/quiz.gift b/paillasse-contenus/01-shell-droits/quiz.gift new file mode 100644 index 0000000..7ca11bb --- /dev/null +++ b/paillasse-contenus/01-shell-droits/quiz.gift @@ -0,0 +1,29 @@ +// Quiz séquence 01 : premiers pas dans le shell et droits (format GIFT) + +::Q1 Créer un dossier::Quelle commande crée le dossier /home/eleve/projet ?{ +=mkdir /home/eleve/projet +~touch /home/eleve/projet +~cd /home/eleve/projet +~rmdir /home/eleve/projet} + +::Q2 chmod 640::Avec les droits 640 sur un fichier, le groupe propriétaire peut :{ +=le lire, mais pas l'écrire ni l'exécuter +~le lire et l'écrire +~l'exécuter seulement +~tout faire} + +::Q3 Appartenance à un groupe::Quelle commande ajoute l'utilisatrice alice au groupe equipe déjà existant ?{ +=usermod -aG equipe alice +~groupadd equipe alice +~chown equipe alice +~adduser equipe alice} + +::Q4 Rendre exécutable::chmod +x sur un script le rend exécutable pour tout le monde, y compris les autres utilisateurs.{T} + +::Q5 chgrp::chgrp change le propriétaire d'un fichier.{F} + +::Q6 Lire un rapport::Quelle commande affiche si le fichier rapport.txt contient le mot « paillasse » ?{ +=grep paillasse rapport.txt +~cat paillasse rapport.txt +~find paillasse rapport.txt +~echo paillasse rapport.txt} diff --git a/paillasse-contenus/02-reseau-paquets-ip/quiz.gift b/paillasse-contenus/02-reseau-paquets-ip/quiz.gift new file mode 100644 index 0000000..6c05a57 --- /dev/null +++ b/paillasse-contenus/02-reseau-paquets-ip/quiz.gift @@ -0,0 +1,29 @@ +// Quiz séquence 02 : configurer le réseau et installer un paquet (format GIFT) + +::Q1 Adresse et masque::Sur un réseau 10.10.0.0/24, l'adresse 10.10.0.11/24 est-elle valide pour un poste ?{T} + +::Q2 Installer un paquet local::Sans accès Internet, quelle commande installe un paquet .deb déjà présent sur la machine ?{ +=dpkg -i paquet.deb +~apt install paquet.deb +~wget paquet.deb +~dpkg -r paquet.deb} + +::Q3 Vérifier qu'un paquet est installé::Quelle commande confirme qu'un paquet Debian est bien installé ?{ +=dpkg -s paquet +~ls /opt/paquets/ +~ping paquet +~ip link show paquet} + +::Q4 Joindre un voisin::Quelle commande teste si deux postes se joignent sur le réseau ?{ +=ping +~dpkg +~ip addr +~tree} + +::Q5 ip addr add::La commande `ip addr add` est permanente après un redémarrage.{F} + +::Q6 Nom d'hôte::Quelle commande affiche le nom d'hôte de la machine locale ?{ +=hostname +~ping +~whoami +~ip route} diff --git a/paillasse-contenus/03-web-client-serveur/quiz.gift b/paillasse-contenus/03-web-client-serveur/quiz.gift new file mode 100644 index 0000000..05a09ce --- /dev/null +++ b/paillasse-contenus/03-web-client-serveur/quiz.gift @@ -0,0 +1,29 @@ +// Quiz séquence 03 : un serveur web et son client (format GIFT) + +::Q1 Démarrer et activer::Quelle commande démarre nginx immédiatement et l'active au démarrage, en une seule fois ?{ +=systemctl enable --now nginx +~systemctl start nginx +~systemctl enable nginx +~systemctl status nginx} + +::Q2 Page par défaut::Où se trouve la page d'accueil par défaut d'nginx sur Debian ?{ +=/var/www/html/ +~/etc/nginx/ +~/home/eleve/ +~/var/log/nginx/} + +::Q3 Voir uniquement les en-têtes::Quelle option de curl affiche seulement les en-têtes HTTP de la réponse ?{ +=-I +~-v +~-o +~-s} + +::Q4 Code de statut::Un serveur web qui répond correctement à une requête renvoie le code HTTP :{ +=200 +~404 +~500 +~301} + +::Q5 Client et serveur::Dans ce lab, le client et le serveur sont la même machine virtuelle.{F} + +::Q6 Redémarrer après une modification::Après avoir changé la page d'accueil d'nginx, il faut redémarrer le service pour que la page soit prise en compte.{F} diff --git a/paillasse-contenus/04-processus/quiz.gift b/paillasse-contenus/04-processus/quiz.gift new file mode 100644 index 0000000..6011258 --- /dev/null +++ b/paillasse-contenus/04-processus/quiz.gift @@ -0,0 +1,29 @@ +// Quiz séquence 04 : processus et services (format GIFT) + +::Q1 Trouver le processus gourmand::Quelle commande trie les processus par consommation CPU décroissante ?{ +=ps aux --sort=-%cpu +~ps aux --sort=+%cpu +~kill -l +~systemctl list-units} + +::Q2 Signal par défaut::La commande `kill ` envoie quel signal par défaut ?{ +=SIGTERM +~SIGKILL +~SIGSTOP +~SIGHUP} + +::Q3 Arrêt forcé::`kill -9 ` envoie un signal que le processus peut ignorer pour se fermer proprement.{F} + +::Q4 Service actif et activé::Quelle commande affiche à la fois si un service tourne maintenant et s'il démarrera au prochain redémarrage ?{ +=systemctl status leservice +~systemctl is-enabled leservice +~ps aux leservice +~journalctl leservice} + +::Q5 Recharger systemd::Après avoir créé un nouveau fichier .service dans /etc/systemd/system/, il faut lancer `systemctl daemon-reload` avant de pouvoir l'activer.{T} + +::Q6 Emplacement des services::Où placer un fichier .service personnalisé ?{ +=/etc/systemd/system/ +~/home/eleve/ +~/var/log/ +~/opt/paquets/} diff --git a/paillasse-contenus/05-routage-ospf/quiz.gift b/paillasse-contenus/05-routage-ospf/quiz.gift new file mode 100644 index 0000000..da54bb5 --- /dev/null +++ b/paillasse-contenus/05-routage-ospf/quiz.gift @@ -0,0 +1,27 @@ +// Quiz séquence 05 : routage dynamique avec OSPF (format GIFT) + +::Q1 Type de protocole::OSPF est un protocole de routage :{ +=dynamique, à état de liens (link-state) +~statique, configuré route par route +~de couche application +~de résolution de noms} + +::Q2 Rôle du routeur::Sans règle de routage, un routeur ne peut transmettre que vers les réseaux directement connectés à ses interfaces.{T} + +::Q3 Convergence::Avec OSPF, si un lien tombe, les routeurs mettent à jour leurs tables de routage automatiquement, sans intervention manuelle.{T} + +::Q4 Vérifier les voisins::Quelle commande affiche les routeurs voisins OSPF découverts ?{ +=show ip ospf neighbor +~show ip route static +~ping voisin +~show interfaces} + +::Q5 Aire OSPF::Dans OSPF, les routeurs d'une même aire (area) échangent leurs informations de topologie entre eux.{T} + +::Q6 Table de routage::Quelle commande affiche la table de routage d'un routeur ?{ +=show ip route +~show ip ospf neighbor +~ping +~traceroute} + +::Q7 Route statique vs dynamique::Une route statique s'adapte automatiquement si la topologie du réseau change.{F} diff --git a/paillasse-contenus/06-chiffrement-https/quiz.gift b/paillasse-contenus/06-chiffrement-https/quiz.gift new file mode 100644 index 0000000..b83d5c6 --- /dev/null +++ b/paillasse-contenus/06-chiffrement-https/quiz.gift @@ -0,0 +1,31 @@ +// Quiz séquence 06 : chiffrer les échanges avec HTTPS (format GIFT) + +::Q1 Rôle du certificat::Un certificat TLS sert à :{ +=prouver l'identité du serveur et permettre le chiffrement de la connexion +~accélérer le chargement des pages +~compresser les données échangées +~remplacer le nom de domaine} + +::Q2 Port HTTPS::Quel port utilise HTTPS par défaut ?{ +=443 +~80 +~22 +~21} + +::Q3 HTTP vs HTTPS::En HTTP simple (sans TLS), le contenu échangé entre le client et le serveur peut être lu en clair par un tiers sur le réseau.{T} + +::Q4 Générer une clé privée et un certificat auto-signé::Quel outil permet de générer une paire clé privée / certificat avec OpenSSL ?{ +=openssl req -x509 -newkey rsa -keyout cle.pem -out cert.pem +~apt install certificat +~ping -T +~curl -X CERT} + +::Q5 Certificat auto-signé::Un navigateur fait automatiquement confiance à un certificat auto-signé, sans avertissement.{F} + +::Q6 Vérifier la configuration TLS::Quelle commande curl force l'utilisation de HTTPS et affiche les détails de la négociation TLS ?{ +=curl -v https://exemple.test +~curl -I http://exemple.test +~ping https://exemple.test +~dpkg -s https} + +::Q7 Redirection::Rediriger le port 80 vers le port 443 permet à un client qui tape l'adresse en HTTP d'être automatiquement basculé en HTTPS.{T} diff --git a/paillasse-contenus/07-sql/quiz.gift b/paillasse-contenus/07-sql/quiz.gift new file mode 100644 index 0000000..9026244 --- /dev/null +++ b/paillasse-contenus/07-sql/quiz.gift @@ -0,0 +1,31 @@ +// Quiz séquence 07 : bases de données et SQL (format GIFT) + +::Q1 Sélectionner des lignes::Quelle instruction SQL renvoie toutes les colonnes de la table eleves ?{ +=SELECT * FROM eleves; +~GET * FROM eleves; +~FIND eleves; +~SHOW eleves;} + +::Q2 Filtrer::Quelle clause permet de ne garder que certaines lignes dans un SELECT ?{ +=WHERE +~ORDER BY +~GROUP BY +~FROM} + +::Q3 Clé primaire::Une clé primaire peut contenir des doublons dans une table.{F} + +::Q4 Jointure::Quelle clause permet de combiner les lignes de deux tables à partir d'une colonne commune ?{ +=JOIN +~UNION +~WHERE +~LIMIT} + +::Q5 Suppression définitive::La commande DELETE FROM table; (sans clause WHERE) supprime toutes les lignes de la table.{T} + +::Q6 Ajouter une ligne::Quelle instruction insère une nouvelle ligne dans une table ?{ +=INSERT INTO table (colonne) VALUES (valeur); +~UPDATE table SET colonne = valeur; +~CREATE TABLE table (colonne); +~ADD table colonne valeur;} + +::Q7 Agrégation::La fonction COUNT(*) dans une requête SQL renvoie la somme des valeurs numériques d'une colonne.{F} diff --git a/portail/app/Console/Commands/SynchroniserBanqueQuiz.php b/portail/app/Console/Commands/SynchroniserBanqueQuiz.php new file mode 100644 index 0000000..8fc2efa --- /dev/null +++ b/portail/app/Console/Commands/SynchroniserBanqueQuiz.php @@ -0,0 +1,74 @@ + $classe->id, 'titre' => $titre], + ['theme' => $themes[$identifiant] ?? null, 'partage' => true], + ); + + $quiz->questions()->delete(); + + foreach ($lecture['questions'] as $ordre => $donnees) { + $question = $quiz->questions()->create([ + 'enonce' => $donnees['enonce'], + 'type' => $donnees['type'], + 'ordre' => $ordre, + 'reponse_vrai_faux' => $donnees['reponse_vrai_faux'], + ]); + + foreach ($donnees['choix'] as $ordreChoix => $choix) { + $question->choix()->create([ + 'texte' => $choix['texte'], + 'est_correct' => $choix['est_correct'], + 'ordre' => $ordreChoix, + ]); + } + } + + $this->line(($quiz->wasRecentlyCreated ? 'créé' : 'à jour')." : {$identifiant} ({$titre}), ".count($lecture['questions']).' question(s)'); + + foreach ($lecture['avertissements'] as $avertissement) { + $this->warn("{$identifiant} : {$avertissement}"); + } + } + } +} diff --git a/portail/app/Http/Controllers/BanqueQuizController.php b/portail/app/Http/Controllers/BanqueQuizController.php index 1b35fe2..762b525 100644 --- a/portail/app/Http/Controllers/BanqueQuizController.php +++ b/portail/app/Http/Controllers/BanqueQuizController.php @@ -15,7 +15,12 @@ use Illuminate\View\View; * même esprit que « Dupliquer et adapter » prévu pour la bibliothèque de * cours (CLAUDE.md). Dette connue : un quiz partagé disparaît de la banque * si la classe d'où il vient est archivée puis purgée (RGPD, 90 jours) — - * la banque n'a pas de propriétaire indépendant d'une classe pour l'instant. + * la banque n'a pas de propriétaire indépendant d'une classe pour l'instant + * (sauf les quiz officiels, voir App\Services\Quiz\BanqueOfficielle). + * + * Regroupés par thème (docs/referentiel-pedagogique-paillasse.md), comme + * la bibliothèque de cours (/bibliotheque) : les quiz officiels d'abord, + * puis ceux partagés par des profs (theme nul) à la fin. */ class BanqueQuizController extends Controller { @@ -23,12 +28,14 @@ class BanqueQuizController extends Controller { $prof = Auth::guard('web')->user(); + $quizzes = Quiz::where('partage', true) + ->with(['classe.etablissement']) + ->withCount('questions') + ->latest() + ->get(); + return view('quiz.banque', [ - 'quizzes' => Quiz::where('partage', true) - ->with(['classe.etablissement']) - ->withCount('questions') - ->latest() - ->get(), + 'groupes' => $quizzes->groupBy(fn (Quiz $quiz) => $quiz->theme ?? '')->sortKeys(), 'mesClasses' => $prof->classes()->whereNull('archivee_a')->orderBy('nom')->get(), ]); } diff --git a/portail/app/Models/Quiz.php b/portail/app/Models/Quiz.php index d9f4002..552a1ed 100644 --- a/portail/app/Models/Quiz.php +++ b/portail/app/Models/Quiz.php @@ -13,7 +13,7 @@ use Illuminate\Support\Facades\DB; */ class Quiz extends Model { - protected $fillable = ['classe_id', 'titre', 'partage']; + protected $fillable = ['classe_id', 'titre', 'theme', 'partage']; protected $casts = [ 'partage' => 'boolean', diff --git a/portail/app/Services/Quiz/BanqueOfficielle.php b/portail/app/Services/Quiz/BanqueOfficielle.php new file mode 100644 index 0000000..05ce5ea --- /dev/null +++ b/portail/app/Services/Quiz/BanqueOfficielle.php @@ -0,0 +1,37 @@ + self::UAI], + ['nom' => 'Bibliothèque Paillasse'], + ); + + $prof = User::firstOrCreate( + ['email' => 'banque-officielle@paillasse.interne'], + ['name' => 'Bibliothèque Paillasse', 'password' => Str::random(40), 'etablissement_id' => $etablissement->id], + ); + + return Classe::firstOrCreate( + ['user_id' => $prof->id, 'etablissement_id' => $etablissement->id, 'nom' => 'Banque officielle'], + ); + } +} diff --git a/portail/app/Services/Quiz/LecteurGift.php b/portail/app/Services/Quiz/LecteurGift.php new file mode 100644 index 0000000..b52135b --- /dev/null +++ b/portail/app/Services/Quiz/LecteurGift.php @@ -0,0 +1,72 @@ +}>, avertissements: array} + */ + public static function lire(string $chemin): array + { + $texte = preg_replace('/^[ \t]*\/\/.*$/m', '', File::get($chemin)); + + $questions = []; + $avertissements = []; + + preg_match_all('/::(.*?)::(.*?)\{(.*?)\}/s', $texte, $blocs, PREG_SET_ORDER); + + foreach ($blocs as $bloc) { + $enonce = trim($bloc[2]); + $contenu = trim($bloc[3]); + + if ($contenu === 'T' || $contenu === 'F') { + $questions[] = [ + 'enonce' => $enonce, + 'type' => QuestionQuiz::VRAI_FAUX, + 'reponse_vrai_faux' => $contenu === 'T', + 'choix' => [], + ]; + + continue; + } + + if (str_starts_with($contenu, '#')) { + $avertissements[] = "question numérique ignorée (type non supporté par la banque) : {$enonce}"; + + continue; + } + + $choix = []; + foreach (preg_split('/\r?\n/', $contenu) as $ligne) { + $ligne = trim($ligne); + if ($ligne === '') { + continue; + } + + $choix[] = [ + 'texte' => trim(substr($ligne, 1)), + 'est_correct' => $ligne[0] === '=', + ]; + } + + $questions[] = [ + 'enonce' => $enonce, + 'type' => QuestionQuiz::QCM, + 'reponse_vrai_faux' => null, + 'choix' => $choix, + ]; + } + + return ['questions' => $questions, 'avertissements' => $avertissements]; + } +} diff --git a/portail/config/contenus.php b/portail/config/contenus.php new file mode 100644 index 0000000..8894258 --- /dev/null +++ b/portail/config/contenus.php @@ -0,0 +1,33 @@ + env('CONTENUS_DOSSIER', dirname(base_path()).'/paillasse-contenus'), + + // Thème de la bibliothèque (docs/referentiel-pedagogique-paillasse.md) + // associé à chaque séquence, pour grouper la banque de quiz officielle. + 'themes' => [ + '01-shell-droits' => 'LIN', + '02-reseau-paquets-ip' => 'NET', + '03-web-client-serveur' => 'SRV', + '04-processus' => 'LIN', + '05-routage-ospf' => 'NET', + '06-chiffrement-https' => 'SEC', + '07-sql' => 'BDD', + '08-deux-lan-routeur' => 'NET', + ], + + // Nom affiché pour chaque code de thème (section correspondante du + // référentiel). Pour l'affichage uniquement. + 'noms_themes' => [ + 'LIN' => 'Linux et systèmes', + 'NET' => 'Réseaux', + 'SRV' => 'Services', + 'SEC' => 'Cybersécurité', + 'BDD' => 'Bases de données', + ], +]; diff --git a/portail/database/migrations/2026_10_13_130000_add_theme_to_quizzes_table.php b/portail/database/migrations/2026_10_13_130000_add_theme_to_quizzes_table.php new file mode 100644 index 0000000..91c3d2c --- /dev/null +++ b/portail/database/migrations/2026_10_13_130000_add_theme_to_quizzes_table.php @@ -0,0 +1,27 @@ +string('theme')->nullable()->after('titre'); + }); + } + + public function down(): void + { + Schema::table('quizzes', function (Blueprint $table) { + $table->dropColumn('theme'); + }); + } +}; diff --git a/portail/resources/views/quiz/banque.blade.php b/portail/resources/views/quiz/banque.blade.php index f5ec9d1..d39c48a 100644 --- a/portail/resources/views/quiz/banque.blade.php +++ b/portail/resources/views/quiz/banque.blade.php @@ -3,41 +3,55 @@ @section('titre', 'Banque de quiz') @section('contenu') -
+

Mes classes

Banque de quiz

-

Les quiz que d'autres profs ont partagés. Dupliquer en fait une copie indépendante dans une de tes classes : tu peux ensuite la modifier sans toucher à l'original.

+

Les quiz officiels, rangés par thème du référentiel, et ceux que d'autres profs ont partagés. Dupliquer en fait une copie indépendante dans une de tes classes : tu peux ensuite la modifier sans toucher à l'original.

@if (session('quiz_duplique'))

Quiz dupliqué dans ta classe.

@endif - @if ($quizzes->isEmpty()) + @if ($groupes->isEmpty())

Aucun quiz partagé pour l'instant.

@elseif ($mesClasses->isEmpty())

Crée d'abord une classe pour pouvoir y dupliquer un quiz.

@else -
    - @foreach ($quizzes as $quiz) -
  • -
    -
    -

    {{ $quiz->titre }}

    -

    {{ $quiz->questions_count }} question(s) · partagé par {{ $quiz->classe->etablissement->nom }}

    -
    -
    - @csrf - - -
    -
    -
  • - @endforeach -
+ @foreach ($groupes as $theme => $quizzes) +
+

+ @if ($theme !== '') + {{ $theme }} + {{ config('contenus.noms_themes.'.$theme, $theme) }} + @else + — + Partagés par des profs + @endif + {{ $quizzes->count() }} quiz +

+
    + @foreach ($quizzes as $quiz) +
  • +
    +
    +

    {{ $quiz->titre }}

    +

    {{ $quiz->questions_count }} question(s) · partagé par {{ $quiz->classe->etablissement->nom }}

    +
    +
    + @csrf + + +
    +
    +
  • + @endforeach +
+
+ @endforeach @endif
@endsection diff --git a/portail/tests/Feature/BanqueQuizSynchronisationTest.php b/portail/tests/Feature/BanqueQuizSynchronisationTest.php new file mode 100644 index 0000000..ea43b2b --- /dev/null +++ b/portail/tests/Feature/BanqueQuizSynchronisationTest.php @@ -0,0 +1,66 @@ + $dossier, 'contenus.themes' => ['01-sequence-test' => 'LIN']]); + + return $dossier; +} + +it('crée la banque officielle et ses quiz depuis les quiz.gift', function () { + $dossier = dossierContenusDeTest(); + + $this->artisan('quiz:synchroniser-banque')->assertSuccessful(); + + $classe = Classe::where('nom', 'Banque officielle')->first(); + expect($classe)->not->toBeNull(); + + $quiz = Quiz::where('classe_id', $classe->id)->where('titre', 'Séquence de test')->first(); + expect($quiz)->not->toBeNull() + ->and($quiz->theme)->toBe('LIN') + ->and($quiz->partage)->toBeTrue() + ->and($quiz->questions)->toHaveCount(2); + + $qcm = $quiz->questions->firstWhere('type', 'qcm'); + expect($qcm->choix)->toHaveCount(2) + ->and($qcm->choix->firstWhere('est_correct', true)->texte)->toBe('bonne réponse'); + + $vraiFaux = $quiz->questions->firstWhere('type', 'vrai_faux'); + expect($vraiFaux->reponse_vrai_faux)->toBeTrue(); + + File::deleteDirectory($dossier); +}); + +it('ne duplique rien et remplace les questions à une nouvelle synchronisation', function () { + $dossier = dossierContenusDeTest(); + + $this->artisan('quiz:synchroniser-banque')->assertSuccessful(); + $this->artisan('quiz:synchroniser-banque')->assertSuccessful(); + + expect(Classe::where('nom', 'Banque officielle')->count())->toBe(1) + ->and(Quiz::where('titre', 'Séquence de test')->count())->toBe(1); + + File::deleteDirectory($dossier); +});