From 9d59e00b50355f0ae121ae493eafef88d4fe2750 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Tue, 13 Aug 2024 14:02:28 +0200 Subject: [PATCH 01/16] group admin routes --- routes/web.php | 45 ++++++++++++----------- tests/Feature/AnswerTest.php | 66 +++++++++++++++++----------------- tests/Feature/QuestionTest.php | 54 ++++++++++++++-------------- tests/Feature/QuizTest.php | 42 +++++++++++----------- 4 files changed, 105 insertions(+), 102 deletions(-) diff --git a/routes/web.php b/routes/web.php index 710fd4fa..fbe960e6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,26 +12,29 @@ Route::get("/", fn(): Response => inertia("Welcome")); -Route::get("/quizzes", [QuizController::class, "index"]); -Route::post("/quizzes", [QuizController::class, "store"]); -Route::get("/quizzes/{quiz}", [QuizController::class, "show"]); -Route::patch("/quizzes/{quiz}", [QuizController::class, "update"])->can("update,quiz"); -Route::delete("/quizzes/{quiz}", [QuizController::class, "destroy"])->can("delete,quiz"); -Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); -Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"]); +Route::group(["prefix" => "admin"], function () { -Route::get("/quizzes/{quiz}/questions", [QuizQuestionController::class, "index"]); -Route::post("/quizzes/{quiz}/questions", [QuizQuestionController::class, "store"])->can("create," . Question::class . ",quiz"); -Route::get("/questions/{question}", [QuizQuestionController::class, "show"]); -Route::patch("/questions/{question}", [QuizQuestionController::class, "update"])->can("update,question"); -Route::delete("/questions/{question}", [QuizQuestionController::class, "destroy"])->can("delete,question"); -Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"])->can("clone,question,quiz"); + Route::get("/quizzes", [QuizController::class, "index"]); + Route::post("/quizzes", [QuizController::class, "store"]); + Route::get("/quizzes/{quiz}", [QuizController::class, "show"]); + Route::patch("/quizzes/{quiz}", [QuizController::class, "update"])->can("update,quiz"); + Route::delete("/quizzes/{quiz}", [QuizController::class, "destroy"])->can("delete,quiz"); + Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); + Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"]); -Route::get("/questions/{question}/answers", [QuestionAnswerController::class, "index"]); -Route::post("/questions/{question}/answers", [QuestionAnswerController::class, "store"])->can("create," . Answer::class . ",question"); -Route::get("/answers/{answer}", [QuestionAnswerController::class, "show"]); -Route::patch("/answers/{answer}", [QuestionAnswerController::class, "update"])->can("update,answer"); -Route::delete("/answers/{answer}", [QuestionAnswerController::class, "destroy"])->can("delete,answer"); -Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"])->can("clone,answer,question"); -Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"])->can("update,answer"); -Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"])->can("update,answer"); + Route::get("/quizzes/{quiz}/questions", [QuizQuestionController::class, "index"]); + Route::post("/quizzes/{quiz}/questions", [QuizQuestionController::class, "store"])->can("create," . Question::class . ",quiz"); + Route::get("/questions/{question}", [QuizQuestionController::class, "show"]); + Route::patch("/questions/{question}", [QuizQuestionController::class, "update"])->can("update,question"); + Route::delete("/questions/{question}", [QuizQuestionController::class, "destroy"])->can("delete,question"); + Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"])->can("clone,question,quiz"); + + Route::get("/questions/{question}/answers", [QuestionAnswerController::class, "index"]); + Route::post("/questions/{question}/answers", [QuestionAnswerController::class, "store"])->can("create," . Answer::class . ",question"); + Route::get("/answers/{answer}", [QuestionAnswerController::class, "show"]); + Route::patch("/answers/{answer}", [QuestionAnswerController::class, "update"])->can("update,answer"); + Route::delete("/answers/{answer}", [QuestionAnswerController::class, "destroy"])->can("delete,answer"); + Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"])->can("clone,answer,question"); + Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"])->can("update,answer"); + Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"])->can("update,answer"); +}); diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php index 6546052c..11bc96ae 100644 --- a/tests/Feature/AnswerTest.php +++ b/tests/Feature/AnswerTest.php @@ -32,7 +32,7 @@ public function testUserCanViewQuestionAnswers(): void $this->assertDatabaseCount("answers", 10); $this->actingAs($this->user) - ->get("/questions/{$question->id}/answers") + ->get("/admin/questions/{$question->id}/answers") ->assertInertia( fn(Assert $page) => $page ->component("Answer/Index") @@ -42,7 +42,7 @@ public function testUserCanViewQuestionAnswers(): void public function testUserCannotViewAnswersOfQuestionThatNotExisted(): void { - $this->actingAs($this->user)->get("/questions/1/answers") + $this->actingAs($this->user)->get("/admin/questions/1/answers") ->assertStatus(404); } @@ -53,7 +53,7 @@ public function testUserCanViewSingleAnswer(): void $this->assertDatabaseCount("answers", 1); $this->actingAs($this->user) - ->get("/answers/{$answer->id}") + ->get("/admin/answers/{$answer->id}") ->assertInertia( fn(Assert $page) => $page ->component("Answer/Show") @@ -68,7 +68,7 @@ public function testUserCanViewLockedAnswer(): void $this->assertDatabaseCount("answers", 1); $this->actingAs($this->user) - ->get("/answers/{$answer->id}") + ->get("/admin/answers/{$answer->id}") ->assertInertia( fn(Assert $page) => $page ->component("Answer/Show") @@ -79,7 +79,7 @@ public function testUserCanViewLockedAnswer(): void public function testUserCannotViewAnswerThatNotExisted(): void { - $this->actingAs($this->user)->get("/answers/1") + $this->actingAs($this->user)->get("/admin/answers/1") ->assertStatus(404); } @@ -89,7 +89,7 @@ public function testUserCanCreateAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer"]) + ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", [ @@ -104,15 +104,15 @@ public function testUserCanCreateMultipleAnswers(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 1"]) ->assertRedirect("/quizzes"); $this->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 2"]) + ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 2"]) ->assertRedirect("/quizzes"); $this->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 3"]) + ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 3"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["text" => "Example answer 1"]); @@ -124,7 +124,7 @@ public function testUserCannotCreateAnswerToQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/1/answers", ["text" => "Example answer"]) + ->post("/admin/questions/1/answers", ["text" => "Example answer"]) ->assertStatus(404); $this->assertDatabaseMissing("answers", [ @@ -138,7 +138,7 @@ public function testUserCannotCreateAnswerToQuestionThatIsLocked(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 1"]) ->assertStatus(403); $this->assertDatabaseMissing("answers", [ @@ -152,11 +152,11 @@ public function testUserCannotCreateInvalidAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/answers", []) + ->post("/admin/questions/{$question->id}/answers", []) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") - ->post("/questions/{$question->id}/answers", ["text" => false]) + ->post("/admin/questions/{$question->id}/answers", ["text" => false]) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("answers", 0); @@ -168,7 +168,7 @@ public function testUserCanEditAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->patch("/admin/answers/{$answer->id}", ["text" => "New answer"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["text" => "New answer"]); @@ -178,7 +178,7 @@ public function testUserCannotEditAnswerThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/1", ["text" => "New answer"]) + ->patch("/admin/answers/1", ["text" => "New answer"]) ->assertStatus(404); } @@ -188,11 +188,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/{$answer->id}", []) + ->patch("/admin/answers/{$answer->id}", []) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") - ->patch("/answers/{$answer->id}", ["text" => true]) + ->patch("/admin/answers/{$answer->id}", ["text" => true]) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("answers", ["text" => "Old answer"]); @@ -204,7 +204,7 @@ public function testUserCannotEditLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->patch("/admin/answers/{$answer->id}", ["text" => "New answer"]) ->assertStatus(403); $this->assertDatabaseHas("answers", ["text" => "Old answer"]); @@ -216,7 +216,7 @@ public function testUserCanDeleteAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/{$answer->id}") + ->delete("/admin/answers/{$answer->id}") ->assertRedirect("/quizzes"); $this->assertDatabaseMissing("answers", ["text" => "answer"]); @@ -228,7 +228,7 @@ public function testUserCannotDeleteLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/{$answer->id}") + ->delete("/admin/answers/{$answer->id}") ->assertStatus(403); $this->assertDatabaseHas("answers", ["text" => "answer"]); @@ -238,7 +238,7 @@ public function testUserCannotDeleteAnswerThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/1") + ->delete("/admin/answers/1") ->assertStatus(404); } @@ -248,7 +248,7 @@ public function testUserCanMarkAnswerAsCorrect(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/correct") + ->post("/admin/answers/{$answer->id}/correct") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -267,7 +267,7 @@ public function testUserCanChangeCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answerB->id}/correct") + ->post("/admin/answers/{$answerB->id}/correct") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerB->id]); @@ -285,7 +285,7 @@ public function testUserCanDeleteCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/answers/{$answer->id}") + ->delete("/admin/answers/{$answer->id}") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => null]); @@ -304,7 +304,7 @@ public function testUserCannotChangeCorrectAnswerInLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answerB->id}/correct") + ->post("/admin/answers/{$answerB->id}/correct") ->assertStatus(403); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); @@ -322,7 +322,7 @@ public function testUserCanChangeCorrectAnswerToInvalid(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/invalid") + ->post("/admin/answers/{$answer->id}/invalid") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -340,7 +340,7 @@ public function testUserCannotChangeCorrectAnswerToInvalidInLockedQuestion(): vo $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/invalid") + ->post("/admin/answers/{$answer->id}/invalid") ->assertStatus(403); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -356,7 +356,7 @@ public function testUserCanCopyAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -372,7 +372,7 @@ public function testUserCanCopyLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -388,7 +388,7 @@ public function testUserCannotCopyAnswerToLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") ->assertStatus(403); $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); @@ -408,7 +408,7 @@ public function testUserCanCopyCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -422,7 +422,7 @@ public function testUserCannotCopyAnswerThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/2/clone/{$question->id}") + ->post("/admin/answers/2/clone/{$question->id}") ->assertStatus(404); } @@ -432,7 +432,7 @@ public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/answers/{$answer->id}/clone/2") + ->post("/admin/answers/{$answer->id}/clone/2") ->assertStatus(404); } } diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index 673235a1..1dd3fc6f 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -36,7 +36,7 @@ public function testUserCanViewQuizQuestions(): void $this->assertDatabaseCount("answers", 10); $this->actingAs($this->user) - ->get("/quizzes/{$quiz->id}/questions") + ->get("/admin/quizzes/{$quiz->id}/questions") ->assertInertia( fn(Assert $page) => $page ->component("Question/Index") @@ -47,7 +47,7 @@ public function testUserCanViewQuizQuestions(): void public function testUserCannotViewQuestionsOfQuizThatNotExisted(): void { - $this->actingAs($this->user)->get("/quizzes/1/questions") + $this->actingAs($this->user)->get("/admin/quizzes/1/questions") ->assertStatus(404); } @@ -58,7 +58,7 @@ public function testUserCanViewSingleQuestion(): void $this->assertDatabaseCount("questions", 1); $this->actingAs($this->user) - ->get("/questions/{$question->id}") + ->get("/admin/questions/{$question->id}") ->assertInertia( fn(Assert $page) => $page ->component("Question/Show") @@ -73,7 +73,7 @@ public function testUserCanViewLockedQuestion(): void $this->assertDatabaseCount("questions", 1); $this->actingAs($this->user) - ->get("/questions/{$question->id}") + ->get("/admin/questions/{$question->id}") ->assertInertia( fn(Assert $page) => $page ->component("Question/Show") @@ -84,7 +84,7 @@ public function testUserCanViewLockedQuestion(): void public function testUserCannotViewQuestionThatNotExisted(): void { - $this->actingAs($this->user)->get("/questions/1") + $this->actingAs($this->user)->get("/admin/questions/1") ->assertStatus(404); } @@ -94,7 +94,7 @@ public function testUserCanCreateQuestion(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question"]) + ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", [ @@ -109,15 +109,15 @@ public function testUserCanCreateMultipleQuestions(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 2"]) + ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 2"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 3"]) + ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 3"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", ["text" => "Example question 1"]); @@ -129,7 +129,7 @@ public function testUserCannotCreateQuestionToQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes/1/questions", ["text" => "Example question"]) + ->post("/admin/quizzes/1/questions", ["text" => "Example question"]) ->assertStatus(404); $this->assertDatabaseMissing("questions", [ @@ -143,7 +143,7 @@ public function testUserCannotCreateQuestionToQuizThatIsLocked(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) ->assertStatus(403); $this->assertDatabaseMissing("questions", [ @@ -157,11 +157,11 @@ public function testUserCannotCreateInvalidQuestion(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/questions", []) + ->post("/admin/quizzes/{$quiz->id}/questions", []) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") - ->post("/quizzes/{$quiz->id}/questions", ["text" => false]) + ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => false]) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("questions", 0); @@ -173,7 +173,7 @@ public function testUserCanEditQuestion(): void $this->actingAs($this->user) ->from("/") - ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->patch("/admin/questions/{$question->id}", ["text" => "New question"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", ["text" => "New question"]); @@ -183,7 +183,7 @@ public function testUserCannotEditQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->patch("/questions/1", ["text" => "New question"]) + ->patch("/admin/questions/1", ["text" => "New question"]) ->assertStatus(404); } @@ -193,11 +193,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/") - ->patch("/questions/{$question->id}", []) + ->patch("/admin/questions/{$question->id}", []) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") - ->patch("/questions/{$question->id}", ["text" => true]) + ->patch("/admin/questions/{$question->id}", ["text" => true]) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("questions", ["text" => "Old questions"]); @@ -209,7 +209,7 @@ public function testUserCannotEditLockedQuestion(): void $this->actingAs($this->user) ->from("/") - ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->patch("/admin/questions/{$question->id}", ["text" => "New question"]) ->assertStatus(403); $this->assertDatabaseHas("questions", ["text" => "Old question"]); @@ -226,7 +226,7 @@ public function testUserCanDeleteQuestion(): void $this->actingAs($this->user) ->from("/") - ->delete("/questions/{$question->id}") + ->delete("/admin/questions/{$question->id}") ->assertRedirect("/"); $this->assertDatabaseMissing("questions", ["text" => "question"]); @@ -241,7 +241,7 @@ public function testUserCannotDeleteLockedQuestion(): void $this->actingAs($this->user) ->from("/") - ->delete("/questions/{$question->id}") + ->delete("/admin/questions/{$question->id}") ->assertStatus(403); $this->assertDatabaseHas("questions", ["text" => "question"]); @@ -251,7 +251,7 @@ public function testUserCannotDeleteQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->delete("/questions/1") + ->delete("/admin/questions/1") ->assertStatus(404); } @@ -267,7 +267,7 @@ public function testUserCanCopyQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); @@ -284,7 +284,7 @@ public function testUserCanCopyLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); @@ -300,7 +300,7 @@ public function testUserCannotCopyAnswerToLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") ->assertStatus(403); $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); @@ -318,7 +318,7 @@ public function testUserCanCopyQuestionWithCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") ->assertRedirect("/quizzes"); $this->assertNotNull($quizA->questions[0]->correctAnswer); @@ -332,7 +332,7 @@ public function testUserCannotCopyQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/2/clone/{$quiz->id}") + ->post("/admin/questions/2/clone/{$quiz->id}") ->assertStatus(404); } @@ -342,7 +342,7 @@ public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/questions/{$question->id}/clone/2") + ->post("/admin/questions/{$question->id}/clone/2") ->assertStatus(404); } } diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index da397026..c089066a 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -35,7 +35,7 @@ public function testUserCanViewQuizzes(): void $this->assertDatabaseCount("questions", 10); $this->actingAs($this->user) - ->get("/quizzes") + ->get("/admin/quizzes") ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Index") @@ -47,7 +47,7 @@ public function testUserCanViewQuizzes(): void public function testUserCannotViewQuizThatNotExisted(): void { - $this->actingAs($this->user)->get("/quizzes/1") + $this->actingAs($this->user)->get("/admin/quizzes/1") ->assertStatus(404); } @@ -58,7 +58,7 @@ public function testUserCanViewSingleQuiz(): void $this->assertDatabaseCount("quizzes", 1); $this->actingAs($this->user) - ->get("/quizzes/{$quiz->id}") + ->get("/admin/quizzes/{$quiz->id}") ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Show") @@ -73,7 +73,7 @@ public function testUserCanViewLockedQuiz(): void $this->assertDatabaseCount("quizzes", 1); $this->actingAs($this->user) - ->get("/quizzes/{$quiz->id}") + ->get("/admin/quizzes/{$quiz->id}") ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Show") @@ -86,7 +86,7 @@ public function testUserCanCreateQuiz(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes", ["name" => "Example quiz"]) + ->post("/admin/quizzes", ["name" => "Example quiz"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", [ @@ -98,15 +98,15 @@ public function testUserCanCreateMultipleQuizzes(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes", ["name" => "Example quiz 1"]) + ->post("/admin/quizzes", ["name" => "Example quiz 1"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes", ["name" => "Example quiz 2"]) + ->post("/admin/quizzes", ["name" => "Example quiz 2"]) ->assertRedirect("/"); $this->from("/") - ->post("/quizzes", ["name" => "Example quiz 3"]) + ->post("/admin/quizzes", ["name" => "Example quiz 3"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 1"]); @@ -118,11 +118,11 @@ public function testUserCannotCreateInvalidQuiz(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes", []) + ->post("/admin/quizzes", []) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->post("/quizzes", ["name" => false]) + ->post("/admin/quizzes", ["name" => false]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->assertDatabaseCount("quizzes", 0); @@ -134,7 +134,7 @@ public function testUserCanEditQuiz(): void $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", ["name" => "New quiz"]); @@ -144,7 +144,7 @@ public function testUserCannotEditQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/1", ["name" => "New quiz"]) + ->patch("/admin/quizzes/1", ["name" => "New quiz"]) ->assertStatus(404); } @@ -154,11 +154,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/{$quiz->id}", []) + ->patch("/admin/quizzes/{$quiz->id}", []) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->patch("/quizzes/{$quiz->id}", ["name" => true]) + ->patch("/admin/quizzes/{$quiz->id}", ["name" => true]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); @@ -170,7 +170,7 @@ public function testUserCannotEditLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz"]) ->assertStatus(403); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); @@ -188,7 +188,7 @@ public function testUserCanDeleteQuiz(): void $this->actingAs($this->user) ->from("/") - ->delete("/quizzes/{$quiz->id}") + ->delete("/admin/quizzes/{$quiz->id}") ->assertRedirect("/"); $this->assertDatabaseMissing("quizzes", ["name" => "quiz"]); @@ -203,7 +203,7 @@ public function testUserCannotDeleteLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->delete("/quizzes/{$quiz->id}") + ->delete("/admin/quizzes/{$quiz->id}") ->assertStatus(403); $this->assertDatabaseHas("quizzes", ["name" => "quiz"]); @@ -213,7 +213,7 @@ public function testUserCannotDeleteQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->delete("/quizzes/1") + ->delete("/admin/quizzes/1") ->assertStatus(404); } @@ -231,7 +231,7 @@ public function testUserCanCopyQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/clone") + ->post("/admin/quizzes/{$quiz->id}/clone") ->assertRedirect("/"); $this->assertDatabaseCount("quizzes", 2); @@ -247,7 +247,7 @@ public function testUserCanCopyLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/clone") + ->post("/admin/quizzes/{$quiz->id}/clone") ->assertRedirect("/"); $this->assertDatabaseCount("quizzes", 2); @@ -257,7 +257,7 @@ public function testUserCannotCopyQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->post("/quizzes/2/clone") + ->post("/admin/quizzes/2/clone") ->assertStatus(404); } } From 6657de640b228b21634553ca6ce70a2e08ced306 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Wed, 14 Aug 2024 12:25:14 +0200 Subject: [PATCH 02/16] rename locked_at to scheduled_at --- app/Http/Controllers/QuizController.php | 11 ----- app/Http/Requests/QuizRequest.php | 1 + app/Models/Quiz.php | 7 +-- database/factories/QuizFactory.php | 2 +- ..._112018_add_scheduled_at_to_quiz_table.php | 23 ++++++++++ ...50_remove_locked_at_from_quizzes_table.php | 23 ++++++++++ routes/web.php | 4 +- tests/Feature/QuizTest.php | 46 +++++++++++++++++-- 8 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 database/migrations/2024_08_13_112018_add_scheduled_at_to_quiz_table.php create mode 100644 database/migrations/2024_08_14_094150_remove_locked_at_from_quizzes_table.php diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index dad324ac..20d2dc55 100644 --- a/app/Http/Controllers/QuizController.php +++ b/app/Http/Controllers/QuizController.php @@ -7,7 +7,6 @@ use App\Http\Requests\QuizRequest; use App\Http\Resources\QuizResource; use App\Models\Quiz; -use Carbon\Carbon; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; use Inertia\Response; @@ -50,16 +49,6 @@ public function update(QuizRequest $request, Quiz $quiz): RedirectResponse ->with("success", "Quiz updated"); } - public function lock(Quiz $quiz): RedirectResponse - { - $quiz->locked_at = Carbon::now(); - $quiz->save(); - - return redirect() - ->back() - ->with("success", "Quiz locked"); - } - public function destroy(Quiz $quiz): RedirectResponse { $quiz->delete(); diff --git a/app/Http/Requests/QuizRequest.php b/app/Http/Requests/QuizRequest.php index ae942518..f7e8369b 100644 --- a/app/Http/Requests/QuizRequest.php +++ b/app/Http/Requests/QuizRequest.php @@ -21,6 +21,7 @@ public function rules(): array { return [ "name" => ["required", "string"], + "scheduled_at" => ["date", "date_format:Y-m-d H:i:s", "after:now"], ]; } } diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 16eb79d3..74021d0b 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -17,7 +17,7 @@ * @property string $name * @property Carbon $created_at * @property Carbon $updated_at - * @property Carbon $locked_at + * @property Carbon $scheduled_at * @property bool $isLocked * @property Collection $questions * @property Collection $answers @@ -28,6 +28,7 @@ class Quiz extends Model protected $fillable = [ "name", + "scheduled_at", ]; public function questions(): HasMany @@ -42,7 +43,7 @@ public function answers(): HasManyThrough public function isLocked(): Attribute { - return Attribute::get(fn(): bool => $this->locked_at !== null); + return Attribute::get(fn(): bool => $this->scheduled_at !== null && $this->scheduled_at <= Carbon::now()); } public function clone(): self @@ -60,7 +61,7 @@ public function clone(): self protected function casts(): array { return [ - "locked_at" => "datetime", + "scheduled_at" => "datetime", ]; } } diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index be5b4a31..80e89a76 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -23,7 +23,7 @@ public function definition(): array public function locked(): static { return $this->state(fn(array $attributes): array => [ - "locked_at" => Carbon::now(), + "scheduled_at" => Carbon::now(), ]); } } diff --git a/database/migrations/2024_08_13_112018_add_scheduled_at_to_quiz_table.php b/database/migrations/2024_08_13_112018_add_scheduled_at_to_quiz_table.php new file mode 100644 index 00000000..4a278e6b --- /dev/null +++ b/database/migrations/2024_08_13_112018_add_scheduled_at_to_quiz_table.php @@ -0,0 +1,23 @@ +timestamp("scheduled_at")->nullable(); + }); + } + + public function down(): void + { + Schema::table("quizzes", function (Blueprint $table): void { + $table->dropColumn("scheduled_at"); + }); + } +}; diff --git a/database/migrations/2024_08_14_094150_remove_locked_at_from_quizzes_table.php b/database/migrations/2024_08_14_094150_remove_locked_at_from_quizzes_table.php new file mode 100644 index 00000000..783b929b --- /dev/null +++ b/database/migrations/2024_08_14_094150_remove_locked_at_from_quizzes_table.php @@ -0,0 +1,23 @@ +dropColumn("locked_at"); + }); + } + + public function down(): void + { + Schema::table("quizzes", function (Blueprint $table): void { + $table->timestamp("locked_at")->nullable(); + }); + } +}; diff --git a/routes/web.php b/routes/web.php index fbe960e6..720c9444 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,14 +12,12 @@ Route::get("/", fn(): Response => inertia("Welcome")); -Route::group(["prefix" => "admin"], function () { - +Route::group(["prefix" => "admin"], function (): void { Route::get("/quizzes", [QuizController::class, "index"]); Route::post("/quizzes", [QuizController::class, "store"]); Route::get("/quizzes/{quiz}", [QuizController::class, "show"]); Route::patch("/quizzes/{quiz}", [QuizController::class, "update"])->can("update,quiz"); Route::delete("/quizzes/{quiz}", [QuizController::class, "destroy"])->can("delete,quiz"); - Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"]); Route::get("/quizzes/{quiz}/questions", [QuizQuestionController::class, "index"]); diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index c089066a..e97760a0 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -8,6 +8,7 @@ use App\Models\Question; use App\Models\Quiz; use App\Models\User; +use Carbon\Carbon; use Illuminate\Foundation\Testing\RefreshDatabase; use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; @@ -22,9 +23,16 @@ protected function setUp(): void { parent::setUp(); + Carbon::setTestNow(Carbon::create(2024, 1, 1, 10)); $this->user = User::factory()->create(); } + protected function tearDown(): void + { + parent::tearDown(); + Carbon::setTestNow(); + } + public function testUserCanViewQuizzes(): void { $quizzes = Quiz::factory()->count(2)->create(); @@ -84,6 +92,21 @@ public function testUserCanViewLockedQuiz(): void public function testUserCanCreateQuiz(): void { + $this->actingAs($this->user) + ->from("/") + ->post("/admin/quizzes", ["name" => "Example quiz", "scheduled_at" => "2024-02-10 11:40:00"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("quizzes", [ + "name" => "Example quiz", + "scheduled_at" => "2024-02-10 11:40:00", + ]); + } + + public function testUserCanCreateQuizWithoutDate(): void + { + Carbon::setTestNow(Carbon::create(2024, 1, 1, 10)); + $this->actingAs($this->user) ->from("/") ->post("/admin/quizzes", ["name" => "Example quiz"]) @@ -91,6 +114,7 @@ public function testUserCanCreateQuiz(): void $this->assertDatabaseHas("quizzes", [ "name" => "Example quiz", + "scheduled_at" => null, ]); } @@ -125,19 +149,27 @@ public function testUserCannotCreateInvalidQuiz(): void ->post("/admin/quizzes", ["name" => false]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); + $this->from("/") + ->post("/admin/quizzes", ["name" => "correct", "scheduled_at" => "invalid format"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + + $this->from("/") + ->post("/admin/quizzes", ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + $this->assertDatabaseCount("quizzes", 0); } public function testUserCanEditQuiz(): void { - $quiz = Quiz::factory()->create(["name" => "Old quiz"]); + $quiz = Quiz::factory()->create(["name" => "Old quiz", "scheduled_at" => "2024-02-10 11:40:00"]); $this->actingAs($this->user) ->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00"]) ->assertRedirect("/"); - $this->assertDatabaseHas("quizzes", ["name" => "New quiz"]); + $this->assertDatabaseHas("quizzes", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00"]); } public function testUserCannotEditQuizThatNotExisted(): void @@ -161,6 +193,14 @@ public function testUserCannotMakeInvalidEdit(): void ->patch("/admin/quizzes/{$quiz->id}", ["name" => true]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); + $this->from("/") + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "scheduled_at" => "invalid format"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + + $this->from("/") + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) + ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); } From 9ea2052e14a4443e47e0901ed6a0caf4b05a8d1b Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Wed, 14 Aug 2024 14:08:12 +0200 Subject: [PATCH 03/16] clean up seeder --- database/seeders/DatabaseSeeder.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index cff3b9ec..9aa46e81 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -12,7 +12,5 @@ class DatabaseSeeder extends Seeder { public function run(): void { - Quiz::factory()->create(); - Answer::factory()->create(); } } From eca98ac2b5bceae9906b00d048692f23453d8fab Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Wed, 14 Aug 2024 14:09:08 +0200 Subject: [PATCH 04/16] fix code style --- database/seeders/DatabaseSeeder.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 9aa46e81..de853239 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,8 +4,6 @@ namespace Database\Seeders; -use App\Models\Answer; -use App\Models\Quiz; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder From f56ad7a2637b75a513f082be04087d62965fc592 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Wed, 14 Aug 2024 15:43:20 +0200 Subject: [PATCH 05/16] add functionality for users to start a scheduled quiz --- app/Http/Controllers/QuizController.php | 9 +++ .../Controllers/QuizSubmissionController.php | 20 ++++++ .../EnsureQuizIsNotAlreadyStarted.php | 28 +++++++++ app/Http/Resources/AnswerRecordResource.php | 33 ++++++++++ app/Http/Resources/QuizSubmissionResource.php | 24 +++++++ app/Models/AnswerRecord.php | 48 ++++++++++++++ app/Models/Quiz.php | 17 +++++ app/Models/QuizSubmission.php | 57 +++++++++++++++++ app/Policies/QuizPolicy.php | 5 ++ app/Policies/QuizSubmissionPolicy.php | 16 +++++ database/factories/AnswerRecordFactory.php | 24 +++++++ database/factories/QuizSubmissionFactory.php | 32 ++++++++++ ...3_104625_create_quiz_submissions_table.php | 27 ++++++++ ..._13_104743_create_answer_records_table.php | 28 +++++++++ database/seeders/DatabaseSeeder.php | 2 + resources/js/Pages/Submission/Show.vue | 0 resources/js/Types/AnswerRecord.d.ts | 14 +++++ resources/js/Types/QuizSubmission.d.ts | 11 ++++ routes/web.php | 5 ++ tests/Feature/QuizSubmissionTest.php | 63 +++++++++++++++++++ tests/Feature/QuizTest.php | 42 ++++++++++++- 21 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/QuizSubmissionController.php create mode 100644 app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php create mode 100644 app/Http/Resources/AnswerRecordResource.php create mode 100644 app/Http/Resources/QuizSubmissionResource.php create mode 100644 app/Models/AnswerRecord.php create mode 100644 app/Models/QuizSubmission.php create mode 100644 app/Policies/QuizSubmissionPolicy.php create mode 100644 database/factories/AnswerRecordFactory.php create mode 100644 database/factories/QuizSubmissionFactory.php create mode 100644 database/migrations/2024_08_13_104625_create_quiz_submissions_table.php create mode 100644 database/migrations/2024_08_13_104743_create_answer_records_table.php create mode 100644 resources/js/Pages/Submission/Show.vue create mode 100644 resources/js/Types/AnswerRecord.d.ts create mode 100644 resources/js/Types/QuizSubmission.d.ts create mode 100644 tests/Feature/QuizSubmissionTest.php diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index 20d2dc55..2da9a4ea 100644 --- a/app/Http/Controllers/QuizController.php +++ b/app/Http/Controllers/QuizController.php @@ -8,6 +8,7 @@ use App\Http\Resources\QuizResource; use App\Models\Quiz; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; use Inertia\Inertia; use Inertia\Response; @@ -66,4 +67,12 @@ public function clone(Quiz $quiz): RedirectResponse ->back() ->with("success", "Quiz cloned"); } + + public function createSubmission(Request $request, Quiz $quiz): RedirectResponse + { + $user = $request->user(); + $submission = $quiz->createSubmission($user); + + return redirect("/submissions/{$submission->id}/"); + } } diff --git a/app/Http/Controllers/QuizSubmissionController.php b/app/Http/Controllers/QuizSubmissionController.php new file mode 100644 index 00000000..d36d2263 --- /dev/null +++ b/app/Http/Controllers/QuizSubmissionController.php @@ -0,0 +1,20 @@ +load(["answerRecords.question.answers", "quiz"]); + + return Inertia::render("Submission/Show", ["submission" => QuizSubmissionResource::make($quizSubmission)]); + } +} diff --git a/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php b/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php new file mode 100644 index 00000000..eda568d5 --- /dev/null +++ b/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php @@ -0,0 +1,28 @@ +user(); + $quiz = $request->route()->parameter("quiz"); + + $submission = QuizSubmission::query()->where(["quiz_id" => $quiz->id, "user_id" => $user->id])->first(); + + if ($submission) { + return redirect("/submissions/{$submission->id}/"); + } + + return $next($request); + } +} diff --git a/app/Http/Resources/AnswerRecordResource.php b/app/Http/Resources/AnswerRecordResource.php new file mode 100644 index 00000000..8134b435 --- /dev/null +++ b/app/Http/Resources/AnswerRecordResource.php @@ -0,0 +1,33 @@ +question->answers as $answer) { + $answers->add([ + "id" => $answer->id, + "text" => $answer->text, + ]); + } + + return [ + "id" => $this->id, + "question" => $this->question->text, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "closed" => $this->isClosed, + "answers" => $answers->shuffle(), + "selected" => $this->answer_id, + ]; + } +} diff --git a/app/Http/Resources/QuizSubmissionResource.php b/app/Http/Resources/QuizSubmissionResource.php new file mode 100644 index 00000000..18cbcedc --- /dev/null +++ b/app/Http/Resources/QuizSubmissionResource.php @@ -0,0 +1,24 @@ + $this->id, + "name" => $this->quiz->name, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "closedAt" => $this->closed_at, + "closed" => $this->isClosed, + "answers" => AnswerRecordResource::collection($this->answerRecords->shuffle()), + ]; + } +} diff --git a/app/Models/AnswerRecord.php b/app/Models/AnswerRecord.php new file mode 100644 index 00000000..ec0f843e --- /dev/null +++ b/app/Models/AnswerRecord.php @@ -0,0 +1,48 @@ +belongsTo(QuizSubmission::class); + } + + public function answer(): BelongsTo + { + return $this->belongsTo(Answer::class); + } + + public function question(): BelongsTo + { + return $this->belongsTo(Question::class); + } + + public function isClosed(): Attribute + { + return Attribute::get(fn(): bool => $this->quizSubmission->isClosed); + } +} diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 74021d0b..8338d684 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -58,6 +58,23 @@ public function clone(): self return $quizCopy; } + public function createSubmission(User $user): QuizSubmission + { + $submission = new QuizSubmission(); + $submission->quiz()->associate($this); + $submission->user()->associate($user); + $submission->save(); + + foreach ($this->questions as $question) { + $answerRecord = new AnswerRecord(); + $answerRecord->quizSubmission()->associate($submission); + $answerRecord->question()->associate($question); + $answerRecord->save(); + } + + return $submission; + } + protected function casts(): array { return [ diff --git a/app/Models/QuizSubmission.php b/app/Models/QuizSubmission.php new file mode 100644 index 00000000..cc40cb68 --- /dev/null +++ b/app/Models/QuizSubmission.php @@ -0,0 +1,57 @@ + $answerRecords + */ +class QuizSubmission extends Model +{ + use HasFactory; + + public function quiz(): BelongsTo + { + return $this->belongsTo(Quiz::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function answerRecords(): HasMany + { + return $this->hasMany(AnswerRecord::class); + } + + public function isClosed(): Attribute + { + return Attribute::get(fn(): bool => $this->closed_at !== null); + } + + protected function casts(): array + { + return [ + "closed_at" => "datetime", + ]; + } +} diff --git a/app/Policies/QuizPolicy.php b/app/Policies/QuizPolicy.php index 510c4332..4b110bf1 100644 --- a/app/Policies/QuizPolicy.php +++ b/app/Policies/QuizPolicy.php @@ -18,4 +18,9 @@ public function delete(User $user, Quiz $quiz): bool { return !$quiz->isLocked; } + + public function submit(User $user, Quiz $quiz): bool + { + return $quiz->isLocked; + } } diff --git a/app/Policies/QuizSubmissionPolicy.php b/app/Policies/QuizSubmissionPolicy.php new file mode 100644 index 00000000..0fbe652d --- /dev/null +++ b/app/Policies/QuizSubmissionPolicy.php @@ -0,0 +1,16 @@ +id === $quizSubmission->user_id; + } +} diff --git a/database/factories/AnswerRecordFactory.php b/database/factories/AnswerRecordFactory.php new file mode 100644 index 00000000..d0ee29f6 --- /dev/null +++ b/database/factories/AnswerRecordFactory.php @@ -0,0 +1,24 @@ + + */ +class AnswerRecordFactory extends Factory +{ + public function definition(): array + { + return [ + "quiz_submission_id" => QuizSubmission::factory(), + "question_id" => Question::factory(), + ]; + } +} diff --git a/database/factories/QuizSubmissionFactory.php b/database/factories/QuizSubmissionFactory.php new file mode 100644 index 00000000..63f52d41 --- /dev/null +++ b/database/factories/QuizSubmissionFactory.php @@ -0,0 +1,32 @@ + + */ +class QuizSubmissionFactory extends Factory +{ + public function definition(): array + { + return [ + "quiz_id" => Quiz::factory()->locked(), + "user_id" => User::factory(), + ]; + } + + public function closed(): static + { + return $this->state(fn(array $attributes): array => [ + "closed_at" => Carbon::now(), + ]); + } +} diff --git a/database/migrations/2024_08_13_104625_create_quiz_submissions_table.php b/database/migrations/2024_08_13_104625_create_quiz_submissions_table.php new file mode 100644 index 00000000..59639aa2 --- /dev/null +++ b/database/migrations/2024_08_13_104625_create_quiz_submissions_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignIdFor(Quiz::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete(); + $table->timestamp("closed_at")->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("quiz_submissions"); + } +}; diff --git a/database/migrations/2024_08_13_104743_create_answer_records_table.php b/database/migrations/2024_08_13_104743_create_answer_records_table.php new file mode 100644 index 00000000..e026e30d --- /dev/null +++ b/database/migrations/2024_08_13_104743_create_answer_records_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignIdFor(QuizSubmission::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Question::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Answer::class)->nullable()->constrained()->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists("answer_records"); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index de853239..7100a3df 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,11 +4,13 @@ namespace Database\Seeders; +use App\Models\AnswerRecord; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { + AnswerRecord::factory()->create(); } } diff --git a/resources/js/Pages/Submission/Show.vue b/resources/js/Pages/Submission/Show.vue new file mode 100644 index 00000000..e69de29b diff --git a/resources/js/Types/AnswerRecord.d.ts b/resources/js/Types/AnswerRecord.d.ts new file mode 100644 index 00000000..c01a2b4b --- /dev/null +++ b/resources/js/Types/AnswerRecord.d.ts @@ -0,0 +1,14 @@ +export interface AnswerRecordQuestion { + id: number + text: string +} + +export interface AnswerRecord { + id: number + question: string + createAt: string + updatedAt: string + closed: boolean + selected?: number + answers: AnswerRecordQuestion[] +} diff --git a/resources/js/Types/QuizSubmission.d.ts b/resources/js/Types/QuizSubmission.d.ts new file mode 100644 index 00000000..f57e5cfc --- /dev/null +++ b/resources/js/Types/QuizSubmission.d.ts @@ -0,0 +1,11 @@ +import {type AnswerRecord} from '@/Types/AnswerRecord' + +export interface QuizSubmission { + id: number + name: string + createAt: string + updatedAt: string + closedAt?: string + closed: boolean + answers: AnswerRecord[] +} diff --git a/routes/web.php b/routes/web.php index 720c9444..a10d38e8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,8 @@ use App\Http\Controllers\QuestionAnswerController; use App\Http\Controllers\QuizController; use App\Http\Controllers\QuizQuestionController; +use App\Http\Controllers\QuizSubmissionController; +use App\Http\Middleware\EnsureQuizIsNotAlreadyStarted; use App\Models\Answer; use App\Models\Question; use Illuminate\Support\Facades\Route; @@ -36,3 +38,6 @@ Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"])->can("update,answer"); Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"])->can("update,answer"); }); + +Route::post("/quizzes/{quiz}/start", [QuizController::class, "createSubmission"])->middleware(EnsureQuizIsNotAlreadyStarted::class)->can("submit,quiz"); +Route::get("/submissions/{quizSubmission}/", [QuizSubmissionController::class, "show"])->can("view,quizSubmission"); diff --git a/tests/Feature/QuizSubmissionTest.php b/tests/Feature/QuizSubmissionTest.php new file mode 100644 index 00000000..61be5d6a --- /dev/null +++ b/tests/Feature/QuizSubmissionTest.php @@ -0,0 +1,63 @@ +user = User::factory()->create(); + } + + public function testUserCanViewSingleSubmission(): void + { + $quiz = Quiz::factory()->create(); + $questions = Question::factory()->count(2)->create(["quiz_id" => $quiz->id]); + Answer::factory()->count(4)->create(["question_id" => $questions[0]->id]); + Answer::factory()->count(4)->create(["question_id" => $questions[1]->id]); + $submission = $quiz->createSubmission($this->user); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 2); + $this->assertDatabaseCount("answers", 8); + $this->assertDatabaseCount("quiz_submissions", 1); + $this->assertDatabaseCount("answer_records", 2); + + $this->actingAs($this->user) + ->get("/submissions/{$submission->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Submission/Show") + ->where("submission.name", $quiz->name) + ->count("submission.answers", 2) + ->count("submission.answers.0.answers", 4) + ->count("submission.answers.1.answers", 4), + ); + } + + public function testUserCannotViewSubmissionThatIsNotHis(): void + { + $submission = QuizSubmission::factory()->create(); + + $this->actingAs($this->user) + ->get("/submissions/{$submission->id}") + ->assertStatus(403); + } +} diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index e97760a0..c3486621 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -7,6 +7,7 @@ use App\Models\Answer; use App\Models\Question; use App\Models\Quiz; +use App\Models\QuizSubmission; use App\Models\User; use Carbon\Carbon; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -22,7 +23,6 @@ class QuizTest extends TestCase protected function setUp(): void { parent::setUp(); - Carbon::setTestNow(Carbon::create(2024, 1, 1, 10)); $this->user = User::factory()->create(); } @@ -300,4 +300,44 @@ public function testUserCannotCopyQuizThatNotExisted(): void ->post("/admin/quizzes/2/clone") ->assertStatus(404); } + + public function testUserCanStartQuiz(): void + { + $quiz = Quiz::factory()->locked()->create(); + + $response = $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/start"); + + $submission = QuizSubmission::query()->where([ + "user_id" => $this->user->id, + "quiz_id" => $quiz->id, + ])->firstOrFail(); + + $response->assertRedirect("/submissions/$submission->id/"); + } + + public function testUserCannotStartAlreadyStartedQuiz(): void + { + $submission = QuizSubmission::factory()->create(["user_id" => $this->user->id]); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$submission->quiz->id}/start") + ->assertRedirect("/submissions/{$submission->id}/"); + + $this->assertDatabaseCount("quiz_submissions", 1); + } + + public function testUserCannotStartUnlockedQuiz(): void + { + $quiz = Quiz::factory()->create(); + + $this->actingAs($this->user) + ->from("/") + ->post("/quizzes/{$quiz->id}/start") + ->assertStatus(403); + + $this->assertDatabaseCount("quiz_submissions", 0); + } } From a72a06d378a7445dad191675c2a4b54688ded6ef Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Fri, 16 Aug 2024 14:53:55 +0200 Subject: [PATCH 06/16] add timer to quiz --- app/Http/Requests/QuizRequest.php | 1 + app/Http/Resources/QuizResource.php | 1 + app/Models/Quiz.php | 16 +++++++++++++- app/Models/QuizSubmission.php | 2 +- database/factories/QuizFactory.php | 2 ++ ...6_095409_add_duration_to_quizzes_table.php | 22 +++++++++++++++++++ resources/js/Types/Quiz.d.ts | 1 + tests/Feature/QuizTest.php | 20 +++++++++++++++-- 8 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php diff --git a/app/Http/Requests/QuizRequest.php b/app/Http/Requests/QuizRequest.php index f7e8369b..0adccb64 100644 --- a/app/Http/Requests/QuizRequest.php +++ b/app/Http/Requests/QuizRequest.php @@ -22,6 +22,7 @@ public function rules(): array return [ "name" => ["required", "string"], "scheduled_at" => ["date", "date_format:Y-m-d H:i:s", "after:now"], + "duration" => ["integer", "min:1"], ]; } } diff --git a/app/Http/Resources/QuizResource.php b/app/Http/Resources/QuizResource.php index 130fdcf1..e60d5053 100644 --- a/app/Http/Resources/QuizResource.php +++ b/app/Http/Resources/QuizResource.php @@ -15,6 +15,7 @@ public function toArray($request): array "name" => $this->name, "createdAt" => $this->created_at, "updatedAt" => $this->updated_at, + "duration" => $this->duration, "locked" => $this->isLocked, "questions" => QuestionResource::collection($this->questions), ]; diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 8338d684..497be3fd 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -18,7 +18,9 @@ * @property Carbon $created_at * @property Carbon $updated_at * @property Carbon $scheduled_at + * @property ?int $duration * @property bool $isLocked + * @property ?Carbon $closeAt * @property Collection $questions * @property Collection $answers */ @@ -29,6 +31,7 @@ class Quiz extends Model protected $fillable = [ "name", "scheduled_at", + "duration" ]; public function questions(): HasMany @@ -43,7 +46,17 @@ public function answers(): HasManyThrough public function isLocked(): Attribute { - return Attribute::get(fn(): bool => $this->scheduled_at !== null && $this->scheduled_at <= Carbon::now()); + return Attribute::get(fn(): bool => $this->canBeScheduled() && $this->scheduled_at <= Carbon::now()); + } + + public function closeAt(): Attribute + { + return Attribute::get(fn(): ?Carbon => $this->canBeScheduled() ? $this->scheduled_at->copy()->addSeconds($this->duration) : null); + } + + protected function canBeScheduled(): bool + { + return $this->scheduled_at !== null && $this->duration !== null; } public function clone(): self @@ -61,6 +74,7 @@ public function clone(): self public function createSubmission(User $user): QuizSubmission { $submission = new QuizSubmission(); + $submission->closed_at = $this->closeAt; $submission->quiz()->associate($this); $submission->user()->associate($user); $submission->save(); diff --git a/app/Models/QuizSubmission.php b/app/Models/QuizSubmission.php index cc40cb68..cc4fdd33 100644 --- a/app/Models/QuizSubmission.php +++ b/app/Models/QuizSubmission.php @@ -16,7 +16,7 @@ * @property int $id * @property Carbon $created_at * @property Carbon $updated_at - * @property ?Carbon $closed_at + * @property Carbon $closed_at * @property int $quiz_id * @property int $user_id * @property bool $isClosed diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index 80e89a76..93c6d3b9 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -7,6 +7,7 @@ use App\Models\Quiz; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\Factory; +use function rand; /** * @extends Factory @@ -17,6 +18,7 @@ public function definition(): array { return [ "name" => fake()->name(), + "duration" => rand(3600, 7200), ]; } diff --git a/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php b/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php new file mode 100644 index 00000000..a6b0aaaf --- /dev/null +++ b/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php @@ -0,0 +1,22 @@ +unsignedInteger('duration')->nullable(); + }); + } + + public function down(): void + { + Schema::table('quizzes', function (Blueprint $table) { + $table->dropColumn('duration'); + }); + } +}; diff --git a/resources/js/Types/Quiz.d.ts b/resources/js/Types/Quiz.d.ts index 197c246e..5d3b430d 100644 --- a/resources/js/Types/Quiz.d.ts +++ b/resources/js/Types/Quiz.d.ts @@ -5,6 +5,7 @@ export interface Quiz { name: string createdAt: number updatedAt: number + duration?: number locked: boolean questions: Question[] } diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index c3486621..6cd5a496 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -157,6 +157,14 @@ public function testUserCannotCreateInvalidQuiz(): void ->post("/admin/quizzes", ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + $this->from("/") + ->post("/admin/quizzes", ["name" => "correct", "duration" => -100]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + + $this->from("/") + ->post("/admin/quizzes", ["name" => "correct", "duration" => 0]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + $this->assertDatabaseCount("quizzes", 0); } @@ -166,10 +174,10 @@ public function testUserCanEditQuiz(): void $this->actingAs($this->user) ->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00"]) + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00", "duration" => 7200]) ->assertRedirect("/"); - $this->assertDatabaseHas("quizzes", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00"]); + $this->assertDatabaseHas("quizzes", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00", "duration" => 7200]); } public function testUserCannotEditQuizThatNotExisted(): void @@ -201,6 +209,14 @@ public function testUserCannotMakeInvalidEdit(): void ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); + $this->from("/") + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "duration" => -100]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + + $this->from("/") + ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "duration" => 0]) + ->assertRedirect("/")->assertSessionHasErrors(["duration"]); + $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); } From eee69083586f8c898f59381b1519c8c1c5dc383a Mon Sep 17 00:00:00 2001 From: Amon De Shir Date: Fri, 16 Aug 2024 14:54:21 +0200 Subject: [PATCH 07/16] fix code style --- app/Models/Quiz.php | 12 ++++++------ database/factories/QuizFactory.php | 1 + ...4_08_16_095409_add_duration_to_quizzes_table.php | 13 +++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 497be3fd..305a83cf 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -31,7 +31,7 @@ class Quiz extends Model protected $fillable = [ "name", "scheduled_at", - "duration" + "duration", ]; public function questions(): HasMany @@ -54,11 +54,6 @@ public function closeAt(): Attribute return Attribute::get(fn(): ?Carbon => $this->canBeScheduled() ? $this->scheduled_at->copy()->addSeconds($this->duration) : null); } - protected function canBeScheduled(): bool - { - return $this->scheduled_at !== null && $this->duration !== null; - } - public function clone(): self { $quizCopy = $this->replicate(); @@ -89,6 +84,11 @@ public function createSubmission(User $user): QuizSubmission return $submission; } + protected function canBeScheduled(): bool + { + return $this->scheduled_at !== null && $this->duration !== null; + } + protected function casts(): array { return [ diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index 93c6d3b9..1faf2385 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -7,6 +7,7 @@ use App\Models\Quiz; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\Factory; + use function rand; /** diff --git a/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php b/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php index a6b0aaaf..0bcb2002 100644 --- a/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php +++ b/database/migrations/2024_08_16_095409_add_duration_to_quizzes_table.php @@ -1,22 +1,23 @@ unsignedInteger('duration')->nullable(); + Schema::table("quizzes", function (Blueprint $table): void { + $table->unsignedInteger("duration")->nullable(); }); } public function down(): void { - Schema::table('quizzes', function (Blueprint $table) { - $table->dropColumn('duration'); + Schema::table("quizzes", function (Blueprint $table): void { + $table->dropColumn("duration"); }); } }; From c824bd840ad8e1c1ee27ba856bc5a19be8d36518 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 08:39:40 +0200 Subject: [PATCH 08/16] replace rand with faker --- database/factories/QuizFactory.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index 1faf2385..25137322 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -8,7 +8,6 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\Factory; -use function rand; /** * @extends Factory @@ -19,7 +18,7 @@ public function definition(): array { return [ "name" => fake()->name(), - "duration" => rand(3600, 7200), + "duration" => fake()->numberBetween(3600, 7200), ]; } From 05259a66e7a32360e034017c1d8baeb990b13302 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 08:49:48 +0200 Subject: [PATCH 09/16] rename canBeScheduled to canBeLocked --- app/Models/Quiz.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index 305a83cf..d77263a8 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -46,12 +46,12 @@ public function answers(): HasManyThrough public function isLocked(): Attribute { - return Attribute::get(fn(): bool => $this->canBeScheduled() && $this->scheduled_at <= Carbon::now()); + return Attribute::get(fn(): bool => $this->canBeLocked() && $this->scheduled_at <= Carbon::now()); } public function closeAt(): Attribute { - return Attribute::get(fn(): ?Carbon => $this->canBeScheduled() ? $this->scheduled_at->copy()->addSeconds($this->duration) : null); + return Attribute::get(fn(): ?Carbon => $this->canBeLocked() ? $this->scheduled_at->copy()->addSeconds($this->duration) : null); } public function clone(): self @@ -84,7 +84,7 @@ public function createSubmission(User $user): QuizSubmission return $submission; } - protected function canBeScheduled(): bool + protected function canBeLocked(): bool { return $this->scheduled_at !== null && $this->duration !== null; } From 1753b7bf5e61d86002a6228672b515da55ce336c Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 08:49:56 +0200 Subject: [PATCH 10/16] fix code style --- database/factories/QuizFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index 25137322..dab62828 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -8,7 +8,6 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\Factory; - /** * @extends Factory */ From 6d0987f78be4ba5deba8aa9f675ee1e6ea075455 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 09:03:47 +0200 Subject: [PATCH 11/16] rename canBeLocked to isReadyToBePublished --- app/Models/Quiz.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index d77263a8..e1e03a36 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -46,12 +46,12 @@ public function answers(): HasManyThrough public function isLocked(): Attribute { - return Attribute::get(fn(): bool => $this->canBeLocked() && $this->scheduled_at <= Carbon::now()); + return Attribute::get(fn(): bool => $this->isReadyToBePublished() && $this->scheduled_at <= Carbon::now()); } public function closeAt(): Attribute { - return Attribute::get(fn(): ?Carbon => $this->canBeLocked() ? $this->scheduled_at->copy()->addSeconds($this->duration) : null); + return Attribute::get(fn(): ?Carbon => $this->isReadyToBePublished() ? $this->scheduled_at->copy()->addSeconds($this->duration) : null); } public function clone(): self @@ -84,7 +84,7 @@ public function createSubmission(User $user): QuizSubmission return $submission; } - protected function canBeLocked(): bool + protected function isReadyToBePublished(): bool { return $this->scheduled_at !== null && $this->duration !== null; } From dd4f914da8020e07de8389f7284d4004ece4e1ee Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 09:32:17 +0200 Subject: [PATCH 12/16] change quiz duration to use minutes instead of seconds --- app/Models/Quiz.php | 2 +- database/factories/QuizFactory.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index e1e03a36..9b5f259e 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -51,7 +51,7 @@ public function isLocked(): Attribute public function closeAt(): Attribute { - return Attribute::get(fn(): ?Carbon => $this->isReadyToBePublished() ? $this->scheduled_at->copy()->addSeconds($this->duration) : null); + return Attribute::get(fn(): ?Carbon => $this->isReadyToBePublished() ? $this->scheduled_at->copy()->addMinutes($this->duration) : null); } public function clone(): self diff --git a/database/factories/QuizFactory.php b/database/factories/QuizFactory.php index dab62828..ea1a0582 100644 --- a/database/factories/QuizFactory.php +++ b/database/factories/QuizFactory.php @@ -17,7 +17,7 @@ public function definition(): array { return [ "name" => fake()->name(), - "duration" => fake()->numberBetween(3600, 7200), + "duration" => fake()->numberBetween(60, 120), ]; } From cae7f91ccffdc4f1b1092a7ee0b1c1dd45eb9df8 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 13:37:46 +0200 Subject: [PATCH 13/16] name routes --- .../EnsureQuizIsNotAlreadyStarted.php | 2 +- routes/web.php | 44 ++++++------ tests/Feature/AnswerTest.php | 66 +++++++++--------- tests/Feature/QuestionTest.php | 53 ++++++++------- tests/Feature/QuizSubmissionTest.php | 4 +- tests/Feature/QuizTest.php | 68 +++++++++---------- 6 files changed, 119 insertions(+), 118 deletions(-) diff --git a/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php b/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php index eda568d5..19924b42 100644 --- a/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php +++ b/app/Http/Middleware/EnsureQuizIsNotAlreadyStarted.php @@ -20,7 +20,7 @@ public function handle(Request $request, Closure $next): Response|RedirectRespon $submission = QuizSubmission::query()->where(["quiz_id" => $quiz->id, "user_id" => $user->id])->first(); if ($submission) { - return redirect("/submissions/{$submission->id}/"); + return redirect(route("submissions.show", $submission->id)); } return $next($request); diff --git a/routes/web.php b/routes/web.php index 0b21c7f8..57c2b9a3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,29 +15,29 @@ Route::get("/", fn(): Response => inertia("Home")); Route::group(["prefix" => "admin"], function (): void { - Route::get("/quizzes", [QuizController::class, "index"]); - Route::post("/quizzes", [QuizController::class, "store"]); - Route::get("/quizzes/{quiz}", [QuizController::class, "show"]); - Route::patch("/quizzes/{quiz}", [QuizController::class, "update"])->can("update,quiz"); - Route::delete("/quizzes/{quiz}", [QuizController::class, "destroy"])->can("delete,quiz"); - Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"]); + Route::get("/quizzes", [QuizController::class, "index"])->name("admin.quizzes.index"); + Route::post("/quizzes", [QuizController::class, "store"])->name("admin.quizzes.store"); + Route::get("/quizzes/{quiz}", [QuizController::class, "show"])->name("admin.quizzes.show"); + Route::patch("/quizzes/{quiz}", [QuizController::class, "update"])->can("update,quiz")->name("admin.quizzes.update"); + Route::delete("/quizzes/{quiz}", [QuizController::class, "destroy"])->can("delete,quiz")->name("admin.quizzes.destroy"); + Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"])->name("admin.quizzes.clone"); - Route::get("/quizzes/{quiz}/questions", [QuizQuestionController::class, "index"]); - Route::post("/quizzes/{quiz}/questions", [QuizQuestionController::class, "store"])->can("create," . Question::class . ",quiz"); - Route::get("/questions/{question}", [QuizQuestionController::class, "show"]); - Route::patch("/questions/{question}", [QuizQuestionController::class, "update"])->can("update,question"); - Route::delete("/questions/{question}", [QuizQuestionController::class, "destroy"])->can("delete,question"); - Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"])->can("clone,question,quiz"); + Route::get("/quizzes/{quiz}/questions", [QuizQuestionController::class, "index"])->name("admin.questions.index"); + Route::post("/quizzes/{quiz}/questions", [QuizQuestionController::class, "store"])->can("create," . Question::class . ",quiz")->name("admin.questions.store"); + Route::get("/questions/{question}", [QuizQuestionController::class, "show"])->name("admin.questions.show"); + Route::patch("/questions/{question}", [QuizQuestionController::class, "update"])->can("update,question")->name("admin.questions.update"); + Route::delete("/questions/{question}", [QuizQuestionController::class, "destroy"])->can("delete,question")->name("admin.questions.destroy"); + Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"])->can("clone,question,quiz")->name("admin.questions.clone"); - Route::get("/questions/{question}/answers", [QuestionAnswerController::class, "index"]); - Route::post("/questions/{question}/answers", [QuestionAnswerController::class, "store"])->can("create," . Answer::class . ",question"); - Route::get("/answers/{answer}", [QuestionAnswerController::class, "show"]); - Route::patch("/answers/{answer}", [QuestionAnswerController::class, "update"])->can("update,answer"); - Route::delete("/answers/{answer}", [QuestionAnswerController::class, "destroy"])->can("delete,answer"); - Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"])->can("clone,answer,question"); - Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"])->can("update,answer"); - Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"])->can("update,answer"); + Route::get("/questions/{question}/answers", [QuestionAnswerController::class, "index"])->name("admin.answers.index"); + Route::post("/questions/{question}/answers", [QuestionAnswerController::class, "store"])->can("create," . Answer::class . ",question")->name("admin.answers.store"); + Route::get("/answers/{answer}", [QuestionAnswerController::class, "show"])->name("admin.answers.show"); + Route::patch("/answers/{answer}", [QuestionAnswerController::class, "update"])->can("update,answer")->name("admin.answers.update"); + Route::delete("/answers/{answer}", [QuestionAnswerController::class, "destroy"])->can("delete,answer")->name("admin.answers.destroy"); + Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"])->can("clone,answer,question")->name("admin.answers.clone"); + Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"])->can("update,answer")->name("admin.answers.correct"); + Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"])->can("update,answer")->name("admin.answers.invalid"); }); -Route::post("/quizzes/{quiz}/start", [QuizController::class, "createSubmission"])->middleware(EnsureQuizIsNotAlreadyStarted::class)->can("submit,quiz"); -Route::get("/submissions/{quizSubmission}/", [QuizSubmissionController::class, "show"])->can("view,quizSubmission"); +Route::post("/quizzes/{quiz}/start", [QuizController::class, "createSubmission"])->middleware(EnsureQuizIsNotAlreadyStarted::class)->can("submit,quiz")->name("quizzes.start"); +Route::get("/submissions/{quizSubmission}/", [QuizSubmissionController::class, "show"])->can("view,quizSubmission")->name("submissions.show"); diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php index 11bc96ae..4a0b4f92 100644 --- a/tests/Feature/AnswerTest.php +++ b/tests/Feature/AnswerTest.php @@ -32,7 +32,7 @@ public function testUserCanViewQuestionAnswers(): void $this->assertDatabaseCount("answers", 10); $this->actingAs($this->user) - ->get("/admin/questions/{$question->id}/answers") + ->get(route("admin.answers.index", $question->id)) ->assertInertia( fn(Assert $page) => $page ->component("Answer/Index") @@ -42,7 +42,7 @@ public function testUserCanViewQuestionAnswers(): void public function testUserCannotViewAnswersOfQuestionThatNotExisted(): void { - $this->actingAs($this->user)->get("/admin/questions/1/answers") + $this->actingAs($this->user)->get(route("admin.answers.index", 1)) ->assertStatus(404); } @@ -53,7 +53,7 @@ public function testUserCanViewSingleAnswer(): void $this->assertDatabaseCount("answers", 1); $this->actingAs($this->user) - ->get("/admin/answers/{$answer->id}") + ->get(route("admin.answers.show", $answer->id)) ->assertInertia( fn(Assert $page) => $page ->component("Answer/Show") @@ -68,7 +68,7 @@ public function testUserCanViewLockedAnswer(): void $this->assertDatabaseCount("answers", 1); $this->actingAs($this->user) - ->get("/admin/answers/{$answer->id}") + ->get(route("admin.answers.show", $answer->id)) ->assertInertia( fn(Assert $page) => $page ->component("Answer/Show") @@ -79,7 +79,7 @@ public function testUserCanViewLockedAnswer(): void public function testUserCannotViewAnswerThatNotExisted(): void { - $this->actingAs($this->user)->get("/admin/answers/1") + $this->actingAs($this->user)->get(route("admin.answers.show", 1)) ->assertStatus(404); } @@ -89,7 +89,7 @@ public function testUserCanCreateAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", [ @@ -104,15 +104,15 @@ public function testUserCanCreateMultipleAnswers(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 1"]) ->assertRedirect("/quizzes"); $this->from("/quizzes") - ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 2"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 2"]) ->assertRedirect("/quizzes"); $this->from("/quizzes") - ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 3"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 3"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["text" => "Example answer 1"]); @@ -124,7 +124,7 @@ public function testUserCannotCreateAnswerToQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/1/answers", ["text" => "Example answer"]) + ->post(route("admin.answers.store", 1), ["text" => "Example answer"]) ->assertStatus(404); $this->assertDatabaseMissing("answers", [ @@ -138,7 +138,7 @@ public function testUserCannotCreateAnswerToQuestionThatIsLocked(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->post(route("admin.answers.store", $question->id), ["text" => "Example answer 1"]) ->assertStatus(403); $this->assertDatabaseMissing("answers", [ @@ -152,11 +152,11 @@ public function testUserCannotCreateInvalidAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/answers", []) + ->post(route("admin.answers.store", $question->id), []) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") - ->post("/admin/questions/{$question->id}/answers", ["text" => false]) + ->post(route("admin.answers.store", $question->id), ["text" => false]) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("answers", 0); @@ -168,7 +168,7 @@ public function testUserCanEditAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/admin/answers/{$answer->id}", ["text" => "New answer"]) + ->patch(route("admin.answers.update", $answer->id), ["text" => "New answer"]) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["text" => "New answer"]); @@ -178,7 +178,7 @@ public function testUserCannotEditAnswerThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->patch("/admin/answers/1", ["text" => "New answer"]) + ->patch(route("admin.answers.update", 1), ["text" => "New answer"]) ->assertStatus(404); } @@ -188,11 +188,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/admin/answers/{$answer->id}", []) + ->patch(route("admin.answers.update", $answer->id), []) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") - ->patch("/admin/answers/{$answer->id}", ["text" => true]) + ->patch(route("admin.answers.update", $answer->id), ["text" => true]) ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("answers", ["text" => "Old answer"]); @@ -204,7 +204,7 @@ public function testUserCannotEditLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->patch("/admin/answers/{$answer->id}", ["text" => "New answer"]) + ->patch(route("admin.answers.update", $answer->id), ["text" => "New answer"]) ->assertStatus(403); $this->assertDatabaseHas("answers", ["text" => "Old answer"]); @@ -216,7 +216,7 @@ public function testUserCanDeleteAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/admin/answers/{$answer->id}") + ->delete(route("admin.answers.destroy", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseMissing("answers", ["text" => "answer"]); @@ -228,7 +228,7 @@ public function testUserCannotDeleteLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/admin/answers/{$answer->id}") + ->delete(route("admin.answers.destroy", $answer->id)) ->assertStatus(403); $this->assertDatabaseHas("answers", ["text" => "answer"]); @@ -238,7 +238,7 @@ public function testUserCannotDeleteAnswerThatNotExisted(): void { $this->actingAs($this->user) ->from("/quizzes") - ->delete("/admin/answers/1") + ->delete(route("admin.answers.destroy", 1)) ->assertStatus(404); } @@ -248,7 +248,7 @@ public function testUserCanMarkAnswerAsCorrect(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/correct") + ->post(route("admin.answers.correct", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -267,7 +267,7 @@ public function testUserCanChangeCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answerB->id}/correct") + ->post(route("admin.answers.correct", $answerB->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerB->id]); @@ -285,7 +285,7 @@ public function testUserCanDeleteCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->delete("/admin/answers/{$answer->id}") + ->delete(route("admin.answers.destroy", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => null]); @@ -304,7 +304,7 @@ public function testUserCannotChangeCorrectAnswerInLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answerB->id}/correct") + ->post(route("admin.answers.correct", $answerA->id)) ->assertStatus(403); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); @@ -322,7 +322,7 @@ public function testUserCanChangeCorrectAnswerToInvalid(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/invalid") + ->post(route("admin.answers.invalid", $answer->id)) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -340,7 +340,7 @@ public function testUserCannotChangeCorrectAnswerToInvalidInLockedQuestion(): vo $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/invalid") + ->post(route("admin.answers.invalid", $answer->id)) ->assertStatus(403); $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); @@ -356,7 +356,7 @@ public function testUserCanCopyAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -372,7 +372,7 @@ public function testUserCanCopyLockedAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -388,7 +388,7 @@ public function testUserCannotCopyAnswerToLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertStatus(403); $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); @@ -408,7 +408,7 @@ public function testUserCanCopyCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/clone/{$questionB->id}") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => $questionB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); @@ -422,7 +422,7 @@ public function testUserCannotCopyAnswerThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/2/clone/{$question->id}") + ->post(route("admin.answers.clone", ["answer" => 2, "question" => $question->id])) ->assertStatus(404); } @@ -432,7 +432,7 @@ public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/answers/{$answer->id}/clone/2") + ->post(route("admin.answers.clone", ["answer" => $answer->id, "question" => 2])) ->assertStatus(404); } } diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index 1dd3fc6f..437b375a 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -36,7 +36,7 @@ public function testUserCanViewQuizQuestions(): void $this->assertDatabaseCount("answers", 10); $this->actingAs($this->user) - ->get("/admin/quizzes/{$quiz->id}/questions") + ->get(route("admin.questions.index", $quiz->id)) ->assertInertia( fn(Assert $page) => $page ->component("Question/Index") @@ -47,7 +47,7 @@ public function testUserCanViewQuizQuestions(): void public function testUserCannotViewQuestionsOfQuizThatNotExisted(): void { - $this->actingAs($this->user)->get("/admin/quizzes/1/questions") + $this->actingAs($this->user)->get(route("admin.questions.index", 1)) ->assertStatus(404); } @@ -58,7 +58,7 @@ public function testUserCanViewSingleQuestion(): void $this->assertDatabaseCount("questions", 1); $this->actingAs($this->user) - ->get("/admin/questions/{$question->id}") + ->get(route("admin.questions.show", $question->id)) ->assertInertia( fn(Assert $page) => $page ->component("Question/Show") @@ -73,7 +73,7 @@ public function testUserCanViewLockedQuestion(): void $this->assertDatabaseCount("questions", 1); $this->actingAs($this->user) - ->get("/admin/questions/{$question->id}") + ->get(route("admin.questions.show", $question->id)) ->assertInertia( fn(Assert $page) => $page ->component("Question/Show") @@ -84,7 +84,7 @@ public function testUserCanViewLockedQuestion(): void public function testUserCannotViewQuestionThatNotExisted(): void { - $this->actingAs($this->user)->get("/admin/questions/1") + $this->actingAs($this->user)->get(route("admin.questions.show", 1)) ->assertStatus(404); } @@ -94,7 +94,7 @@ public function testUserCanCreateQuestion(): void $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", [ @@ -109,15 +109,15 @@ public function testUserCanCreateMultipleQuestions(): void $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 1"]) ->assertRedirect("/"); $this->from("/") - ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 2"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 2"]) ->assertRedirect("/"); $this->from("/") - ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 3"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 3"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", ["text" => "Example question 1"]); @@ -129,7 +129,7 @@ public function testUserCannotCreateQuestionToQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes/1/questions", ["text" => "Example question"]) + ->post(route("admin.questions.store", 1), ["text" => "Example question"]) ->assertStatus(404); $this->assertDatabaseMissing("questions", [ @@ -143,7 +143,7 @@ public function testUserCannotCreateQuestionToQuizThatIsLocked(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->post(route("admin.questions.store", $quiz->id), ["text" => "Example question 1"]) ->assertStatus(403); $this->assertDatabaseMissing("questions", [ @@ -157,11 +157,11 @@ public function testUserCannotCreateInvalidQuestion(): void $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes/{$quiz->id}/questions", []) + ->post(route("admin.questions.store", $quiz->id), []) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") - ->post("/admin/quizzes/{$quiz->id}/questions", ["text" => false]) + ->post(route("admin.questions.store", $quiz->id), ["text" => false]) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("questions", 0); @@ -173,7 +173,7 @@ public function testUserCanEditQuestion(): void $this->actingAs($this->user) ->from("/") - ->patch("/admin/questions/{$question->id}", ["text" => "New question"]) + ->patch(route("admin.questions.update", $question->id), ["text" => "New question"]) ->assertRedirect("/"); $this->assertDatabaseHas("questions", ["text" => "New question"]); @@ -183,7 +183,7 @@ public function testUserCannotEditQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->patch("/admin/questions/1", ["text" => "New question"]) + ->patch(route("admin.questions.update", 1), ["text" => "New question"]) ->assertStatus(404); } @@ -193,11 +193,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/") - ->patch("/admin/questions/{$question->id}", []) + ->patch(route("admin.questions.update", $question->id), []) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") - ->patch("/admin/questions/{$question->id}", ["text" => true]) + ->patch(route("admin.questions.update", $question->id), ["text" => true]) ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("questions", ["text" => "Old questions"]); @@ -209,13 +209,14 @@ public function testUserCannotEditLockedQuestion(): void $this->actingAs($this->user) ->from("/") - ->patch("/admin/questions/{$question->id}", ["text" => "New question"]) + ->patch(route("admin.questions.update", $question->id), ["text" => "New question"]) ->assertStatus(403); $this->assertDatabaseHas("questions", ["text" => "Old question"]); } public function testUserCanDeleteQuestion(): void + { $question = Question::factory()->create(["text" => "question"]); Answer::factory()->create(["question_id" => $question->id]); @@ -226,7 +227,7 @@ public function testUserCanDeleteQuestion(): void $this->actingAs($this->user) ->from("/") - ->delete("/admin/questions/{$question->id}") + ->delete(route("admin.questions.destroy", $question->id)) ->assertRedirect("/"); $this->assertDatabaseMissing("questions", ["text" => "question"]); @@ -241,7 +242,7 @@ public function testUserCannotDeleteLockedQuestion(): void $this->actingAs($this->user) ->from("/") - ->delete("/admin/questions/{$question->id}") + ->delete(route("admin.questions.destroy", $question->id)) ->assertStatus(403); $this->assertDatabaseHas("questions", ["text" => "question"]); @@ -267,7 +268,7 @@ public function testUserCanCopyQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); @@ -284,7 +285,7 @@ public function testUserCanCopyLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertRedirect("/quizzes"); $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); @@ -300,7 +301,7 @@ public function testUserCannotCopyAnswerToLockedQuestion(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertStatus(403); $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); @@ -318,7 +319,7 @@ public function testUserCanCopyQuestionWithCorrectAnswer(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/clone/{$quizB->id}") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => $quizB->id])) ->assertRedirect("/quizzes"); $this->assertNotNull($quizA->questions[0]->correctAnswer); @@ -332,7 +333,7 @@ public function testUserCannotCopyQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/2/clone/{$quiz->id}") + ->post(route("admin.questions.clone", ["question" => 2, "quiz" => $quiz->id])) ->assertStatus(404); } @@ -342,7 +343,7 @@ public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void $this->actingAs($this->user) ->from("/quizzes") - ->post("/admin/questions/{$question->id}/clone/2") + ->post(route("admin.questions.clone", ["question" => $question->id, "quiz" => 2])) ->assertStatus(404); } } diff --git a/tests/Feature/QuizSubmissionTest.php b/tests/Feature/QuizSubmissionTest.php index 61be5d6a..354afc7b 100644 --- a/tests/Feature/QuizSubmissionTest.php +++ b/tests/Feature/QuizSubmissionTest.php @@ -41,7 +41,7 @@ public function testUserCanViewSingleSubmission(): void $this->assertDatabaseCount("answer_records", 2); $this->actingAs($this->user) - ->get("/submissions/{$submission->id}") + ->get(route("submissions.show", $submission->id)) ->assertInertia( fn(Assert $page) => $page ->component("Submission/Show") @@ -57,7 +57,7 @@ public function testUserCannotViewSubmissionThatIsNotHis(): void $submission = QuizSubmission::factory()->create(); $this->actingAs($this->user) - ->get("/submissions/{$submission->id}") + ->get(route("submissions.show", $submission->id)) ->assertStatus(403); } } diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index 6cd5a496..a7127fe9 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -43,7 +43,7 @@ public function testUserCanViewQuizzes(): void $this->assertDatabaseCount("questions", 10); $this->actingAs($this->user) - ->get("/admin/quizzes") + ->get(route("admin.quizzes.index")) ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Index") @@ -55,7 +55,7 @@ public function testUserCanViewQuizzes(): void public function testUserCannotViewQuizThatNotExisted(): void { - $this->actingAs($this->user)->get("/admin/quizzes/1") + $this->actingAs($this->user)->get(route("admin.quizzes.show", 1)) ->assertStatus(404); } @@ -66,7 +66,7 @@ public function testUserCanViewSingleQuiz(): void $this->assertDatabaseCount("quizzes", 1); $this->actingAs($this->user) - ->get("/admin/quizzes/{$quiz->id}") + ->get(route("admin.quizzes.show", $quiz->id)) ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Show") @@ -81,7 +81,7 @@ public function testUserCanViewLockedQuiz(): void $this->assertDatabaseCount("quizzes", 1); $this->actingAs($this->user) - ->get("/admin/quizzes/{$quiz->id}") + ->get(route("admin.quizzes.show", $quiz->id)) ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Show") @@ -94,7 +94,7 @@ public function testUserCanCreateQuiz(): void { $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes", ["name" => "Example quiz", "scheduled_at" => "2024-02-10 11:40:00"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz", "scheduled_at" => "2024-02-10 11:40:00"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", [ @@ -109,7 +109,7 @@ public function testUserCanCreateQuizWithoutDate(): void $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes", ["name" => "Example quiz"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", [ @@ -122,15 +122,15 @@ public function testUserCanCreateMultipleQuizzes(): void { $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes", ["name" => "Example quiz 1"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz 1"]) ->assertRedirect("/"); $this->from("/") - ->post("/admin/quizzes", ["name" => "Example quiz 2"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz 2"]) ->assertRedirect("/"); $this->from("/") - ->post("/admin/quizzes", ["name" => "Example quiz 3"]) + ->post(route("admin.quizzes.store"), ["name" => "Example quiz 3"]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 1"]); @@ -142,27 +142,27 @@ public function testUserCannotCreateInvalidQuiz(): void { $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes", []) + ->post(route("admin.quizzes.store"), []) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->post("/admin/quizzes", ["name" => false]) + ->post(route("admin.quizzes.store"), ["name" => false]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->post("/admin/quizzes", ["name" => "correct", "scheduled_at" => "invalid format"]) + ->post(route("admin.quizzes.store"), ["name" => "correct", "scheduled_at" => "invalid format"]) ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); $this->from("/") - ->post("/admin/quizzes", ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) + ->post(route("admin.quizzes.store"), ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); $this->from("/") - ->post("/admin/quizzes", ["name" => "correct", "duration" => -100]) + ->post(route("admin.quizzes.store"), ["name" => "correct", "duration" => -100]) ->assertRedirect("/")->assertSessionHasErrors(["duration"]); $this->from("/") - ->post("/admin/quizzes", ["name" => "correct", "duration" => 0]) + ->post(route("admin.quizzes.store"), ["name" => "correct", "duration" => 0]) ->assertRedirect("/")->assertSessionHasErrors(["duration"]); $this->assertDatabaseCount("quizzes", 0); @@ -174,7 +174,7 @@ public function testUserCanEditQuiz(): void $this->actingAs($this->user) ->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00", "duration" => 7200]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00", "duration" => 7200]) ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", ["name" => "New quiz", "scheduled_at" => "2024-03-10 12:15:00", "duration" => 7200]); @@ -184,7 +184,7 @@ public function testUserCannotEditQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->patch("/admin/quizzes/1", ["name" => "New quiz"]) + ->patch(route("admin.quizzes.update", 1), ["name" => "New quiz"]) ->assertStatus(404); } @@ -194,27 +194,27 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($this->user) ->from("/") - ->patch("/admin/quizzes/{$quiz->id}", []) + ->patch(route("admin.quizzes.update", $quiz->id), []) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => true]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => true]) ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "scheduled_at" => "invalid format"]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "scheduled_at" => "invalid format"]) ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); $this->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "scheduled_at" => "2022-01-01 01:01:01"]) ->assertRedirect("/")->assertSessionHasErrors(["scheduled_at"]); $this->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "duration" => -100]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "duration" => -100]) ->assertRedirect("/")->assertSessionHasErrors(["duration"]); $this->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "correct", "duration" => 0]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "correct", "duration" => 0]) ->assertRedirect("/")->assertSessionHasErrors(["duration"]); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); @@ -226,7 +226,7 @@ public function testUserCannotEditLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->patch("/admin/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->patch(route("admin.quizzes.update", $quiz->id), ["name" => "New quiz"]) ->assertStatus(403); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); @@ -244,7 +244,7 @@ public function testUserCanDeleteQuiz(): void $this->actingAs($this->user) ->from("/") - ->delete("/admin/quizzes/{$quiz->id}") + ->delete(route("admin.quizzes.destroy", $quiz->id)) ->assertRedirect("/"); $this->assertDatabaseMissing("quizzes", ["name" => "quiz"]); @@ -259,7 +259,7 @@ public function testUserCannotDeleteLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->delete("/admin/quizzes/{$quiz->id}") + ->delete(route("admin.quizzes.destroy", $quiz->id)) ->assertStatus(403); $this->assertDatabaseHas("quizzes", ["name" => "quiz"]); @@ -269,7 +269,7 @@ public function testUserCannotDeleteQuestionThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->delete("/admin/quizzes/1") + ->delete(route("admin.quizzes.destroy", 1)) ->assertStatus(404); } @@ -287,7 +287,7 @@ public function testUserCanCopyQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes/{$quiz->id}/clone") + ->post(route("admin.quizzes.clone", $quiz->id)) ->assertRedirect("/"); $this->assertDatabaseCount("quizzes", 2); @@ -303,7 +303,7 @@ public function testUserCanCopyLockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes/{$quiz->id}/clone") + ->post(route("admin.quizzes.clone", $quiz->id)) ->assertRedirect("/"); $this->assertDatabaseCount("quizzes", 2); @@ -313,7 +313,7 @@ public function testUserCannotCopyQuizThatNotExisted(): void { $this->actingAs($this->user) ->from("/") - ->post("/admin/quizzes/2/clone") + ->post(route("admin.quizzes.clone", 2)) ->assertStatus(404); } @@ -323,7 +323,7 @@ public function testUserCanStartQuiz(): void $response = $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/start"); + ->post(route("quizzes.start", $quiz->id)); $submission = QuizSubmission::query()->where([ "user_id" => $this->user->id, @@ -339,8 +339,8 @@ public function testUserCannotStartAlreadyStartedQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$submission->quiz->id}/start") - ->assertRedirect("/submissions/{$submission->id}/"); + ->post(route("quizzes.start", $submission->quiz->id)) + ->assertRedirect(route("submissions.show", $submission->id)); $this->assertDatabaseCount("quiz_submissions", 1); } @@ -351,7 +351,7 @@ public function testUserCannotStartUnlockedQuiz(): void $this->actingAs($this->user) ->from("/") - ->post("/quizzes/{$quiz->id}/start") + ->post(route("quizzes.start", $quiz->id)) ->assertStatus(403); $this->assertDatabaseCount("quiz_submissions", 0); From 768a63cf04653e0a66f0fbeb0ba4b916bb0ce0c3 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 13:38:52 +0200 Subject: [PATCH 14/16] fix code style --- tests/Feature/QuestionTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index 437b375a..3eb614b3 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -216,7 +216,6 @@ public function testUserCannotEditLockedQuestion(): void } public function testUserCanDeleteQuestion(): void - { $question = Question::factory()->create(["text" => "question"]); Answer::factory()->create(["question_id" => $question->id]); From 79db92b2a57e09381bcc4472dec40f76f8dd831a Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 19 Aug 2024 13:47:58 +0200 Subject: [PATCH 15/16] move date_format to helper --- app/Helpers/DateFormatHelper.php | 10 ++++++++++ app/Http/Requests/QuizRequest.php | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 app/Helpers/DateFormatHelper.php diff --git a/app/Helpers/DateFormatHelper.php b/app/Helpers/DateFormatHelper.php new file mode 100644 index 00000000..c1fc1e23 --- /dev/null +++ b/app/Helpers/DateFormatHelper.php @@ -0,0 +1,10 @@ + ["required", "string"], - "scheduled_at" => ["date", "date_format:Y-m-d H:i:s", "after:now"], + "scheduled_at" => ["date", "date_format:" . DateFormatHelper::DATETIME_FORMAT, "after:now"], "duration" => ["integer", "min:1"], ]; } From cc11c2e7073772c8560d7c69c2b4e5b994e24fd8 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Tue, 20 Aug 2024 10:02:27 +0200 Subject: [PATCH 16/16] change url to route --- tests/Feature/QuizTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index a7127fe9..09147abc 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -330,7 +330,7 @@ public function testUserCanStartQuiz(): void "quiz_id" => $quiz->id, ])->firstOrFail(); - $response->assertRedirect("/submissions/$submission->id/"); + $response->assertRedirect(route("submissions.show", $submission->id)); } public function testUserCannotStartAlreadyStartedQuiz(): void