ajoute la surveillance d'examen (chantier vrai cockpit, dernière étape)
Une séance peut être lancée en mode examen avec une durée dédiée. Réutilise le mécanisme d'expiration déjà en place (labs:expirer-sessions, expire_a) plutôt que d'en construire un nouveau. Le navigateur de l'élève signale les changements d'onglet et les sorties de plein écran (evenements_examen : un type et un horodatage, rien d'autre - pas de capture d'écran ni de webcam) ; le prof voit le compte par élève et le temps restant dans le cockpit de sa classe et la salle d'attente. Choix d'Adam après discussion des options (suivi chronométré simple / + détection d'onglet / + tentative de plein écran) : la troisième, honnêtement présentée - un navigateur ne permet pas de vraiment empêcher de sortir, seulement de le signaler. Avec cette étape, les quatre fonctions fictives du tableau de bord démo (quiz, devoirs, observations, examen) sont maintenant réelles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
954bb6ca6c
commit
f2bca621cd
15 changed files with 392 additions and 11 deletions
|
|
@ -44,7 +44,7 @@ class ClasseController extends Controller
|
|||
// d'attente, la plus récemment démarrée d'abord.
|
||||
$seance = $classe->seances()
|
||||
->whereIn('statut', [Seance::SALLE_ATTENTE, Seance::EN_COURS])
|
||||
->with(['lab', 'labSessions' => fn ($q) => $q->with(['eleve', 'derniereCorrection', 'machines'])])
|
||||
->with(['lab', 'labSessions' => fn ($q) => $q->with(['eleve', 'derniereCorrection', 'machines', 'evenementsExamen'])])
|
||||
->latest('demarree_a')
|
||||
->get()
|
||||
->sortByDesc(fn ($s) => $s->statut === Seance::EN_COURS)
|
||||
|
|
|
|||
35
portail/app/Http/Controllers/EvenementExamenController.php
Normal file
35
portail/app/Http/Controllers/EvenementExamenController.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EvenementExamen;
|
||||
use App\Models\LabSession;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* Surveillance d'examen (chantier « vrai cockpit », 2026-10-14) : le
|
||||
* navigateur de l'élève signale un changement d'onglet ou une sortie du
|
||||
* plein écran. Jamais de capture d'écran ni de webcam — juste un type et
|
||||
* un horodatage (CLAUDE.md : minimisation).
|
||||
*/
|
||||
class EvenementExamenController extends Controller
|
||||
{
|
||||
public function store(Request $request, LabSession $labSession): JsonResponse
|
||||
{
|
||||
$eleve = Auth::guard('eleve')->user();
|
||||
abort_if($labSession->eleve_id !== $eleve->id, 403);
|
||||
|
||||
$seance = $labSession->seance;
|
||||
abort_unless($seance->mode_examen, 403);
|
||||
|
||||
$donnees = $request->validate([
|
||||
'type' => ['required', 'in:'.implode(',', [EvenementExamen::CHANGEMENT_ONGLET, EvenementExamen::SORTIE_PLEIN_ECRAN])],
|
||||
]);
|
||||
|
||||
$labSession->evenementsExamen()->create($donnees);
|
||||
|
||||
return response()->json([], 201);
|
||||
}
|
||||
}
|
||||
|
|
@ -31,8 +31,12 @@ class SeanceController extends Controller
|
|||
$donnees = $request->validate([
|
||||
'lab_id' => ['required', 'exists:labs,id'],
|
||||
'prevue_a' => ['nullable', 'date_format:Y-m-d\TH:i'],
|
||||
'mode_examen' => ['nullable', 'boolean'],
|
||||
'duree_examen_min' => ['required_if:mode_examen,1', 'nullable', 'integer', 'min:5', 'max:300'],
|
||||
]);
|
||||
|
||||
$modeExamen = $request->boolean('mode_examen');
|
||||
|
||||
if (! empty($donnees['prevue_a'])) {
|
||||
$prevueA = Carbon::createFromFormat('Y-m-d\TH:i', $donnees['prevue_a'], config('app.fuseau_utilisateurs'))->utc();
|
||||
|
||||
|
|
@ -44,6 +48,8 @@ class SeanceController extends Controller
|
|||
'lab_id' => $donnees['lab_id'],
|
||||
'statut' => Seance::PLANIFIEE,
|
||||
'prevue_a' => $prevueA,
|
||||
'mode_examen' => $modeExamen,
|
||||
'duree_examen_min' => $modeExamen ? $donnees['duree_examen_min'] : null,
|
||||
]);
|
||||
|
||||
return redirect()->route('mes-classes.show', $classe)->with('seance_planifiee', true);
|
||||
|
|
@ -53,6 +59,8 @@ class SeanceController extends Controller
|
|||
'lab_id' => $donnees['lab_id'],
|
||||
'statut' => Seance::SALLE_ATTENTE,
|
||||
'demarree_a' => now(),
|
||||
'mode_examen' => $modeExamen,
|
||||
'duree_examen_min' => $modeExamen ? $donnees['duree_examen_min'] : null,
|
||||
]);
|
||||
|
||||
return redirect()->route('seances.attente', $seance);
|
||||
|
|
@ -74,7 +82,7 @@ class SeanceController extends Controller
|
|||
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
|
||||
return view('seances.attente', [
|
||||
'seance' => $seance->load(['classe', 'lab', 'labSessions.eleve', 'labSessions.machines', 'labSessions.derniereCorrection']),
|
||||
'seance' => $seance->load(['classe', 'lab', 'labSessions.eleve', 'labSessions.machines', 'labSessions.derniereCorrection', 'labSessions.evenementsExamen']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +115,7 @@ class SeanceController extends Controller
|
|||
->update([
|
||||
'etat' => LabSession::ADMIS,
|
||||
'raison_echec' => null,
|
||||
'expire_a' => now()->addMinutes($labSession->lab->duree_min ?? 45),
|
||||
'expire_a' => now()->addMinutes($seance->mode_examen ? $seance->duree_examen_min : ($labSession->lab->duree_min ?? 45)),
|
||||
]);
|
||||
|
||||
if ($pris === 0) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue