208 lines
6.7 KiB
PHP
208 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Quiz;
|
|
use App\Models\QuizCategory;
|
|
use App\Models\QuizSubject;
|
|
use App\Models\UserQuizAttempt;
|
|
use App\Models\UserQuizAnswer;
|
|
use App\Models\Question;
|
|
use App\Traits\BaseApiResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class QuizController extends Controller
|
|
{
|
|
use BaseApiResponse;
|
|
|
|
/**
|
|
* 1. Get all active Categories
|
|
*/
|
|
public function categories()
|
|
{
|
|
$categories = QuizCategory::where('is_active', true)->get();
|
|
return $this->success($categories, 'موفق', 'لیست دستهبندیها دریافت شد');
|
|
}
|
|
|
|
/**
|
|
* 2. Get all Subjects
|
|
*/
|
|
public function subjects()
|
|
{
|
|
$subjects = QuizSubject::all();
|
|
return $this->success($subjects, 'موفق', 'لیست دروس دریافت شد');
|
|
}
|
|
|
|
/**
|
|
* 3. List Quizzes (Filterable by Category or Subject)
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$query = Quiz::where('is_published', true)->with('category');
|
|
|
|
if ($request->filled('category_id')) {
|
|
$query->where('category_id', $request->category_id);
|
|
}
|
|
|
|
if ($request->filled('subject_id')) {
|
|
$query->whereHas('questions', function ($q) use ($request) {
|
|
$q->where('subject_id', $request->subject_id);
|
|
});
|
|
}
|
|
|
|
$quizzes = $query->latest()->get();
|
|
|
|
return $this->success($quizzes, 'موفق', 'لیست آزمونها دریافت شد');
|
|
}
|
|
|
|
/**
|
|
* 4. Start a Quiz & Fetch Questions (SECURELY)
|
|
*/
|
|
public function start(Quiz $quiz)
|
|
{
|
|
if (!$quiz->is_published) {
|
|
return response()->json(['message' => 'این آزمون در دسترس نیست.'], 403);
|
|
}
|
|
|
|
$attempt = UserQuizAttempt::create([
|
|
'user_id' => auth()->id(),
|
|
'quiz_id' => $quiz->id,
|
|
'started_at' => now(),
|
|
'status' => 'in_progress'
|
|
]);
|
|
|
|
$quiz->load(['questions' => function($q) {
|
|
$q->orderBy('order', 'asc');
|
|
}, 'questions.options']);
|
|
|
|
// SECURITY: Hide the correct answers and explanations before sending to mobile app
|
|
$quiz->questions->each(function ($question) {
|
|
$question->makeHidden(['solution_explanation']);
|
|
$question->options->each->makeHidden(['is_correct']);
|
|
});
|
|
|
|
$data = [
|
|
'attempt_id' => $attempt->id,
|
|
'quiz' => $quiz
|
|
];
|
|
|
|
return $this->success($data, 'موفق', 'آزمون با موفقیت شروع شد');
|
|
}
|
|
|
|
/**
|
|
* 5. Submit the Quiz & Calculate Score
|
|
*/
|
|
public function submit(Request $request, UserQuizAttempt $attempt)
|
|
{
|
|
if ($attempt->user_id !== auth()->id() || $attempt->status !== 'in_progress') {
|
|
return response()->json(['message' => 'دسترسی غیرمجاز یا آزمون قبلاً پایان یافته است.'], 403);
|
|
}
|
|
|
|
$request->validate([
|
|
'answers' => 'required|array',
|
|
'answers.*.question_id' => 'required|exists:questions,id',
|
|
'answers.*.selected_option_id' => 'nullable|exists:question_options,id',
|
|
'answers.*.text_answer' => 'nullable|string',
|
|
]);
|
|
|
|
$quiz = $attempt->quiz;
|
|
|
|
$totalScore = 0.00;
|
|
$correctCount = 0;
|
|
$wrongCount = 0;
|
|
$unansweredCount = 0;
|
|
$needsGrading = false;
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
foreach ($request->answers as $ans) {
|
|
$question = Question::with('options')->find($ans['question_id']);
|
|
|
|
$isCorrect = false;
|
|
$earnedScore = 0.00;
|
|
|
|
// Handle Multiple Choice
|
|
if ($question->type === 'multiple_choice') {
|
|
if (empty($ans['selected_option_id'])) {
|
|
$unansweredCount++;
|
|
$isCorrect = null;
|
|
} else {
|
|
$correctOption = $question->options->where('is_correct', true)->first();
|
|
|
|
if ($correctOption && $correctOption->id == $ans['selected_option_id']) {
|
|
$isCorrect = true;
|
|
$earnedScore = $question->score;
|
|
$totalScore += $earnedScore;
|
|
$correctCount++;
|
|
} else {
|
|
$isCorrect = false;
|
|
if ($quiz->has_negative_score) {
|
|
$earnedScore = -($question->score / 3);
|
|
$totalScore += $earnedScore;
|
|
}
|
|
$wrongCount++;
|
|
}
|
|
}
|
|
}
|
|
// Handle Descriptive
|
|
else if ($question->type === 'descriptive') {
|
|
$needsGrading = true;
|
|
$isCorrect = null;
|
|
}
|
|
|
|
UserQuizAnswer::create([
|
|
'attempt_id' => $attempt->id,
|
|
'question_id' => $question->id,
|
|
'selected_option_id' => $ans['selected_option_id'] ?? null,
|
|
'text_answer' => $ans['text_answer'] ?? null,
|
|
'is_correct' => $isCorrect,
|
|
'earned_score' => $earnedScore,
|
|
]);
|
|
}
|
|
|
|
$attempt->update([
|
|
'completed_at' => now(),
|
|
'total_score' => $totalScore,
|
|
'correct_answers' => $correctCount,
|
|
'wrong_answers' => $wrongCount,
|
|
'unanswered' => $unansweredCount,
|
|
'status' => $needsGrading ? 'pending_grading' : 'completed',
|
|
]);
|
|
|
|
DB::commit();
|
|
|
|
$data = [
|
|
'attempt_id' => $attempt->id,
|
|
'status' => $attempt->status
|
|
];
|
|
|
|
return $this->success($data, 'موفق', 'آزمون با موفقیت ثبت شد.');
|
|
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json(['message' => 'خطا در ثبت اطلاعات آزمون.'], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 6. Fetch Results
|
|
*/
|
|
public function result(UserQuizAttempt $attempt)
|
|
{
|
|
if ($attempt->user_id !== auth()->id()) {
|
|
return response()->json(['message' => 'غیرمجاز'], 403);
|
|
}
|
|
|
|
$attempt->load([
|
|
'quiz.category',
|
|
'answers.question.options',
|
|
'answers.question.subject',
|
|
'answers.selectedOption'
|
|
]);
|
|
|
|
return $this->success($attempt, 'موفق', 'نتیجه آزمون با موفقیت دریافت شد');
|
|
}
|
|
}
|