ajoute Quiz flash (chantier vrai cockpit, étape 2) : QCM et vrai/faux, lancé en direct, indépendant d'une séance de lab

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-13 12:14:30 +02:00
parent 45f8a69e26
commit 16d1293911
24 changed files with 1249 additions and 5 deletions

View file

@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\QuestionQuiz;
use App\Models\Quiz;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
/**
* Chantier « vrai cockpit », étape 2 : Quiz flash. La banque de questions
* (ce contrôleur) est distincte de son lancement en direct
* (SessionQuizController) : un quiz se prépare à l'avance, se relance
* autant de fois qu'on veut.
*/
class QuizController extends Controller
{
public function index(Classe $classe): View
{
$this->verifierProprietaire($classe);
return view('quiz.index', ['classe' => $classe, 'quizzes' => $classe->quizzes()->withCount('questions')->get()]);
}
public function store(Request $request, Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
$donnees = $request->validate(['titre' => ['required', 'string', 'max:150']]);
$quiz = $classe->quizzes()->create($donnees);
return redirect()->route('mes-classes.quiz.show', [$classe, $quiz]);
}
public function show(Classe $classe, Quiz $quiz): View
{
$this->verifierProprietaire($classe);
$this->verifierAppartenance($classe, $quiz);
return view('quiz.show', ['classe' => $classe, 'quiz' => $quiz->load('questions.choix')]);
}
public function destroy(Classe $classe, Quiz $quiz): RedirectResponse
{
$this->verifierProprietaire($classe);
$this->verifierAppartenance($classe, $quiz);
$quiz->delete();
return redirect()->route('mes-classes.quiz.index', $classe);
}
/**
* Une question à la fois (même esprit que « ajouter un élève ») : plus
* simple à valider et à tester qu'un formulaire unique avec un nombre
* de lignes variable.
*/
public function storeQuestion(Request $request, Classe $classe, Quiz $quiz): RedirectResponse
{
$this->verifierProprietaire($classe);
$this->verifierAppartenance($classe, $quiz);
$donnees = $request->validate([
'enonce' => ['required', 'string', 'max:500'],
'type' => ['required', 'in:'.QuestionQuiz::QCM.','.QuestionQuiz::VRAI_FAUX],
'reponse_vrai_faux' => ['required_if:type,'.QuestionQuiz::VRAI_FAUX, 'boolean'],
'choix' => ['required_if:type,'.QuestionQuiz::QCM, 'array', 'min:2', 'max:5'],
'choix.*' => ['required_if:type,'.QuestionQuiz::QCM, 'string', 'max:200'],
'bonne_reponse' => ['required_if:type,'.QuestionQuiz::QCM, 'integer'],
]);
DB::transaction(function () use ($donnees, $quiz) {
$question = $quiz->questions()->create([
'enonce' => $donnees['enonce'],
'type' => $donnees['type'],
'ordre' => $quiz->questions()->count(),
'reponse_vrai_faux' => $donnees['type'] === QuestionQuiz::VRAI_FAUX ? $donnees['reponse_vrai_faux'] : null,
]);
if ($donnees['type'] === QuestionQuiz::QCM) {
foreach ($donnees['choix'] as $i => $texte) {
$question->choix()->create(['texte' => $texte, 'est_correct' => $i === (int) $donnees['bonne_reponse'], 'ordre' => $i]);
}
}
});
return redirect()->route('mes-classes.quiz.show', [$classe, $quiz]);
}
public function destroyQuestion(Classe $classe, Quiz $quiz, QuestionQuiz $question): RedirectResponse
{
$this->verifierProprietaire($classe);
$this->verifierAppartenance($classe, $quiz);
abort_if($question->quiz_id !== $quiz->id, 404);
$question->delete();
return redirect()->route('mes-classes.quiz.show', [$classe, $quiz]);
}
private function verifierProprietaire(Classe $classe): void
{
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
}
private function verifierAppartenance(Classe $classe, Quiz $quiz): void
{
abort_if($quiz->classe_id !== $classe->id, 404);
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers;
use App\Models\QuestionQuiz;
use App\Models\ReponseQuiz;
use App\Models\SessionQuiz;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* Côté élève du quiz flash (chantier « vrai cockpit », étape 2) : pas de
* code à saisir, pas d'invité — l'élève est déjà un compte réel de la
* classe, le quiz apparaît directement sur son tableau de bord.
*/
class QuizEleveController extends Controller
{
public function etat(): JsonResponse
{
$eleve = Auth::guard('eleve')->user();
$session = SessionQuiz::query()
->whereIn('statut', [SessionQuiz::EN_ATTENTE, SessionQuiz::EN_COURS])
->whereHas('quiz', fn ($q) => $q->where('classe_id', $eleve->classe_id))
->with('questionCourante.choix')
->latest('demarree_a')
->first();
if (! $session) {
return response()->json(['session' => null]);
}
$dejaRepondu = $session->question_quiz_courante_id
? ReponseQuiz::where('session_quiz_id', $session->id)
->where('question_quiz_id', $session->question_quiz_courante_id)
->where('eleve_id', $eleve->id)
->exists()
: false;
$question = $session->questionCourante;
return response()->json([
'session' => [
'id' => $session->id,
'statut' => $session->statut,
'deja_repondu' => $dejaRepondu,
'question' => $question ? [
'id' => $question->id,
'enonce' => $question->enonce,
'type' => $question->type,
'choix' => $question->type === QuestionQuiz::QCM
? $question->choix->map(fn ($c) => ['id' => $c->id, 'texte' => $c->texte])->values()
: [],
] : null,
],
]);
}
public function repondre(Request $request, SessionQuiz $session): JsonResponse
{
$eleve = Auth::guard('eleve')->user();
abort_if($session->quiz->classe_id !== $eleve->classe_id, 403);
abort_if($session->statut !== SessionQuiz::EN_COURS || ! $session->question_quiz_courante_id, 409, 'Aucune question en cours.');
$question = $session->questionCourante;
abort_if(
ReponseQuiz::where('session_quiz_id', $session->id)
->where('question_quiz_id', $question->id)
->where('eleve_id', $eleve->id)
->exists(),
409,
'Déjà répondu.'
);
$donnees = $request->validate([
'choix_id' => ['nullable', 'integer'],
'reponse_vrai_faux' => ['nullable', 'boolean'],
]);
$correcte = $question->estCorrecte($donnees['choix_id'] ?? null, $donnees['reponse_vrai_faux'] ?? null);
try {
ReponseQuiz::create([
'session_quiz_id' => $session->id,
'question_quiz_id' => $question->id,
'eleve_id' => $eleve->id,
'choix_question_quiz_id' => $donnees['choix_id'] ?? null,
'reponse_vrai_faux' => $donnees['reponse_vrai_faux'] ?? null,
'correcte' => $correcte,
'repondu_a' => now(),
]);
} catch (QueryException $e) {
// Double clic : la contrainte d'unicité (session, question, élève)
// a bloqué un second envoi arrivé en même temps que le premier.
abort(409, 'Déjà répondu.');
}
return response()->json(['correcte' => $correcte]);
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\Quiz;
use App\Models\SessionQuiz;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
/**
* Lancement en direct d'un quiz (chantier « vrai cockpit », étape 2) :
* indépendant d'une séance de lab — le prof peut le lancer à tout moment
* pour sa classe, les élèves le voient apparaître sur leur tableau de bord.
*/
class SessionQuizController extends Controller
{
public function lancer(Classe $classe, Quiz $quiz): RedirectResponse
{
$this->verifierProprietaire($classe);
abort_if($quiz->classe_id !== $classe->id, 404);
abort_if($quiz->questions()->count() === 0, 422, 'Ce quiz n’a encore aucune question.');
$session = $quiz->sessions()->create(['statut' => SessionQuiz::EN_ATTENTE, 'demarree_a' => now()]);
return redirect()->route('sessions-quiz.show', $session);
}
public function show(SessionQuiz $session): View
{
$this->verifierProprietaireSession($session);
return view('quiz.session-prof', [
'session' => $session->load(['quiz.questions.choix', 'questionCourante.choix']),
'decompte' => $session->decompteQuestionCourante(),
'total_eleves' => $session->quiz->classe->eleves()->count(),
]);
}
public function etat(SessionQuiz $session): JsonResponse
{
$this->verifierProprietaireSession($session);
return response()->json([
'statut' => $session->statut,
'question_id' => $session->question_quiz_courante_id,
'decompte' => $session->decompteQuestionCourante(),
]);
}
public function suivante(SessionQuiz $session): RedirectResponse
{
$this->verifierProprietaireSession($session);
$session->passerALaQuestionSuivante();
return redirect()->route('sessions-quiz.show', $session);
}
public function terminer(SessionQuiz $session): RedirectResponse
{
$this->verifierProprietaireSession($session);
$session->terminer();
return redirect()->route('mes-classes.quiz.show', [$session->quiz->classe, $session->quiz]);
}
private function verifierProprietaireSession(SessionQuiz $session): void
{
abort_if($session->quiz->classe->user_id !== Auth::guard('web')->id(), 403);
}
private function verifierProprietaire(Classe $classe): void
{
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
}
}