peuple la banque de quiz avec du vrai contenu, rangé par thème
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 <noreply@anthropic.com>
This commit is contained in:
parent
33531de44d
commit
a41951d4e6
21 changed files with 628 additions and 32 deletions
74
portail/app/Console/Commands/SynchroniserBanqueQuiz.php
Normal file
74
portail/app/Console/Commands/SynchroniserBanqueQuiz.php
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Quiz;
|
||||
use App\Services\Quiz\BanqueOfficielle;
|
||||
use App\Services\Quiz\LecteurGift;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Peuple la banque de quiz officielle depuis les quiz.gift de
|
||||
* paillasse-contenus/ (demande d'Adam, 2026-10-13) : même esprit que
|
||||
* labs:synchroniser, jamais de suppression — les questions d'un quiz
|
||||
* officiel sont remplacées à chaque synchronisation (lui seul les écrit,
|
||||
* aucun prof ne peut l'éditer), jamais le quiz lui-même.
|
||||
*/
|
||||
class SynchroniserBanqueQuiz extends Command
|
||||
{
|
||||
protected $signature = 'quiz:synchroniser-banque';
|
||||
|
||||
protected $description = 'Crée ou met à jour la banque de quiz officielle depuis les quiz.gift de paillasse-contenus/';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$dossier = config('contenus.dossier');
|
||||
$themes = config('contenus.themes');
|
||||
$classe = BanqueOfficielle::classe();
|
||||
|
||||
foreach (File::directories($dossier) as $chemin) {
|
||||
$identifiant = basename($chemin);
|
||||
$cheminGift = "{$chemin}/quiz.gift";
|
||||
$cheminLab = "{$chemin}/lab.yaml";
|
||||
|
||||
if (! File::exists($cheminGift) || ! File::exists($cheminLab)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$titre = Yaml::parseFile($cheminLab)['titre'];
|
||||
$lecture = LecteurGift::lire($cheminGift);
|
||||
|
||||
$quiz = Quiz::updateOrCreate(
|
||||
['classe_id' => $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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
37
portail/app/Services/Quiz/BanqueOfficielle.php
Normal file
37
portail/app/Services/Quiz/BanqueOfficielle.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Quiz;
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\Etablissement;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Propriétaire des quiz synchronisés depuis paillasse-contenus/ (voir
|
||||
* quiz:synchroniser-banque) : une classe qui n'est jamais archivée, pour
|
||||
* que ce contenu officiel ne disparaisse pas à la purge RGPD (90 jours)
|
||||
* comme le ferait un quiz partagé depuis une classe de prof — voir
|
||||
* docs/dette.md.
|
||||
*/
|
||||
class BanqueOfficielle
|
||||
{
|
||||
public const UAI = 'PAILLASSE-BANQUE';
|
||||
|
||||
public static function classe(): Classe
|
||||
{
|
||||
$etablissement = Etablissement::firstOrCreate(
|
||||
['uai' => 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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
72
portail/app/Services/Quiz/LecteurGift.php
Normal file
72
portail/app/Services/Quiz/LecteurGift.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Quiz;
|
||||
|
||||
use App\Models\QuestionQuiz;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Lit un fichier quiz.gift (format GIFT, voir paillasse-contenus/README.md).
|
||||
* Seuls les types QCM et vrai/faux sont supportés par la banque (schéma de
|
||||
* questions_quiz) : un type numérique ({#N}, utilisable dans Moodle) est
|
||||
* ignoré avec un avertissement plutôt que d'inventer une réponse.
|
||||
*/
|
||||
class LecteurGift
|
||||
{
|
||||
/**
|
||||
* @return array{questions: array<int, array{enonce: string, type: string, reponse_vrai_faux: ?bool, choix: array<int, array{texte: string, est_correct: bool}>}>, avertissements: array<int, string>}
|
||||
*/
|
||||
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];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue