'required|string|max:255', 'description' => 'nullable|string', 'type' => 'required|in:single,multiple,text', 'difficulty' => 'required|integer', 'category_id' => 'nullable|exists:categories,id', 'variants' => ['array', 'nullable'], 'variants.*.id' => ['required', 'integer'], 'variants.*.text' => ['required', 'string'], 'correct_answers' => 'required|array' ]; } /** * @OA\Get( * path="/api/questions", * summary="Get a paginated list of questions (paginated)", * description="Retrieve questions. Optional filter by category_id.", * tags={"Questions"}, * @OA\Parameter( * name="category_id", * in="query", * description="Filter questions by category ID", * required=false, * @OA\Schema(type="integer") * ), * @OA\Parameter( * name="page", * in="query", * description="Page number for pagination", * required=false, * @OA\Schema(type="integer", default=1) * ), * @OA\Response( * response=200, * description="Paginated list of questions", * @OA\JsonContent( * @OA\Property( * property="data", * type="array", * @OA\Items(ref="#/components/schemas/QuestionResource") * ), * @OA\Property( * property="links", * type="object", * example={ * "first": "http://localhost/api/questions?page=1", * "last": "http://localhost/api/questions?page=10", * "prev": null, * "next": "http://localhost/api/questions?page=2" * } * ), * @OA\Property( * property="meta", * type="object", * example={ * "current_page": 1, * "from": 1, * "last_page": 10, * "path": "http://localhost/api/questions", * "per_page": 15, * "to": 15, * "total": 150 * } * ) * ) * ) * ) */ public function index(Request $request) { $query = Question::with(['author', 'category']); if ($request->has('category_id')) { $query->where('category_id', $request->query('category_id')); } $questions = $query->paginate(self::PAGINATION_COUNT); return QuestionResource::collection($questions); } /** * @OA\Get( * path="/api/questions/{id}", * summary="Get a single question", * description="Retrieve a single question by its ID, including author and category", * tags={"Questions"}, * @OA\Parameter( * name="id", * in="path", * description="ID of the question to retrieve", * required=true, * @OA\Schema(type="integer") * ), * @OA\Response( * response=200, * description="Question retrieved successfully", * @OA\JsonContent(ref="#/components/schemas/QuestionResource") * ) * ) */ public function show(Question $question) { $question->load(['author', 'category']); return new QuestionResource($question); } /** * @OA\Post( * path="/api/questions", * summary="Create a new question (only admin or creator)", * description="Store a new question in the system.", * tags={"Questions"}, * security={{"bearerAuth":{}}}, * @OA\RequestBody( * required=true, * @OA\JsonContent( * required={"title","type","difficulty","variants","correct_answers"}, * @OA\Property(property="title", type="string", maxLength=255, example="Sample question"), * @OA\Property(property="description", type="string", nullable=true, example="Optional description"), * @OA\Property(property="type", type="string", enum={"single","multiply","text"}, example="single"), * @OA\Property(property="difficulty", type="integer", example=2), * @OA\Property( * property="variants", * type="array", * @OA\Items( * type="object", * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="text", type="string", example="Option 1") * ) * ), * @OA\Property( * property="correct_answers", * type="array", * @OA\Items( * type="integer", * example=1 * ) * ), * @OA\Property(property="category_id", type="integer", nullable=true, example=3) * ) * ), * @OA\Response( * response=200, * description="Question created successfully", * @OA\JsonContent(ref="#/components/schemas/QuestionResource") * ) * ) */ public function store(Request $request) { $this->authorize('create', Question::class); $fields = $request->validate(self::get_field_rules()); $fields['author_id'] = $request->user()->id; $question = Question::create($fields); Log::writeLog("Question '" . $question->title . "' is created by " . $request->user()->username); return new QuestionResource($question); } /** * @OA\Put( * path="/api/questions/{id}", * summary="Update a question (only admin or creator)", * description="Update an existing question by ID.", * tags={"Questions"}, * security={{"bearerAuth":{}}}, * @OA\Parameter( * name="id", * in="path", * required=true, * description="ID of the question to update", * @OA\Schema(type="integer") * ), * @OA\RequestBody( * required=true, * @OA\JsonContent( * required={"title","type","difficulty","variants","correct_answers"}, * @OA\Property(property="title", type="string", maxLength=255, example="Updated question"), * @OA\Property(property="description", type="string", nullable=true, example="Updated description"), * @OA\Property(property="type", type="string", enum={"single","multiply","text"}, example="multiply"), * @OA\Property(property="difficulty", type="integer", example=3), * @OA\Property( * property="variants", * type="array", * @OA\Items( * type="object", * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="text", type="string", example="Option 1") * ) * ), * @OA\Property( * property="correct_answers", * type="array", * @OA\Items( * type="integer", * example=1 * ) * ), * @OA\Property(property="category_id", type="integer", nullable=true, example=2) * ) * ), * @OA\Response( * response=200, * description="Question updated successfully", * @OA\JsonContent(ref="#/components/schemas/QuestionResource") * ) * ) */ public function update(Request $request, Question $question) { $this->authorize('update', $question); $fields = $request->validate(self::get_field_rules()); $fields['variants'] = json_encode($fields['variants']); $question->update($fields); Log::writeLog("Question '" . $question->title . "' is updated by " . $request->user()->username); return new QuestionResource($question); } /** * @OA\Delete( * path="/api/questions/{id}", * summary="Delete a question (only admin or creator)", * description="Delete a question by ID .", * tags={"Questions"}, * security={{"bearerAuth":{}}}, * @OA\Parameter( * name="id", * in="path", * required=true, * description="ID of the question to delete", * @OA\Schema(type="integer") * ), * @OA\Response( * response=200, * description="Question deleted successfully", * @OA\JsonContent( * @OA\Property(property="message", type="string", example="The hitcount was deleted") * ) * ) * ) */ public function destroy(Request $request, Question $question) { $this->authorize('delete', $question); $question->delete(); Log::writeLog("Question '" . $question->title . "' is deleted by " . $request->user()->username); return ['message' => 'The question was deleted']; } /** * @OA\Post( * path="/api/questions/openai-generate", * summary="Generate a question using OpenAI (Creator or Admin)", * description="Generates a new question via OpenAI API. Only authorized users (creator or admin) can call this endpoint.", * tags={"Questions"}, * security={{"bearerAuth":{}}}, * @OA\RequestBody( * required=true, * @OA\JsonContent( * required={"type","category_id","language","difficulty","promt"}, * @OA\Property(property="type", type="string", description="Type of the question", enum={"single","multiple","text"}, example="single"), * @OA\Property(property="category_id", type="integer", description="ID of the category", example=3), * @OA\Property(property="language", type="string", description="Language for AI prompt", example="en"), * @OA\Property(property="difficulty", type="integer", description="Difficulty level (0-10)", example=5), * @OA\Property(property="promt", type="string", description="The prompt for AI to generate the question", example="Explain HTTP methods") * ) * ), * @OA\Response( * response=200, * description="AI-generated question", * @OA\JsonContent( * @OA\Property(property="question", type="object", * @OA\Property(property="title", type="string", example="Sample question"), * @OA\Property(property="description", type="string", example="Optional description"), * @OA\Property(property="type", type="string", enum={"single","multiple","text"}), * @OA\Property(property="difficulty", type="integer", example=5), * @OA\Property(property="variants", type="array", * @OA\Items( * @OA\Property(property="id", type="integer"), * @OA\Property(property="text", type="string") * ) * ), * @OA\Property(property="correct_answers", type="array", * @OA\Items(oneOf={@OA\Schema(type="integer"), @OA\Schema(type="string")}) * ), * @OA\Property(property="category_id", type="integer", example=3) * ) * ) * ), * @OA\Response( * response=422, * description="Validation error", * @OA\JsonContent( * @OA\Property(property="message", type="string"), * @OA\Property(property="errors", type="object") * ) * ), * @OA\Response( * response=500, * description="AI returned invalid JSON", * @OA\JsonContent( * @OA\Property(property="error", type="string"), * @OA\Property(property="raw", type="string") * ) * ) * ) */ public function openai_generate(Request $request) { $this->authorize('create', Question::class); $fields = $request->validate([ 'type' => 'required|string|in:single,multiple,text', 'category_id' => 'required|exists:categories,id', 'language' => 'required|string', 'difficulty' => 'required|integer', 'promt' => 'required|string' ]); $type = $fields['type']; $lang = $fields['language']; $difficulty = $fields['difficulty']; $cat_id = $fields['category_id']; $promt_json = json_encode($fields['promt']); $question_type_promt = ""; $answer = ""; $variations = <<< EOT [ { "id": 1, "text": "Option 1" }, { "id": 2, "text": "Option 2" }, { "id": 3, "text": "Option 3" }, { "id": 4, "text": "Option 4" } ] EOT; switch ($type) { case 'single': $question_type_promt = "Create 4 answer options, with one of them being correct (the \"correct_answers\" field is an array containing the ID of the correct option)."; $answer = json_encode([2]); ; break; case 'multiple': $question_type_promt = "Create between 4 and 6 answer options, with several of them being correct (the \"correct_answers\" field is an array containing the IDs of the correct options)."; $answer = json_encode([1,2]); ; break; case 'text': $question_type_promt = "Generate a question where there can only be a single, precise answer that can be written without an extended explanation. The \"correct_answers\" field should be an array containing only ONE answer as a string. The \"variations\" must be empty array"; $answer = json_encode(["Answer"]); $variations = '[]'; break; } $openai_promt = <<< EOT Generate 1 question using the following template (this is example): { "title": "Sample question", "description": "Optional description", "type": "$type", "difficulty": $difficulty, "variants": $variations, "correct_answers": $answer, "category_id": $cat_id } You can change only variants, correct_answers, description and title. Your response should be only JSON with no explanations, exactly following the template. $question_type_promt Question difficulty: $difficulty/10 The question should be based on the following prompt with language $lang: $promt_json EOT; $client = OpenAI::client(env("OPENAI_KEY")); $response = $client->responses()->create([ 'model' => env('OPENAI_MODEL', 'gpt-4.1-mini'), 'input' => $openai_promt, 'temperature' => 0, ]); $ai_answer = $response->outputText; $question_json = json_decode($ai_answer, true); if (json_last_error() !== JSON_ERROR_NONE) { return response()->json([ 'error' => 'AI returned invalid JSON', 'raw' => $ai_answer, ], 500); } return response()->json([ 'question' => $question_json, ]); } }