UserTests and Answers
This commit is contained in:
parent
8e2d51660a
commit
4e30af1841
@ -80,10 +80,6 @@ class QuestionController extends Controller
|
|||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=401,
|
|
||||||
* description="Unauthorized"
|
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
@ -117,14 +113,6 @@ class QuestionController extends Controller
|
|||||||
* response=200,
|
* response=200,
|
||||||
* description="Question retrieved successfully",
|
* description="Question retrieved successfully",
|
||||||
* @OA\JsonContent(ref="#/components/schemas/QuestionResource")
|
* @OA\JsonContent(ref="#/components/schemas/QuestionResource")
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=401,
|
|
||||||
* description="Unauthorized"
|
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=404,
|
|
||||||
* description="Question not found"
|
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
@ -174,14 +162,6 @@ class QuestionController extends Controller
|
|||||||
* response=200,
|
* response=200,
|
||||||
* description="Question created successfully",
|
* description="Question created successfully",
|
||||||
* @OA\JsonContent(ref="#/components/schemas/QuestionResource")
|
* @OA\JsonContent(ref="#/components/schemas/QuestionResource")
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=401,
|
|
||||||
* description="Unauthorized"
|
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=422,
|
|
||||||
* description="Validation error"
|
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
@ -245,14 +225,6 @@ class QuestionController extends Controller
|
|||||||
* response=200,
|
* response=200,
|
||||||
* description="Question updated successfully",
|
* description="Question updated successfully",
|
||||||
* @OA\JsonContent(ref="#/components/schemas/QuestionResource")
|
* @OA\JsonContent(ref="#/components/schemas/QuestionResource")
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=401,
|
|
||||||
* description="Unauthorized"
|
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=422,
|
|
||||||
* description="Validation error"
|
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
@ -289,18 +261,6 @@ class QuestionController extends Controller
|
|||||||
* @OA\JsonContent(
|
* @OA\JsonContent(
|
||||||
* @OA\Property(property="message", type="string", example="The hitcount was deleted")
|
* @OA\Property(property="message", type="string", example="The hitcount was deleted")
|
||||||
* )
|
* )
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=401,
|
|
||||||
* description="Unauthorized"
|
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=403,
|
|
||||||
* description="Forbidden"
|
|
||||||
* ),
|
|
||||||
* @OA\Response(
|
|
||||||
* response=404,
|
|
||||||
* description="Question not found"
|
|
||||||
* )
|
* )
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
@ -310,6 +270,6 @@ class QuestionController extends Controller
|
|||||||
$question->delete();
|
$question->delete();
|
||||||
Log::writeLog("Question '" . $question->title . "' is deleted by " . $request->user()->username);
|
Log::writeLog("Question '" . $question->title . "' is deleted by " . $request->user()->username);
|
||||||
|
|
||||||
return ['message' => 'The hitcount was deleted'];
|
return ['message' => 'The question was deleted'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,7 +95,7 @@ class TestController extends Controller
|
|||||||
* required={"title","closed_at","questions"},
|
* required={"title","closed_at","questions"},
|
||||||
* @OA\Property(property="title", type="string", maxLength=255, example="Sample Test"),
|
* @OA\Property(property="title", type="string", maxLength=255, example="Sample Test"),
|
||||||
* @OA\Property(property="description", type="string", nullable=true, example="Optional description"),
|
* @OA\Property(property="description", type="string", nullable=true, example="Optional description"),
|
||||||
* @OA\Property(property="closed_at", type="string", format="date-time", nullable=true, example="2025-12-01T23:59:59Z"),
|
* @OA\Property(property="closed_at", type="string", format="date-time", nullable=true, example="2025-12-01 23:59:59"),
|
||||||
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
|
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
|
||||||
* @OA\Property(
|
* @OA\Property(
|
||||||
* property="questions",
|
* property="questions",
|
||||||
|
|||||||
394
app/Http/Controllers/UserTestController.php
Normal file
394
app/Http/Controllers/UserTestController.php
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Resources\UserTestAnswerResource;
|
||||||
|
use App\Http\Resources\UserTestResource;
|
||||||
|
use App\Models\Log;
|
||||||
|
use App\Models\Question;
|
||||||
|
use App\Models\Test;
|
||||||
|
use App\Models\UserTest;
|
||||||
|
use App\Models\UserTestAnswer;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UserTestController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/user-tests",
|
||||||
|
* summary="Get list of user tests for the authenticated user",
|
||||||
|
* description="Retrieve all UserTests belonging to the authenticated user, with related category, test, and answers.",
|
||||||
|
* tags={"UserTests"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="List of user tests",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(
|
||||||
|
* property="data",
|
||||||
|
* type="array",
|
||||||
|
* @OA\Items(ref="#/components/schemas/UserTestResource")
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$userTests = UserTest::where('user_id', $user->id)
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->with(['user', 'category', 'answers.question'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return UserTestResource::collection($userTests);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/user-tests",
|
||||||
|
* summary="Create a new user test",
|
||||||
|
* description="Generate a new UserTest for the authenticated user with questions from a specific category and difficulty range.",
|
||||||
|
* tags={"UserTests"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* required={"category_id"},
|
||||||
|
* @OA\Property(property="category_id", type="integer", example=3, description="Category from which to select questions"),
|
||||||
|
* @OA\Property(property="min_difficulty", type="integer", nullable=true, example=0, description="Minimum difficulty of questions"),
|
||||||
|
* @OA\Property(property="max_difficulty", type="integer", nullable=true, example=10, description="Maximum difficulty of questions")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error or no questions found"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorize('create', UserTest::class);
|
||||||
|
|
||||||
|
$fields = $request->validate([
|
||||||
|
'category_id' => 'required|exists:categories,id',
|
||||||
|
'min_difficulty' => 'nullable|integer',
|
||||||
|
'max_difficulty' => 'nullable|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$minDifficulty = $fields['min_difficulty'] ?? 0;
|
||||||
|
$maxDifficulty = $fields['max_difficulty'] ?? 10;
|
||||||
|
|
||||||
|
$questions = Question::where('category_id', $fields['category_id'])
|
||||||
|
->whereBetween('difficulty', [$minDifficulty, $maxDifficulty])
|
||||||
|
->inRandomOrder()
|
||||||
|
->take(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($questions->isEmpty()) {
|
||||||
|
return response()->json(['message' => 'No questions found for this criteria'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userTest = new UserTest();
|
||||||
|
$userTest->category_id = $fields['category_id'];
|
||||||
|
$userTest->user_id = $request->user()->id;
|
||||||
|
$userTest->closed_at = now()->addDay();
|
||||||
|
$userTest->save();
|
||||||
|
$userTest->generateEmptyAnswers($questions);
|
||||||
|
|
||||||
|
return new UserTestResource($userTest);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/user-tests/by-test",
|
||||||
|
* summary="Create a UserTest based on an existing Test",
|
||||||
|
* description="Generates a UserTest from an existing Test, including all its questions. Only available if the Test is available.",
|
||||||
|
* tags={"UserTests"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* required={"test_id"},
|
||||||
|
* @OA\Property(property="test_id", type="integer", example=1, description="ID of the existing Test to create a UserTest from")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="UserTest created successfully",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/UserTestResource")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=409,
|
||||||
|
* description="Test is not available at this time"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function storeByTest(Request $request)
|
||||||
|
{
|
||||||
|
$this->authorize('create', UserTest::class);
|
||||||
|
|
||||||
|
$fields = $request->validate([
|
||||||
|
'test_id' => 'required|exists:tests,id'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$test = Test::findOrFail($fields['test_id']);
|
||||||
|
|
||||||
|
if(!$test->isAvailable())
|
||||||
|
return response(['message' => 'This test isn\'t available now!'], 409);
|
||||||
|
|
||||||
|
$test->load(['questions']);
|
||||||
|
|
||||||
|
$userTest = new UserTest();
|
||||||
|
$userTest->category_id = $test->category_id;
|
||||||
|
$userTest->test_id = $test->id;
|
||||||
|
$userTest->closed_at = $test->closed_at;
|
||||||
|
$userTest->user_id = $request->user()->id;
|
||||||
|
$userTest->save();
|
||||||
|
$userTest->generateEmptyAnswers($test->questions->toArray());
|
||||||
|
|
||||||
|
return new UserTestResource($userTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/user-test-answers/{answer}/submit",
|
||||||
|
* summary="Submit an answer for a UserTestAnswer",
|
||||||
|
* description="Records the user's answer for a specific UserTestAnswer. The answer must match the question type (string for text, integer for single/multiple choice).",
|
||||||
|
* tags={"UserTestAnswers"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="answer",
|
||||||
|
* in="path",
|
||||||
|
* required=true,
|
||||||
|
* description="ID of the UserTestAnswer to submit",
|
||||||
|
* @OA\Schema(type="integer")
|
||||||
|
* ),
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* required={"answer"},
|
||||||
|
* @OA\Property(
|
||||||
|
* property="answer",
|
||||||
|
* type="array",
|
||||||
|
* description="Array of answers. Should be string(s) if the question type is 'text' or integer(s) otherwise",
|
||||||
|
* @OA\Items(
|
||||||
|
* oneOf={
|
||||||
|
* @OA\Schema(type="string"),
|
||||||
|
* @OA\Schema(type="integer")
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Answer recorded successfully",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* @OA\Property(property="message", type="string", example="Answer is recorded")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=422,
|
||||||
|
* description="Validation error (wrong type or empty array)"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=403,
|
||||||
|
* description="Forbidden (user cannot edit this answer)"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function answerByTest(Request $request, UserTestAnswer $answer)
|
||||||
|
{
|
||||||
|
$answer->load(['userTest', 'question']);
|
||||||
|
$answerType = $answer->question->type == 'text' ? 'string' : 'integer';
|
||||||
|
|
||||||
|
$this->authorize('canBeEdit', $answer->userTest);
|
||||||
|
|
||||||
|
$fields = $request->validate([
|
||||||
|
'answer' => 'required|array|min:1',
|
||||||
|
'answer.*' => 'required|' . $answerType
|
||||||
|
]);
|
||||||
|
$answer->answer = $fields['answer'];
|
||||||
|
$answer->save();
|
||||||
|
|
||||||
|
return ['message' => 'Answer is recorded'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/user-test-answers/{answer}",
|
||||||
|
* summary="Get a UserTestAnswer",
|
||||||
|
* description="Retrieve a specific UserTestAnswer by ID. Only the owner or authorized user can view it.",
|
||||||
|
* tags={"UserTestAnswers"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="answer",
|
||||||
|
* in="path",
|
||||||
|
* required=true,
|
||||||
|
* description="ID of the UserTestAnswer to retrieve",
|
||||||
|
* @OA\Schema(type="integer")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="UserTestAnswer retrieved successfully",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/UserTestAnswerResource")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=403,
|
||||||
|
* description="Forbidden: user cannot view this answer"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=404,
|
||||||
|
* description="UserTestAnswer not found"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function showAnswer(UserTestAnswer $answer)
|
||||||
|
{
|
||||||
|
$answer->load(['userTest', 'question']);
|
||||||
|
|
||||||
|
$this->authorize('view', $answer->userTest);
|
||||||
|
|
||||||
|
return new UserTestAnswerResource($answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/user-tests/{userTest}/complete",
|
||||||
|
* summary="Complete a UserTest",
|
||||||
|
* description="Marks a UserTest as completed, calculates the score based on answers, and prevents further editing.",
|
||||||
|
* tags={"UserTests"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="userTest",
|
||||||
|
* in="path",
|
||||||
|
* required=true,
|
||||||
|
* description="ID of the UserTest to complete",
|
||||||
|
* @OA\Schema(type="integer")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Test successfully completed",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* @OA\Property(property="message", type="string", example="Test is completed")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=403,
|
||||||
|
* description="Forbidden: user cannot edit this test"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function completeTest(UserTest $userTest)
|
||||||
|
{
|
||||||
|
$this->authorize('canBeEdit', $userTest);
|
||||||
|
|
||||||
|
$userTest->load(['category', 'answers.question', 'user']);
|
||||||
|
$userTest->completeTest();
|
||||||
|
|
||||||
|
return ['message' => 'Test is completed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/user-tests/{userTest}",
|
||||||
|
* summary="Get a single UserTest",
|
||||||
|
* description="Retrieve a UserTest with its answers, question details, category, and user info.",
|
||||||
|
* tags={"UserTests"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="userTest",
|
||||||
|
* in="path",
|
||||||
|
* required=true,
|
||||||
|
* description="ID of the UserTest to retrieve",
|
||||||
|
* @OA\Schema(type="integer")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="UserTest details",
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/UserTestResource")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=403,
|
||||||
|
* description="Forbidden: user cannot view this test"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=404,
|
||||||
|
* description="UserTest not found"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function show(UserTest $userTest)
|
||||||
|
{
|
||||||
|
$this->authorize('view', $userTest);
|
||||||
|
$userTest->load(['user', 'category', 'answers.question']);
|
||||||
|
|
||||||
|
return new UserTestResource($userTest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Delete(
|
||||||
|
* path="/api/user-tests/{userTest}",
|
||||||
|
* summary="Delete a UserTest",
|
||||||
|
* description="Delete a UserTest by ID. Only the owner or admin can delete it.",
|
||||||
|
* tags={"UserTests"},
|
||||||
|
* security={{"bearerAuth":{}}},
|
||||||
|
* @OA\Parameter(
|
||||||
|
* name="userTest",
|
||||||
|
* in="path",
|
||||||
|
* required=true,
|
||||||
|
* description="ID of the UserTest to delete",
|
||||||
|
* @OA\Schema(type="integer")
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="UserTest deleted successfully",
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="message", type="string", example="The UserTest was deleted")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=401,
|
||||||
|
* description="Unauthorized"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=403,
|
||||||
|
* description="Forbidden: user cannot delete this test"
|
||||||
|
* ),
|
||||||
|
* @OA\Response(
|
||||||
|
* response=404,
|
||||||
|
* description="UserTest not found"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, UserTest $userTest)
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $userTest);
|
||||||
|
$userTest->load('user');
|
||||||
|
|
||||||
|
$username = $userTest->user ? $userTest->user->username : 'unknown';
|
||||||
|
$userTestId = $userTest->id;
|
||||||
|
|
||||||
|
$userTest->delete();
|
||||||
|
|
||||||
|
Log::writeLog("UserTest #$userTestId ($username)' is deleted by " . $request->user()->username);
|
||||||
|
|
||||||
|
return ['message' => 'The UserTest was deleted'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,7 +33,6 @@ class QuestionResource extends JsonResource
|
|||||||
* type="array",
|
* type="array",
|
||||||
* @OA\Items(type="integer", example=1)
|
* @OA\Items(type="integer", example=1)
|
||||||
* ),
|
* ),
|
||||||
* @OA\Property(property="is_pending_question", type="integer", example=0),
|
|
||||||
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
|
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
|
||||||
* @OA\Property(property="category", ref="#/components/schemas/CategoryResource"),
|
* @OA\Property(property="category", ref="#/components/schemas/CategoryResource"),
|
||||||
* @OA\Property(property="author_id", type="integer", nullable=true, example=2),
|
* @OA\Property(property="author_id", type="integer", nullable=true, example=2),
|
||||||
@ -52,7 +51,6 @@ class QuestionResource extends JsonResource
|
|||||||
'difficulty' => $this->difficulty,
|
'difficulty' => $this->difficulty,
|
||||||
'variants' => $this->variants,
|
'variants' => $this->variants,
|
||||||
'correct_answers' => $this->correct_answers,
|
'correct_answers' => $this->correct_answers,
|
||||||
'is_pending_question' => $this->is_pending_question,
|
|
||||||
|
|
||||||
'category_id' => $this->category_id,
|
'category_id' => $this->category_id,
|
||||||
'category' => new CategoryResource($this->whenLoaded('category')),
|
'category' => new CategoryResource($this->whenLoaded('category')),
|
||||||
|
|||||||
54
app/Http/Resources/UserTestAnswerResource.php
Normal file
54
app/Http/Resources/UserTestAnswerResource.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class UserTestAnswerResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="UserTestAnswerResource",
|
||||||
|
* type="object",
|
||||||
|
* title="UserTestAnswer",
|
||||||
|
* properties={
|
||||||
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="user_test_id", type="integer", example=5),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="question_id", type="integer", example=12),
|
||||||
|
* @OA\Property(property="question", ref="#/components/schemas/QuestionResource"),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="answer", type="array", @OA\Items(type="string"), example={"42","43"}),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="user_id", type="integer", example=10),
|
||||||
|
* @OA\Property(property="user", ref="#/components/schemas/UserResource"),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="is_correct", type="boolean", example=true),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-11-11T20:39:22Z"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-11-11T20:39:22Z")
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
|
||||||
|
'user_test_id' => $this->user_test_id,
|
||||||
|
|
||||||
|
'question_id' => $this->question_id,
|
||||||
|
'question' => new QuestionResource($this->whenLoaded('question')),
|
||||||
|
|
||||||
|
'answer' => $this->answer,
|
||||||
|
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'user' => new UserResource($this->whenLoaded('user')),
|
||||||
|
|
||||||
|
'is_correct' => $this->isCorrect(),
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Http/Resources/UserTestResource.php
Normal file
62
app/Http/Resources/UserTestResource.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class UserTestResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="UserTestResource",
|
||||||
|
* type="object",
|
||||||
|
* title="UserTest",
|
||||||
|
* properties={
|
||||||
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="test_id", type="integer", example=5),
|
||||||
|
* @OA\Property(property="test", ref="#/components/schemas/TestResource"),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="user_id", type="integer", example=10),
|
||||||
|
* @OA\Property(property="user", ref="#/components/schemas/UserResource"),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="closed_at", type="string", format="date-time", example="2025-12-01T23:59:59Z"),
|
||||||
|
* @OA\Property(property="is_completed", type="boolean", example=false),
|
||||||
|
* @OA\Property(property="score", type="integer", example=85),
|
||||||
|
* @OA\Property(property="is_available", type="boolean", example=true),
|
||||||
|
*
|
||||||
|
* @OA\Property(
|
||||||
|
* property="answers",
|
||||||
|
* type="array",
|
||||||
|
* @OA\Items(ref="#/components/schemas/UserTestAnswerResource")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-11-11T20:39:22Z"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-11-11T20:39:22Z")
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
|
||||||
|
'test_id' => $this->test_id,
|
||||||
|
'test' => new TestResource($this->whenLoaded('test')),
|
||||||
|
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'user' => new UserResource($this->whenLoaded('user')),
|
||||||
|
|
||||||
|
'closed_at' => $this->closed_at,
|
||||||
|
'is_completed' => $this->is_completed,
|
||||||
|
'score' => $this->score,
|
||||||
|
|
||||||
|
'is_available' => $this->isAvailable(),
|
||||||
|
'answers' => UserTestAnswerResource::collection($this->whenLoaded('answers')),
|
||||||
|
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,6 +24,11 @@ class Test extends Model
|
|||||||
'updated_at',
|
'updated_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return $this->closed_at === null || now()->lt($this->closed_at);
|
||||||
|
}
|
||||||
|
|
||||||
public function category()
|
public function category()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Category::class);
|
return $this->belongsTo(Category::class);
|
||||||
|
|||||||
92
app/Models/UserTest.php
Normal file
92
app/Models/UserTest.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UserTest extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'category_id',
|
||||||
|
'test_id',
|
||||||
|
'closed_at',
|
||||||
|
'is_completed',
|
||||||
|
'score',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'closed_at' => 'datetime',
|
||||||
|
'is_completed' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generating empty answers to fill later
|
||||||
|
* @param Collection<Question>|array<Question> $questions
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function generateEmptyAnswers(Collection|array $questions)
|
||||||
|
{
|
||||||
|
$answers = [];
|
||||||
|
foreach ($questions as $question) {
|
||||||
|
$questionId = is_array($question) ? $question['id'] : $question->id;
|
||||||
|
|
||||||
|
$answers[] = [
|
||||||
|
'question_id' => $questionId,
|
||||||
|
'user_test_id' => $this->id,
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
UserTestAnswer::insert($answers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function completeTest()
|
||||||
|
{
|
||||||
|
$correctAnswers = 0;
|
||||||
|
$allAnswers = $this->answers->count();
|
||||||
|
|
||||||
|
foreach ($this->answers as $answer) {
|
||||||
|
if($answer->isCorrect())
|
||||||
|
$correctAnswers++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->score = intval($correctAnswers * 100 / $allAnswers);
|
||||||
|
$this->is_completed = true;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check cooldown for current test
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isAvailable()
|
||||||
|
{
|
||||||
|
return (!$this->is_completed) &&
|
||||||
|
($this->closed_at === null || now()->lt($this->closed_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function category()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Category::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Test::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
public function answers()
|
||||||
|
{
|
||||||
|
return $this->hasMany(UserTestAnswer::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
app/Models/UserTestAnswer.php
Normal file
63
app/Models/UserTestAnswer.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UserTestAnswer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'question_id',
|
||||||
|
'answer',
|
||||||
|
'user_test_id',
|
||||||
|
'user_id',
|
||||||
|
];
|
||||||
|
protected $casts = [
|
||||||
|
'answer' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function isCorrect() {
|
||||||
|
if(empty($this->answer))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if($this->question->type == 'text') {
|
||||||
|
$correctAnswer = trim(strtolower($this->question->correct_answers[0]));
|
||||||
|
$answer = trim(strtolower($this->answer[0]));
|
||||||
|
|
||||||
|
return $correctAnswer == $answer;
|
||||||
|
}
|
||||||
|
if($this->question->type == 'single') {
|
||||||
|
$correctAnswer = intval($this->question->correct_answers[0]);
|
||||||
|
$answer = intval($this->answer[0]);
|
||||||
|
|
||||||
|
return $correctAnswer == $answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->question->type == 'multiple') {
|
||||||
|
$correctAnswers = collect($this->question->correct_answers)->map(fn($a) => (int)$a)->sort()->values()->all();
|
||||||
|
$answers = collect($this->answer)->map(fn($a) => (int)$a)->sort()->values()->all();
|
||||||
|
|
||||||
|
return $correctAnswers === $answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function question()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Question::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userTest()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserTest::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
app/Policies/UserTestPolicy.php
Normal file
71
app/Policies/UserTestPolicy.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserTest;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
|
class UserTestPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, UserTest $userTest): bool
|
||||||
|
{
|
||||||
|
return $user->id == $userTest->user_id || $user->type == 'creator' || $user->type == 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeEdit(User $user, UserTest $userTest): bool
|
||||||
|
{
|
||||||
|
return ($user->id == $userTest->user_id && $userTest->isAvailable()) || $user->type == 'creator' || $user->type == 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->type != 'banned';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, UserTest $userTest): bool
|
||||||
|
{
|
||||||
|
return $user->type == 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, UserTest $userTest): bool
|
||||||
|
{
|
||||||
|
return $user->type == 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, UserTest $userTest): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, UserTest $userTest): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,6 @@ return new class extends Migration
|
|||||||
$table->unsignedBigInteger('category_id')->nullable();
|
$table->unsignedBigInteger('category_id')->nullable();
|
||||||
$table->unsignedBigInteger('author_id')->nullable();
|
$table->unsignedBigInteger('author_id')->nullable();
|
||||||
$table->text('variants');
|
$table->text('variants');
|
||||||
$table->tinyInteger('is_pending_question')->unsigned()->default(0);
|
|
||||||
$table->text('correct_answers')->default('[]');
|
$table->text('correct_answers')->default('[]');
|
||||||
|
|
||||||
$table->foreign('author_id')->references('id')->on('users')->onDelete('set null');
|
$table->foreign('author_id')->references('id')->on('users')->onDelete('set null');
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
<?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::create('user_tests', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('category_id')->nullable();
|
||||||
|
$table->unsignedBigInteger('test_id')->nullable();
|
||||||
|
$table->unsignedBigInteger('user_id');
|
||||||
|
$table->dateTime('closed_at')->nullable();
|
||||||
|
$table->boolean('is_completed')->default(false);
|
||||||
|
$table->integer('score')->default(0);
|
||||||
|
|
||||||
|
$table->foreign('category_id')
|
||||||
|
->references('id')
|
||||||
|
->on('categories')
|
||||||
|
->onDelete('set null');
|
||||||
|
|
||||||
|
$table->foreign('test_id')
|
||||||
|
->references('id')
|
||||||
|
->on('tests')
|
||||||
|
->onDelete('set null');
|
||||||
|
|
||||||
|
$table->foreign('user_id')
|
||||||
|
->references('id')
|
||||||
|
->on('users')
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_tests');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?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::create('user_test_answers', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('question_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->text('answer')->nullable();
|
||||||
|
$table->foreignId('user_test_id')->constrained('user_tests')->cascadeOnDelete();
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('user_test_answers');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -7,8 +7,27 @@ use App\Http\Controllers\LogController;
|
|||||||
use App\Http\Controllers\QuestionController;
|
use App\Http\Controllers\QuestionController;
|
||||||
use App\Http\Controllers\TestController;
|
use App\Http\Controllers\TestController;
|
||||||
use App\Http\Controllers\UserController;
|
use App\Http\Controllers\UserController;
|
||||||
|
use App\Http\Controllers\UserTestController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
|
||||||
|
// UserTests
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::get('user-tests', [UserTestController::class, 'index']);
|
||||||
|
Route::post('user-tests', [UserTestController::class, 'store']);
|
||||||
|
Route::post('user-tests/by-test', [UserTestController::class, 'storeByTest']);
|
||||||
|
|
||||||
|
Route::get('user-tests/{userTest}', [UserTestController::class, 'show']);
|
||||||
|
Route::post('user-tests/{userTest}/complete', [UserTestController::class, 'completeTest']);
|
||||||
|
Route::delete('user-tests/{userTest}', [UserTestController::class, 'destroy']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// UserTestAnswers
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::get('user-test-answers/{answer}', [UserTestController::class, 'showAnswer']);
|
||||||
|
Route::post('user-test-answers/{answer}/submit', [UserTestController::class, 'answerByTest']);
|
||||||
|
});
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
Route::get('tests', [TestController::class, 'index']);
|
Route::get('tests', [TestController::class, 'index']);
|
||||||
Route::get('tests/{test}', [TestController::class, 'show']);
|
Route::get('tests/{test}', [TestController::class, 'show']);
|
||||||
|
|||||||
@ -1178,9 +1178,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1275,12 +1272,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation error"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@ -1319,12 +1310,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Question not found"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1430,12 +1415,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation error"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@ -1478,15 +1457,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Question not found"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@ -1626,7 +1596,7 @@
|
|||||||
"closed_at": {
|
"closed_at": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"example": "2025-12-01T23:59:59Z",
|
"example": "2025-12-01 23:59:59",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
"category_id": {
|
"category_id": {
|
||||||
@ -2187,6 +2157,415 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/user-tests": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"UserTests"
|
||||||
|
],
|
||||||
|
"summary": "Get list of user tests for the authenticated user",
|
||||||
|
"description": "Retrieve all UserTests belonging to the authenticated user, with related category, test, and answers.",
|
||||||
|
"operationId": "bf312e7c74799e8dbaa29025b8f52f46",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of user tests",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserTestResource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"UserTests"
|
||||||
|
],
|
||||||
|
"summary": "Create a new user test",
|
||||||
|
"description": "Generate a new UserTest for the authenticated user with questions from a specific category and difficulty range.",
|
||||||
|
"operationId": "dab4526b31f8a1bf1abf3048c3a4ecbe",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"required": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"category_id": {
|
||||||
|
"description": "Category from which to select questions",
|
||||||
|
"type": "integer",
|
||||||
|
"example": 3
|
||||||
|
},
|
||||||
|
"min_difficulty": {
|
||||||
|
"description": "Minimum difficulty of questions",
|
||||||
|
"type": "integer",
|
||||||
|
"example": 0,
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"max_difficulty": {
|
||||||
|
"description": "Maximum difficulty of questions",
|
||||||
|
"type": "integer",
|
||||||
|
"example": 10,
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error or no questions found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/user-tests/by-test": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"UserTests"
|
||||||
|
],
|
||||||
|
"summary": "Create a UserTest based on an existing Test",
|
||||||
|
"description": "Generates a UserTest from an existing Test, including all its questions. Only available if the Test is available.",
|
||||||
|
"operationId": "81b613bf7df7621756ee71dc88027236",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"required": [
|
||||||
|
"test_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"test_id": {
|
||||||
|
"description": "ID of the existing Test to create a UserTest from",
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "UserTest created successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserTestResource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Test is not available at this time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/user-test-answers/{answer}/submit": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"UserTestAnswers"
|
||||||
|
],
|
||||||
|
"summary": "Submit an answer for a UserTestAnswer",
|
||||||
|
"description": "Records the user's answer for a specific UserTestAnswer. The answer must match the question type (string for text, integer for single/multiple choice).",
|
||||||
|
"operationId": "4a7d5822bbbe5fe72fed7aca775ab792",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "answer",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of the UserTestAnswer to submit",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"required": [
|
||||||
|
"answer"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"answer": {
|
||||||
|
"description": "Array of answers. Should be string(s) if the question type is 'text' or integer(s) otherwise",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Answer recorded successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Answer is recorded"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error (wrong type or empty array)"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden (user cannot edit this answer)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/user-test-answers/{answer}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"UserTestAnswers"
|
||||||
|
],
|
||||||
|
"summary": "Get a UserTestAnswer",
|
||||||
|
"description": "Retrieve a specific UserTestAnswer by ID. Only the owner or authorized user can view it.",
|
||||||
|
"operationId": "ea81ce3e7c5383b8fabeef5338bb1db9",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "answer",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of the UserTestAnswer to retrieve",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "UserTestAnswer retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserTestAnswerResource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden: user cannot view this answer"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "UserTestAnswer not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/user-tests/{userTest}/complete": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"UserTests"
|
||||||
|
],
|
||||||
|
"summary": "Complete a UserTest",
|
||||||
|
"description": "Marks a UserTest as completed, calculates the score based on answers, and prevents further editing.",
|
||||||
|
"operationId": "d9265537e9cfb510ba68eede4930ea09",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "userTest",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of the UserTest to complete",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Test successfully completed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Test is completed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden: user cannot edit this test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/user-tests/{userTest}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"UserTests"
|
||||||
|
],
|
||||||
|
"summary": "Get a single UserTest",
|
||||||
|
"description": "Retrieve a UserTest with its answers, question details, category, and user info.",
|
||||||
|
"operationId": "0de523e8221fc7a1b13fb6dbfd40e3f3",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "userTest",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of the UserTest to retrieve",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "UserTest details",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserTestResource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden: user cannot view this test"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "UserTest not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"UserTests"
|
||||||
|
],
|
||||||
|
"summary": "Delete a UserTest",
|
||||||
|
"description": "Delete a UserTest by ID. Only the owner or admin can delete it.",
|
||||||
|
"operationId": "a5e4b2f9860f4428f72bd99b5eb9688d",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "userTest",
|
||||||
|
"in": "path",
|
||||||
|
"description": "ID of the UserTest to delete",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "UserTest deleted successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "The UserTest was deleted"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden: user cannot delete this test"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "UserTest not found"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@ -2326,10 +2705,6 @@
|
|||||||
"example": 1
|
"example": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is_pending_question": {
|
|
||||||
"type": "integer",
|
|
||||||
"example": 0
|
|
||||||
},
|
|
||||||
"category_id": {
|
"category_id": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"example": 3,
|
"example": 3,
|
||||||
@ -2449,6 +2824,115 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
},
|
||||||
|
"UserTestAnswerResource": {
|
||||||
|
"title": "UserTestAnswer",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"user_test_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 5
|
||||||
|
},
|
||||||
|
"question_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 12
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"$ref": "#/components/schemas/QuestionResource"
|
||||||
|
},
|
||||||
|
"answer": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": [
|
||||||
|
"42",
|
||||||
|
"43"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"$ref": "#/components/schemas/UserResource"
|
||||||
|
},
|
||||||
|
"is_correct": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-11-11T20:39:22Z"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-11-11T20:39:22Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"UserTestResource": {
|
||||||
|
"title": "UserTest",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 1
|
||||||
|
},
|
||||||
|
"test_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 5
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"$ref": "#/components/schemas/TestResource"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 10
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"$ref": "#/components/schemas/UserResource"
|
||||||
|
},
|
||||||
|
"closed_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-12-01T23:59:59Z"
|
||||||
|
},
|
||||||
|
"is_completed": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": false
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "integer",
|
||||||
|
"example": 85
|
||||||
|
},
|
||||||
|
"is_available": {
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"answers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserTestAnswerResource"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-11-11T20:39:22Z"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"example": "2025-11-11T20:39:22Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {
|
"securitySchemes": {
|
||||||
@ -2487,6 +2971,14 @@
|
|||||||
{
|
{
|
||||||
"name": "Users",
|
"name": "Users",
|
||||||
"description": "Users"
|
"description": "Users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UserTests",
|
||||||
|
"description": "UserTests"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UserTestAnswers",
|
||||||
|
"description": "UserTestAnswers"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user