Admin: manual scoring for open-ended questions
This commit is contained in:
@@ -110,26 +110,9 @@ class AttemptController extends Controller
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
$attempt->load(['quiz.questions.options', 'answers']);
|
||||
|
||||
$score = 0;
|
||||
$maxScore = $attempt->quiz->questions->sum('points');
|
||||
|
||||
foreach ($attempt->quiz->questions as $question) {
|
||||
if ($question->type === 'qcm') {
|
||||
$userAnswer = $attempt->answers->where('question_id', $question->id)->first();
|
||||
if ($userAnswer && $userAnswer->option_id) {
|
||||
$option = $question->options->where('id', $userAnswer->option_id)->first();
|
||||
if ($option && $option->is_correct) {
|
||||
$score += $question->points;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->recalculateScore($attempt);
|
||||
|
||||
$attempt->update([
|
||||
'score' => $score,
|
||||
'max_score' => $maxScore,
|
||||
'finished_at' => now(),
|
||||
]);
|
||||
|
||||
@@ -137,4 +120,47 @@ class AttemptController extends Controller
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
public function updateAnswerScore(Request $request, Answer $answer)
|
||||
{
|
||||
$this->authorizeAdmin();
|
||||
|
||||
$request->validate([
|
||||
'score' => 'required|numeric|min:0'
|
||||
]);
|
||||
|
||||
$answer->update(['score' => $request->score]);
|
||||
|
||||
$this->recalculateScore($answer->attempt);
|
||||
|
||||
return back()->with('success', 'Note mise à jour et score total recalculé.');
|
||||
}
|
||||
|
||||
private function recalculateScore(Attempt $attempt)
|
||||
{
|
||||
$attempt->load(['quiz.questions.options', 'answers.option']);
|
||||
|
||||
$score = 0;
|
||||
$maxScore = 0;
|
||||
|
||||
foreach ($attempt->quiz->questions as $question) {
|
||||
$maxScore += $question->points;
|
||||
$userAnswer = $attempt->answers->where('question_id', $question->id)->first();
|
||||
|
||||
if ($userAnswer) {
|
||||
if ($question->type === 'qcm') {
|
||||
if ($userAnswer->option && $userAnswer->option->is_correct) {
|
||||
$score += $question->points;
|
||||
}
|
||||
} else if ($question->type === 'open') {
|
||||
$score += (float) $userAnswer->score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$attempt->update([
|
||||
'score' => $score,
|
||||
'max_score' => $maxScore,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
#[Fillable(['attempt_id', 'question_id', 'option_id', 'text_content'])]
|
||||
#[Fillable(['attempt_id', 'question_id', 'option_id', 'text_content', 'score'])]
|
||||
class Answer extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'score' => 'float',
|
||||
];
|
||||
|
||||
public function attempt(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Attempt::class);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('answers', function (Blueprint $table) {
|
||||
$table->decimal('score', 8, 2)->nullable()->after('text_content');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('answers', function (Blueprint $table) {
|
||||
$table->dropColumn('score');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -100,6 +100,14 @@ const saveScores = () => {
|
||||
const openPreview = (doc) => {
|
||||
selectedDocument.value = doc;
|
||||
};
|
||||
|
||||
const updateAnswerScore = (answerId, score) => {
|
||||
router.patch(route('admin.answers.update-score', answerId), {
|
||||
score: score
|
||||
}, {
|
||||
preserveScroll: true,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -461,10 +469,28 @@ const openPreview = (doc) => {
|
||||
</div>
|
||||
|
||||
<!-- Open Answer -->
|
||||
<div v-else>
|
||||
<div v-else class="space-y-4">
|
||||
<div class="p-6 bg-slate-50 dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-3xl text-[13px] text-slate-600 dark:text-slate-400 leading-relaxed italic shadow-inner">
|
||||
" {{ answer.text_content || 'Aucune réponse fournie.' }} "
|
||||
</div>
|
||||
|
||||
<!-- Score Input for Open Question -->
|
||||
<div class="flex items-center gap-4 bg-slate-50 dark:bg-slate-900/50 p-4 rounded-2xl border border-dashed border-slate-200 dark:border-slate-700">
|
||||
<div class="text-[10px] font-black uppercase tracking-widest text-slate-400">Note Attribuée :</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
v-model="answer.score"
|
||||
min="0"
|
||||
:max="answer.question.points"
|
||||
step="0.5"
|
||||
@change="updateAnswerScore(answer.id, answer.score)"
|
||||
class="w-20 bg-white dark:bg-slate-800 border-none rounded-xl py-1 px-3 font-black text-indigo-600 focus:ring-2 focus:ring-indigo-500/20 transition-all text-center"
|
||||
/>
|
||||
<span class="text-xs font-bold text-slate-300">/ {{ answer.question.points }}</span>
|
||||
</div>
|
||||
<div class="text-[9px] text-slate-400 italic">Enregistré au clic/changement</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,6 +80,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::resource('quizzes', \App\Http\Controllers\QuizController::class)->only(['index', 'store', 'show', 'update', 'destroy']);
|
||||
Route::resource('quizzes.questions', \App\Http\Controllers\QuestionController::class)->only(['store', 'update', 'destroy']);
|
||||
Route::delete('/attempts/{attempt}', [\App\Http\Controllers\AttemptController::class, 'destroy'])->name('attempts.destroy');
|
||||
Route::patch('/answers/{answer}/score', [\App\Http\Controllers\AttemptController::class, 'updateAnswerScore'])->name('answers.update-score');
|
||||
});
|
||||
|
||||
// Candidate Routes
|
||||
|
||||
Reference in New Issue
Block a user