From 041a3e7bd5716bfda52db5c2c6fa1fd2f0605881 Mon Sep 17 00:00:00 2001 From: "m.razaghi" Date: Thu, 6 Aug 2026 14:20:38 +0330 Subject: [PATCH] Added quiz features --- app/Http/Controllers/Admin/QuizController.php | 109 ++++++++++++++ app/Models/Question.php | 46 ++++++ app/Models/QuestionOption.php | 26 ++++ app/Models/Quiz.php | 47 ++++++ app/Models/QuizCategory.php | 27 ++++ app/Models/QuizSubject.php | 21 +++ app/Models/UserQuizAnswer.php | 41 +++++ app/Models/UserQuizAttempt.php | 47 ++++++ .../views/admin/quizzes/create.blade.php | 80 ++++++++++ resources/views/admin/quizzes/index.blade.php | 71 +++++++++ .../views/admin/quizzes/questions.blade.php | 140 ++++++++++++++++++ routes/web.php | 11 +- 12 files changed, 665 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Admin/QuizController.php create mode 100644 app/Models/Question.php create mode 100644 app/Models/QuestionOption.php create mode 100644 app/Models/Quiz.php create mode 100644 app/Models/QuizCategory.php create mode 100644 app/Models/QuizSubject.php create mode 100644 app/Models/UserQuizAnswer.php create mode 100644 app/Models/UserQuizAttempt.php create mode 100644 resources/views/admin/quizzes/create.blade.php create mode 100644 resources/views/admin/quizzes/index.blade.php create mode 100644 resources/views/admin/quizzes/questions.blade.php diff --git a/app/Http/Controllers/Admin/QuizController.php b/app/Http/Controllers/Admin/QuizController.php new file mode 100644 index 0000000..a612700 --- /dev/null +++ b/app/Http/Controllers/Admin/QuizController.php @@ -0,0 +1,109 @@ +withCount('questions')->latest()->paginate(15); + return view('admin.quizzes.index', compact('quizzes')); + } + + public function create() + { + $categories = QuizCategory::where('is_active', true)->get(); + return view('admin.quizzes.create', compact('categories')); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'category_id' => 'required|exists:quiz_categories,id', + 'year' => 'nullable|integer', + 'type' => 'required|in:multiple_choice,descriptive,mixed', + 'duration_minutes' => 'required|integer|min:1', + 'has_negative_score' => 'boolean', + 'is_free' => 'boolean', + 'is_published' => 'boolean', + 'description' => 'nullable|string', + ]); + + $validated['slug'] = Str::slug($validated['title']) . '-' . time(); + $validated['has_negative_score'] = $request->has('has_negative_score'); + $validated['is_free'] = $request->has('is_free'); + $validated['is_published'] = $request->has('is_published'); + + $quiz = Quiz::create($validated); + + return redirect()->route('quizzes.questions.index', $quiz->id) + ->with('success', 'آزمون با موفقیت ایجاد شد. اکنون می‌توانید سوالات را اضافه کنید.'); + } + + public function destroy(Quiz $quiz) + { + $quiz->delete(); + return redirect()->route('quizzes.index')->with('success', 'آزمون حذف شد.'); + } + + // --- Questions Management --- + + public function questions(Quiz $quiz) + { + $quiz->load(['questions.options', 'questions.subject']); + $subjects = QuizSubject::all(); + return view('admin.quizzes.questions', compact('quiz', 'subjects')); + } + + public function storeQuestion(Request $request, Quiz $quiz) + { + $request->validate([ + 'question_text' => 'required|string', + 'type' => 'required|in:multiple_choice,descriptive', + 'subject_id' => 'nullable|exists:quiz_subjects,id', + 'score' => 'required|numeric|min:0.5', + 'solution_explanation' => 'nullable|string', + // Options validation for multiple-choice questions + 'options' => 'required_if:type,multiple_choice|array|min:4|max:4', + 'options.*' => 'required_if:type,multiple_choice|string', + 'correct_option' => 'required_if:type,multiple_choice|integer|between:0,3', + ]); + + $question = $quiz->questions()->create([ + 'subject_id' => $request->subject_id, + 'question_text' => $request->question_text, + 'type' => $request->type, + 'score' => $request->score, + 'solution_explanation' => $request->solution_explanation, + 'order' => $quiz->questions()->count() + 1, + ]); + + // Save options if question is multiple choice + if ($request->type === 'multiple_choice' && $request->has('options')) { + foreach ($request->options as $index => $optionText) { + $question->options()->create([ + 'option_text' => $optionText, + 'is_correct' => ($index == $request->correct_option), + ]); + } + } + + return back()->with('success', 'سوال با موفقیت ثبت شد.'); + } + + public function destroyQuestion(Question $question) + { + $question->delete(); + return back()->with('success', 'سوال حذف شد.'); + } +} diff --git a/app/Models/Question.php b/app/Models/Question.php new file mode 100644 index 0000000..ab17241 --- /dev/null +++ b/app/Models/Question.php @@ -0,0 +1,46 @@ + 'float', + 'order' => 'integer', + ]; + + public function quiz() + { + return $this->belongsTo(Quiz::class, 'quiz_id'); + } + + public function subject() + { + return $this->belongsTo(QuizSubject::class, 'subject_id'); + } + + public function options() + { + return $this->hasMany(QuestionOption::class, 'question_id'); + } + + public function userAnswers() + { + return $this->hasMany(UserQuizAnswer::class, 'question_id'); + } +} diff --git a/app/Models/QuestionOption.php b/app/Models/QuestionOption.php new file mode 100644 index 0000000..5edc25c --- /dev/null +++ b/app/Models/QuestionOption.php @@ -0,0 +1,26 @@ + 'boolean', + ]; + + public function question() + { + return $this->belongsTo(Question::class, 'question_id'); + } +} diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php new file mode 100644 index 0000000..62b80b0 --- /dev/null +++ b/app/Models/Quiz.php @@ -0,0 +1,47 @@ + 'integer', + 'duration_minutes' => 'integer', + 'has_negative_score' => 'boolean', + 'is_free' => 'boolean', + 'is_published' => 'boolean', + ]; + + public function category() + { + return $this->belongsTo(QuizCategory::class, 'category_id'); + } + + public function questions() + { + return $this->hasMany(Question::class, 'quiz_id')->orderBy('order'); + } + + public function attempts() + { + return $this->hasMany(UserQuizAttempt::class, 'quiz_id'); + } +} diff --git a/app/Models/QuizCategory.php b/app/Models/QuizCategory.php new file mode 100644 index 0000000..fe4924a --- /dev/null +++ b/app/Models/QuizCategory.php @@ -0,0 +1,27 @@ + 'boolean', + ]; + + public function quizzes() + { + return $this->hasMany(Quiz::class, 'category_id'); + } +} diff --git a/app/Models/QuizSubject.php b/app/Models/QuizSubject.php new file mode 100644 index 0000000..7646b9e --- /dev/null +++ b/app/Models/QuizSubject.php @@ -0,0 +1,21 @@ +hasMany(Question::class, 'subject_id'); + } +} diff --git a/app/Models/UserQuizAnswer.php b/app/Models/UserQuizAnswer.php new file mode 100644 index 0000000..96eb941 --- /dev/null +++ b/app/Models/UserQuizAnswer.php @@ -0,0 +1,41 @@ + 'boolean', + 'earned_score' => 'float', + ]; + + public function attempt() + { + return $this->belongsTo(UserQuizAttempt::class, 'attempt_id'); + } + + public function question() + { + return $this->belongsTo(Question::class, 'question_id'); + } + + public function selectedOption() + { + return $this->belongsTo(QuestionOption::class, 'selected_option_id'); + } +} diff --git a/app/Models/UserQuizAttempt.php b/app/Models/UserQuizAttempt.php new file mode 100644 index 0000000..990117d --- /dev/null +++ b/app/Models/UserQuizAttempt.php @@ -0,0 +1,47 @@ + 'datetime', + 'completed_at' => 'datetime', + 'total_score' => 'float', + 'correct_answers' => 'integer', + 'wrong_answers' => 'integer', + 'unanswered' => 'integer', + ]; + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function quiz() + { + return $this->belongsTo(Quiz::class, 'quiz_id'); + } + + public function answers() + { + return $this->hasMany(UserQuizAnswer::class, 'attempt_id'); + } +} diff --git a/resources/views/admin/quizzes/create.blade.php b/resources/views/admin/quizzes/create.blade.php new file mode 100644 index 0000000..60c2252 --- /dev/null +++ b/resources/views/admin/quizzes/create.blade.php @@ -0,0 +1,80 @@ +@extends('admin.layouts.app') + +@section('content') +
+
+
تعریف آزمون جدید
+
+
+
+ @csrf +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ انصراف + +
+
+
+
+
+@endsection diff --git a/resources/views/admin/quizzes/index.blade.php b/resources/views/admin/quizzes/index.blade.php new file mode 100644 index 0000000..e395598 --- /dev/null +++ b/resources/views/admin/quizzes/index.blade.php @@ -0,0 +1,71 @@ +@extends('admin.layouts.app') + +@section('content') +
+

