optimise MariaDB : deux index confirmés par EXPLAIN, N+1 supprimé
Jeu de données de charge (500 classes, 20 000 élèves, 30 000 sessions de lab) généré localement contre une MariaDB jetable, jamais en production, via la nouvelle commande charge:generer (refuse de tourner si APP_ENV=production). EXPLAIN confirme deux manques réels : lab_sessions.etat (30 056 lignes parcourues sans index, lu chaque minute par l'expiration des sessions et le self-healing) et classes.archivee_a (lu chaque jour par la purge RGPD). devoirs.echeance n'avait besoin de rien : l'index existant sur classe_id suffisait déjà, EXPLAIN l'a confirmé plutôt que d'ajouter un index inutile. ResultatsController faisait une requête de corrections et une d'observations par classe du prof (N+1) : remplacé par une requête de chaque pour toutes les classes, regroupées en mémoire. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ca544ac620
commit
e35e0e3c0b
5 changed files with 305 additions and 6 deletions
|
|
@ -1125,3 +1125,26 @@ La leçon : un test local qui passe ne prouve rien sur un comportement qui dépe
|
||||||
**Suite**
|
**Suite**
|
||||||
|
|
||||||
Prochain chantier du bloc B : optimisation MariaDB (index, `EXPLAIN`, requêtes N+1, jeu de données de charge).
|
Prochain chantier du bloc B : optimisation MariaDB (index, `EXPLAIN`, requêtes N+1, jeu de données de charge).
|
||||||
|
|
||||||
|
## 2026-10-14 — Optimisation MariaDB (bloc B)
|
||||||
|
|
||||||
|
Dernier chantier annoncé du bloc B pour l'instant : index vérifiés par `EXPLAIN`, requêtes N+1 supprimées, sur un vrai jeu de données de charge — jamais deviné, jamais sur la base de production.
|
||||||
|
|
||||||
|
**Fait**
|
||||||
|
|
||||||
|
- `php artisan charge:generer` : nouvelle commande, refuse de tourner si `APP_ENV=production`. Remplit une base avec des classes, élèves, séances, sessions de lab, devoirs et rendus, en insertions par lot (pas un `Hash::make()` par élève — 20 000 hachages individuels auraient pris des minutes). Lancée en local contre un conteneur MariaDB 11 jetable (même version qu'en production), jamais contre le serveur réel : 20 000 lignes de test géneraient les vrais profs pendant la mesure, pour un bénéfice qui ne demande pas d'y toucher.
|
||||||
|
- Jeu généré : 500 classes, 20 000 élèves, 1 500 séances, 30 000 sessions de lab, 1 000 devoirs, 13 000 rendus — l'échelle de référence du CLAUDE.md.
|
||||||
|
- `EXPLAIN` sur les requêtes qui filtrent par statut/état/archivage a confirmé deux vrais manques (et écarté une fausse piste) :
|
||||||
|
- `lab_sessions.etat` (lu chaque minute par `labs:expirer-sessions` et la surveillance du self-healing) : parcours complet de la table, 30 056 lignes examinées. Avec l'index : 1 161 lignes (-96 %).
|
||||||
|
- `classes.archivee_a` (lu chaque jour par la purge RGPD) : 500 lignes examinées sans index, 1 avec.
|
||||||
|
- `devoirs.echeance` : pas touché. L'index existant sur `classe_id` suffisait déjà (2 lignes examinées, la requête filtre d'abord par classe) — `EXPLAIN` a évité d'ajouter un index inutile.
|
||||||
|
- N+1 supprimé dans `ResultatsController` (« Résultats », un prof avec plusieurs classes) : une requête de corrections et une d'observations étaient lancées par classe. Remplacé par une requête de chaque pour **toutes** les classes du prof, regroupées en mémoire — 4 requêtes au total, qu'il y ait 1 classe ou 50.
|
||||||
|
- 2 nouveaux tests Pest (117 au total) : le nombre de requêtes de `/resultats` ne bouge pas avec 1 ou 5 classes (compté via `DB::listen()`, pas deviné) ; les données restent correctement associées à la bonne classe après le regroupement en mémoire.
|
||||||
|
|
||||||
|
**Comprendre**
|
||||||
|
|
||||||
|
Deux fois dans ce chantier, mesurer plutôt que deviner a changé la décision : `devoirs.echeance` avait l'air d'un candidat évident à un index composite, `EXPLAIN` a montré que non. À l'inverse, `lab_sessions.etat` n'avait rien d'alarmant en lisant juste le code — c'est le volume réel (30 000 lignes, après des mois d'usage) qui rend le manque d'index coûteux, invisible sur les quelques lignes qu'on a aujourd'hui en production.
|
||||||
|
|
||||||
|
**Suite**
|
||||||
|
|
||||||
|
Déployer la migration d'index et le correctif N+1, vérifier en réel, puis reprendre le dernier point du bloc B : stockage S3 des travaux d'élèves.
|
||||||
|
|
|
||||||
153
portail/app/Console/Commands/GenererJeuDeChargeTest.php
Normal file
153
portail/app/Console/Commands/GenererJeuDeChargeTest.php
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jeu de données de charge (bloc B, CLAUDE.md : « 500 classes, 20 000
|
||||||
|
* élèves ») pour mesurer l'effet des index avec EXPLAIN sur un volume
|
||||||
|
* réaliste. Jamais en production : efface et recrée des données, et
|
||||||
|
* 20 000 lignes sur un serveur de production gênerait les vrais profs
|
||||||
|
* pendant la mesure. À lancer contre une base locale ou jetable.
|
||||||
|
*
|
||||||
|
* Insertions en lot (DB::table()->insert() par paquets), pas par Eloquent
|
||||||
|
* un par un : 20 000 `Hash::make()` individuels prendraient des minutes.
|
||||||
|
*/
|
||||||
|
class GenererJeuDeChargeTest extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'charge:generer {--classes=500} {--eleves-par-classe=40}';
|
||||||
|
|
||||||
|
protected $description = 'Remplit la base avec un jeu de données de charge réaliste (jamais en production)';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if ($this->laravel->environment('production')) {
|
||||||
|
$this->error('Jamais en production : lance ceci contre une base locale ou jetable.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nombreClasses = (int) $this->option('classes');
|
||||||
|
$elevesParClasse = (int) $this->option('eleves-par-classe');
|
||||||
|
$maintenant = now();
|
||||||
|
$codeSecretHache = Hash::make('123456'); // un seul hachage, réutilisé pour toutes les lignes
|
||||||
|
|
||||||
|
$this->info("Génération de {$nombreClasses} classes, {$elevesParClasse} élève(s) chacune…");
|
||||||
|
|
||||||
|
$etablissementIds = $this->inserer('etablissements', range(1, 100), fn (int $i) => [
|
||||||
|
'nom' => "Établissement de charge {$i}", 'ville' => 'Test', 'uai' => "CHARGE{$i}",
|
||||||
|
'created_at' => $maintenant, 'updated_at' => $maintenant,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profIds = $this->inserer('users', range(1, (int) ceil($nombreClasses / 2)), fn (int $i) => [
|
||||||
|
'name' => "Prof de charge {$i}", 'email' => "prof-charge-{$i}@charge.test",
|
||||||
|
'password' => $codeSecretHache, 'etablissement_id' => $etablissementIds[$i % count($etablissementIds)],
|
||||||
|
'created_at' => $maintenant, 'updated_at' => $maintenant,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$classeIds = $this->inserer('classes', range(1, $nombreClasses), fn (int $i) => [
|
||||||
|
'etablissement_id' => $etablissementIds[$i % count($etablissementIds)],
|
||||||
|
'user_id' => $profIds[$i % count($profIds)],
|
||||||
|
'nom' => "Classe de charge {$i}", 'code_classe' => Str::upper(Str::random(5)).$i,
|
||||||
|
'archivee_a' => null, 'created_at' => $maintenant, 'updated_at' => $maintenant,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$labId = DB::table('labs')->first()->id ?? DB::table('labs')->insertGetId([
|
||||||
|
'identifiant' => 'charge-test', 'titre' => 'Lab de charge', 'created_at' => $maintenant, 'updated_at' => $maintenant,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$compteurEleve = 0;
|
||||||
|
$this->withProgressBar($classeIds, function (int $classeId) use (&$compteurEleve, $elevesParClasse, $maintenant, $codeSecretHache, $labId) {
|
||||||
|
$eleveIds = $this->inserer('eleves', range(1, $elevesParClasse), function (int $i) use (&$compteurEleve, $classeId, $maintenant, $codeSecretHache) {
|
||||||
|
$compteurEleve++;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'classe_id' => $classeId, 'prenom' => "Élève{$compteurEleve}", 'nom' => 'Charge',
|
||||||
|
'identifiant' => "eleve-charge-{$compteurEleve}", 'code_secret' => $codeSecretHache,
|
||||||
|
'derniere_connexion_a' => $maintenant->copy()->subDays(random_int(0, 30)),
|
||||||
|
'created_at' => $maintenant, 'updated_at' => $maintenant,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3 séances par classe : deux terminées (historique), une sur dix encore en cours (activité réelle).
|
||||||
|
$enCours = random_int(1, 10) === 1;
|
||||||
|
$statuts = [$enCours ? 'en_cours' : 'terminee', 'terminee', 'terminee'];
|
||||||
|
foreach ($statuts as $indice => $statut) {
|
||||||
|
$demarreeA = $maintenant->copy()->subDays(random_int(1, 90));
|
||||||
|
$seanceId = DB::table('seances')->insertGetId([
|
||||||
|
'classe_id' => $classeId, 'lab_id' => $labId, 'statut' => $statut,
|
||||||
|
'demarree_a' => $demarreeA, 'terminee_a' => $statut === 'terminee' ? $demarreeA->copy()->addMinutes(45) : null,
|
||||||
|
'mode_examen' => false, 'created_at' => $demarreeA, 'updated_at' => $maintenant,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// La moitié des élèves de la classe a une session de lab sur cette séance, avec sa correction.
|
||||||
|
$participants = array_slice($eleveIds, 0, (int) (count($eleveIds) / 2));
|
||||||
|
$labSessions = [];
|
||||||
|
foreach ($participants as $eleveId) {
|
||||||
|
$labSessions[] = [
|
||||||
|
'seance_id' => $seanceId, 'eleve_id' => $eleveId, 'lab_id' => $labId,
|
||||||
|
'etat' => $statut === 'terminee' ? 'termine' : 'actif',
|
||||||
|
'created_at' => $demarreeA, 'updated_at' => $maintenant,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($labSessions !== []) {
|
||||||
|
DB::table('lab_sessions')->insert($labSessions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 devoirs par classe, rendus par un peu plus d'un tiers des élèves.
|
||||||
|
foreach ([1, 2] as $n) {
|
||||||
|
$devoirId = DB::table('devoirs')->insertGetId([
|
||||||
|
'classe_id' => $classeId, 'titre' => "Devoir {$n}", 'consigne' => 'Consigne de charge.',
|
||||||
|
'echeance' => $maintenant->copy()->addDays(random_int(-30, 30)),
|
||||||
|
'created_at' => $maintenant, 'updated_at' => $maintenant,
|
||||||
|
]);
|
||||||
|
$rendus = [];
|
||||||
|
foreach (array_slice($eleveIds, 0, (int) (count($eleveIds) / 3)) as $eleveId) {
|
||||||
|
$rendus[] = [
|
||||||
|
'devoir_id' => $devoirId, 'eleve_id' => $eleveId, 'reponse' => 'Réponse de charge.',
|
||||||
|
'note' => random_int(5, 20), 'rendu_a' => $maintenant, 'created_at' => $maintenant, 'updated_at' => $maintenant,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($rendus !== []) {
|
||||||
|
DB::table('rendus_devoirs')->insert($rendus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$this->newLine(2);
|
||||||
|
|
||||||
|
$this->info('Terminé : '.DB::table('classes')->count().' classes, '.DB::table('eleves')->count().' élèves, '
|
||||||
|
.DB::table('seances')->count().' séances, '.DB::table('lab_sessions')->count().' sessions de lab, '
|
||||||
|
.DB::table('devoirs')->count().' devoirs, '.DB::table('rendus_devoirs')->count().' rendus.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insère en lots de 500 et renvoie les ids créés, dans l'ordre.
|
||||||
|
*
|
||||||
|
* @param array<int, int> $indices
|
||||||
|
* @param callable(int): array<string, mixed> $fabrique
|
||||||
|
* @return array<int, int>
|
||||||
|
*/
|
||||||
|
private function inserer(string $table, array $indices, callable $fabrique): array
|
||||||
|
{
|
||||||
|
$ids = [];
|
||||||
|
|
||||||
|
foreach (array_chunk($indices, 500) as $paquet) {
|
||||||
|
$lignes = array_map($fabrique, $paquet);
|
||||||
|
DB::table($table)->insert($lignes);
|
||||||
|
// insert() en lot : LAST_INSERT_ID() renvoie l'id du PREMIER rang
|
||||||
|
// inséré par l'instruction (comportement MySQL/MariaDB documenté),
|
||||||
|
// les suivants sont consécutifs (innodb_autoinc_lock_mode par défaut).
|
||||||
|
$premier = (int) DB::getPdo()->lastInsertId();
|
||||||
|
$ids = array_merge($ids, range($premier, $premier + count($lignes) - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Classe;
|
use App\Models\Classe;
|
||||||
use App\Models\Correction;
|
use App\Models\Correction;
|
||||||
|
use App\Models\Observation;
|
||||||
|
use App\Models\Seance;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
|
@ -11,6 +13,10 @@ use Illuminate\View\View;
|
||||||
* « Résultats » (séparation vitrine/application, 2026-10-13) : vue
|
* « Résultats » (séparation vitrine/application, 2026-10-13) : vue
|
||||||
* d'ensemble, par classe, des corrections et des observations. Le détail
|
* d'ensemble, par classe, des corrections et des observations. Le détail
|
||||||
* séance par séance reste dans le cockpit de chaque classe.
|
* séance par séance reste dans le cockpit de chaque classe.
|
||||||
|
*
|
||||||
|
* Tout en 4 requêtes au total, quel que soit le nombre de classes du prof
|
||||||
|
* (bloc B, optimisation MariaDB, 2026-10-14) : une requête par classe pour
|
||||||
|
* les corrections et une pour les observations étaient un N+1 classique.
|
||||||
*/
|
*/
|
||||||
class ResultatsController extends Controller
|
class ResultatsController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -18,12 +24,24 @@ class ResultatsController extends Controller
|
||||||
{
|
{
|
||||||
$prof = Auth::guard('web')->user();
|
$prof = Auth::guard('web')->user();
|
||||||
$classes = $prof->classes()->whereNull('archivee_a')->orderBy('nom')->get();
|
$classes = $prof->classes()->whereNull('archivee_a')->orderBy('nom')->get();
|
||||||
|
$classeIds = $classes->pluck('id');
|
||||||
|
|
||||||
$resultats = $classes->map(function (Classe $classe) {
|
$classeIdParSeanceId = Seance::whereIn('classe_id', $classeIds)->pluck('classe_id', 'id');
|
||||||
$corrections = Correction::whereHas('labSession', fn ($q) => $q->whereIn('seance_id', $classe->seances()->select('id')))
|
|
||||||
->latest()
|
$correctionsParClasse = Correction::whereHas('labSession', fn ($q) => $q->whereIn('seance_id', $classeIdParSeanceId->keys()))
|
||||||
->limit(50)
|
->with('labSession:id,seance_id')
|
||||||
->get();
|
->latest()
|
||||||
|
->get()
|
||||||
|
->groupBy(fn (Correction $c): int => $classeIdParSeanceId[$c->labSession->seance_id] ?? 0);
|
||||||
|
|
||||||
|
$observationsParClasse = Observation::whereIn('classe_id', $classeIds)
|
||||||
|
->with('eleve')
|
||||||
|
->latest()
|
||||||
|
->get()
|
||||||
|
->groupBy('classe_id');
|
||||||
|
|
||||||
|
$resultats = $classes->map(function (Classe $classe) use ($correctionsParClasse, $observationsParClasse) {
|
||||||
|
$corrections = ($correctionsParClasse->get($classe->id) ?? collect())->take(50);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'classe' => $classe,
|
'classe' => $classe,
|
||||||
|
|
@ -31,7 +49,7 @@ class ResultatsController extends Controller
|
||||||
'reussiteMoyenne' => $corrections->isNotEmpty()
|
'reussiteMoyenne' => $corrections->isNotEmpty()
|
||||||
? (int) round($corrections->avg(fn ($c) => $c->note_max > 0 ? $c->note / $c->note_max * 100 : 0))
|
? (int) round($corrections->avg(fn ($c) => $c->note_max > 0 ? $c->note / $c->note_max * 100 : 0))
|
||||||
: null,
|
: null,
|
||||||
'observations' => $classe->observations()->with('eleve')->limit(5)->get(),
|
'observations' => ($observationsParClasse->get($classe->id) ?? collect())->take(5),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimisation MariaDB (bloc B, 2026-10-14) : deux manques confirmés par
|
||||||
|
* EXPLAIN sur un jeu de données de charge (500 classes, 20 000 élèves,
|
||||||
|
* 30 000 sessions de lab — `php artisan charge:generer`, jamais en
|
||||||
|
* production), pas devinés. `lab_sessions.etat` faisait un parcours complet
|
||||||
|
* de la table (`type: ALL`, 30 056 lignes) : lu chaque minute par
|
||||||
|
* ExpirerSessionsLab et SurveillanceVm. `classes.archivee_a` aussi
|
||||||
|
* (`type: ALL`), lu chaque jour par la purge RGPD. En comparaison,
|
||||||
|
* `devoirs.echeance` s'appuyait déjà bien sur l'index existant de
|
||||||
|
* `classe_id` (2 lignes examinées) : pas touché, un index de plus n'aurait
|
||||||
|
* rien amélioré.
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('lab_sessions', function (Blueprint $table) {
|
||||||
|
$table->index('etat');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('classes', function (Blueprint $table) {
|
||||||
|
$table->index('archivee_a');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('lab_sessions', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['etat']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('classes', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['archivee_a']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
63
portail/tests/Feature/OptimisationMariadbTest.php
Normal file
63
portail/tests/Feature/OptimisationMariadbTest.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Classe;
|
||||||
|
use App\Models\Correction;
|
||||||
|
use App\Models\Eleve;
|
||||||
|
use App\Models\Lab;
|
||||||
|
use App\Models\LabSession;
|
||||||
|
use App\Models\Observation;
|
||||||
|
use App\Models\Seance;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bloc B, optimisation MariaDB (2026-10-14) : « Résultats » faisait une
|
||||||
|
* requête de corrections et une d'observations PAR classe — un N+1
|
||||||
|
* classique. Vérifié en comptant les vraies requêtes SQL, pas en devinant :
|
||||||
|
* le nombre doit rester le même, que le prof ait 1 classe ou 5.
|
||||||
|
*/
|
||||||
|
function resultatsPourProfAvecNClasses($test, int $nombreClasses): int
|
||||||
|
{
|
||||||
|
$prof = User::factory()->create();
|
||||||
|
|
||||||
|
for ($i = 0; $i < $nombreClasses; $i++) {
|
||||||
|
$classe = Classe::factory()->create(['user_id' => $prof->id]);
|
||||||
|
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||||
|
$seance = Seance::create(['classe_id' => $classe->id, 'lab_id' => Lab::factory()->create()->id, 'statut' => Seance::TERMINEE]);
|
||||||
|
$labSession = LabSession::create(['seance_id' => $seance->id, 'eleve_id' => $eleve->id, 'lab_id' => $seance->lab_id, 'etat' => LabSession::TERMINE]);
|
||||||
|
Correction::create(['lab_session_id' => $labSession->id, 'reussi' => true, 'note' => 8, 'note_max' => 10, 'details' => []]);
|
||||||
|
Observation::create(['classe_id' => $classe->id, 'eleve_id' => $eleve->id, 'user_id' => $prof->id, 'contenu' => 'Observation de test']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$requetes = 0;
|
||||||
|
DB::listen(function () use (&$requetes) {
|
||||||
|
$requetes++;
|
||||||
|
});
|
||||||
|
|
||||||
|
prof($test, $prof)->get(route('resultats'))->assertOk();
|
||||||
|
|
||||||
|
return $requetes;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('le nombre de requêtes de /resultats ne dépend pas du nombre de classes (pas de N+1)', function () {
|
||||||
|
$requetesUneClasse = resultatsPourProfAvecNClasses($this, 1);
|
||||||
|
$requetesCinqClasses = resultatsPourProfAvecNClasses($this, 5);
|
||||||
|
|
||||||
|
expect($requetesCinqClasses)->toBe($requetesUneClasse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('les corrections et observations affichées restent correctes après le regroupement en lot', function () {
|
||||||
|
$prof = User::factory()->create();
|
||||||
|
$classeA = Classe::factory()->create(['user_id' => $prof->id, 'nom' => 'Classe A']);
|
||||||
|
$classeB = Classe::factory()->create(['user_id' => $prof->id, 'nom' => 'Classe B']);
|
||||||
|
|
||||||
|
$seanceA = Seance::create(['classe_id' => $classeA->id, 'lab_id' => Lab::factory()->create()->id, 'statut' => Seance::TERMINEE]);
|
||||||
|
$eleveA = Eleve::factory()->create(['classe_id' => $classeA->id, 'prenom' => 'Alice']);
|
||||||
|
$labSessionA = LabSession::create(['seance_id' => $seanceA->id, 'eleve_id' => $eleveA->id, 'lab_id' => $seanceA->lab_id, 'etat' => LabSession::TERMINE]);
|
||||||
|
Correction::create(['lab_session_id' => $labSessionA->id, 'reussi' => true, 'note' => 9, 'note_max' => 10, 'details' => []]);
|
||||||
|
Observation::create(['classe_id' => $classeA->id, 'eleve_id' => $eleveA->id, 'user_id' => $prof->id, 'contenu' => 'Bonne participation']);
|
||||||
|
|
||||||
|
// Classe B : aucune activité, ne doit jamais récupérer les données de la classe A.
|
||||||
|
$page = prof($this, $prof)->get(route('resultats'));
|
||||||
|
$page->assertOk()->assertSeeInOrder(['Classe A', '1 correction(s)', 'Bonne participation', 'Classe B', '0 correction(s)']);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue