From c7fee56a3d072026906bd7f292338bec460b1f99 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Thu, 8 Aug 2024 20:55:10 +0200 Subject: [PATCH 01/24] create models --- app/Models/Answer.php | 41 ++++++++++++++ app/Models/Question.php | 56 +++++++++++++++++++ app/Models/Test.php | 56 +++++++++++++++++++ database/factories/AnswerFactory.php | 29 ++++++++++ database/factories/QuestionFactory.php | 30 ++++++++++ database/factories/TestFactory.php | 29 ++++++++++ .../2024_08_08_123620_create_tests_table.php | 24 ++++++++ ...24_08_08_123627_create_questions_table.php | 25 +++++++++ ...2024_08_08_162910_create_answers_table.php | 25 +++++++++ ...d_correct_answer_id_to_questions_table.php | 24 ++++++++ 10 files changed, 339 insertions(+) create mode 100644 app/Models/Answer.php create mode 100644 app/Models/Question.php create mode 100644 app/Models/Test.php create mode 100644 database/factories/AnswerFactory.php create mode 100644 database/factories/QuestionFactory.php create mode 100644 database/factories/TestFactory.php create mode 100644 database/migrations/2024_08_08_123620_create_tests_table.php create mode 100644 database/migrations/2024_08_08_123627_create_questions_table.php create mode 100644 database/migrations/2024_08_08_162910_create_answers_table.php create mode 100644 database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php diff --git a/app/Models/Answer.php b/app/Models/Answer.php new file mode 100644 index 00000000..78ccb3c1 --- /dev/null +++ b/app/Models/Answer.php @@ -0,0 +1,41 @@ +belongsTo(self::class); + } + + protected function isLocked(): Attribute + { + return Attribute::get(fn(): bool => $this->question->isLocked); + } +} diff --git a/app/Models/Question.php b/app/Models/Question.php new file mode 100644 index 00000000..61cbbf31 --- /dev/null +++ b/app/Models/Question.php @@ -0,0 +1,56 @@ + $answers + * @property Test $test + */ +class Question extends Model +{ + use HasFactory; + + protected $fillable = [ + "text", + ]; + + public function correctAnswer(): BelongsTo + { + return $this->belongsTo(Answer::class, "correct_answer_id"); + } + + protected function test(): BelongsTo + { + return $this->belongsTo(Test::class); + } + + protected function answers(): HasMany + { + return $this->hasMany(Answer::class); + } + + protected function isLocked(): Attribute + { + return Attribute::get(fn(): bool => $this->test->isLocked); + } +} diff --git a/app/Models/Test.php b/app/Models/Test.php new file mode 100644 index 00000000..94ae6488 --- /dev/null +++ b/app/Models/Test.php @@ -0,0 +1,56 @@ + $questions + * @property Collection $answers + */ +class Test extends Model +{ + use HasFactory; + + protected $fillable = [ + "name", + ]; + + protected function questions(): HasMany + { + return $this->hasMany(Question::class); + } + + protected function answers(): HasManyThrough + { + return $this->hasManyThrough(Answer::class, Question::class); + } + + protected function isLocked(): Attribute + { + return Attribute::get(fn(): bool => $this->locked_at !== null); + } + + protected function casts(): array + { + return [ + "locked_at" => "datetime", + ]; + } +} diff --git a/database/factories/AnswerFactory.php b/database/factories/AnswerFactory.php new file mode 100644 index 00000000..000a4431 --- /dev/null +++ b/database/factories/AnswerFactory.php @@ -0,0 +1,29 @@ + + */ +class AnswerFactory extends Factory +{ + public function definition(): array + { + return [ + "text" => fake()->realText(), + "question_id" => QuestionFactory::factory(), + ]; + } + + public function locked(): static + { + return $this->state(fn(array $attributes): array => [ + "question_id" => QuestionFactory::factory()->locked(), + ]); + } +} diff --git a/database/factories/QuestionFactory.php b/database/factories/QuestionFactory.php new file mode 100644 index 00000000..8d8c5d8f --- /dev/null +++ b/database/factories/QuestionFactory.php @@ -0,0 +1,30 @@ + + */ +class QuestionFactory extends Factory +{ + public function definition(): array + { + return [ + "text" => fake()->realText(), + "test_id" => Test::factory(), + ]; + } + + public function locked(): static + { + return $this->state(fn(array $attributes): array => [ + "test_id" => Test::factory()->locked(), + ]); + } +} diff --git a/database/factories/TestFactory.php b/database/factories/TestFactory.php new file mode 100644 index 00000000..8486c711 --- /dev/null +++ b/database/factories/TestFactory.php @@ -0,0 +1,29 @@ + + */ +class TestFactory extends Factory +{ + public function definition(): array + { + return [ + "name" => fake()->name(), + ]; + } + + public function locked(): static + { + return $this->state(fn(array $attributes): array => [ + "locked_at" => Carbon::now(), + ]); + } +} diff --git a/database/migrations/2024_08_08_123620_create_tests_table.php b/database/migrations/2024_08_08_123620_create_tests_table.php new file mode 100644 index 00000000..8a9176f9 --- /dev/null +++ b/database/migrations/2024_08_08_123620_create_tests_table.php @@ -0,0 +1,24 @@ +id(); + $table->timestamps(); + $table->timestamp("locked_at")->nullable(); + $table->string("name"); + }); + } + + public function down(): void + { + Schema::dropIfExists("tests"); + } +}; diff --git a/database/migrations/2024_08_08_123627_create_questions_table.php b/database/migrations/2024_08_08_123627_create_questions_table.php new file mode 100644 index 00000000..7ba0e506 --- /dev/null +++ b/database/migrations/2024_08_08_123627_create_questions_table.php @@ -0,0 +1,25 @@ +id(); + $table->timestamps(); + $table->text("text"); + $table->foreignIdFor(Test::class)->constrained()->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists("questions"); + } +}; diff --git a/database/migrations/2024_08_08_162910_create_answers_table.php b/database/migrations/2024_08_08_162910_create_answers_table.php new file mode 100644 index 00000000..7cd314be --- /dev/null +++ b/database/migrations/2024_08_08_162910_create_answers_table.php @@ -0,0 +1,25 @@ +id(); + $table->timestamps(); + $table->text("text"); + $table->foreignIdFor(Question::class)->constrained()->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::dropIfExists("answers"); + } +}; diff --git a/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php b/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php new file mode 100644 index 00000000..072bd902 --- /dev/null +++ b/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php @@ -0,0 +1,24 @@ +foreignIdFor(Answer::class, "correct_answer_id")->nullable()->constrained("answers")->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::table("questions", function (Blueprint $table): void { + $table->dropColumn("correct_answer_id"); + }); + } +}; From 9bfdae39d0b86afbb452205b9a3137083b37ca56 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Thu, 8 Aug 2024 20:57:19 +0200 Subject: [PATCH 02/24] ignore .vite folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 80e45f7c..0bea5fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ google-credentials.json .idea/ .vscode/ .composer +.vite /public/build/ supervisord.pid *.cache From 3cec4a0a498289251da180528dd078852925f5a9 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Fri, 9 Aug 2024 12:32:27 +0200 Subject: [PATCH 03/24] create basic quiz CRUD --- .../Controllers/QuestionAnswerController.php | 67 +++++++++++++++++ app/Http/Controllers/QuizController.php | 71 +++++++++++++++++++ .../Controllers/QuizQuestionController.php | 66 +++++++++++++++++ app/Http/Middleware/HandleInertiaRequests.php | 42 +++++++++++ app/Http/Requests/AnswerRequest.php | 26 +++++++ app/Http/Requests/QuestionRequest.php | 26 +++++++ app/Http/Requests/QuizRequest.php | 26 +++++++ app/Http/Resources/AnswerResource.php | 22 ++++++ app/Http/Resources/QuestionResource.php | 23 ++++++ app/Http/Resources/QuizResource.php | 22 ++++++ app/Models/Answer.php | 10 ++- app/Models/Question.php | 12 ++-- app/Models/{Test.php => Quiz.php} | 8 +-- app/Providers/AppServiceProvider.php | 2 + bootstrap/app.php | 4 +- database/factories/AnswerFactory.php | 5 +- database/factories/QuestionFactory.php | 6 +- .../{TestFactory.php => QuizFactory.php} | 6 +- ...024_08_08_123620_create_quizzes_table.php} | 4 +- ...24_08_08_123627_create_questions_table.php | 3 +- ...d_correct_answer_id_to_questions_table.php | 2 +- database/seeders/DatabaseSeeder.php | 4 ++ resources/js/Pages/Answer/Index.vue | 15 ++++ resources/js/Pages/Answer/Show.vue | 23 ++++++ resources/js/Pages/Question/Index.vue | 15 ++++ resources/js/Pages/Question/Show.vue | 23 ++++++ resources/js/Pages/Quiz/Index.vue | 15 ++++ resources/js/Pages/Quiz/Show.vue | 24 +++++++ resources/js/Types/Answer.d.ts | 8 +++ resources/js/Types/Question.d.ts | 11 +++ resources/js/Types/Quiz.d.ts | 10 +++ routes/web.php | 9 +++ 32 files changed, 585 insertions(+), 25 deletions(-) create mode 100644 app/Http/Controllers/QuestionAnswerController.php create mode 100644 app/Http/Controllers/QuizController.php create mode 100644 app/Http/Controllers/QuizQuestionController.php create mode 100644 app/Http/Middleware/HandleInertiaRequests.php create mode 100644 app/Http/Requests/AnswerRequest.php create mode 100644 app/Http/Requests/QuestionRequest.php create mode 100644 app/Http/Requests/QuizRequest.php create mode 100644 app/Http/Resources/AnswerResource.php create mode 100644 app/Http/Resources/QuestionResource.php create mode 100644 app/Http/Resources/QuizResource.php rename app/Models/{Test.php => Quiz.php} (86%) rename database/factories/{TestFactory.php => QuizFactory.php} (84%) rename database/migrations/{2024_08_08_123620_create_tests_table.php => 2024_08_08_123620_create_quizzes_table.php} (80%) create mode 100644 resources/js/Pages/Answer/Index.vue create mode 100644 resources/js/Pages/Answer/Show.vue create mode 100644 resources/js/Pages/Question/Index.vue create mode 100644 resources/js/Pages/Question/Show.vue create mode 100644 resources/js/Pages/Quiz/Index.vue create mode 100644 resources/js/Pages/Quiz/Show.vue create mode 100644 resources/js/Types/Answer.d.ts create mode 100644 resources/js/Types/Question.d.ts create mode 100644 resources/js/Types/Quiz.d.ts diff --git a/app/Http/Controllers/QuestionAnswerController.php b/app/Http/Controllers/QuestionAnswerController.php new file mode 100644 index 00000000..c2aaa7d6 --- /dev/null +++ b/app/Http/Controllers/QuestionAnswerController.php @@ -0,0 +1,67 @@ + AnswerResource::collection($question->answers), + ]); + } + + public function store(Question $question, AnswerRequest $request): RedirectResponse + { + Answer::query() + ->create($request->validated()) + ->question()->associate($question) + ->save(); + + return redirect() + ->back() + ->with("success", "Answer added successfully"); + } + + public function show(Answer $answer): Response + { + return Inertia::render("Answer/show", ["answer" => new AnswerResource($answer)]); + } + + public function markAsCorrect(Answer $answer): RedirectResponse + { + $answer->question->correctAnswer()->associate($answer)->save(); + + return redirect() + ->back() + ->with("success", "Answer marked as the correct one"); + } + + public function update(AnswerRequest $request, Answer $answer): RedirectResponse + { + $answer->update($request->validated()); + + return redirect() + ->back() + ->with("success", "Answer updated"); + } + + public function destroy(Answer $answer): RedirectResponse + { + $answer->delete(); + + return redirect() + ->back() + ->with("success", "Answer deleted"); + } +} diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php new file mode 100644 index 00000000..6a9d63f4 --- /dev/null +++ b/app/Http/Controllers/QuizController.php @@ -0,0 +1,71 @@ +with("questions.answers") + ->get(); + + return Inertia::render("Quiz/Index", ["quizzes" => QuizResource::collection($quizzes)]); + } + + public function store(QuizRequest $request): RedirectResponse + { + Quiz::query()->create($request->validated()); + + return redirect() + ->back() + ->with("success", "Quiz added successfully"); + } + + public function show(int $quiz): Response + { + $quiz = Quiz::query() + ->with("questions.answers") + ->findOrFail($quiz); + + return Inertia::render("Quiz/Show", ["quiz" => new QuizResource($quiz)]); + } + + public function update(QuizRequest $request, Quiz $quiz): RedirectResponse + { + $quiz->update($request->validated()); + + return redirect() + ->back() + ->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(); + + return redirect() + ->back() + ->with("success", "Quiz deleted"); + } +} diff --git a/app/Http/Controllers/QuizQuestionController.php b/app/Http/Controllers/QuizQuestionController.php new file mode 100644 index 00000000..ef990f89 --- /dev/null +++ b/app/Http/Controllers/QuizQuestionController.php @@ -0,0 +1,66 @@ +questions() + ->with("answers") + ->get(); + + return Inertia::render("Question/Index", [ + "questions" => QuestionResource::collection($questions), + ]); + } + + public function store(Quiz $quiz, QuestionRequest $request): RedirectResponse + { + Question::query() + ->create($request->validated()) + ->quiz()->associate($quiz) + ->save(); + + return redirect() + ->back() + ->with("success", "Question added successfully"); + } + + public function show(int $question): Response + { + $test = Question::query() + ->with("answers") + ->findOrFail($question); + + return Inertia::render("Question/Show", ["question" => new QuestionResource($test)]); + } + + public function update(QuestionRequest $request, Question $question): RedirectResponse + { + $question->update($request->validated()); + + return redirect() + ->back() + ->with("success", "Question updated"); + } + + public function destroy(Question $question): RedirectResponse + { + $question->delete(); + + return redirect() + ->back() + ->with("success", "Question deleted"); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php new file mode 100644 index 00000000..81c999cc --- /dev/null +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -0,0 +1,42 @@ + + */ + public function share(Request $request): array + { + return array_merge(parent::share($request), [ + // + ]); + } +} diff --git a/app/Http/Requests/AnswerRequest.php b/app/Http/Requests/AnswerRequest.php new file mode 100644 index 00000000..677d5a0c --- /dev/null +++ b/app/Http/Requests/AnswerRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + "text" => ["required", "string"], + ]; + } +} diff --git a/app/Http/Requests/QuestionRequest.php b/app/Http/Requests/QuestionRequest.php new file mode 100644 index 00000000..83c80d58 --- /dev/null +++ b/app/Http/Requests/QuestionRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + "text" => ["required", "string"], + ]; + } +} diff --git a/app/Http/Requests/QuizRequest.php b/app/Http/Requests/QuizRequest.php new file mode 100644 index 00000000..ae942518 --- /dev/null +++ b/app/Http/Requests/QuizRequest.php @@ -0,0 +1,26 @@ + + */ + public function rules(): array + { + return [ + "name" => ["required", "string"], + ]; + } +} diff --git a/app/Http/Resources/AnswerResource.php b/app/Http/Resources/AnswerResource.php new file mode 100644 index 00000000..1ee6bb24 --- /dev/null +++ b/app/Http/Resources/AnswerResource.php @@ -0,0 +1,22 @@ + $this->id, + "text" => $this->text, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "locked" => $this->isLocked, + "correct" => $this->isCorrect, + ]; + } +} diff --git a/app/Http/Resources/QuestionResource.php b/app/Http/Resources/QuestionResource.php new file mode 100644 index 00000000..a90c926b --- /dev/null +++ b/app/Http/Resources/QuestionResource.php @@ -0,0 +1,23 @@ + $this->id, + "text" => $this->text, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "locked" => $this->isLocked, + "correct" => $this->correctAnswer?->id, + "answers" => AnswerResource::collection($this->answers) + ]; + } +} diff --git a/app/Http/Resources/QuizResource.php b/app/Http/Resources/QuizResource.php new file mode 100644 index 00000000..130fdcf1 --- /dev/null +++ b/app/Http/Resources/QuizResource.php @@ -0,0 +1,22 @@ + $this->id, + "name" => $this->name, + "createdAt" => $this->created_at, + "updatedAt" => $this->updated_at, + "locked" => $this->isLocked, + "questions" => QuestionResource::collection($this->questions), + ]; + } +} diff --git a/app/Models/Answer.php b/app/Models/Answer.php index 78ccb3c1..dd6b8353 100644 --- a/app/Models/Answer.php +++ b/app/Models/Answer.php @@ -18,6 +18,7 @@ * @property Carbon $updated_at * * @property bool $isLocked + * @property bool $isCorrect * * @property Question $question */ @@ -31,11 +32,16 @@ class Answer extends Model public function question(): BelongsTo { - return $this->belongsTo(self::class); + return $this->belongsTo(Question::class); } - protected function isLocked(): Attribute + public function isLocked(): Attribute { return Attribute::get(fn(): bool => $this->question->isLocked); } + + public function isCorrect(): Attribute + { + return Attribute::get(fn(): bool => $this->question->correctAnswer()->is($this)); + } } diff --git a/app/Models/Question.php b/app/Models/Question.php index 61cbbf31..98baf5ca 100644 --- a/app/Models/Question.php +++ b/app/Models/Question.php @@ -24,7 +24,7 @@ * * @property ?Answer $correctAnswer * @property Collection $answers - * @property Test $test + * @property Quiz $quiz */ class Question extends Model { @@ -39,18 +39,18 @@ public function correctAnswer(): BelongsTo return $this->belongsTo(Answer::class, "correct_answer_id"); } - protected function test(): BelongsTo + public function quiz(): BelongsTo { - return $this->belongsTo(Test::class); + return $this->belongsTo(Quiz::class); } - protected function answers(): HasMany + public function answers(): HasMany { return $this->hasMany(Answer::class); } - protected function isLocked(): Attribute + public function isLocked(): Attribute { - return Attribute::get(fn(): bool => $this->test->isLocked); + return Attribute::get(fn(): bool => $this->quiz->isLocked); } } diff --git a/app/Models/Test.php b/app/Models/Quiz.php similarity index 86% rename from app/Models/Test.php rename to app/Models/Quiz.php index 94ae6488..c38b5bf3 100644 --- a/app/Models/Test.php +++ b/app/Models/Quiz.php @@ -24,7 +24,7 @@ * @property Collection $questions * @property Collection $answers */ -class Test extends Model +class Quiz extends Model { use HasFactory; @@ -32,17 +32,17 @@ class Test extends Model "name", ]; - protected function questions(): HasMany + public function questions(): HasMany { return $this->hasMany(Question::class); } - protected function answers(): HasManyThrough + public function answers(): HasManyThrough { return $this->hasManyThrough(Answer::class, Question::class); } - protected function isLocked(): Attribute + public function isLocked(): Attribute { return Attribute::get(fn(): bool => $this->locked_at !== null); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dfdc1c7a..fcd238e8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ namespace App\Providers; +use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -14,5 +15,6 @@ public function register(): void public function boot(): void { + JsonResource::withoutWrapping(); } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 91570f99..9eba92b4 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Http\Middleware\HandleInertiaRequests; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; @@ -13,6 +14,7 @@ health: "/up", ) ->withMiddleware(function (Middleware $middleware): void { + $middleware->web(append: HandleInertiaRequests::class); }) ->withExceptions(function (Exceptions $exceptions): void { - })->create(); +})->create(); diff --git a/database/factories/AnswerFactory.php b/database/factories/AnswerFactory.php index 000a4431..ef9f0a0c 100644 --- a/database/factories/AnswerFactory.php +++ b/database/factories/AnswerFactory.php @@ -5,6 +5,7 @@ namespace Database\Factories; use App\Models\Answer; +use App\Models\Question; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -16,14 +17,14 @@ public function definition(): array { return [ "text" => fake()->realText(), - "question_id" => QuestionFactory::factory(), + "question_id" => Question::factory(), ]; } public function locked(): static { return $this->state(fn(array $attributes): array => [ - "question_id" => QuestionFactory::factory()->locked(), + "question_id" => Question::factory()->locked(), ]); } } diff --git a/database/factories/QuestionFactory.php b/database/factories/QuestionFactory.php index 8d8c5d8f..cc4f5719 100644 --- a/database/factories/QuestionFactory.php +++ b/database/factories/QuestionFactory.php @@ -5,7 +5,7 @@ namespace Database\Factories; use App\Models\Question; -use App\Models\Test; +use App\Models\Quiz; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -17,14 +17,14 @@ public function definition(): array { return [ "text" => fake()->realText(), - "test_id" => Test::factory(), + "quiz_id" => Quiz::factory(), ]; } public function locked(): static { return $this->state(fn(array $attributes): array => [ - "test_id" => Test::factory()->locked(), + "quiz_id" => Quiz::factory()->locked(), ]); } } diff --git a/database/factories/TestFactory.php b/database/factories/QuizFactory.php similarity index 84% rename from database/factories/TestFactory.php rename to database/factories/QuizFactory.php index 8486c711..be5b4a31 100644 --- a/database/factories/TestFactory.php +++ b/database/factories/QuizFactory.php @@ -4,14 +4,14 @@ namespace Database\Factories; -use App\Models\Test; +use App\Models\Quiz; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends Factory + * @extends Factory */ -class TestFactory extends Factory +class QuizFactory extends Factory { public function definition(): array { diff --git a/database/migrations/2024_08_08_123620_create_tests_table.php b/database/migrations/2024_08_08_123620_create_quizzes_table.php similarity index 80% rename from database/migrations/2024_08_08_123620_create_tests_table.php rename to database/migrations/2024_08_08_123620_create_quizzes_table.php index 8a9176f9..c8a60917 100644 --- a/database/migrations/2024_08_08_123620_create_tests_table.php +++ b/database/migrations/2024_08_08_123620_create_quizzes_table.php @@ -9,7 +9,7 @@ return new class() extends Migration { public function up(): void { - Schema::create("tests", function (Blueprint $table): void { + Schema::create("quizzes", function (Blueprint $table): void { $table->id(); $table->timestamps(); $table->timestamp("locked_at")->nullable(); @@ -19,6 +19,6 @@ public function up(): void public function down(): void { - Schema::dropIfExists("tests"); + Schema::dropIfExists("quizzes"); } }; diff --git a/database/migrations/2024_08_08_123627_create_questions_table.php b/database/migrations/2024_08_08_123627_create_questions_table.php index 7ba0e506..78cb32d8 100644 --- a/database/migrations/2024_08_08_123627_create_questions_table.php +++ b/database/migrations/2024_08_08_123627_create_questions_table.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Models\Quiz; use App\Models\Test; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; @@ -14,7 +15,7 @@ public function up(): void $table->id(); $table->timestamps(); $table->text("text"); - $table->foreignIdFor(Test::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Quiz::class)->constrained()->cascadeOnDelete(); }); } diff --git a/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php b/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php index 072bd902..5b6ed6d0 100644 --- a/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php +++ b/database/migrations/2024_08_08_184933_add_correct_answer_id_to_questions_table.php @@ -11,7 +11,7 @@ public function up(): void { Schema::table("questions", function (Blueprint $table): void { - $table->foreignIdFor(Answer::class, "correct_answer_id")->nullable()->constrained("answers")->cascadeOnDelete(); + $table->foreignIdFor(Answer::class, "correct_answer_id")->nullable()->constrained("answers")->nullOnDelete(); }); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index de853239..cff3b9ec 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -4,11 +4,15 @@ namespace Database\Seeders; +use App\Models\Answer; +use App\Models\Quiz; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run(): void { + Quiz::factory()->create(); + Answer::factory()->create(); } } diff --git a/resources/js/Pages/Answer/Index.vue b/resources/js/Pages/Answer/Index.vue new file mode 100644 index 00000000..c42753a0 --- /dev/null +++ b/resources/js/Pages/Answer/Index.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Pages/Answer/Show.vue b/resources/js/Pages/Answer/Show.vue new file mode 100644 index 00000000..6b9ebc30 --- /dev/null +++ b/resources/js/Pages/Answer/Show.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/Pages/Question/Index.vue b/resources/js/Pages/Question/Index.vue new file mode 100644 index 00000000..d297b957 --- /dev/null +++ b/resources/js/Pages/Question/Index.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Pages/Question/Show.vue b/resources/js/Pages/Question/Show.vue new file mode 100644 index 00000000..528785ed --- /dev/null +++ b/resources/js/Pages/Question/Show.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/Pages/Quiz/Index.vue b/resources/js/Pages/Quiz/Index.vue new file mode 100644 index 00000000..aa4fabe3 --- /dev/null +++ b/resources/js/Pages/Quiz/Index.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Pages/Quiz/Show.vue b/resources/js/Pages/Quiz/Show.vue new file mode 100644 index 00000000..494277ae --- /dev/null +++ b/resources/js/Pages/Quiz/Show.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/Types/Answer.d.ts b/resources/js/Types/Answer.d.ts new file mode 100644 index 00000000..f14cd96c --- /dev/null +++ b/resources/js/Types/Answer.d.ts @@ -0,0 +1,8 @@ +export interface Answer { + id: number + text: string + createdAt: number + updatedAt: number + locked: boolean + correct: boolean +} diff --git a/resources/js/Types/Question.d.ts b/resources/js/Types/Question.d.ts new file mode 100644 index 00000000..1fb2e4ef --- /dev/null +++ b/resources/js/Types/Question.d.ts @@ -0,0 +1,11 @@ +import {type Answer} from "@/Types/Answer"; + +export interface Question { + id: number + text: string + createdAt: number + updatedAt: number + locked: boolean + correct?: number + answers: Answer[] +} diff --git a/resources/js/Types/Quiz.d.ts b/resources/js/Types/Quiz.d.ts new file mode 100644 index 00000000..54fb3bd1 --- /dev/null +++ b/resources/js/Types/Quiz.d.ts @@ -0,0 +1,10 @@ +import {type Question} from "@/Types/Question"; + +export interface Quiz { + id: number + name: string + createdAt: number + updatedAt: number + locked: boolean + questions: Question[] +} diff --git a/routes/web.php b/routes/web.php index 2301789d..52228c0a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,7 +2,16 @@ declare(strict_types=1); +use App\Http\Controllers\QuestionAnswerController; +use App\Http\Controllers\QuizController; +use App\Http\Controllers\QuizQuestionController; use Illuminate\Support\Facades\Route; use Inertia\Response; Route::get("/", fn(): Response => inertia("Welcome")); + +Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); +Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"]); +Route::resource("quizzes", QuizController::class)->except(["create", "edit"]); +Route::resource("quizzes.questions", QuizQuestionController::class)->except(["create", "edit"])->shallow(); +Route::resource("questions.answers", QuestionAnswerController::class)->except(["create", "edit"])->shallow(); From 7ea0548c4624c62d18630e57ebed5b922df70f25 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Fri, 9 Aug 2024 12:33:08 +0200 Subject: [PATCH 04/24] fix code style --- app/Http/Controllers/QuizController.php | 2 +- app/Http/Middleware/HandleInertiaRequests.php | 8 ++++---- app/Http/Resources/QuestionResource.php | 2 +- bootstrap/app.php | 2 +- ...2024_08_08_123627_create_questions_table.php | 1 - tests/Feature/ExampleTest.php | 17 ----------------- 6 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 tests/Feature/ExampleTest.php diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index 6a9d63f4..eb538f35 100644 --- a/app/Http/Controllers/QuizController.php +++ b/app/Http/Controllers/QuizController.php @@ -36,7 +36,7 @@ public function show(int $quiz): Response { $quiz = Quiz::query() ->with("questions.answers") - ->findOrFail($quiz); + ->findOrFail($quiz); return Inertia::render("Quiz/Show", ["quiz" => new QuizResource($quiz)]); } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 81c999cc..1036bf69 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -1,5 +1,7 @@ $this->updated_at, "locked" => $this->isLocked, "correct" => $this->correctAnswer?->id, - "answers" => AnswerResource::collection($this->answers) + "answers" => AnswerResource::collection($this->answers), ]; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 9eba92b4..1a0dc62f 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -17,4 +17,4 @@ $middleware->web(append: HandleInertiaRequests::class); }) ->withExceptions(function (Exceptions $exceptions): void { -})->create(); + })->create(); diff --git a/database/migrations/2024_08_08_123627_create_questions_table.php b/database/migrations/2024_08_08_123627_create_questions_table.php index 78cb32d8..842358c1 100644 --- a/database/migrations/2024_08_08_123627_create_questions_table.php +++ b/database/migrations/2024_08_08_123627_create_questions_table.php @@ -3,7 +3,6 @@ declare(strict_types=1); use App\Models\Quiz; -use App\Models\Test; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 55a42c3a..00000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,17 +0,0 @@ -get("/"); - - $response->assertStatus(200); - } -} From bfb20428a3a55a451410352cd920805bc55c4f68 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 09:27:19 +0200 Subject: [PATCH 05/24] create tests for answer controller --- app/Http/Controllers/Controller.php | 3 + .../Controllers/QuestionAnswerController.php | 27 +- app/Policies/AnswerPolicy.php | 19 ++ phpunit.xml | 2 +- tests/Feature/AnswerTest.php | 259 ++++++++++++++++++ 5 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 app/Policies/AnswerPolicy.php create mode 100644 tests/Feature/AnswerTest.php diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 77e9631f..ded7c1ff 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,6 +4,9 @@ namespace App\Http\Controllers; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; + abstract class Controller { + use AuthorizesRequests; } diff --git a/app/Http/Controllers/QuestionAnswerController.php b/app/Http/Controllers/QuestionAnswerController.php index c2aaa7d6..cf30fef8 100644 --- a/app/Http/Controllers/QuestionAnswerController.php +++ b/app/Http/Controllers/QuestionAnswerController.php @@ -8,6 +8,7 @@ use App\Http\Resources\AnswerResource; use App\Models\Answer; use App\Models\Question; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; use Inertia\Response; @@ -21,10 +22,17 @@ public function index(Question $question): Response ]); } + /** + * @throws AuthorizationException + */ public function store(Question $question, AnswerRequest $request): RedirectResponse { + if ($question->isLocked) { + throw new AuthorizationException(); + } + Answer::query() - ->create($request->validated()) + ->make($request->validated()) ->question()->associate($question) ->save(); @@ -35,11 +43,16 @@ public function store(Question $question, AnswerRequest $request): RedirectRespo public function show(Answer $answer): Response { - return Inertia::render("Answer/show", ["answer" => new AnswerResource($answer)]); + return Inertia::render("Answer/Show", ["answer" => new AnswerResource($answer)]); } + /** + * @throws AuthorizationException + */ public function markAsCorrect(Answer $answer): RedirectResponse { + $this->authorize('modify', $answer); + $answer->question->correctAnswer()->associate($answer)->save(); return redirect() @@ -47,8 +60,13 @@ public function markAsCorrect(Answer $answer): RedirectResponse ->with("success", "Answer marked as the correct one"); } + /** + * @throws AuthorizationException + */ public function update(AnswerRequest $request, Answer $answer): RedirectResponse { + $this->authorize('modify', $answer); + $answer->update($request->validated()); return redirect() @@ -56,8 +74,13 @@ public function update(AnswerRequest $request, Answer $answer): RedirectResponse ->with("success", "Answer updated"); } + /** + * @throws AuthorizationException + */ public function destroy(Answer $answer): RedirectResponse { + $this->authorize('destroy', $answer); + $answer->delete(); return redirect() diff --git a/app/Policies/AnswerPolicy.php b/app/Policies/AnswerPolicy.php new file mode 100644 index 00000000..4b382840 --- /dev/null +++ b/app/Policies/AnswerPolicy.php @@ -0,0 +1,19 @@ +isLocked; + } + + public function destroy(User $user, Answer $answer): bool + { + return !$answer->isLocked; + } +} diff --git a/phpunit.xml b/phpunit.xml index fe6dd827..b5ee5401 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -20,7 +20,7 @@ - + diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php new file mode 100644 index 00000000..a298625e --- /dev/null +++ b/tests/Feature/AnswerTest.php @@ -0,0 +1,259 @@ +create(); + $question = Question::factory()->create(); + Answer::factory()->count(10)->create(["question_id" => $question->id]); + + $this->assertDatabaseCount("answers", 10); + + $this->actingAs($user) + ->get("/questions/{$question->id}/answers") + ->assertInertia( + fn(Assert $page) => $page + ->component("Answer/Index") + ->has("answers", 10), + ); + } + + public function testUserCannotViewAnswersOfQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user)->get("/questions/1/answers") + ->assertStatus(404); + } + + public function testUserCanViewSingleAnswer(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->create(); + + $this->assertDatabaseCount("answers", 1); + + $this->actingAs($user) + ->get("/answers/{$answer->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Answer/Show") + ->where("answer.id", $answer->id) + ); + } + + public function testUserCanViewLockedAnswer(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->locked()->create(); + + $this->assertDatabaseCount("answers", 1); + + $this->actingAs($user) + ->get("/answers/{$answer->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Answer/Show") + ->where("answer.id", $answer->id) + ->where("answer.locked", true) + ); + } + + public function testUserCannotViewAnswerThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user)->get("/answers/1") + ->assertStatus(404); + } + + public function testUserCanCreateAnswer(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer"]) + ->assertRedirect('/quizzes'); + + $this->assertDatabaseHas("answers", [ + "text" => "Example answer", + "question_id" => $question->id, + ]); + } + + public function testUserCanCreateMultipleAnswers(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->assertRedirect('/quizzes'); + + $this->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 2"]) + ->assertRedirect('/quizzes'); + + $this->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 3"]) + ->assertRedirect('/quizzes'); + + + $this->assertDatabaseHas("answers", ["text" => "Example answer 1" ]); + $this->assertDatabaseHas("answers", ["text" => "Example answer 2" ]); + $this->assertDatabaseHas("answers", ["text" => "Example answer 2" ]); + } + + public function testUserCannotCreateAnswerToQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/1/answers", ["text" => "Example answer"]) + ->assertStatus(404); + + $this->assertDatabaseMissing("answers", [ + "text" => "Example answer", + ]); + } + + public function testUserCannotCreateAnswerToQuestionThatIsLocked(): void + { + $user = User::factory()->create(); + $question = Question::factory()->locked()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) + ->assertStatus(403); + + $this->assertDatabaseMissing("answers", [ + "text" => "Example answer", + ]); + } + + + public function testUserCannotCreateInvalidAnswer(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/answers", []) + ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + + $this->from("/quizzes") + ->post("/questions/{$question->id}/answers", ["text" => false]) + ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + + $this->assertDatabaseCount("answers", 0); + } + + public function testUserCanEditAnswer(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->create(["text" => "Old answer"]); + + $this->actingAs($user) + ->from("/quizzes") + ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["text" => "New answer"]); + } + + public function testUserCannotEditAnswerThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->patch("/answers/1", ["text" => "New answer"]) + ->assertStatus(404); + } + + public function testUserCannotMakeInvalidEdit(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->create(["text" => "Old answer"]); + + $this->actingAs($user) + ->from("/quizzes") + ->patch("/answers/{$answer->id}", []) + ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + + $this->from("/quizzes") + ->patch("/answers/{$answer->id}", ["text" => true]) + ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + + $this->assertDatabaseHas("answers", ["text" => "Old answer"]); + } + + public function testUserCannotEditLockedAnswer(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->create(["text" => "Old answer"]); + + $this->actingAs($user) + ->from("/quizzes") + ->patch("/answers/{$answer->id}", ["text" => "New answer"]) + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["text" => "New answer"]); + } + + public function testUserCanDeleteAnswer(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->create(["text" => "answer"]); + + $this->actingAs($user) + ->from("/quizzes") + ->delete("/answers/{$answer->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseMissing("answers", ["text" => "answer"]); + } + + public function testUserCannotDeleteLockedAnswer(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->locked()->create(["text" => "answer"]); + + $this->actingAs($user) + ->from("/quizzes") + ->delete("/answers/{$answer->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("answers", ["text" => "answer"]); + } + + public function testUserCannotDeleteAnswerThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->delete("/answers/1") + ->assertStatus(404); + } +} From bb93c84a84da7f5829ade615e834ca9bbc70f9b7 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 09:37:35 +0200 Subject: [PATCH 06/24] create tests for question controller --- .../Controllers/QuizQuestionController.php | 20 +- app/Policies/QuestionPolicy.php | 20 ++ tests/Feature/QuestionTest.php | 264 ++++++++++++++++++ 3 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 app/Policies/QuestionPolicy.php create mode 100644 tests/Feature/QuestionTest.php diff --git a/app/Http/Controllers/QuizQuestionController.php b/app/Http/Controllers/QuizQuestionController.php index ef990f89..a8e5ff8e 100644 --- a/app/Http/Controllers/QuizQuestionController.php +++ b/app/Http/Controllers/QuizQuestionController.php @@ -8,6 +8,7 @@ use App\Http\Resources\QuestionResource; use App\Models\Question; use App\Models\Quiz; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; use Inertia\Response; @@ -25,10 +26,17 @@ public function index(Quiz $quiz): Response ]); } + /** + * @throws AuthorizationException + */ public function store(Quiz $quiz, QuestionRequest $request): RedirectResponse { + if ($quiz->isLocked) { + throw new AuthorizationException(); + } + Question::query() - ->create($request->validated()) + ->make($request->validated()) ->quiz()->associate($quiz) ->save(); @@ -46,8 +54,13 @@ public function show(int $question): Response return Inertia::render("Question/Show", ["question" => new QuestionResource($test)]); } + /** + * @throws AuthorizationException + */ public function update(QuestionRequest $request, Question $question): RedirectResponse { + $this->authorize('modify', $question); + $question->update($request->validated()); return redirect() @@ -55,8 +68,13 @@ public function update(QuestionRequest $request, Question $question): RedirectRe ->with("success", "Question updated"); } + /** + * @throws AuthorizationException + */ public function destroy(Question $question): RedirectResponse { + $this->authorize('destroy', $question); + $question->delete(); return redirect() diff --git a/app/Policies/QuestionPolicy.php b/app/Policies/QuestionPolicy.php new file mode 100644 index 00000000..9a0b8aa0 --- /dev/null +++ b/app/Policies/QuestionPolicy.php @@ -0,0 +1,20 @@ +isLocked; + } + + public function destroy(User $user, Question $question): bool + { + return !$question->isLocked; + } +} diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php new file mode 100644 index 00000000..471a3c88 --- /dev/null +++ b/tests/Feature/QuestionTest.php @@ -0,0 +1,264 @@ +create(); + $quiz = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quiz->id]); + Answer::factory()->count(10)->create(["question_id" => $question->id]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 1); + $this->assertDatabaseCount("answers", 10); + + $this->actingAs($user) + ->get("/quizzes/{$quiz->id}/questions") + ->assertInertia( + fn(Assert $page) => $page + ->component("Question/Index") + ->has("questions", 1) + ->has('questions.0.answers', 10) + ); + } + + public function testUserCannotViewQuestionsOfQuiThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user)->get("/quizzes/1/questions") + ->assertStatus(404); + } + + public function testUserCanViewSingleQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + + $this->assertDatabaseCount("questions", 1); + + $this->actingAs($user) + ->get("/questions/{$question->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Question/Show") + ->where("question.id", $question->id) + ); + } + + public function testUserCanViewLockedQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->locked()->create(); + + $this->assertDatabaseCount("questions", 1); + + $this->actingAs($user) + ->get("/questions/{$question->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Question/Show") + ->where("question.id", $question->id) + ->where("question.locked", true) + ); + } + + public function testUserCannotViewQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user)->get("/questions/1") + ->assertStatus(404); + } + + public function testUserCanCreateQuestion(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question"]) + ->assertRedirect('/'); + + $this->assertDatabaseHas("questions", [ + "text" => "Example question", + "quiz_id" => $quiz->id, + ]); + } + + public function testUserCanCreateMultipleQuestions(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->assertRedirect('/'); + + $this->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 2"]) + ->assertRedirect('/'); + + $this->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 3"]) + ->assertRedirect('/'); + + + $this->assertDatabaseHas("questions", ["text" => "Example question 1" ]); + $this->assertDatabaseHas("questions", ["text" => "Example question 2" ]); + $this->assertDatabaseHas("questions", ["text" => "Example question 2" ]); + } + + public function testUserCannotCreateQuestionToQuizThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes/1/questions", ["text" => "Example question"]) + ->assertStatus(404); + + $this->assertDatabaseMissing("questions", [ + "text" => "Example question", + ]); + } + + public function testUserCannotCreateQuestionToQuizThatIsLocked(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->locked()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) + ->assertStatus(403); + + $this->assertDatabaseMissing("questions", [ + "text" => "Example question", + ]); + } + + + public function testUserCannotCreateInvalidQuestion(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes/{$quiz->id}/questions", []) + ->assertRedirect('/')->assertSessionHasErrors(["text"]); + + $this->from("/") + ->post("/quizzes/{$quiz->id}/questions", ["text" => false]) + ->assertRedirect('/')->assertSessionHasErrors(["text"]); + + $this->assertDatabaseCount("questions", 0); + } + + public function testUserCanEditQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(["text" => "Old questions"]); + + $this->actingAs($user) + ->from("/") + ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("questions", ["text" => "New question"]); + } + + public function testUserCannotEditQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->patch("/questions/1", ["text" => "New question"]) + ->assertStatus(404); + } + + public function testUserCannotMakeInvalidEdit(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(["text" => "Old questions"]); + + $this->actingAs($user) + ->from("/") + ->patch("/questions/{$question->id}", []) + ->assertRedirect('/')->assertSessionHasErrors(["text"]); + + $this->from("/") + ->patch("/questions/{$question->id}", ["text" => true]) + ->assertRedirect('/')->assertSessionHasErrors(["text"]); + + $this->assertDatabaseHas("questions", ["text" => "Old questions"]); + } + + public function testUserCannotEditLockedQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(["text" => "Old questions"]); + + $this->actingAs($user) + ->from("/") + ->patch("/questions/{$question->id}", ["text" => "New question"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("questions", ["text" => "New question"]); + } + + public function testUserCanDeleteQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(["text" => "question"]); + + $this->actingAs($user) + ->from("/") + ->delete("/questions/{$question->id}") + ->assertRedirect("/"); + + $this->assertDatabaseMissing("questions", ["text" => "question"]); + } + + public function testUserCannotDeleteLockedQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->locked()->create(["text" => "question"]); + + $this->actingAs($user) + ->from("/") + ->delete("/questions/{$question->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["text" => "question"]); + } + + public function testUserCannotDeleteQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->delete("/questions/1") + ->assertStatus(404); + } +} From 8ce75bb4b979b7f223a76b9f67efaa597a49aaed Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 09:54:48 +0200 Subject: [PATCH 07/24] fix edit locked tests --- tests/Feature/AnswerTest.php | 6 +++--- tests/Feature/QuestionTest.php | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php index a298625e..bab6a583 100644 --- a/tests/Feature/AnswerTest.php +++ b/tests/Feature/AnswerTest.php @@ -211,14 +211,14 @@ public function testUserCannotMakeInvalidEdit(): void public function testUserCannotEditLockedAnswer(): void { $user = User::factory()->create(); - $answer = Answer::factory()->create(["text" => "Old answer"]); + $answer = Answer::factory()->locked()->create(["text" => "Old answer"]); $this->actingAs($user) ->from("/quizzes") ->patch("/answers/{$answer->id}", ["text" => "New answer"]) - ->assertRedirect("/quizzes"); + ->assertStatus(403); - $this->assertDatabaseHas("answers", ["text" => "New answer"]); + $this->assertDatabaseHas("answers", ["text" => "Old answer"]); } public function testUserCanDeleteAnswer(): void diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index 471a3c88..9779b9df 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -37,7 +37,7 @@ public function testUserCanViewQuizQuestions(): void ); } - public function testUserCannotViewQuestionsOfQuiThatNotExisted(): void + public function testUserCannotViewQuestionsOfQuizThatNotExisted(): void { $user = User::factory()->create(); @@ -216,14 +216,14 @@ public function testUserCannotMakeInvalidEdit(): void public function testUserCannotEditLockedQuestion(): void { $user = User::factory()->create(); - $question = Question::factory()->create(["text" => "Old questions"]); + $question = Question::factory()->locked()->create(["text" => "Old questions"]); $this->actingAs($user) ->from("/") ->patch("/questions/{$question->id}", ["text" => "New question"]) - ->assertRedirect("/"); + ->assertStatus(403); - $this->assertDatabaseHas("questions", ["text" => "New question"]); + $this->assertDatabaseHas("questions", ["text" => "Old question"]); } public function testUserCanDeleteQuestion(): void From 58bda4dd158bc9867cd70fc162b05455af4b3cc8 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 10:00:58 +0200 Subject: [PATCH 08/24] fix typo --- tests/Feature/QuizTest.php | 232 +++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 tests/Feature/QuizTest.php diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php new file mode 100644 index 00000000..b9dfff33 --- /dev/null +++ b/tests/Feature/QuizTest.php @@ -0,0 +1,232 @@ +create(); + $quizzes = Quiz::factory()->count(2)->create(); + Question::factory()->count(5)->create(["quiz_id" => $quizzes[0]->id]); + Question::factory()->count(5)->create(["quiz_id" => $quizzes[1]->id]); + + $this->assertDatabaseCount("quizzes", 2); + $this->assertDatabaseCount("questions", 10); + $this->assertDatabaseCount("answers", 10); + + $this->actingAs($user) + ->get("/quizzes") + ->assertInertia( + fn(Assert $page) => $page + ->component("Quiz/Index") + ->has("quizzes", 2) + ->has('quizzes.0.questions', 5) + ->has('quizzes.0.questions.0.answer', 1) + ->has('quizzes.1.questions', 5) + ->has('quizzes.1.questions.0.answer', 1) + ); + } + + public function testUserCannotViewQuizThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user)->get("/quizzes/1") + ->assertStatus(404); + } + + public function testUserCanViewSingleQuiz(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(); + + $this->assertDatabaseCount("quizzes", 1); + + $this->actingAs($user) + ->get("/quizzes/{$quiz->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Quiz/Show") + ->where("quiz.id", $quiz->id) + ); + } + + public function testUserCanViewLockedQuiz(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->locked()->create(); + + $this->assertDatabaseCount("quizzes", 1); + + $this->actingAs($user) + ->get("/quizzes/{$quiz->id}") + ->assertInertia( + fn(Assert $page) => $page + ->component("Quiz/Show") + ->where("quiz.id", $quiz->id) + ->where("quiz.locked", true) + ); + } + + public function testUserCanCreateQuiz(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes", ["name" => "Example quiz"]) + ->assertRedirect('/'); + + $this->assertDatabaseHas("quizzes", [ + "name" => "Example quiz", + ]); + } + + public function testUserCanCreateMultipleQuizzes(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes", ["name" => "Example quiz 1"]) + ->assertRedirect('/'); + + $this->from("/") + ->post("/quizzes", ["name" => "Example quiz 2"]) + ->assertRedirect('/'); + + $this->from("/") + ->post("/quizzes", ["name" => "Example quiz 3"]) + ->assertRedirect('/'); + + + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 1" ]); + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2" ]); + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2" ]); + } + + public function testUserCannotCreateInvalidQuiz(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes", []) + ->assertRedirect('/')->assertSessionHasErrors(["name"]); + + $this->from("/") + ->post("/quizzes", ["name" => false]) + ->assertRedirect('/')->assertSessionHasErrors(["name"]); + + $this->assertDatabaseCount("quizzes", 0); + } + + public function testUserCanEditQuiz(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(["name" => "Old quiz"]); + + $this->actingAs($user) + ->from("/") + ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) + ->assertRedirect("/"); + + $this->assertDatabaseHas("quizzes", ["name" => "New quiz"]); + } + + public function testUserCannotEditQuizThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->patch("/quizzes/1", ["name" => "New quiz"]) + ->assertStatus(404); + } + + public function testUserCannotMakeInvalidEdit(): void + { + $user = User::factory()->create(); + $quiz = Question::factory()->create(["name" => "Old quiz"]); + + $this->actingAs($user) + ->from("/") + ->patch("/quizzes/{$quiz->id}", []) + ->assertRedirect('/')->assertSessionHasErrors(["name"]); + + $this->from("/") + ->patch("/quizzes/{$quiz->id}", ["name" => true]) + ->assertRedirect('/')->assertSessionHasErrors(["text"]); + + $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); + } + + public function testUserCannotEditLockedQuiz(): void + { + $user = User::factory()->create(); + $quiz = Question::factory()->locked()->create(["name" => "Old quiz"]); + + $this->actingAs($user) + ->from("/") + ->patch("/questions/{$quiz->id}", ["name" => "New quiz"]) + ->assertStatus(403); + + $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); + } + + public function testUserCanDeleteQuiz(): void + { + $user = User::factory()->create(); + $quiz = Question::factory()->create(["name" => "quiz"]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 1); + $this->assertDatabaseCount("answers", 1); + + $this->actingAs($user) + ->from("/") + ->delete("/quizzes/{$quiz->id}") + ->assertRedirect("/"); + + $this->assertDatabaseMissing("quizzes", ["name" => "quiz"]); + $this->assertDatabaseCount("quizzes", 0); + $this->assertDatabaseCount("questions", 0); + $this->assertDatabaseCount("answers", 0); + } + + public function testUserCannotDeleteLockedQuiz(): void + { + $user = User::factory()->create(); + $quiz = Question::factory()->locked()->create(["name" => "quiz"]); + + $this->actingAs($user) + ->from("/") + ->delete("/quizzes/{$quiz->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("quizzes", ["name" => "quiz"]); + } + + public function testUserCannotDeleteQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->delete("/quizzes/1") + ->assertStatus(404); + } +} From e7078a1bc9a7b7851308e41860e40291dbd2d304 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 10:05:20 +0200 Subject: [PATCH 09/24] create tests for quiz controller --- app/Http/Controllers/QuizController.php | 11 +++++++++++ app/Policies/QuizPolicy.php | 19 +++++++++++++++++++ tests/Feature/QuestionTest.php | 2 +- tests/Feature/QuizTest.php | 17 ++++++++--------- 4 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 app/Policies/QuizPolicy.php diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index eb538f35..1d800a1d 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 Carbon\Carbon; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; use Inertia\Response; @@ -41,8 +42,13 @@ public function show(int $quiz): Response return Inertia::render("Quiz/Show", ["quiz" => new QuizResource($quiz)]); } + /** + * @throws AuthorizationException + */ public function update(QuizRequest $request, Quiz $quiz): RedirectResponse { + $this->authorize('modify', $quiz); + $quiz->update($request->validated()); return redirect() @@ -60,8 +66,13 @@ public function lock(Quiz $quiz): RedirectResponse ->with("success", "Quiz locked"); } + /** + * @throws AuthorizationException + */ public function destroy(Quiz $quiz): RedirectResponse { + $this->authorize('destroy', $quiz); + $quiz->delete(); return redirect() diff --git a/app/Policies/QuizPolicy.php b/app/Policies/QuizPolicy.php new file mode 100644 index 00000000..8d6757b9 --- /dev/null +++ b/app/Policies/QuizPolicy.php @@ -0,0 +1,19 @@ +isLocked; + } + + public function destroy(User $user, Quiz $quiz): bool + { + return !$quiz->isLocked; + } +} diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index 9779b9df..7b49e366 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -216,7 +216,7 @@ public function testUserCannotMakeInvalidEdit(): void public function testUserCannotEditLockedQuestion(): void { $user = User::factory()->create(); - $question = Question::factory()->locked()->create(["text" => "Old questions"]); + $question = Question::factory()->locked()->create(["text" => "Old question"]); $this->actingAs($user) ->from("/") diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index b9dfff33..89fe4133 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -25,7 +25,6 @@ public function testUserCanViewQuizzes(): void $this->assertDatabaseCount("quizzes", 2); $this->assertDatabaseCount("questions", 10); - $this->assertDatabaseCount("answers", 10); $this->actingAs($user) ->get("/quizzes") @@ -34,9 +33,7 @@ public function testUserCanViewQuizzes(): void ->component("Quiz/Index") ->has("quizzes", 2) ->has('quizzes.0.questions', 5) - ->has('quizzes.0.questions.0.answer', 1) ->has('quizzes.1.questions', 5) - ->has('quizzes.1.questions.0.answer', 1) ); } @@ -160,7 +157,7 @@ public function testUserCannotEditQuizThatNotExisted(): void public function testUserCannotMakeInvalidEdit(): void { $user = User::factory()->create(); - $quiz = Question::factory()->create(["name" => "Old quiz"]); + $quiz = Quiz::factory()->create(["name" => "Old quiz"]); $this->actingAs($user) ->from("/") @@ -169,7 +166,7 @@ public function testUserCannotMakeInvalidEdit(): void $this->from("/") ->patch("/quizzes/{$quiz->id}", ["name" => true]) - ->assertRedirect('/')->assertSessionHasErrors(["text"]); + ->assertRedirect('/')->assertSessionHasErrors(["name"]); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); } @@ -177,11 +174,11 @@ public function testUserCannotMakeInvalidEdit(): void public function testUserCannotEditLockedQuiz(): void { $user = User::factory()->create(); - $quiz = Question::factory()->locked()->create(["name" => "Old quiz"]); + $quiz = Quiz::factory()->locked()->create(["name" => "Old quiz"]); $this->actingAs($user) ->from("/") - ->patch("/questions/{$quiz->id}", ["name" => "New quiz"]) + ->patch("/quizzes/{$quiz->id}", ["name" => "New quiz"]) ->assertStatus(403); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); @@ -190,7 +187,9 @@ public function testUserCannotEditLockedQuiz(): void public function testUserCanDeleteQuiz(): void { $user = User::factory()->create(); - $quiz = Question::factory()->create(["name" => "quiz"]); + $quiz = Quiz::factory()->create(["name" => "quiz"]); + $question = Question::factory()->create(["quiz_id" => $quiz->id]); + Answer::factory()->create(["question_id" => $question->id]); $this->assertDatabaseCount("quizzes", 1); $this->assertDatabaseCount("questions", 1); @@ -210,7 +209,7 @@ public function testUserCanDeleteQuiz(): void public function testUserCannotDeleteLockedQuiz(): void { $user = User::factory()->create(); - $quiz = Question::factory()->locked()->create(["name" => "quiz"]); + $quiz = Quiz::factory()->locked()->create(["name" => "quiz"]); $this->actingAs($user) ->from("/") From 6570c9370ad71cd221a3aed444c0e1749d5b8b08 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 10:05:34 +0200 Subject: [PATCH 10/24] fix code style --- .../Controllers/QuestionAnswerController.php | 6 ++-- app/Http/Controllers/QuizController.php | 4 +-- .../Controllers/QuizQuestionController.php | 4 +-- app/Policies/AnswerPolicy.php | 2 ++ app/Policies/QuestionPolicy.php | 3 +- app/Policies/QuizPolicy.php | 2 ++ tests/Feature/AnswerTest.php | 28 ++++++++--------- tests/Feature/QuestionTest.php | 30 +++++++++--------- tests/Feature/QuizTest.php | 31 +++++++++---------- 9 files changed, 55 insertions(+), 55 deletions(-) diff --git a/app/Http/Controllers/QuestionAnswerController.php b/app/Http/Controllers/QuestionAnswerController.php index cf30fef8..4d1d76f2 100644 --- a/app/Http/Controllers/QuestionAnswerController.php +++ b/app/Http/Controllers/QuestionAnswerController.php @@ -51,7 +51,7 @@ public function show(Answer $answer): Response */ public function markAsCorrect(Answer $answer): RedirectResponse { - $this->authorize('modify', $answer); + $this->authorize("modify", $answer); $answer->question->correctAnswer()->associate($answer)->save(); @@ -65,7 +65,7 @@ public function markAsCorrect(Answer $answer): RedirectResponse */ public function update(AnswerRequest $request, Answer $answer): RedirectResponse { - $this->authorize('modify', $answer); + $this->authorize("modify", $answer); $answer->update($request->validated()); @@ -79,7 +79,7 @@ public function update(AnswerRequest $request, Answer $answer): RedirectResponse */ public function destroy(Answer $answer): RedirectResponse { - $this->authorize('destroy', $answer); + $this->authorize("destroy", $answer); $answer->delete(); diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index 1d800a1d..111501dd 100644 --- a/app/Http/Controllers/QuizController.php +++ b/app/Http/Controllers/QuizController.php @@ -47,7 +47,7 @@ public function show(int $quiz): Response */ public function update(QuizRequest $request, Quiz $quiz): RedirectResponse { - $this->authorize('modify', $quiz); + $this->authorize("modify", $quiz); $quiz->update($request->validated()); @@ -71,7 +71,7 @@ public function lock(Quiz $quiz): RedirectResponse */ public function destroy(Quiz $quiz): RedirectResponse { - $this->authorize('destroy', $quiz); + $this->authorize("destroy", $quiz); $quiz->delete(); diff --git a/app/Http/Controllers/QuizQuestionController.php b/app/Http/Controllers/QuizQuestionController.php index a8e5ff8e..a243f805 100644 --- a/app/Http/Controllers/QuizQuestionController.php +++ b/app/Http/Controllers/QuizQuestionController.php @@ -59,7 +59,7 @@ public function show(int $question): Response */ public function update(QuestionRequest $request, Question $question): RedirectResponse { - $this->authorize('modify', $question); + $this->authorize("modify", $question); $question->update($request->validated()); @@ -73,7 +73,7 @@ public function update(QuestionRequest $request, Question $question): RedirectRe */ public function destroy(Question $question): RedirectResponse { - $this->authorize('destroy', $question); + $this->authorize("destroy", $question); $question->delete(); diff --git a/app/Policies/AnswerPolicy.php b/app/Policies/AnswerPolicy.php index 4b382840..d95d3402 100644 --- a/app/Policies/AnswerPolicy.php +++ b/app/Policies/AnswerPolicy.php @@ -1,5 +1,7 @@ assertInertia( fn(Assert $page) => $page ->component("Answer/Show") - ->where("answer.id", $answer->id) + ->where("answer.id", $answer->id), ); } @@ -69,7 +69,7 @@ public function testUserCanViewLockedAnswer(): void fn(Assert $page) => $page ->component("Answer/Show") ->where("answer.id", $answer->id) - ->where("answer.locked", true) + ->where("answer.locked", true), ); } @@ -89,7 +89,7 @@ public function testUserCanCreateAnswer(): void $this->actingAs($user) ->from("/quizzes") ->post("/questions/{$question->id}/answers", ["text" => "Example answer"]) - ->assertRedirect('/quizzes'); + ->assertRedirect("/quizzes"); $this->assertDatabaseHas("answers", [ "text" => "Example answer", @@ -105,20 +105,19 @@ public function testUserCanCreateMultipleAnswers(): void $this->actingAs($user) ->from("/quizzes") ->post("/questions/{$question->id}/answers", ["text" => "Example answer 1"]) - ->assertRedirect('/quizzes'); + ->assertRedirect("/quizzes"); $this->from("/quizzes") ->post("/questions/{$question->id}/answers", ["text" => "Example answer 2"]) - ->assertRedirect('/quizzes'); + ->assertRedirect("/quizzes"); $this->from("/quizzes") ->post("/questions/{$question->id}/answers", ["text" => "Example answer 3"]) - ->assertRedirect('/quizzes'); - + ->assertRedirect("/quizzes"); - $this->assertDatabaseHas("answers", ["text" => "Example answer 1" ]); - $this->assertDatabaseHas("answers", ["text" => "Example answer 2" ]); - $this->assertDatabaseHas("answers", ["text" => "Example answer 2" ]); + $this->assertDatabaseHas("answers", ["text" => "Example answer 1"]); + $this->assertDatabaseHas("answers", ["text" => "Example answer 2"]); + $this->assertDatabaseHas("answers", ["text" => "Example answer 2"]); } public function testUserCannotCreateAnswerToQuestionThatNotExisted(): void @@ -150,7 +149,6 @@ public function testUserCannotCreateAnswerToQuestionThatIsLocked(): void ]); } - public function testUserCannotCreateInvalidAnswer(): void { $user = User::factory()->create(); @@ -159,11 +157,11 @@ public function testUserCannotCreateInvalidAnswer(): void $this->actingAs($user) ->from("/quizzes") ->post("/questions/{$question->id}/answers", []) - ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") ->post("/questions/{$question->id}/answers", ["text" => false]) - ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("answers", 0); } @@ -199,11 +197,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($user) ->from("/quizzes") ->patch("/answers/{$answer->id}", []) - ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->from("/quizzes") ->patch("/answers/{$answer->id}", ["text" => true]) - ->assertRedirect('/quizzes')->assertSessionHasErrors(["text"]); + ->assertRedirect("/quizzes")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("answers", ["text" => "Old answer"]); } diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index 7b49e366..c652d7f3 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -33,7 +33,7 @@ public function testUserCanViewQuizQuestions(): void fn(Assert $page) => $page ->component("Question/Index") ->has("questions", 1) - ->has('questions.0.answers', 10) + ->has("questions.0.answers", 10), ); } @@ -57,7 +57,7 @@ public function testUserCanViewSingleQuestion(): void ->assertInertia( fn(Assert $page) => $page ->component("Question/Show") - ->where("question.id", $question->id) + ->where("question.id", $question->id), ); } @@ -74,7 +74,7 @@ public function testUserCanViewLockedQuestion(): void fn(Assert $page) => $page ->component("Question/Show") ->where("question.id", $question->id) - ->where("question.locked", true) + ->where("question.locked", true), ); } @@ -94,7 +94,7 @@ public function testUserCanCreateQuestion(): void $this->actingAs($user) ->from("/") ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question"]) - ->assertRedirect('/'); + ->assertRedirect("/"); $this->assertDatabaseHas("questions", [ "text" => "Example question", @@ -110,20 +110,19 @@ public function testUserCanCreateMultipleQuestions(): void $this->actingAs($user) ->from("/") ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 1"]) - ->assertRedirect('/'); + ->assertRedirect("/"); $this->from("/") ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 2"]) - ->assertRedirect('/'); + ->assertRedirect("/"); $this->from("/") ->post("/quizzes/{$quiz->id}/questions", ["text" => "Example question 3"]) - ->assertRedirect('/'); - + ->assertRedirect("/"); - $this->assertDatabaseHas("questions", ["text" => "Example question 1" ]); - $this->assertDatabaseHas("questions", ["text" => "Example question 2" ]); - $this->assertDatabaseHas("questions", ["text" => "Example question 2" ]); + $this->assertDatabaseHas("questions", ["text" => "Example question 1"]); + $this->assertDatabaseHas("questions", ["text" => "Example question 2"]); + $this->assertDatabaseHas("questions", ["text" => "Example question 2"]); } public function testUserCannotCreateQuestionToQuizThatNotExisted(): void @@ -155,7 +154,6 @@ public function testUserCannotCreateQuestionToQuizThatIsLocked(): void ]); } - public function testUserCannotCreateInvalidQuestion(): void { $user = User::factory()->create(); @@ -164,11 +162,11 @@ public function testUserCannotCreateInvalidQuestion(): void $this->actingAs($user) ->from("/") ->post("/quizzes/{$quiz->id}/questions", []) - ->assertRedirect('/')->assertSessionHasErrors(["text"]); + ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") ->post("/quizzes/{$quiz->id}/questions", ["text" => false]) - ->assertRedirect('/')->assertSessionHasErrors(["text"]); + ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseCount("questions", 0); } @@ -204,11 +202,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($user) ->from("/") ->patch("/questions/{$question->id}", []) - ->assertRedirect('/')->assertSessionHasErrors(["text"]); + ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->from("/") ->patch("/questions/{$question->id}", ["text" => true]) - ->assertRedirect('/')->assertSessionHasErrors(["text"]); + ->assertRedirect("/")->assertSessionHasErrors(["text"]); $this->assertDatabaseHas("questions", ["text" => "Old questions"]); } diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index 89fe4133..ef680a89 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -32,8 +32,8 @@ public function testUserCanViewQuizzes(): void fn(Assert $page) => $page ->component("Quiz/Index") ->has("quizzes", 2) - ->has('quizzes.0.questions', 5) - ->has('quizzes.1.questions', 5) + ->has("quizzes.0.questions", 5) + ->has("quizzes.1.questions", 5), ); } @@ -57,7 +57,7 @@ public function testUserCanViewSingleQuiz(): void ->assertInertia( fn(Assert $page) => $page ->component("Quiz/Show") - ->where("quiz.id", $quiz->id) + ->where("quiz.id", $quiz->id), ); } @@ -74,7 +74,7 @@ public function testUserCanViewLockedQuiz(): void fn(Assert $page) => $page ->component("Quiz/Show") ->where("quiz.id", $quiz->id) - ->where("quiz.locked", true) + ->where("quiz.locked", true), ); } @@ -85,7 +85,7 @@ public function testUserCanCreateQuiz(): void $this->actingAs($user) ->from("/") ->post("/quizzes", ["name" => "Example quiz"]) - ->assertRedirect('/'); + ->assertRedirect("/"); $this->assertDatabaseHas("quizzes", [ "name" => "Example quiz", @@ -99,20 +99,19 @@ public function testUserCanCreateMultipleQuizzes(): void $this->actingAs($user) ->from("/") ->post("/quizzes", ["name" => "Example quiz 1"]) - ->assertRedirect('/'); + ->assertRedirect("/"); $this->from("/") ->post("/quizzes", ["name" => "Example quiz 2"]) - ->assertRedirect('/'); + ->assertRedirect("/"); $this->from("/") ->post("/quizzes", ["name" => "Example quiz 3"]) - ->assertRedirect('/'); - + ->assertRedirect("/"); - $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 1" ]); - $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2" ]); - $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2" ]); + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 1"]); + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2"]); + $this->assertDatabaseHas("quizzes", ["name" => "Example quiz 2"]); } public function testUserCannotCreateInvalidQuiz(): void @@ -122,11 +121,11 @@ public function testUserCannotCreateInvalidQuiz(): void $this->actingAs($user) ->from("/") ->post("/quizzes", []) - ->assertRedirect('/')->assertSessionHasErrors(["name"]); + ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") ->post("/quizzes", ["name" => false]) - ->assertRedirect('/')->assertSessionHasErrors(["name"]); + ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->assertDatabaseCount("quizzes", 0); } @@ -162,11 +161,11 @@ public function testUserCannotMakeInvalidEdit(): void $this->actingAs($user) ->from("/") ->patch("/quizzes/{$quiz->id}", []) - ->assertRedirect('/')->assertSessionHasErrors(["name"]); + ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->from("/") ->patch("/quizzes/{$quiz->id}", ["name" => true]) - ->assertRedirect('/')->assertSessionHasErrors(["name"]); + ->assertRedirect("/")->assertSessionHasErrors(["name"]); $this->assertDatabaseHas("quizzes", ["name" => "Old quiz"]); } From 0b21e37ed84ef83034c69a4d5578f4608cf6f248 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 10:07:53 +0200 Subject: [PATCH 11/24] improve question delete test --- tests/Feature/QuestionTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index c652d7f3..e037f10d 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -228,6 +228,11 @@ public function testUserCanDeleteQuestion(): void { $user = User::factory()->create(); $question = Question::factory()->create(["text" => "question"]); + Answer::factory()->create(["question_id" => $question->id]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 1); + $this->assertDatabaseCount("answers", 1); $this->actingAs($user) ->from("/") @@ -235,6 +240,9 @@ public function testUserCanDeleteQuestion(): void ->assertRedirect("/"); $this->assertDatabaseMissing("questions", ["text" => "question"]); + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 0); + $this->assertDatabaseCount("answers", 0); } public function testUserCannotDeleteLockedQuestion(): void From 6283b1727409f6a0344f6e24c43cda033e6ffdee Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 10:42:22 +0200 Subject: [PATCH 12/24] add marking answers as correct/incorrect --- .../Controllers/QuestionAnswerController.php | 19 ++- routes/web.php | 1 + tests/Feature/AnswerTest.php | 110 ++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/QuestionAnswerController.php b/app/Http/Controllers/QuestionAnswerController.php index 4d1d76f2..23a9ed2f 100644 --- a/app/Http/Controllers/QuestionAnswerController.php +++ b/app/Http/Controllers/QuestionAnswerController.php @@ -57,7 +57,24 @@ public function markAsCorrect(Answer $answer): RedirectResponse return redirect() ->back() - ->with("success", "Answer marked as the correct one"); + ->with("success", "Answer marked as correct"); + } + + /** + * @throws AuthorizationException + */ + public function markAsInvalid(Answer $answer): RedirectResponse + { + $this->authorize("modify", $answer); + + if ($answer->isCorrect) { + $answer->question->correct_answer_id = null; + $answer->save(); + } + + return redirect() + ->back() + ->with("success", "Answer marked as incorrect"); } /** diff --git a/routes/web.php b/routes/web.php index 52228c0a..028a4487 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,6 +12,7 @@ Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"]); +Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"]); Route::resource("quizzes", QuizController::class)->except(["create", "edit"]); Route::resource("quizzes.questions", QuizQuestionController::class)->except(["create", "edit"])->shallow(); Route::resource("questions.answers", QuestionAnswerController::class)->except(["create", "edit"])->shallow(); diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php index 25ee4a0d..83f63339 100644 --- a/tests/Feature/AnswerTest.php +++ b/tests/Feature/AnswerTest.php @@ -254,4 +254,114 @@ public function testUserCannotDeleteAnswerThatNotExisted(): void ->delete("/answers/1") ->assertStatus(404); } + + public function testUserCanMarkAnswerAsCorrect(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->create(["text" => "answer"]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/correct") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + } + + public function testUserCanChangeCorrectAnswerOne(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + $answerA = Answer::factory()->create(["text" => "answer A", "question_id" => $question->id]); + $answerB = Answer::factory()->create(["text" => "answer B", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answerA); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answerB->id}/correct") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerB->id]); + } + + public function testUserCanDeleteCorrectAnswer(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + $answer = Answer::factory()->create(["text" => "answer", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->delete("/answers/{$answer->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => null]); + } + + public function testUserCannotChangeCorrectAnswerInLockedQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->locked()->create(); + $answerA = Answer::factory()->create(["text" => "answer A", "question_id" => $question->id]); + $answerB = Answer::factory()->create(["text" => "answer B", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answerA); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answerB->id}/correct") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answerA->id]); + } + + public function testUserCanChangeCorrectAnswerToInvalid(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + $answer = Answer::factory()->create(["text" => "answer", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/invalid") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + } + + public function testUserCannotChangeCorrectAnswerToInvalidInLockedQuestion(): void + { + $user = User::factory()->create(); + $question = Question::factory()->locked()->create(); + $answer = Answer::factory()->create(["text" => "answer", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/invalid") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); + } } From 1952ef15b345c5487b97ed163583dee8601b9ed0 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 11:32:02 +0200 Subject: [PATCH 13/24] add answer cloning --- app/Models/Answer.php | 16 ++++++ routes/web.php | 1 + tests/Feature/AnswerTest.php | 98 +++++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/app/Models/Answer.php b/app/Models/Answer.php index dd6b8353..c439d6b2 100644 --- a/app/Models/Answer.php +++ b/app/Models/Answer.php @@ -5,6 +5,7 @@ namespace App\Models; use Carbon\Carbon; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -44,4 +45,19 @@ public function isCorrect(): Attribute { return Attribute::get(fn(): bool => $this->question->correctAnswer()->is($this)); } + + /** + * @throws AuthorizationException + */ + public function cloneTo(Question $question): self + { + if ($question->isLocked) { + throw new AuthorizationException(); + } + + $cloned = $this->replicate(); + $cloned->question()->associate($question)->save(); + + return $cloned; + } } diff --git a/routes/web.php b/routes/web.php index 028a4487..1f635218 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,6 +13,7 @@ Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"]); Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"]); +Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"]); Route::resource("quizzes", QuizController::class)->except(["create", "edit"]); Route::resource("quizzes.questions", QuizQuestionController::class)->except(["create", "edit"])->shallow(); Route::resource("questions.answers", QuestionAnswerController::class)->except(["create", "edit"])->shallow(); diff --git a/tests/Feature/AnswerTest.php b/tests/Feature/AnswerTest.php index 83f63339..6847a28f 100644 --- a/tests/Feature/AnswerTest.php +++ b/tests/Feature/AnswerTest.php @@ -268,7 +268,7 @@ public function testUserCanMarkAnswerAsCorrect(): void $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); } - public function testUserCanChangeCorrectAnswerOne(): void + public function testUserCanChangeCorrectAnswer(): void { $user = User::factory()->create(); $question = Question::factory()->create(); @@ -364,4 +364,100 @@ public function testUserCannotChangeCorrectAnswerToInvalidInLockedQuestion(): vo $this->assertDatabaseHas("questions", ["correct_answer_id" => $answer->id]); } + + public function testUserCanCopyAnswer(): void + { + $user = User::factory()->create(); + $questionA = Question::factory()->create(); + $questionB = Question::factory()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); + } + + public function testUserCanCopyLockedAnswer(): void + { + $user = User::factory()->create(); + $questionA = Question::factory()->locked()->create(); + $questionB = Question::factory()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); + } + + public function testUserCannotCopyAnswerToLockedQuestion(): void + { + $user = User::factory()->create(); + $questionA = Question::factory()->create(); + $questionB = Question::factory()->locked()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + } + + public function testUserCanCopyCorrectAnswer(): void + { + $user = User::factory()->create(); + $questionA = Question::factory()->create(); + $questionB = Question::factory()->create(); + $answer = Answer::factory()->create(["question_id" => $questionA->id]); + + $questionA->correctAnswer()->associate($answer); + $questionA->save(); + + $this->assertDatabaseHas("answers", ["question_id" => $questionA->id]); + $this->assertDatabaseHas("questions", ["id" => $questionA->id, "correct_answer_id" => $answer->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/{$questionB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("answers", ["question_id" => $questionB->id]); + $this->assertDatabaseHas("questions", ["id" => $questionA->id, "correct_answer_id" => $answer->id]); + $this->assertDatabaseHas("questions", ["id" => $questionB->id, "correct_answer_id" => null]); + } + + public function testUserCannotCopyAnswerThatNotExisted(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/2/clone/{$question->id}") + ->assertStatus(404); + } + + public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + $answer = Answer::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/answers/{$answer->id}/clone/2") + ->assertStatus(404); + } } From 21634382c40d0671b99d18c5c29139f9e38277f5 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 11:43:16 +0200 Subject: [PATCH 14/24] fix typo --- app/Models/Answer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Models/Answer.php b/app/Models/Answer.php index c439d6b2..6195593d 100644 --- a/app/Models/Answer.php +++ b/app/Models/Answer.php @@ -55,9 +55,9 @@ public function cloneTo(Question $question): self throw new AuthorizationException(); } - $cloned = $this->replicate(); - $cloned->question()->associate($question)->save(); + $clone = $this->replicate(); + $clone->question()->associate($question)->save(); - return $cloned; + return $clone; } } From d189c2ddb90d94f4c58ad0cce3bfe9ca48ea36ce Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 12:20:19 +0200 Subject: [PATCH 15/24] add question cloning --- .../Controllers/QuestionAnswerController.php | 12 +++ .../Controllers/QuizQuestionController.php | 12 +++ app/Models/Question.php | 26 +++++ routes/web.php | 1 + tests/Feature/QuestionTest.php | 97 +++++++++++++++++++ 5 files changed, 148 insertions(+) diff --git a/app/Http/Controllers/QuestionAnswerController.php b/app/Http/Controllers/QuestionAnswerController.php index 23a9ed2f..e1dfa4a3 100644 --- a/app/Http/Controllers/QuestionAnswerController.php +++ b/app/Http/Controllers/QuestionAnswerController.php @@ -104,4 +104,16 @@ public function destroy(Answer $answer): RedirectResponse ->back() ->with("success", "Answer deleted"); } + + /** + * @throws AuthorizationException + */ + public function clone(Answer $answer, Question $question): RedirectResponse + { + $answer->cloneTo($question); + + return redirect() + ->back() + ->with("success", "Answer cloned"); + } } diff --git a/app/Http/Controllers/QuizQuestionController.php b/app/Http/Controllers/QuizQuestionController.php index a243f805..f2389a19 100644 --- a/app/Http/Controllers/QuizQuestionController.php +++ b/app/Http/Controllers/QuizQuestionController.php @@ -81,4 +81,16 @@ public function destroy(Question $question): RedirectResponse ->back() ->with("success", "Question deleted"); } + + /** + * @throws AuthorizationException + */ + public function clone(Question $question, Quiz $quiz): RedirectResponse + { + $question->cloneTo($quiz); + + return redirect() + ->back() + ->with("success", "Question cloned"); + } } diff --git a/app/Models/Question.php b/app/Models/Question.php index 98baf5ca..d05bf809 100644 --- a/app/Models/Question.php +++ b/app/Models/Question.php @@ -5,6 +5,7 @@ namespace App\Models; use Carbon\Carbon; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -53,4 +54,29 @@ public function isLocked(): Attribute { return Attribute::get(fn(): bool => $this->quiz->isLocked); } + + /** + * @throws AuthorizationException + */ + public function cloneTo(Quiz $quiz): self + { + if ($quiz->isLocked) { + throw new AuthorizationException(); + } + + $questionCopy = $this->replicate(); + $questionCopy->quiz()->associate($quiz)->save(); + + foreach ($this->answers as $answer) { + $answerCopy = $answer->cloneTo($questionCopy); + + if ($answer->isCorrect) { + $questionCopy->correctAnswer()->associate($answerCopy); + } + } + + $questionCopy->save(); + + return $questionCopy; + } } diff --git a/routes/web.php b/routes/web.php index 1f635218..c8a188e0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,6 +11,7 @@ Route::get("/", fn(): Response => inertia("Welcome")); Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); +Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"]); Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"]); Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"]); Route::post("/answers/{answer}/clone/{question}", [QuestionAnswerController::class, "clone"]); diff --git a/tests/Feature/QuestionTest.php b/tests/Feature/QuestionTest.php index e037f10d..1d6fb8b8 100644 --- a/tests/Feature/QuestionTest.php +++ b/tests/Feature/QuestionTest.php @@ -267,4 +267,101 @@ public function testUserCannotDeleteQuestionThatNotExisted(): void ->delete("/questions/1") ->assertStatus(404); } + + public function testUserCanCopyQuestion(): void + { + $user = User::factory()->create(); + $quizA = Quiz::factory()->create(); + $quizB = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + Answer::factory()->count(10)->create(["question_id" => $question->id]); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + $this->assertDatabaseCount("answers", 10); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); + $this->assertDatabaseCount("answers", 20); + } + + public function testUserCanCopyLockedQuestion(): void + { + $user = User::factory()->create(); + $quizA = Quiz::factory()->locked()->create(); + $quizB = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertRedirect("/quizzes"); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizB->id]); + } + + public function testUserCannotCopyAnswerToLockedQuestion(): void + { + $user = User::factory()->create(); + $quizA = Quiz::factory()->create(); + $quizB = Quiz::factory()->locked()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertStatus(403); + + $this->assertDatabaseHas("questions", ["quiz_id" => $quizA->id]); + } + + public function testUserCanCopyQuestionWithCorrectAnswer(): void + { + $user = User::factory()->create(); + $quizA = Quiz::factory()->create(); + $quizB = Quiz::factory()->create(); + $question = Question::factory()->create(["quiz_id" => $quizA->id]); + $answer = Answer::factory()->create(["text" => "correct", "question_id" => $question->id]); + + $question->correctAnswer()->associate($answer); + $question->save(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/{$quizB->id}") + ->assertRedirect("/quizzes"); + + $this->assertNotNull($quizA->questions[0]->correctAnswer); + $this->assertNotNull($quizB->questions[0]->correctAnswer); + $this->assertNotEquals($quizA->questions[0]->correctAnswer->id, $quizB->questions[0]->correctAnswer->id); + } + + public function testUserCannotCopyQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + $quiz = Question::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/2/clone/{$quiz->id}") + ->assertStatus(404); + } + + public function testUserCannotCopyAnswerToQuestionThatNotExisted(): void + { + $user = User::factory()->create(); + $question = Question::factory()->create(); + + $this->actingAs($user) + ->from("/quizzes") + ->post("/questions/{$question->id}/clone/2") + ->assertStatus(404); + } } From 34551e085aa7c231674785f7eaec49887237bafa Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 12:36:26 +0200 Subject: [PATCH 16/24] add quiz cloning --- app/Http/Controllers/QuizController.php | 12 +++++++ app/Models/Quiz.php | 16 +++++++++ routes/web.php | 1 + tests/Feature/QuizTest.php | 48 +++++++++++++++++++++++++ 4 files changed, 77 insertions(+) diff --git a/app/Http/Controllers/QuizController.php b/app/Http/Controllers/QuizController.php index 111501dd..7fb67303 100644 --- a/app/Http/Controllers/QuizController.php +++ b/app/Http/Controllers/QuizController.php @@ -79,4 +79,16 @@ public function destroy(Quiz $quiz): RedirectResponse ->back() ->with("success", "Quiz deleted"); } + + /** + * @throws AuthorizationException + */ + public function clone(Quiz $quiz): RedirectResponse + { + $quiz->clone(); + + return redirect() + ->back() + ->with("success", "Quiz cloned"); + } } diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index c38b5bf3..7576f3bd 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -5,6 +5,7 @@ namespace App\Models; use Carbon\Carbon; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -47,6 +48,21 @@ public function isLocked(): Attribute return Attribute::get(fn(): bool => $this->locked_at !== null); } + /** + * @throws AuthorizationException + */ + public function clone(): self + { + $quizCopy = $this->replicate(); + $quizCopy->save(); + + foreach ($this->questions as $question) { + $question->cloneTo($quizCopy); + } + + return $quizCopy; + } + protected function casts(): array { return [ diff --git a/routes/web.php b/routes/web.php index c8a188e0..54d9c4c6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,6 +11,7 @@ Route::get("/", fn(): Response => inertia("Welcome")); Route::post("/quizzes/{quiz}/lock", [QuizController::class, "lock"]); +Route::post("/quizzes/{quiz}/clone/", [QuizController::class, "clone"]); Route::post("/questions/{question}/clone/{quiz}", [QuizQuestionController::class, "clone"]); Route::post("/answers/{answer}/correct", [QuestionAnswerController::class, "markAsCorrect"]); Route::post("/answers/{answer}/invalid", [QuestionAnswerController::class, "markAsInvalid"]); diff --git a/tests/Feature/QuizTest.php b/tests/Feature/QuizTest.php index ef680a89..bacd41d7 100644 --- a/tests/Feature/QuizTest.php +++ b/tests/Feature/QuizTest.php @@ -227,4 +227,52 @@ public function testUserCannotDeleteQuestionThatNotExisted(): void ->delete("/quizzes/1") ->assertStatus(404); } + + public function testUserCanCopyQuiz(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->create(); + $questions = Question::factory()->count(2)->create(["quiz_id" => $quiz->id]); + + Answer::factory()->count(10)->create(["question_id" => $questions[0]->id]); + Answer::factory()->count(10)->create(["question_id" => $questions[1]->id]); + + $this->assertDatabaseCount("quizzes", 1); + $this->assertDatabaseCount("questions", 2); + $this->assertDatabaseCount("answers", 20); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes/{$quiz->id}/clone") + ->assertRedirect("/"); + + $this->assertDatabaseCount("quizzes", 2); + $this->assertDatabaseCount("questions", 4); + $this->assertDatabaseCount("answers", 40); + } + + public function testUserCanCopyLockedQuiz(): void + { + $user = User::factory()->create(); + $quiz = Quiz::factory()->locked()->create(); + + $this->assertDatabaseCount("quizzes", 1); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes/{$quiz->id}/clone") + ->assertRedirect("/"); + + $this->assertDatabaseCount("quizzes", 2); + } + + public function testUserCannotCopyQuizThatNotExisted(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->from("/") + ->post("/quizzes/2/clone") + ->assertStatus(404); + } } From d81855b6b7a48609158cc9515f75008522e4f1ba Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 12:58:22 +0200 Subject: [PATCH 17/24] fix database config --- .env.ci | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.env.ci b/.env.ci index c1d1a570..ff188562 100644 --- a/.env.ci +++ b/.env.ci @@ -14,3 +14,10 @@ QUEUE_CONNECTION=sync SESSION_DRIVER=array SESSION_LIFETIME=120 MAIL_MAILER=array + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=interns2024b +DB_USERNAME=interns2024b +DB_PASSWORD=password From 84d6919f5f66cb25b72d2cf920826484b76e5c77 Mon Sep 17 00:00:00 2001 From: AmonDeShir Date: Mon, 12 Aug 2024 13:01:40 +0200 Subject: [PATCH 18/24] fix linter errors --- resources/js/Pages/Answer/Index.vue | 6 +++--- resources/js/Pages/Answer/Show.vue | 19 +++++++++---------- resources/js/Pages/Question/Index.vue | 6 +++--- resources/js/Pages/Question/Show.vue | 20 ++++++++++---------- resources/js/Pages/Quiz/Index.vue | 6 +++--- resources/js/Pages/Quiz/Show.vue | 19 +++++++++---------- resources/js/Types/Question.d.ts | 2 +- resources/js/Types/Quiz.d.ts | 2 +- 8 files changed, 39 insertions(+), 41 deletions(-) diff --git a/resources/js/Pages/Answer/Index.vue b/resources/js/Pages/Answer/Index.vue index c42753a0..105a879f 100644 --- a/resources/js/Pages/Answer/Index.vue +++ b/resources/js/Pages/Answer/Index.vue @@ -1,6 +1,6 @@ diff --git a/resources/js/Pages/Question/Index.vue b/resources/js/Pages/Question/Index.vue index d297b957..44657e5d 100644 --- a/resources/js/Pages/Question/Index.vue +++ b/resources/js/Pages/Question/Index.vue @@ -1,6 +1,6 @@