مدیریت آزمون‌ها

+ + تعریف آزمون جدید +
+ +
+
+
+ + + + + + + + + + + + + + + @forelse($quizzes as $quiz) + + + + + + + + + + + @empty + + + + @endforelse + +
عنوان آزموندسته‌بندیسالنوعتعداد سوالاتمدت (دقیقه)وضعیتعملیات
{{ $quiz->title }}{{ optional($quiz->category)->title }}{{ $quiz->year ?? '-' }} + @if($quiz->type === 'multiple_choice') + چهار گزینه‌ای + @elseif($quiz->type === 'descriptive') + تشریحی + @else + ترکیبی + @endif + {{ $quiz->questions_count }} سوال{{ $quiz->duration_minutes }} دقیقه + @if($quiz->is_published) + منتشر شده + @else + پیش‌نویس + @endif + + مدیریت سوالات +
+ @csrf + @method('DELETE') + +
+
هیچ آزمونی ثبت نشده است.
+
+
+ {{ $quizzes->links() }} +
+
+
+@endsection diff --git a/resources/views/admin/quizzes/questions.blade.php b/resources/views/admin/quizzes/questions.blade.php new file mode 100644 index 0000000..f694bdf --- /dev/null +++ b/resources/views/admin/quizzes/questions.blade.php @@ -0,0 +1,140 @@ +@extends('admin.layouts.app') + +@section('content') +
+

مدیریت سوالات: {{ $quiz->title }}

+ بازگشت به لیست آزمون‌ها +
+ +
+ +
+
+
+
افزودن سوال جدید
+
+
+
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
گزینه‌ها و انتخاب پاسخ صحیح:
+ @for($i = 0; $i < 4; $i++) +
+
+ +
+ +
+ @endfor + دکمه رادیویی کنار هر گزینه نشان‌دهنده گزینه صحیح است. +
+ +
+ + +
+ + +
+
+
+
+ + +
+
+
+
سوالات ثبت شده ({{ $quiz->questions->count() }})
+
+
+ @forelse($quiz->questions as $index => $question) +
+
+ سوال شماره {{ $index + 1 }} +
+ @if($question->subject) + {{ $question->subject->title }} + @endif + + {{ $question->type === 'multiple_choice' ? 'تستی' : 'تشریحی' }} + +
+
+ +

{{ $question->question_text }}

+ + @if($question->type === 'multiple_choice') +
+ @foreach($question->options as $optIndex => $option) +
+
+ {{ $optIndex + 1 }}) {{ $option->option_text }} +
+
+ @endforeach +
+ @endif + + @if($question->solution_explanation) +
+ پاسخ تشریحی / مستند قانون: {{ $question->solution_explanation }} +
+ @endif + +
+
+ @csrf + @method('DELETE') + +
+
+
+ @empty +

هنوز سوالی برای این آزمون ثبت نشده است.

+ @endforelse +
+
+
+
+ + +@endsection diff --git a/routes/web.php b/routes/web.php index 67bfbf1..9308f66 100644 --- a/routes/web.php +++ b/routes/web.php @@ -31,10 +31,14 @@ use App\Http\Controllers\api\PayController; use Illuminate\Support\Facades\Route; use App\Http\Controllers\Admin\TicketController; use App\Http\Controllers\Admin\DashboardController; +// 1. Add the QuizController import here +use App\Http\Controllers\Admin\QuizController; Route::get('payment/callback', [PayController::class, 'callback']); + Route::middleware(['auth', config('jetstream.auth_session')])->group(function () { - Route::get('dashboard', [HomeController::class, 'index'])->name('dashboard'); + + Route::resource('categories', CategoryController::class); Route::resource('versions', VersionController::class); Route::resource('users', UsersController::class); @@ -60,6 +64,7 @@ Route::middleware(['auth', config('jetstream.auth_session')])->group(function () Route::resource('bylaw-node-type', BylawNodeTypeController::class); Route::resource('bylaw-node', BylawNodeController::class); Route::resource('bylaw-codification-link', BylawCodificationLinkController::class); + Route::get('suggestions', [SuggestionController::class, 'index'])->name('suggestions.index'); Route::get('tickets', [TicketController::class, 'index'])->name('tickets.index'); @@ -67,6 +72,10 @@ Route::middleware(['auth', config('jetstream.auth_session')])->group(function () Route::post('tickets/{ticket}/reply', [TicketController::class, 'reply'])->name('tickets.reply'); Route::post('tickets/{ticket}/close', [TicketController::class, 'close'])->name('tickets.close'); + Route::resource('quizzes', QuizController::class); + Route::get('quizzes/{quiz}/questions', [QuizController::class, 'questions'])->name('quizzes.questions.index'); + Route::post('quizzes/{quiz}/questions', [QuizController::class, 'storeQuestion'])->name('quizzes.questions.store'); + Route::delete('questions/{question}', [QuizController::class, 'destroyQuestion'])->name('questions.destroy'); Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); });