├── .gitignore ├── .styleci.yml ├── src ├── LaravelQuiz.php ├── Models │ ├── Topicable.php │ ├── QuizAuthor.php │ ├── QuizAttemptAnswer.php │ ├── QuizQuestion.php │ ├── QuestionType.php │ ├── QuestionOption.php │ ├── Topic.php │ ├── Question.php │ ├── Quiz.php │ └── QuizAttempt.php ├── Traits │ ├── CanAuthorQuiz.php │ └── QuizParticipant.php ├── LaravelQuizFacade.php └── LaravelQuizServiceProvider.php ├── CHANGELOG.md ├── phpstan.neon ├── tests ├── Models │ ├── Editor.php │ └── Author.php ├── migrations │ └── create_authors_table.php ├── TestCase.php ├── Unit │ ├── TopicTest.php │ ├── QuizAuthorTest.php │ ├── QuestionTest.php │ ├── QuizTest.php │ └── QuizAttemptTest.php └── Feature │ └── QuizTest.php ├── database ├── factories │ ├── QuestionTypeFactory.php │ ├── QuizQuestionFactory.php │ ├── TopicFactory.php │ ├── QuestionOptionFactory.php │ ├── QuestionFactory.php │ └── QuizFactory.php ├── seeders │ └── QuestionTypeSeeder.php └── migrations │ ├── 2022_05_27_163432_add_negative_marks_columns_to_quiz_questions_table.php │ ├── 2022_06_12_061720_create_quiz_authors_table.php │ ├── 2022_08_01_153324_remove_unique_constraint_for_slugs.php │ ├── 2022_08_15_033405_remove_unq_constraint_from_quiz_questions_table.php │ ├── 2022_06_09_203138_rename_quizzes_tables.php │ └── 2021_05_22_053359_create_quizzes_table.php ├── phpunit.xml ├── LICENSE.md ├── phpunit.xml.bak ├── composer.json ├── .github └── workflows │ └── main.yml ├── CONTRIBUTING.md ├── .phpunit.cache └── test-results ├── docs └── LaravelQuiz.drawio ├── config └── config.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .phpunit.result.cache 3 | logfile.txt 4 | .vscode/ 5 | .DS_Store 6 | .idea/ -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /src/LaravelQuiz.php: -------------------------------------------------------------------------------- 1 | morphMany(config('laravel-quiz.models.quiz_author'), 'author'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Traits/QuizParticipant.php: -------------------------------------------------------------------------------- 1 | morphMany(config('laravel-quiz.models.quiz_attempt'), 'participant'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | 4 | parameters: 5 | 6 | paths: 7 | - src/ 8 | 9 | # The level 8 is the highest level 10 | level: 5 11 | 12 | ignoreErrors: 13 | 14 | 15 | excludePaths: 16 | - 'src/vendor' 17 | 18 | checkMissingIterableValueType: false 19 | -------------------------------------------------------------------------------- /tests/Models/Editor.php: -------------------------------------------------------------------------------- 1 | $this->faker->words(1, true) 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Models/Author.php: -------------------------------------------------------------------------------- 1 | null, 17 | 'question_id' => null, 18 | 'marks' => 0, 19 | 'is_optional' => false, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/factories/TopicFactory.php: -------------------------------------------------------------------------------- 1 | faker->words(4, true); 16 | return [ 17 | 'name' => $name, 18 | 'slug' => Str::slug($name, '-'), 19 | 'parent_id' => null, 20 | 'is_active' => $this->faker->numberBetween(0, 1) 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /database/factories/QuestionOptionFactory.php: -------------------------------------------------------------------------------- 1 | null, 16 | 'name' => $this->faker->word, 17 | 'media_url' => $this->faker->url, 18 | 'is_correct' => $this->faker->numberBetween(0, 1), 19 | 'media_type' => 'image', 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/migrations/create_authors_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | $table->string('name'); 18 | $table->timestamps(); 19 | }); 20 | Schema::create('editors', function (Blueprint $table) { 21 | $table->increments('id'); 22 | $table->string('name'); 23 | $table->timestamps(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/seeders/QuestionTypeSeeder.php: -------------------------------------------------------------------------------- 1 | 'multiple_choice_single_answer', 20 | ], 21 | [ 22 | 'name' => 'multiple_choice_multiple_answer', 23 | ], 24 | [ 25 | 'name' => 'fill_the_blank', 26 | ] 27 | ]; 28 | foreach ($questionTypes as $questionType) { 29 | QuestionType::create($questionType); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/factories/QuestionFactory.php: -------------------------------------------------------------------------------- 1 | 'fill_the_blank', 19 | ] 20 | ); 21 | return [ 22 | 'name' => $this->faker->words(4, true), 23 | 'question_type_id' => $question_type->id, 24 | 'media_url' => $this->faker->url, 25 | 'is_active' => $this->faker->numberBetween(0, 1), 26 | 'media_type' => 'image', 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Models/QuizAuthor.php: -------------------------------------------------------------------------------- 1 | belongsTo(Quiz::class); 31 | } 32 | 33 | public function author() 34 | { 35 | return $this->morphTo(__FUNCTION__, 'author_type', 'author_id'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/Unit 6 | 7 | 8 | ./tests/Feature 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | src/ 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | up(); 30 | (new \CreateAuthorsTable)->up(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_05_27_163432_add_negative_marks_columns_to_quiz_questions_table.php: -------------------------------------------------------------------------------- 1 | tableNames = config('laravel-quiz.table_names'); 13 | } 14 | /** 15 | * Run the migrations. 16 | * 17 | * @return void 18 | */ 19 | public function up() 20 | { 21 | Schema::table($this->tableNames['quizzes'], function (Blueprint $table) { 22 | $table->json('negative_marking_settings')->nullable()->after('pass_marks'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::table($this->tableNames['quizzes'], function (Blueprint $table) { 34 | $table->dropColumn('negative_marking_settings'); 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Harish Durga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /phpunit.xml.bak: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | src/ 19 | 20 | 21 | 22 | 23 | ./tests/Unit 24 | 25 | 26 | ./tests/Feature 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /database/factories/QuizFactory.php: -------------------------------------------------------------------------------- 1 | faker->name; 16 | 17 | return [ 18 | 'name' => $name, 19 | 'slug' => Str::slug($name), 20 | 'description' => $this->faker->paragraph, 21 | 'total_marks' => 0, 22 | 'pass_marks' => 0, 23 | 'max_attempts' => 0, 24 | 'is_published' => 1, 25 | 'media_url' => $this->faker->imageUrl(300, 300), 26 | 'media_type' => 'image', 27 | 'duration' => 0, 28 | 'valid_from' => date('Y-m-d H:i:s'), 29 | 'valid_upto' => null, 30 | 'negative_marking_settings' => [ 31 | 'enable_negative_marks' => true, 32 | 'negative_marking_type' => 'fixed', 33 | 'negative_mark_value' => 0, 34 | ] 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Models/QuizAttemptAnswer.php: -------------------------------------------------------------------------------- 1 | belongsTo(config('laravel-quiz.models.quiz_attempt')); 39 | } 40 | 41 | public function quiz_question() 42 | { 43 | return $this->belongsTo(config('laravel-quiz.models.quiz_question')); 44 | } 45 | 46 | public function question_option() 47 | { 48 | return $this->belongsTo(config('laravel-quiz.models.question_option')); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /database/migrations/2022_06_12_061720_create_quiz_authors_table.php: -------------------------------------------------------------------------------- 1 | tableNames = config('laravel-quiz.table_names'); 13 | } 14 | /** 15 | * Run the migrations. 16 | * 17 | * @return void 18 | */ 19 | public function up() 20 | { 21 | Schema::create($this->tableNames['quiz_authors'], function (Blueprint $table) { 22 | $table->id(); 23 | $table->foreignId('quiz_id')->nullable()->constrained($this->tableNames['quizzes'])->cascadeOnDelete(); 24 | $table->unsignedInteger('author_id'); 25 | $table->string('author_type'); 26 | $table->string('author_role')->nullable(); 27 | $table->boolean('is_active')->default(true); 28 | $table->timestamps(); 29 | $table->softDeletes(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists($this->tableNames['quiz_authors']); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/Models/QuizQuestion.php: -------------------------------------------------------------------------------- 1 | belongsTo(config('laravel-quiz.models.quiz')); 40 | } 41 | 42 | public function question() 43 | { 44 | return $this->belongsTo(config('laravel-quiz.models.question')); 45 | } 46 | 47 | public function answers() 48 | { 49 | return $this->hasMany(config('laravel-quiz.models.quiz_attempt_answer')); 50 | } 51 | 52 | protected static function newFactory() 53 | { 54 | return new QuizQuestionFactory(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "harishdurga/laravel-quiz", 3 | "description": "Provides Quiz Functionality", 4 | "keywords": [ 5 | "harishdurga", 6 | "laravel-quiz" 7 | ], 8 | "homepage": "https://github.com/harishdurga/laravel-quiz", 9 | "license": "MIT", 10 | "type": "library", 11 | "authors": [ 12 | { 13 | "name": "Harish Durga", 14 | "email": "durgaharish5@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2.0", 20 | "illuminate/support": "^11.0" 21 | }, 22 | "require-dev": { 23 | "nunomaduro/larastan": "^2.0", 24 | "orchestra/testbench": "^9.0", 25 | "phpunit/phpunit": "^11.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Harishdurga\\LaravelQuiz\\": "src", 30 | "Harishdurga\\LaravelQuiz\\Database\\Factories\\": "database/factories", 31 | "Harishdurga\\LaravelQuiz\\Database\\Seeders\\": "database/seeders" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Harishdurga\\LaravelQuiz\\Tests\\": "tests" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "vendor/bin/phpunit", 41 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Harishdurga\\LaravelQuiz\\LaravelQuizServiceProvider" 50 | ], 51 | "aliases": { 52 | "LaravelQuiz": "Harishdurga\\LaravelQuiz\\LaravelQuizFacade" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /database/migrations/2022_08_01_153324_remove_unique_constraint_for_slugs.php: -------------------------------------------------------------------------------- 1 | tableNames = config('laravel-quiz.table_names'); 13 | } 14 | /** 15 | * Run the migrations. 16 | * 17 | * @return void 18 | */ 19 | public function up() 20 | { 21 | Schema::table($this->tableNames['topics'], function (Blueprint $table) { 22 | // $sm = Schema::getConnection()->getDoctrineSchemaManager(); 23 | $sm = Schema::getConnection()->getSchemaBuilder(); 24 | $indexesFound = $sm->getIndexes($this->tableNames['topics']); 25 | if (array_key_exists($this->tableNames['topics'] . "_slug_unique", $indexesFound)) 26 | $table->dropUnique($this->tableNames['topics'] . "_slug_unique"); 27 | }); 28 | 29 | Schema::table($this->tableNames['quizzes'], function (Blueprint $table) { 30 | // $sm = Schema::getConnection()->getDoctrineSchemaManager(); 31 | $sm = Schema::getConnection()->getSchemaBuilder(); 32 | $indexesFound = $sm->getIndexes($this->tableNames['quizzes']); 33 | if (array_key_exists($this->tableNames['quizzes'] . "_slug_unique", $indexesFound)) 34 | $table->dropUnique($this->tableNames['quizzes'] . "_slug_unique"); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | // 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/Models/QuestionType.php: -------------------------------------------------------------------------------- 1 | $attributes['name'], 50 | set: fn ($value) => ['name' => $value], 51 | ); 52 | } 53 | 54 | public function questions() 55 | { 56 | return $this->hasMany(config('laravel-quiz.models.question')); 57 | } 58 | 59 | protected static function newFactory() 60 | { 61 | return QuestionTypeFactory::new(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Models/QuestionOption.php: -------------------------------------------------------------------------------- 1 | $attributes['name'], 48 | set: fn ($value) => ['name' => $value], 49 | ); 50 | } 51 | 52 | public function question() 53 | { 54 | return $this->belongsTo(config('laravel-quiz.models.question')); 55 | } 56 | 57 | public function answers() 58 | { 59 | return $this->hasMany(config('laravel-quiz.models.quiz_attempt_answer')); 60 | } 61 | 62 | protected static function newFactory() 63 | { 64 | return QuestionOptionFactory::new(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Unit/TopicTest.php: -------------------------------------------------------------------------------- 1 | create([ 19 | 'name' => 'Test Topic', 20 | ]); 21 | $this->assertEquals('Test Topic', $topic->name); 22 | } 23 | 24 | #[Test] 25 | function topic_parent_child_relation() 26 | { 27 | $parentTopic = Topic::factory()->create([ 28 | 'name' => 'Parent Topic', 29 | ]); 30 | $parentTopic->children()->saveMany([ 31 | Topic::factory()->make(['name' => 'Child Topic 1']), 32 | Topic::factory()->make(['name' => 'Child Topic 2']), 33 | ]); 34 | $this->assertEquals(2, $parentTopic->children()->count()); 35 | } 36 | 37 | #[Test] 38 | function topic_question_relation() 39 | { 40 | $topic = Topic::factory()->create([ 41 | 'name' => 'Test Topic', 42 | ]); 43 | $question1 = Question::factory()->create([ 44 | 'name' => 'Test Question', 45 | ]); 46 | $question2 = Question::factory()->create([ 47 | 'name' => 'Test Question', 48 | ]); 49 | $question3 = Question::factory()->create([ 50 | 'name' => 'Test Question', 51 | ]); 52 | $topic->questions()->attach($question1); 53 | $topic->questions()->attach([$question2->id, $question3->id]); 54 | $this->assertEquals(3, $topic->questions()->count()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /database/migrations/2022_08_15_033405_remove_unq_constraint_from_quiz_questions_table.php: -------------------------------------------------------------------------------- 1 | tableNames = config('laravel-quiz.table_names'); 13 | } 14 | /** 15 | * Run the migrations. 16 | * 17 | * @return void 18 | */ 19 | public function up() 20 | { 21 | Schema::table($this->tableNames['quiz_questions'], function (Blueprint $table) { 22 | // $sm = Schema::getConnection()->getDoctrineSchemaManager(); 23 | $sm = Schema::getConnection()->getSchemaBuilder(); 24 | $indexesFound = $sm->getIndexes($this->tableNames['quiz_questions']); 25 | if (array_key_exists($this->tableNames['quiz_questions'] . "_quiz_id_question_id_unique", $indexesFound)) { 26 | $table->dropForeign($this->tableNames['quiz_questions'] . '_question_id_foreign'); 27 | $table->dropForeign($this->tableNames['quiz_questions'] . '_quiz_id_foreign'); 28 | $table->dropUnique($this->tableNames['quiz_questions'] . "_quiz_id_question_id_unique"); 29 | $table->foreignId('quiz_id')->change()->constrained($this->tableNames['quizzes'])->cascadeOnDelete(); 30 | $table->foreignId('question_id')->change()->constrained($this->tableNames['questions'])->cascadeOnDelete(); 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | * 38 | * @return void 39 | */ 40 | public function down() 41 | { 42 | Schema::table('quiz_questions', function (Blueprint $table) { 43 | // 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest] 16 | php: [8.2] 17 | laravel: [11.*] 18 | stability: [prefer-stable] 19 | include: 20 | - laravel: 11.* 21 | testbench: ^9.0 22 | env: 23 | DB_CONNECTION: testing 24 | DB_DATABASE: laravel 25 | DB_USERNAME: root 26 | DB_PASSWORD: TestDB@1234 27 | services: 28 | mysql: 29 | image: mysql:latest 30 | env: 31 | MYSQL_ALLOW_EMPTY_PASSWORD: false 32 | MYSQL_ROOT_PASSWORD: TestDB@1234 33 | MYSQL_DATABASE: laravel 34 | ports: 35 | - 3306/tcp 36 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 37 | 38 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 39 | 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v2 43 | 44 | - name: Setup PHP 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: ${{ matrix.php }} 48 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, mysql, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 49 | coverage: none 50 | 51 | - name: Setup problem matchers 52 | run: | 53 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 54 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 55 | - name: Install dependencies 56 | run: | 57 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 58 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 59 | - name: Execute tests 60 | run: vendor/bin/phpunit 61 | env: 62 | DB_PORT: ${{ job.services.mysql.ports['3306'] }} 63 | -------------------------------------------------------------------------------- /src/Models/Topic.php: -------------------------------------------------------------------------------- 1 | $attributes['name'], 48 | set: fn ($value) => ['name' => $value], 49 | ); 50 | } 51 | 52 | public function children() 53 | { 54 | return $this->hasMany(config('laravel-quiz.models.topic'), 'parent_id'); 55 | } 56 | 57 | public function parent() 58 | { 59 | return $this->belongsTo(config('laravel-quiz.models.topic'), 'parent_id', 'id'); 60 | } 61 | 62 | public function questions() 63 | { 64 | return $this->morphedByMany(config('laravel-quiz.models.question'), 'topicable'); 65 | } 66 | 67 | public function quizzes() 68 | { 69 | return $this->morphedByMany(config('laravel-quiz.models.quiz'), 'topicable'); 70 | } 71 | 72 | protected static function newFactory() 73 | { 74 | return TopicFactory::new(); 75 | } 76 | 77 | public function getRouteKeyName() 78 | { 79 | return 'slug'; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /database/migrations/2022_06_09_203138_rename_quizzes_tables.php: -------------------------------------------------------------------------------- 1 | tableNames = config('laravel-quiz.table_names'); 13 | } 14 | /** 15 | * Run the migrations. 16 | * 17 | * @return void 18 | */ 19 | public function up() 20 | { 21 | Schema::table($this->tableNames['topics'], function (Blueprint $table) { 22 | $table->renameColumn('topic', 'name'); 23 | }); 24 | Schema::table($this->tableNames['question_types'], function (Blueprint $table) { 25 | $table->renameColumn('question_type', 'name'); 26 | }); 27 | Schema::table($this->tableNames['questions'], function (Blueprint $table) { 28 | $table->renameColumn('question', 'name'); 29 | }); 30 | Schema::table($this->tableNames['question_options'], function (Blueprint $table) { 31 | $table->renameColumn('option', 'name'); 32 | }); 33 | Schema::table($this->tableNames['quizzes'], function (Blueprint $table) { 34 | $table->renameColumn('title', 'name'); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::table($this->tableNames['topics'], function (Blueprint $table) { 46 | $table->renameColumn('name', 'topic'); 47 | }); 48 | Schema::table($this->tableNames['question_types'], function (Blueprint $table) { 49 | $table->renameColumn('name', 'question_type'); 50 | }); 51 | Schema::table($this->tableNames['questions'], function (Blueprint $table) { 52 | $table->renameColumn('name', 'question'); 53 | }); 54 | Schema::table($this->tableNames['question_options'], function (Blueprint $table) { 55 | $table->renameColumn('name', 'option'); 56 | }); 57 | Schema::table($this->tableNames['quizzes'], function (Blueprint $table) { 58 | $table->renameColumn('name', 'title'); 59 | }); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /tests/Unit/QuizAuthorTest.php: -------------------------------------------------------------------------------- 1 | "John Doe"] 22 | ); 23 | $editor_one = Editor::create( 24 | ['name' => "Jane Doe"] 25 | ); 26 | $editor_two = Editor::create( 27 | ['name' => "Mike Poe"] 28 | ); 29 | $quiz = Quiz::factory()->make()->create([ 30 | 'name' => 'Sample Quiz', 31 | 'slug' => 'sample-quiz' 32 | ]); 33 | QuizAuthor::create([ 34 | 'quiz_id' => $quiz->id, 35 | 'author_id' => $admin->id, 36 | 'author_type' => get_class($admin), 37 | 'author_role' => 'admin', 38 | ]); 39 | QuizAuthor::create([ 40 | 'quiz_id' => $quiz->id, 41 | 'author_id' => $editor_one->id, 42 | 'author_type' => get_class($editor_one), 43 | 'author_role' => 'editor', 44 | ]); 45 | QuizAuthor::create([ 46 | 'quiz_id' => $quiz->id, 47 | 'author_id' => $editor_two->id, 48 | 'author_type' => get_class($editor_two), 49 | 'author_role' => 'editor', 50 | ]); 51 | $this->assertEquals(3, $quiz->quizAuthors->count()); 52 | $quiAdmin = $quiz->quizAuthors()->where('author_role', 'admin')->first(); 53 | $this->assertEquals($admin->id, $quiAdmin->author->id); 54 | $this->assertEquals(get_class($admin), get_class($quiAdmin->author)); 55 | $quiEditors = $quiz->quizAuthors()->where('author_role', 'editor')->get(); 56 | $this->assertEquals(2, $quiEditors->count()); 57 | $this->assertEquals($editor_one->id, $quiEditors->first()->author->id); 58 | $this->assertEquals(get_class($editor_one), get_class($quiEditors->first()->author)); 59 | $this->assertEquals(1, $editor_one->quizzes->count()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Unit/QuestionTest.php: -------------------------------------------------------------------------------- 1 | create(); 22 | $this->assertEquals(Question::count(), 1); 23 | } 24 | 25 | #[Test] 26 | function question_question_type_relation() 27 | { 28 | $questionType = QuestionType::factory()->create([ 29 | 'name' => 'fill_the_blank' 30 | ]); 31 | $questionType->questions()->saveMany([ 32 | Question::factory()->make(), 33 | Question::factory()->make() 34 | ]); 35 | $this->assertEquals($questionType->questions->count(), 2); 36 | } 37 | 38 | #[Test] 39 | function question_and_topics_relation() 40 | { 41 | $topic1 = Topic::factory()->create(['topic' => 'Test Topic One']); 42 | $topic2 = Topic::factory()->create(['topic' => 'Test Topic Two']); 43 | $question = Question::factory()->create(); 44 | $question->topics()->attach($topic1); 45 | $question->topics()->attach($topic2); 46 | $this->assertEquals(2, $question->topics->count()); 47 | } 48 | 49 | #[Test] 50 | function question_and_question_options_relation() 51 | { 52 | $question = Question::factory()->create(); 53 | $question->options()->saveMany([ 54 | QuestionOption::factory()->make([ 55 | 'question_id' => $question->id, 56 | ]), 57 | QuestionOption::factory()->make([ 58 | 'question_id' => $question->id, 59 | ]), 60 | ]); 61 | $this->assertEquals(2, $question->options->count()); 62 | } 63 | 64 | #[Test] 65 | function testQuestionTypeSeeding() 66 | { 67 | $this->seed(QuestionTypeSeeder::class); 68 | $this->assertDatabaseCount(config('laravel-quiz.table_names.question_types'), 3); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/LaravelQuizServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadTranslationsFrom(__DIR__.'/../resources/lang', 'laravel-quiz'); 18 | // $this->loadViewsFrom(__DIR__.'/../resources/views', 'laravel-quiz'); 19 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations', 'laravel-quiz'); 20 | // $this->loadRoutesFrom(__DIR__.'/routes.php'); 21 | 22 | if ($this->app->runningInConsole()) { 23 | $this->publishes([ 24 | __DIR__ . '/../config/config.php' => config_path('laravel-quiz.php'), 25 | ], 'config'); 26 | $this->publishes([ 27 | __DIR__ . '/../database/seeders/' => database_path('seeders/'), 28 | ], 'seeds'); 29 | $this->publishes([ 30 | __DIR__ . '/../database/migrations/' => database_path('migrations/laravel-quiz'), 31 | ], 'migrations'); 32 | 33 | // Publishing the views. 34 | /*$this->publishes([ 35 | __DIR__.'/../resources/views' => resource_path('views/vendor/laravel-quiz'), 36 | ], 'views');*/ 37 | 38 | // Publishing assets. 39 | /*$this->publishes([ 40 | __DIR__.'/../resources/assets' => public_path('vendor/laravel-quiz'), 41 | ], 'assets');*/ 42 | 43 | // Publishing the translation files. 44 | /*$this->publishes([ 45 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/laravel-quiz'), 46 | ], 'lang');*/ 47 | 48 | // Registering package commands. 49 | // $this->commands([]); 50 | } 51 | } 52 | 53 | /** 54 | * Register the application services. 55 | */ 56 | public function register() 57 | { 58 | // Automatically apply the package configuration 59 | $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'laravel-quiz'); 60 | 61 | // Register the main class to use with the facade 62 | $this->app->singleton('laravel-quiz', function () { 63 | return new LaravelQuiz; 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Models/Question.php: -------------------------------------------------------------------------------- 1 | $attributes['name'], 49 | set: fn ($value) => ['name' => $value], 50 | ); 51 | } 52 | 53 | public function question_type() 54 | { 55 | return $this->belongsTo(config('laravel-quiz.models.question_type')); 56 | } 57 | 58 | public function topics() 59 | { 60 | return $this->morphToMany(config('laravel-quiz.models.topic'), 'topicable'); 61 | } 62 | 63 | public function options() 64 | { 65 | return $this->hasMany(config('laravel-quiz.models.question_option')); 66 | } 67 | 68 | public function quiz_questions() 69 | { 70 | return $this->hasMany(config('laravel-quiz.models.quiz_question')); 71 | } 72 | 73 | protected static function newFactory() 74 | { 75 | return QuestionFactory::new(); 76 | } 77 | 78 | public function correct_options(): Collection 79 | { 80 | return $this->options()->where('is_correct', 1)->get(); 81 | } 82 | 83 | /** 84 | * Scope a query to only include question with options. 85 | * 86 | * @param \Illuminate\Database\Eloquent\Builder $query 87 | * @return \Illuminate\Database\Eloquent\Builder 88 | */ 89 | public function scopeHasOptions($query) 90 | { 91 | return $query->has('options', '>', 0); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Models/Quiz.php: -------------------------------------------------------------------------------- 1 | 'json', 40 | ]; 41 | 42 | const FIXED_NEGATIVE_TYPE = 'fixed'; 43 | const PERCENTAGE_NEGATIVE_TYPE = 'percentage'; 44 | 45 | public function getTable() 46 | { 47 | return config('laravel-quiz.table_names.quizzes'); 48 | } 49 | 50 | /** 51 | * Backward compatibility of the attribute 52 | * 53 | * @param string $value 54 | * @return \Illuminate\Database\Eloquent\Casts\Attribute 55 | */ 56 | protected function title(): Attribute 57 | { 58 | return new Attribute( 59 | get: fn ($value, $attributes) => $attributes['name'], 60 | set: fn ($value) => ['name' => $value], 61 | ); 62 | } 63 | 64 | public function topics() 65 | { 66 | return $this->morphToMany(config('laravel-quiz.models.topic'), 'topicable'); 67 | } 68 | 69 | public function questions() 70 | { 71 | return $this->hasMany(config('laravel-quiz.models.quiz_question')); 72 | } 73 | 74 | public function attempts() 75 | { 76 | return $this->hasMany(config('laravel-quiz.models.quiz_attempt')); 77 | } 78 | 79 | public static function newFactory() 80 | { 81 | return QuizFactory::new(); 82 | } 83 | 84 | /** 85 | * Interact with the user's address. 86 | * 87 | * @return \Illuminate\Database\Eloquent\Casts\Attribute 88 | */ 89 | protected function negativeMarkingSettings(): Attribute 90 | { 91 | return Attribute::make( 92 | get: fn ($value) => empty($value) ? [ 93 | 'enable_negative_marks' => true, 94 | 'negative_marking_type' => Quiz::FIXED_NEGATIVE_TYPE, 95 | 'negative_mark_value' => 0 96 | ] : json_decode($value, true), 97 | ); 98 | } 99 | 100 | public function quizAuthors() 101 | { 102 | return $this->hasMany(config('laravel-quiz.models.quiz_author')); 103 | } 104 | 105 | public function getRouteKeyName() 106 | { 107 | return 'slug'; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuestionTest::question":0.013,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuestionTest::question_question_type_relation":0.003,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuestionTest::question_and_topics_relation":0.02,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuestionTest::question_and_question_options_relation":0.002,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuestionTest::testQuestionTypeSeeding":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_1_question_no_negative_marks":0.007,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_1_question_with_negative_marks_question_fixed":0.005,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_1_question_with_negative_marks_question_percentage":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_1_question_with_negative_marks_quiz_fixed":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_1_question_with_negative_marks_quiz_percentage":0.005,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_2_question_no_negative_marks":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_2_question_with_negative_marks_question_fixed":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_2_question_with_negative_marks_question_percentage":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_2_question_with_negative_marks_quiz_fixed":0.005,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_2_question_with_negative_marks_quiz_percentage":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_3_question_no_negative_marks":0.003,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_3_question_with_negative_marks_question_fixed":0.003,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_3_question_with_negative_marks_question_percentage":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_3_question_with_negative_marks_quiz_fixed":0.003,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_score_for_type_3_question_with_negative_marks_quiz_percentage":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_negative_marks_for_question":0.005,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::get_quiz_attempt_result_with_and_without_quiz_question":0.019,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAttemptTest::test_quiz_attempt_validate":0.029,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizAuthorTest::quiz_authors":0.004,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizTest::quiz":0.001,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizTest::quiz_topics_relation":0.003,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizTest::quiz_questions_relation":0.003,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizTest::quiz_attempts_relation":0.005,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizTest::quiz_attempt_answers":0.006,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\QuizTest::quiz_check_negative_marking_settings":0.002,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\TopicTest::topic":0.001,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\TopicTest::topic_parent_child_relation":0.001,"Harishdurga\\LaravelQuiz\\Tests\\Unit\\TopicTest::topic_question_relation":0.003,"Harishdurga\\LaravelQuiz\\Tests\\Feature\\QuizTest::quiz_multi_user_attempts_multi_question_types_few_wrong_answers":0.012}} -------------------------------------------------------------------------------- /docs/LaravelQuiz.drawio: -------------------------------------------------------------------------------- 1 | 7V1dc5s4FP01fsyO+TL2Y+I03c2mu+0mbbL74lGMYtNgREFu4vz6lUDYBiSMUyTIRDOZiZEBC50j6d5zr8TAmq6eP8YgWn5CHgwG5tB7HljnA9M0Xdsk/2jJhpXY9iQrWcS+l5UZu4Jr/wWywiErXfseTAonYoQC7EfFwjkKQzjHhTIQx+ipeNoDCoq/GoEFrBRcz0FQLb31PbzMSsemuyv/HfqLZf7Lxog93wrkJ7MnSZbAQ097RdaHgTWNEcLZp9XzFAa09fJ2uf1jcxtcPY4+Xn5JfoCvZ3/e/PXtJLvZxTGXbB8hhiF+9a3Ht/8ll7OrBC4uz79Zw7uvG+s5v/VPEKxZe92gyJ+zB8abvBWTJ38VgJAcnT2gEF+zb4bkeL70A+8KbNCa1i3BYP6YH50tUey/kPNBQL4ySAH5OsaMJOaI3s0PgikKUEwKQpT+wO6ia3oz9jMxTMhln/M2MEpFn8Bz4cQrkOC8gigIQJT492mV6YUrEC/88AxhjFbspIZNnLcXjDF83iMYa/KPEK0gjjfklLwHMfKw7mNM2B2edlw0Ruyc5T4PnRHrA4z/i+2tt7/2D+kvIFyQxzrm53i/ZpZ+DAQYxiHA8AytQy/Z5xX5sPecu6KUbUcwz6wwb2CesYtO6f8QwwWMK0wk7Y5TIsXoEZaYwyETCPxFSA4D+EAvo8D5ZHA4ZcUYRfRmEZj74eIqPefc3pX8w5qIFiFy7UOQDgBL3/NgSLmGMMDgftsXIkSqnbahc0b+SEtPh785A4dUfEqOjd0x+aOnx3iKQvIswE9pBwlrnyBlLoeQtR34MCE3RagPEbBMiX3+FbhwLPD5xFBBPgQrmGFPGoQ0voa+Zegds2PoLQHySbBeaOQlIu+O1SF/+fPPL7fww9x6nE5j62I+xqFx4lQghR4x09ghaZElWqAQBB92paSdydwDPdbKu3OuEMUvnci/Q4w3zJwAa4yo9YBXubEBn318xy6nn/+lnwkS2dH5895X55v8ICSPe7d/sHcVPdxdlh7l12XPRx+qZHY2gzJB63gOa86zmelMLBdYdz+Tz4MYBgD7P4uVa71/24L+nbXCTE/tcnu5MbQ7HuAdkVGXzMA84x8lwD1xACEINQHaJoA17JgAowoBvqzJM/sovNlEsAK4ditf61baRUfPbgh8bggc6VU2+DWjH26lq52LVsaeUWNG9savrGpZWlFQgXznbqVR1ZLyWUfPOG3NOM6oJC02tDWNsSkLdpGcoDv8cWam2ZgLvRnrRZ7mbppPQdbIt4x892O9yMX8wUb8GSaOhpYaZBNBpaLIF7rGAiKsoOeD2ToO9DggD/7upSZzUos/HQW0vyeVAp2LTXkFtNzYFQWcrg3B/MZvKLBkDF4RWGrqzx2MI+X208FAUi6nlEJaXYWWTJ6wNwpw1sHCAgVGP9Y0J+uM9u8T1mvpOJB23O235NOC/q8Yjuy2pJrZnbPTRKPHHi3yAWJOoCI2Z3WIWJHOnjGQuvbCzk77MyVdkvHPEI1Rx1GjuatvW0VXf9JU7h3KGuirRn+appa2oFZ4WoLdGhZhN3k2ns2TeCxpwFfjSVriecXUvu1AvyTx8LCXZ9yJIjmY9nzt4MsmAE/pkUUAbspQLf60MTUHChyoQNuYFkeJPEo5INJ4dhzQfr5sEnClHqVTQf5j2slr6OTlc2ezbME2fbz00tM4Bpu9ExjLd3f+TAt2hmd51YI5cfY5c/h8a1jiWFaDVhNL8oVQ7TugW4PmzTie6TBGo+uSk5xKfqjpVh1Rrtokyx2xqxHngjViTksTU/+gk4WV7RzGii8NSgPL7nLWyD+/oVmDJw3WWeY9UQbtqjiUZ//8HekcoDY7+aQ08W5DBgcVImlJQLZWiFoRCOy3pxDZIoVgq+hrDkjmgEqRiF9lkUKg871lY69SHOJXuT4DZJsBpAkghwDdC0O5z6FzgLqiAC8HSC0FhKs+ktkcxXG6pYxOApLJAV4SkFoOmBVUtT5c5+nbTfXhfIlFT1x9p6q/7dv670dtc3MEWR/kDMNjlVqbU12P8WXtv+hVWLKBH/FkVrUKjCNekeG/aBf8uOl325HejgzjiBQ4LcOo4kDnMowjkuLI0PiYZOiThgZ6JU7r2HeeoyNahRvCRWoSzjQHfjlFJ6NFj5UYV7gWO5mhNAoHAu2HSx0IlGox7ubm0xAuTk8ujKvp1xfv4RtyxKuytQEgG/vuNZhqIIZ6gNrzay3BJnfoc5ff5HT3CQdy25C22Zco+Kb7+3H9fdyYDDVOHw97ed1dFHvTcVfZ2POcPaXYj0RRN72Nr2zsec6eWuxF4TYPJvPYz/Lt9L4b0gjA9fTUMsAUMACT5gyKzj5AmgLtU4Dn56mlgMjZj0CSaLlHPgN43p5aBog8/RV4ngGM4SrCifYB5LIgj+h1x4Ka7b6j9X3gJ0vIPEHshxtNBokO4bBzy1AUBtTZmGoYYHVuGQqDgDobU5Eq1LllKJKFvHUMdr6hngZkMWCs0DLkRoOF3qG/grN7SFoAhtpCbCssPK5lg8Vbnqd0PHBFUiEp8L3ZQ0zDJqeMHgkGq+ids0DCmGBZXZuGrkg0zFiwjmhqtWaBXBaMFJqH8OoP9+L8483F9dfRZGZ5lz/HsxPRUFDIEyKtP0sgJu7igs0O3xNO/rCmw6/Sgbeno9pBwaygqhdtwDqQm+7cmofle7Jow61mCfR70cbe7jYTmRkd49w6qtkyRekqDrfqveU5/P1DSRYqrlFExeKg4khChZtVV503aWLVaeZCVYDR+VWvxN0o5Vc5Q84MKW33W34+pcBi0ulVIuuotgP1ckkNt8YiEUWvqFKAvsrFNNwai6PrFBo/AvpVytJJoHJVDbfGNa/S3pJAR1Qks0DpuhpulZ0Kpn3fw3B7INtJrps5e+wjc6tdjZ2/P8fLKu0gypOolDpe1XD2nuN1GiZPnHlXu1+vdb8mRfRd3jvGue6XIyuUqd2vFuKW4p7VX/dLtKwpHZNZ7Fpb4MfZXq6ALf11w0SpLCkL9N4WimjQuSNmVPXXjAcgswD0YhdZ0HfvfRkiE2Db/bO9DfQoIJsK3e9tYFZA7Xu4ultPnI3b+554XZCjJ464wdthsGj1vR+PfPu+jnyzOV7SyFClS25oPey4Xjhp2gvttrshu7T06ia39AYJ1ylRJaspu2rHlmNfKWWVXh00Zh27rVdE8RtRIOP1O91F1ujhlCBwVb5GmI+P++ZGD2UpZ7X270E5PX8LS19m8YkG+rjcwlFDoNvfEPpVw/uk9Kpq16p/Y2D5fKfd2SCK/11Fvvf98/3dGo4nwzsbIY6KmIYP1oRXauIG1nDQUZCg2JVkTTAn5Uw93gTDdRKNFmYYbshApBWp1Qe2BsKrJYIWJYGy2sDjxoEYgrh7ifPbG1Khjdw9bvVq1WNNh1o6NMP+GP2ItzBWKR0EKvLUHJwOQTolaFKoJkXT6UIeKfjy8j4pFKd4aVaYBm9fTbW0MA/SIkaBpoVaWjid04KfF5zSwk9mYJ65W+r22Nas2C0a6Y4Vb+8t18oU8TrDvMcZonycq3tt9TdFlDOWHIdicxHALKnMtlHtkbKyRrkSQDVxn5Ms8IYgO+Cbi1EUBwZKMaAxzzszVWJWjdtkj/juodq+DrJmhylXJVKdCvvqNg0oTTotKv0jjtLPbWlBju6vznbkMEYI70vtMYiWn5AH6Rn/Aw== -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'topics' => 'topics', 16 | 'question_types' => 'question_types', 17 | 'questions' => 'questions', 18 | 'topicables' => 'topicables', 19 | 'question_options' => 'question_options', 20 | 'quizzes' => 'quizzes', 21 | 'quiz_questions' => 'quiz_questions', 22 | 'quiz_attempts' => 'quiz_attempts', 23 | 'quiz_attempt_answers' => 'quiz_attempt_answers', 24 | 'quiz_authors' => 'quiz_authors' 25 | ], 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Models Name 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Allow to override Quiz table to extend code 33 | | 34 | */ 35 | 36 | 'models' => [ 37 | 38 | /* 39 | * Default Harishdurga\LaravelQuiz\Models\Question::class 40 | */ 41 | 42 | 'question' => Harishdurga\LaravelQuiz\Models\Question::class, 43 | 44 | /* 45 | * Default Harishdurga\LaravelQuiz\Models\Question::class 46 | */ 47 | 48 | 'question_option' => Harishdurga\LaravelQuiz\Models\QuestionOption::class, 49 | 50 | /* 51 | * Default Harishdurga\LaravelQuiz\Models\Question::class 52 | */ 53 | 54 | 'question_type' => Harishdurga\LaravelQuiz\Models\QuestionType::class, 55 | 56 | /* 57 | * Default Harishdurga\LaravelQuiz\Models\Quiz::class 58 | */ 59 | 60 | 'quiz' => Harishdurga\LaravelQuiz\Models\Quiz::class, 61 | 62 | /* 63 | * Default Harishdurga\LaravelQuiz\Models\QuizAttempt::class 64 | */ 65 | 66 | 'quiz_attempt' => Harishdurga\LaravelQuiz\Models\QuizAttempt::class, 67 | 68 | /* 69 | * Default Harishdurga\LaravelQuiz\Models\QuizAttempt::class 70 | */ 71 | 72 | 'quiz_attempt_answer' => Harishdurga\LaravelQuiz\Models\QuizAttemptAnswer::class, 73 | 74 | /* 75 | * Default Harishdurga\LaravelQuiz\Models\QuizAttempt::class 76 | */ 77 | 78 | 'quiz_author' => Harishdurga\LaravelQuiz\Models\QuizAuthor::class, 79 | 80 | /* 81 | * Default Harishdurga\LaravelQuiz\Models\QuizAttempt::class 82 | */ 83 | 84 | 'quiz_question' => Harishdurga\LaravelQuiz\Models\QuizQuestion::class, 85 | 86 | /* 87 | * Default Harishdurga\LaravelQuiz\Models\QuizAttempt::class 88 | */ 89 | 90 | 'topic' => Harishdurga\LaravelQuiz\Models\Topic::class, 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Question type mapping 96 | |-------------------------------------------------------------------------- 97 | | 98 | | You can choose which method to use for scoring. 99 | | 100 | */ 101 | 102 | 'get_score_for_question_type' => [ 103 | 1 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_1_question', 104 | 2 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_2_question', 105 | 3 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_3_question', 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Question type answer/solution render 111 | |-------------------------------------------------------------------------- 112 | | 113 | | Render correct answer and given response for different question types 114 | | 115 | */ 116 | 'render_answers_responses' => [ 117 | 1 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType1Answers', 118 | 2 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType2Answers', 119 | 3 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType3Answers', 120 | ] 121 | 122 | ]; 123 | -------------------------------------------------------------------------------- /database/migrations/2021_05_22_053359_create_quizzes_table.php: -------------------------------------------------------------------------------- 1 | tableNames = config('laravel-quiz.table_names'); 14 | } 15 | /** 16 | * Run the migrations. 17 | * 18 | * @return void 19 | */ 20 | public function up() 21 | { 22 | // Topics Table 23 | Schema::create($this->tableNames['topics'], function (Blueprint $table) { 24 | $table->id(); 25 | $table->string('topic'); 26 | $table->string('slug'); 27 | $table->foreignId('parent_id')->nullable()->constrained($this->tableNames['topics'])->nullOnDelete(); 28 | $table->boolean('is_active')->default(true); 29 | $table->timestamps(); 30 | $table->softDeletes(); 31 | }); 32 | 33 | // Question Types Table 34 | Schema::create($this->tableNames['question_types'], function (Blueprint $table) { 35 | $table->id(); 36 | $table->string('question_type'); 37 | $table->timestamps(); 38 | $table->softDeletes(); 39 | }); 40 | 41 | // Questions Table 42 | Schema::create($this->tableNames['questions'], function (Blueprint $table) { 43 | $table->id(); 44 | $table->text('question'); 45 | $table->foreignId('question_type_id')->nullable()->constrained($this->tableNames['question_types'])->cascadeOnDelete(); 46 | $table->text('media_url')->nullable(); 47 | $table->string('media_type')->nullable(); 48 | $table->boolean('is_active')->default(true); 49 | $table->timestamps(); 50 | $table->softDeletes(); 51 | }); 52 | 53 | // Quiz, Questions and Topics Relations Table 54 | Schema::create($this->tableNames['topicables'], function (Blueprint $table) { 55 | $table->id(); 56 | $table->foreignId('topic_id')->nullable()->constrained($this->tableNames['topics'])->cascadeOnDelete(); 57 | $table->unsignedInteger('topicable_id'); 58 | $table->string('topicable_type'); 59 | $table->timestamps(); 60 | }); 61 | 62 | // Question Options Table 63 | Schema::create($this->tableNames['question_options'], function (Blueprint $table) { 64 | $table->id(); 65 | $table->foreignId('question_id')->nullable()->constrained($this->tableNames['questions'])->cascadeOnDelete(); 66 | $table->string('option')->nullable(); 67 | $table->string('media_url')->nullable(); 68 | $table->string('media_type')->nullable(); 69 | $table->boolean('is_correct')->default(false); 70 | $table->timestamps(); 71 | $table->softDeletes(); 72 | }); 73 | 74 | // Quizzes Table 75 | Schema::create($this->tableNames['quizzes'], function (Blueprint $table) { 76 | $table->id(); 77 | $table->string('title'); 78 | $table->string('slug'); 79 | $table->text('description')->nullable(); 80 | $table->float('total_marks')->default(0); //0 means no marks 81 | $table->float('pass_marks')->default(0); //0 means no pass marks 82 | $table->unsignedInteger('max_attempts')->default(0); //0 means unlimited attempts 83 | $table->tinyInteger('is_published')->default(0); //0 means not published, 1 means published 84 | $table->string('media_url')->nullable(); //Can be used for cover image, logo etc. 85 | $table->string('media_type')->nullable(); //image,video,audio etc. 86 | $table->unsignedInteger('duration')->default(0); //0 means no duration 87 | $table->timestamp('valid_from')->default(now()); 88 | $table->timestamp('valid_upto')->nullable(); //null means no expiry 89 | $table->unsignedInteger('time_between_attempts')->default(0); //0 means no time between attempts, immediately 90 | $table->timestamps(); 91 | $table->softDeletes(); 92 | }); 93 | 94 | // Quiz Questions Table 95 | Schema::create($this->tableNames['quiz_questions'], function (Blueprint $table) { 96 | $table->id(); 97 | $table->foreignId('quiz_id')->nullable()->constrained($this->tableNames['quizzes'])->cascadeOnDelete(); 98 | $table->foreignId('question_id')->nullable()->constrained($this->tableNames['questions'])->cascadeOnDelete(); 99 | $table->float('marks')->default(0); //0 means no marks 100 | $table->float('negative_marks')->default(0); //0 means no negative marks in case of wrong answer 101 | $table->boolean('is_optional')->default(false); //0 means not optional, 1 means optional 102 | $table->unsignedInteger('order')->default(0); 103 | $table->timestamps(); 104 | $table->softDeletes(); 105 | }); 106 | 107 | // Quiz Attempts Table 108 | Schema::create($this->tableNames['quiz_attempts'], function (Blueprint $table) { 109 | $table->id(); 110 | $table->foreignId('quiz_id')->nullable()->constrained($this->tableNames['quizzes'])->cascadeOnDelete(); 111 | $table->unsignedInteger('participant_id'); 112 | $table->string('participant_type'); 113 | $table->timestamps(); 114 | $table->softDeletes(); 115 | }); 116 | 117 | // Quiz Attempt Answers Table 118 | Schema::create($this->tableNames['quiz_attempt_answers'], function (Blueprint $table) { 119 | $table->id(); 120 | $table->foreignId('quiz_attempt_id')->nullable()->constrained($this->tableNames['quiz_attempts'])->cascadeOnDelete(); 121 | $table->foreignId('quiz_question_id')->nullable()->constrained($this->tableNames['quiz_questions'])->cascadeOnDelete(); 122 | $table->foreignId('question_option_id')->nullable()->constrained($this->tableNames['question_options'])->cascadeOnDelete(); 123 | $table->string('answer')->nullable(); 124 | $table->timestamps(); 125 | $table->softDeletes(); 126 | }); 127 | } 128 | 129 | /** 130 | * Reverse the migrations. 131 | * 132 | * @return void 133 | */ 134 | public function down() 135 | { 136 | Schema::table($this->tableNames['topics'], function (Blueprint $table) { 137 | $table->dropForeign(['parent_id']); 138 | }); 139 | Schema::table($this->tableNames['topicables'], function (Blueprint $table) { 140 | $table->dropForeign(['topic_id']); 141 | }); 142 | Schema::table($this->tableNames['question_options'], function (Blueprint $table) { 143 | $table->dropForeign(['question_id']); 144 | }); 145 | Schema::table($this->tableNames['quiz_questions'], function (Blueprint $table) { 146 | $table->dropForeign(['quiz_id']); 147 | $table->dropForeign(['question_id']); 148 | }); 149 | Schema::table($this->tableNames['quiz_attempts'], function (Blueprint $table) { 150 | $table->dropForeign(['quiz_id']); 151 | }); 152 | Schema::table($this->tableNames['quiz_attempt_answers'], function (Blueprint $table) { 153 | $table->dropForeign(['quiz_attempt_id']); 154 | $table->dropForeign(['quiz_question_id']); 155 | $table->dropForeign(['question_option_id']); 156 | }); 157 | Schema::drop($this->tableNames['quiz_attempt_answers']); 158 | Schema::drop($this->tableNames['quiz_attempts']); 159 | Schema::drop($this->tableNames['quiz_questions']); 160 | Schema::drop($this->tableNames['topicables']); 161 | Schema::drop($this->tableNames['question_options']); 162 | Schema::drop($this->tableNames['questions']); 163 | Schema::drop($this->tableNames['topics']); 164 | Schema::drop($this->tableNames['question_types']); 165 | Schema::drop($this->tableNames['quizzes']); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/Unit/QuizTest.php: -------------------------------------------------------------------------------- 1 | make()->create([ 25 | 'name' => 'Sample Quiz', 26 | 'slug' => 'sample-quiz', 27 | ]); 28 | $this->assertEquals(1, Quiz::count()); 29 | $this->assertEquals('Sample Quiz', Quiz::find($quiz->id)->name); 30 | } 31 | 32 | #[Test] 33 | function quiz_topics_relation() 34 | { 35 | $quiz = Quiz::factory()->make()->create([ 36 | 'name' => 'Sample Quiz', 37 | 'slug' => 'sample-quiz', 38 | ]); 39 | $topic_one = Topic::factory()->make()->create([ 40 | 'name' => 'Topic One', 41 | 'slug' => 'topic-one', 42 | ]); 43 | $topic_two = Topic::factory()->make()->create([ 44 | 'name' => 'Topic Two', 45 | 'slug' => 'topic-two', 46 | ]); 47 | $quiz->topics()->attach([$topic_one->id, $topic_two->id]); 48 | $this->assertEquals(2, $quiz->topics()->count()); 49 | } 50 | 51 | #[Test] 52 | function quiz_questions_relation() 53 | { 54 | $quiz = Quiz::factory()->make()->create([ 55 | 'name' => 'Sample Quiz', 56 | 'slug' => 'sample-quiz', 57 | ]); 58 | $question_one = Question::factory()->create(); 59 | $question_two = Question::factory()->create(); 60 | $quiz_question_one = QuizQuestion::factory()->make()->create([ 61 | 'quiz_id' => $quiz->id, 62 | 'question_id' => $question_one->id, 63 | ]); 64 | $quiz_question_two = QuizQuestion::factory()->make()->create([ 65 | 'quiz_id' => $quiz->id, 66 | 'question_id' => $question_two->id, 67 | ]); 68 | $this->assertEquals(2, $quiz->questions()->count()); 69 | } 70 | 71 | #[Test] 72 | function quiz_attempts_relation() 73 | { 74 | $user = Author::create( 75 | ['name' => "John Doe"] 76 | ); 77 | $userTwo = Author::create( 78 | ['name' => "John Doe"] 79 | ); 80 | $quiz = Quiz::factory()->make()->create([ 81 | 'name' => 'Sample Quiz', 82 | 'slug' => 'sample-quiz', 83 | ]); 84 | $question_one = Question::factory()->create(); 85 | $question_one->options()->save(QuestionOption::factory()->make([ 86 | 'is_correct' => true 87 | ])); 88 | $question_two = Question::factory()->create(); 89 | $question_two->options()->save(QuestionOption::factory()->make([ 90 | 'is_correct' => true 91 | ])); 92 | $quiz_question_one = QuizQuestion::factory()->make()->create([ 93 | 'quiz_id' => $quiz->id, 94 | 'question_id' => $question_one->id, 95 | ]); 96 | $quiz_question_two = QuizQuestion::factory()->make()->create([ 97 | 'quiz_id' => $quiz->id, 98 | 'question_id' => $question_two->id, 99 | ]); 100 | $attempt = QuizAttempt::create([ 101 | 'quiz_id' => $quiz->id, 102 | 'participant_id' => $user->id, 103 | 'participant_type' => get_class($user), 104 | ]); 105 | $attemptTwo = QuizAttempt::create([ 106 | 'quiz_id' => $quiz->id, 107 | 'participant_id' => $userTwo->id, 108 | 'participant_type' => get_class($userTwo), 109 | ]); 110 | $this->assertEquals(1, $user->quiz_attempts()->count()); 111 | $this->assertEquals($user->id, $attempt->participant->id); 112 | $this->assertEquals(2, $quiz->attempts()->count()); 113 | } 114 | 115 | #[Test] 116 | function quiz_attempt_answers() 117 | { 118 | $user = Author::create( 119 | ['name' => "John Doe"] 120 | ); 121 | $userTwo = Author::create( 122 | ['name' => "John Doe"] 123 | ); 124 | $quiz = Quiz::factory()->make()->create([ 125 | 'name' => 'Sample Quiz', 126 | 'slug' => 'sample-quiz', 127 | ]); 128 | $question_one = Question::factory()->create(); 129 | $question_one->options()->save(QuestionOption::factory()->make([ 130 | 'is_correct' => true, 131 | 'option' => 'Doe' 132 | ])); 133 | $question_two = Question::factory()->create(); 134 | $question_two->options()->save(QuestionOption::factory()->make([ 135 | 'is_correct' => true, 136 | 'option' => 'John' 137 | ])); 138 | $quiz_question_one = QuizQuestion::factory()->make()->create([ 139 | 'quiz_id' => $quiz->id, 140 | 'question_id' => $question_one->id, 141 | ]); 142 | $quiz_question_two = QuizQuestion::factory()->make()->create([ 143 | 'quiz_id' => $quiz->id, 144 | 'question_id' => $question_two->id, 145 | ]); 146 | $attempt = QuizAttempt::create([ 147 | 'quiz_id' => $quiz->id, 148 | 'participant_id' => $user->id, 149 | 'participant_type' => get_class($user), 150 | ]); 151 | $attemptTwo = QuizAttempt::create([ 152 | 'quiz_id' => $quiz->id, 153 | 'participant_id' => $userTwo->id, 154 | 'participant_type' => get_class($userTwo), 155 | ]); 156 | QuizAttemptAnswer::create([ 157 | 'quiz_attempt_id' => $attempt->id, 158 | 'quiz_question_id' => $quiz_question_one->id, 159 | 'question_option_id' => $question_one->options()->first()->id, 160 | 'answer' => 'Doe' 161 | ]); 162 | QuizAttemptAnswer::create([ 163 | 'quiz_attempt_id' => $attempt->id, 164 | 'quiz_question_id' => $quiz_question_two->id, 165 | 'question_option_id' => $quiz_question_two->question->options()->first()->id, 166 | 'answer' => 'John' 167 | ]); 168 | $this->assertEquals(2, $attempt->answers()->count()); 169 | $this->assertEquals(1, $quiz_question_one->answers()->count()); 170 | $this->assertEquals(1, $quiz_question_two->answers()->count()); 171 | $this->assertEquals('Doe', $quiz_question_one->answers()->first()->answer); 172 | $this->assertEquals('John', $quiz_question_two->answers()->first()->answer); 173 | $this->assertEquals(1, $question_one->options()->first()->answers()->count()); 174 | $this->assertEquals(1, $question_two->options()->first()->answers()->count()); 175 | } 176 | 177 | #[Test] 178 | function quiz_check_negative_marking_settings() 179 | { 180 | $quizOne = Quiz::factory()->make()->create([ 181 | 'name' => 'Sample Quiz', 182 | 'slug' => 'sample-quiz', 183 | 'negative_marking_settings' => [ 184 | 'enable_negative_marks' => false, 185 | 'negative_marking_type' => 'fixed', 186 | 'negative_mark_value' => 0, 187 | ] 188 | ]); 189 | $this->assertEquals(false, $quizOne->negative_marking_settings['enable_negative_marks']); 190 | $quizTwo = Quiz::create([ 191 | 'name' => "Sample Quiz", 192 | 'slug' => "sample-quiz-two", 193 | 'description' => "", 194 | 'media_url' => null, 195 | 'total_marks' => 0, 196 | 'pass_marks' => 0, 197 | 'max_attempts' => 0, 198 | 'is_published' => 1, 199 | 'media_type' => 'image', 200 | 'duration' => 0, 201 | 'valid_from' => date('Y-m-d H:i:s'), 202 | 'valid_upto' => null, 203 | ]); 204 | $quizTwo = Quiz::find($quizTwo->id); 205 | $this->assertEquals(true, $quizTwo->negative_marking_settings['enable_negative_marks']); 206 | $quizThree = Quiz::factory()->make()->create([ 207 | 'name' => 'Sample Quiz', 208 | 'slug' => 'sample-quiz-3', 209 | 'negative_marking_settings' => [ 210 | 'enable_negative_marks' => true, 211 | 'negative_marking_type' => 'percentage', 212 | 'negative_mark_value' => 10, 213 | ] 214 | ]); 215 | $this->assertEquals(true, $quizThree->negative_marking_settings['enable_negative_marks']); 216 | $this->assertEquals('percentage', $quizThree->negative_marking_settings['negative_marking_type']); 217 | $this->assertEquals(10, $quizThree->negative_marking_settings['negative_mark_value']); 218 | $quizThree->negative_marking_settings = [ 219 | 'enable_negative_marks' => true, 220 | 'negative_marking_type' => 'percentage', 221 | 'negative_mark_value' => 23, 222 | ]; 223 | $quizThree->save(); 224 | $this->assertEquals(23, $quizThree->negative_marking_settings['negative_mark_value']); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Models/QuizAttempt.php: -------------------------------------------------------------------------------- 1 | belongsTo(config('laravel-quiz.models.quiz')); 42 | } 43 | 44 | public function participant() 45 | { 46 | return $this->morphTo(__FUNCTION__, 'participant_type', 'participant_id'); 47 | } 48 | 49 | public function answers() 50 | { 51 | return $this->hasMany(config('laravel-quiz.models.quiz_attempt_answer')); 52 | } 53 | 54 | public function calculate_score($data = null): float 55 | { 56 | $score = 0; 57 | $quiz_questions_collection = $this->quiz->questions()->with('question')->orderBy('id', 'ASC')->get(); 58 | $quiz_attempt_answers = []; 59 | foreach ($this->answers as $quiz_attempt_answer) { 60 | $quiz_attempt_answers[$quiz_attempt_answer->quiz_question_id][] = $quiz_attempt_answer; 61 | } 62 | foreach ($quiz_questions_collection as $quiz_question) { 63 | $question = $quiz_question->question; 64 | $score += call_user_func_array(config('laravel-quiz.get_score_for_question_type')[$question->question_type_id], [$this, $quiz_question, $quiz_attempt_answers[$quiz_question->id] ?? [], $data]); 65 | } 66 | return $score; 67 | } 68 | 69 | /** 70 | * @param QuizAttemptAnswer[] $quizQuestionAnswers All the answers of the quiz question 71 | */ 72 | public static function get_score_for_type_1_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array | Collection $quizQuestionAnswers, $data = null): float 73 | { 74 | $quiz = $quizAttempt->quiz; 75 | $question = $quizQuestion->question; 76 | $correct_answer = ($question->correct_options())->first()->id; 77 | $negative_marks = self::get_negative_marks_for_question($quiz, $quizQuestion); 78 | if (!empty($correct_answer)) { 79 | if (count($quizQuestionAnswers)) { 80 | if (is_array($quizQuestionAnswers)) { 81 | return $quizQuestionAnswers[0]->question_option_id == $correct_answer ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks 82 | } 83 | return $quizQuestionAnswers->first()->question_option_id == $correct_answer ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks 84 | } 85 | return $quizQuestion->is_optional ? 0 : -$negative_marks; // If the question is optional, then the negative marks will be 0 86 | } 87 | return count($quizQuestionAnswers) ? (float) $quizQuestion->marks : 0; // Incase of no correct answer, if there is any answer then give full marks 88 | } 89 | 90 | /** 91 | * @param QuizAttemptAnswer[] $quizQuestionAnswers All the answers of the quiz question 92 | */ 93 | public static function get_score_for_type_2_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array | Collection $quizQuestionAnswers, $data = null): float 94 | { 95 | $quiz = $quizAttempt->quiz; 96 | $question = $quizQuestion->question; 97 | $correct_answer = ($question->correct_options())->pluck('id'); 98 | $negative_marks = self::get_negative_marks_for_question($quiz, $quizQuestion); 99 | if (!empty($correct_answer)) { 100 | if (count($quizQuestionAnswers)) { 101 | $temp_arr = []; 102 | foreach ($quizQuestionAnswers as $answer) { 103 | $temp_arr[] = $answer->question_option_id; 104 | } 105 | return $correct_answer->toArray() == $temp_arr ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks 106 | } 107 | return $quizQuestion->is_optional ? 0 : -$negative_marks; // If the question is optional, then the negative marks will be 0 108 | } 109 | return count($quizQuestionAnswers) ? (float) $quizQuestion->marks : 0; // Incase of no correct answer, if there is any answer then give full marks 110 | } 111 | 112 | /** 113 | * @param QuizAttemptAnswer[] $quizQuestionAnswers All the answers of the quiz question 114 | */ 115 | public static function get_score_for_type_3_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array | Collection $quizQuestionAnswers, $data = null): float 116 | { 117 | $quiz = $quizAttempt->quiz; 118 | $question = $quizQuestion->question; 119 | $correct_answer = ($question->correct_options())->first()->option; 120 | $negative_marks = self::get_negative_marks_for_question($quiz, $quizQuestion); 121 | if (!empty($correct_answer)) { 122 | 123 | if (count($quizQuestionAnswers)) { 124 | if (is_array($quizQuestionAnswers)) { 125 | return $quizQuestionAnswers[0]->answer == $correct_answer ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks 126 | } 127 | return $quizQuestionAnswers->first()->answer == $correct_answer ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks 128 | } 129 | return $quizQuestion->is_optional ? 0 : -$negative_marks; // If the question is optional, then the negative marks will be 0 130 | } 131 | return count($quizQuestionAnswers) ? (float) $quizQuestion->marks : 0; // Incase of no correct answer, if there is any answer then give full marks 132 | } 133 | 134 | public static function get_negative_marks_for_question(Quiz $quiz, QuizQuestion $quizQuestion): float 135 | { 136 | $negative_marking_settings = $quiz->negative_marking_settings ?? [ 137 | 'enable_negative_marks' => true, 138 | 'negative_marking_type' => 'fixed', 139 | 'negative_mark_value' => 0, 140 | ]; 141 | if (!$negative_marking_settings['enable_negative_marks']) { // If negative marking is disabled 142 | return 0; 143 | } 144 | if (!empty($quizQuestion->negative_marks)) { 145 | return $negative_marking_settings['negative_marking_type'] == 'fixed' ? 146 | ($quizQuestion->negative_marks < 0 ? -$quizQuestion->negative_marks : $quizQuestion->negative_marks) : ($quizQuestion->marks * (($quizQuestion->negative_marks < 0 ? -$quizQuestion->negative_marks : $quizQuestion->negative_marks) / 100)); 147 | } 148 | return $negative_marking_settings['negative_marking_type'] == 'fixed' ? ($negative_marking_settings['negative_mark_value'] < 0 ? -$negative_marking_settings['negative_mark_value'] : $negative_marking_settings['negative_mark_value']) : ($quizQuestion->marks * (($negative_marking_settings['negative_mark_value'] < 0 ? -$negative_marking_settings['negative_mark_value'] : $negative_marking_settings['negative_mark_value']) / 100)); 149 | } 150 | 151 | private function validateQuizQuestion(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, mixed $data = null): array 152 | { 153 | $isCorrect = true; 154 | $actualQuestion = $quizQuestion->question; 155 | $answers = $quizQuestion->answers->where('quiz_attempt_id', $quizAttempt->id); 156 | $questionType = $actualQuestion->question_type; 157 | $score = call_user_func_array(config('laravel-quiz.get_score_for_question_type')[$questionType->id], [$this, $quizQuestion, $answers ?? [], $data]); 158 | if ($score <= 0) { 159 | $isCorrect = false; 160 | } 161 | list($correctAnswer, $userAnswer) = config('laravel-quiz.render_answers_responses')[$questionType->id]($quizQuestion, $quizAttempt, $data); 162 | return [ 163 | 'score' => $score, 164 | 'is_correct' => $isCorrect, 165 | 'correct_answer' => $correctAnswer, 166 | 'user_answer' => $userAnswer, 167 | ]; 168 | } 169 | 170 | /** 171 | * @param int|null $quizQuestionId 172 | * @param $data mixed|null data to be passed to the user defined function to evaluate different question types 173 | * @return array|null [1=>['score'=>10,'is_correct'=>true,'correct_answer'=>'a','user_answer'=>'a']] 174 | */ 175 | public function validate(int | null $quizQuestionId = null, mixed $data = null): array | null 176 | { 177 | if ($quizQuestionId) { 178 | $quizQuestion = QuizQuestion::where(['quiz_id' => $this->quiz_id, 'id' => $quizQuestionId]) 179 | ->with('question') 180 | ->with('answers') 181 | ->with('question.options')->first(); 182 | if ($quizQuestion) { 183 | return [$quizQuestionId => $this->validateQuizQuestion($this, $quizQuestion, $data)]; 184 | } 185 | return null; //QuizQuestion is empty 186 | } 187 | $quizQuestions = QuizQuestion::where(['quiz_id' => $this->quiz_id])->get(); 188 | if ($quizQuestions->count() == 0) { 189 | return null; 190 | } 191 | $result = []; 192 | foreach ($quizQuestions as $quizQuestion) { 193 | $result[$quizQuestion->id] = $this->validateQuizQuestion($this, $quizQuestion); 194 | } 195 | return $result; 196 | } 197 | 198 | public static function renderQuestionType1Answers(QuizQuestion $quizQuestion, QuizAttempt $quizAttempt, mixed $data = null) 199 | { 200 | /** 201 | * @var Question $actualQuestion 202 | */ 203 | $actualQuestion = $quizQuestion->question; 204 | $answers = $quizQuestion->answers->where('quiz_attempt_id', $quizAttempt->id); 205 | $questionOptions = $actualQuestion->options; 206 | $correctAnswer = $actualQuestion->correct_options()->first()?->option; 207 | $givenAnswer = $answers->first()?->question_option_id; 208 | foreach ($questionOptions as $questionOption) { 209 | if ($questionOption->id == $givenAnswer) { 210 | $givenAnswer = $questionOption->option; 211 | break; 212 | } 213 | } 214 | return [$correctAnswer, $givenAnswer]; 215 | } 216 | 217 | public static function renderQuestionType2Answers(QuizQuestion $quizQuestion, QuizAttempt $quizAttempt, mixed $data = null) 218 | { 219 | $actualQuestion = $quizQuestion->question; 220 | $userAnswersCollection = $quizQuestion->answers->where('quiz_attempt_id', $quizAttempt->id); 221 | $correctAnswersCollection = $actualQuestion->correct_options(); 222 | $correctAnswers = $userAnswers = []; 223 | foreach ($correctAnswersCollection as $correctAnswer) { 224 | $correctAnswers[] = $correctAnswer->option; 225 | } 226 | foreach ($userAnswersCollection as $userAnswer) { 227 | $userAnswers[] = $userAnswer?->question_option?->option; 228 | } 229 | return [$correctAnswers, $userAnswers]; 230 | } 231 | 232 | public static function renderQuestionType3Answers(QuizQuestion $quizQuestion, QuizAttempt $quizAttempt, mixed $data = null) 233 | { 234 | $actualQuestion = $quizQuestion->question; 235 | $userAnswersCollection = $quizQuestion->answers->where('quiz_attempt_id', $quizAttempt->id); 236 | $correctAnswersCollection = $actualQuestion->correct_options(); 237 | $userAnswer = $userAnswersCollection->first()?->answer; 238 | $correctAnswer = $correctAnswersCollection->first()?->option; 239 | return [$correctAnswer, $userAnswer]; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Laravel Quiz](https://user-images.githubusercontent.com/10380630/172400217-b9192a50-3227-4d30-8e00-7a301fe68ddc.png) 2 | 3 | # Laravel Quiz 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/harishdurga/laravel-quiz.svg?style=flat-square)](https://packagist.org/packages/harishdurga/laravel-quiz) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/harishdurga/laravel-quiz.svg?style=flat-square)](https://packagist.org/packages/harishdurga/laravel-quiz) 7 | ![GitHub Actions](https://github.com/harishdurga/laravel-quiz/actions/workflows/main.yml/badge.svg) 8 | 9 | With this package, you can easily get quiz functionality into your Laravel project. 10 | 11 | ## Features 12 | 13 | - Add Topics to Questions, Quizzes and to other Topics 14 | - Supported Question Types: Multiple Choice, Single Choice, and Fill In The Blank 15 | - Add your own Question Types and define your own methods to handle them 16 | - Flexible Negative Marking Settings 17 | - Flexible Quiz with most of the useful settings (Ex: Total marks, Pass marks, Negative Marking, Duration, Valid between date, Description etc) 18 | - Any type of User of your application can be a Participant of a Quiz 19 | - Any type of User, and any number of Users of your application can be Authors (different roles) For a Quiz 20 | - Generate Random Quizzes (In progress) 21 | 22 | ## Installation 23 | 24 | You can install the package via Composer: 25 | 26 | ```bash 27 | composer require harishdurga/laravel-quiz 28 | ``` 29 | 30 | - Laravel Version: 9.X 31 | - PHP Version: 8.X 32 | 33 | ## Usage 34 | 35 | ### Class Diagram 36 | 37 | ![LaravelQuiz](https://user-images.githubusercontent.com/10380630/182762726-de5d4b61-af3c-4d0f-b25d-dad986ff5b6e.jpg) 38 | 39 | ### Publish Vendor Files (config, migrations, seeder) 40 | 41 | ```bash 42 | php artisan vendor:publish --provider="Harishdurga\LaravelQuiz\LaravelQuizServiceProvider" 43 | ``` 44 | 45 | If you are updating the package, you may need to run the above command to publish the vendor files. But please take a backup of the config file. Also, run the migration command to add new columns to the existing tables. 46 | 47 | ### Create Topic 48 | 49 | ```php 50 | $computer_science = Topic::create([ 51 | 'name' => 'Computer Science', 52 | 'slug' => 'computer-science', 53 | ]); 54 | ``` 55 | 56 | #### Create Sub Topics 57 | 58 | ```php 59 | $algorithms = Topic::create([ 60 | 'name' => 'Algorithms', 61 | 'slug' => 'algorithms' 62 | ]); 63 | $computer_science->children()->save($algorithms); 64 | ``` 65 | 66 | ### Question Types 67 | 68 | A seeder class `QuestionTypeSeeder ` will be published into the `database/seeders` folder. Run the following command to seed question types. 69 | 70 | ```bash 71 | php artisan db:seed --class=\\Harishdurga\\LaravelQuiz\\Database\\Seeders\\QuestionTypeSeeder 72 | ``` 73 | 74 | Currently, this package is configured to only handle the following types of questions 75 | 76 | - `multiple_choice_single_answer` 77 | - `multiple_choice_multiple_answer` 78 | - `fill_the_blank` 79 | 80 | Create a QuestionType: 81 | 82 | ```php 83 | QuestionType::create(['name'=>'select_all']); 84 | ``` 85 | 86 | ### User-Defined Methods To Evaluate The Answer For Each Question Type 87 | 88 | Though this package provides three question types you can easily change the method that is used to evaluate the answer. You can do this by updating the `get_score_for_question_type` property in config file. 89 | 90 | ```php 91 | 'get_score_for_question_type' => [ 92 | 1 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_1_question', 93 | 2 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_2_question', 94 | 3 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_3_question', 95 | 4 => 'Your custom method' 96 | ] 97 | ``` 98 | 99 | But your method needs to have the following signature 100 | 101 | ```php 102 | /** 103 | * @param QuizAttemptAnswer[] $quizQuestionAnswers All the answers of the quiz question 104 | */ 105 | public static function get_score_for_type_3_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array $quizQuestionAnswers, $data = null): float 106 | { 107 | // Your logic here 108 | } 109 | ``` 110 | 111 | If you need to pass any data to your method then you can pass it as the last `$data` parameter. When you call the `calculate_score()` method of `QuizAttempt` then you can pass the data as the parameter. 112 | 113 | ### Create Question 114 | 115 | ```php 116 | $question_one = Question::create([ 117 | 'name' => 'What is an algorithm?', 118 | 'question_type_id' => 1, 119 | 'is_active' => true, 120 | 'media_url' => 'url', 121 | 'media_type' => 'image' 122 | ]); 123 | ``` 124 | 125 | ### Fetch Questions Of A Question Type 126 | 127 | ```php 128 | $question_type->questions 129 | ``` 130 | 131 | ### Fetch only questions with an option (valid question) 132 | 133 | ```php 134 | Question::hasOptions()->get() 135 | ``` 136 | 137 | ### Attach Topics To Question 138 | 139 | ```php 140 | $question->topics()->attach([$computer_science->id, $algorithms->id]); 141 | ``` 142 | 143 | ### Question Option 144 | 145 | ```php 146 | $question_two_option_one = QuestionOption::create([ 147 | 'question_id' => $question_two->id, 148 | 'name' => 'array', 149 | 'is_correct' => true, 150 | 'media_type'=>'image', 151 | 'media_url'=>'media url' 152 | ]); 153 | ``` 154 | 155 | ### Fetch Options Of A Question 156 | 157 | ```php 158 | $question->options 159 | ``` 160 | 161 | ### Create Quiz 162 | 163 | ```php 164 | $quiz = Quiz::create([ 165 | 'name' => 'Computer Science Quiz', 166 | 'description' => 'Test your knowledge of computer science', 167 | 'slug' => 'computer-science-quiz', 168 | 'time_between_attempts' => 0, //Time in seconds between each attempt 169 | 'total_marks' => 10, 170 | 'pass_marks' => 6, 171 | 'max_attempts' => 1, 172 | 'is_published' => 1, 173 | 'valid_from' => now(), 174 | 'valid_upto' => now()->addDay(5), 175 | 'media_url'=>'', 176 | 'media_type'=>'', 177 | 'negative_marking_settings'=>[ 178 | 'enable_negative_marks' => true, 179 | 'negative_marking_type' => 'fixed', 180 | 'negative_mark_value' => 0, 181 | ] 182 | ]); 183 | ``` 184 | 185 | ### Attach Topics To A Quiz 186 | 187 | ```php 188 | $quiz->topics()->attach([$topic_one->id, $topic_two->id]); 189 | ``` 190 | 191 | ### Topicable 192 | 193 | Topics can be attached to a quiz or a question. Questions can exist outside of the quiz context. For example, you can create a question bank which you can filter based on the topics if attached. 194 | 195 | ### Negative Marking Settings 196 | 197 | By default negative marking is enabled for backward compatibility. You can disable it by setting the `enable_negative_marks` to false. Two types of negative marking are supported(`negative_marking_type`). `fixed` and `percentage`. Negative marking value defined at question level will be given precedence over the value defined at quiz level. If you want to set the negative marking value at quiz level, set the `negative_mark_value` to the value you want to set. If you want to set the negative marking value at question level, set the `negative_marks` of `QuizQuestion` to your desired value. No need to give a negative number instead the negative marks or percentage should be given in positive. 198 | 199 | ### Adding An Author(s) To A Quiz 200 | 201 | ```php 202 | $admin = Author::create( 203 | ['name' => "John Doe"] 204 | ); 205 | $quiz = Quiz::factory()->make()->create([ 206 | 'name' => 'Sample Quiz', 207 | 'slug' => 'sample-quiz' 208 | ]); 209 | QuizAuthor::create([ 210 | 'quiz_id' => $quiz->id, 211 | 'author_id' => $admin->id, 212 | 'author_type' => get_class($admin), 213 | 'author_role' => 'admin', 214 | ]); 215 | $quiz->quizAuthors->first()->author; //Original User 216 | ``` 217 | 218 | Add `CanAuthorQuiz` trait to your model and you can get all the quizzes associated by calling the `quizzes` relation. You can give any author role you want and implement ACL as per your use case. 219 | 220 | ### Add Question To Quiz 221 | 222 | ```php 223 | $quiz_question = QuizQuestion::create([ 224 | 'quiz_id' => $quiz->id, 225 | 'question_id' => $question->id, 226 | 'marks' => 3, 227 | 'order' => 1, 228 | 'negative_marks'=>1, 229 | 'is_optional'=>false 230 | ]); 231 | ``` 232 | 233 | ### Fetch Quiz Questions 234 | 235 | ```php 236 | $quiz->questions 237 | ``` 238 | 239 | ### Attempt The Quiz 240 | 241 | ```php 242 | $quiz_attempt = QuizAttempt::create([ 243 | 'quiz_id' => $quiz->id, 244 | 'participant_id' => $participant->id, 245 | 'participant_type' => get_class($participant) 246 | ]); 247 | ``` 248 | 249 | ### Get the Quiz Attempt Participant 250 | 251 | `MorphTo` relation. 252 | 253 | ```php 254 | $quiz_attempt->participant 255 | ``` 256 | 257 | ### Answer Quiz Attempt 258 | 259 | ```php 260 | QuizAttemptAnswer::create( 261 | [ 262 | 'quiz_attempt_id' => $quiz_attempt->id, 263 | 'quiz_question_id' => $quiz_question->id, 264 | 'question_option_id' => $question_option->id, 265 | ] 266 | ); 267 | ``` 268 | 269 | A `QuizAttemptAnswer` belongs to `QuizAttempt`,`QuizQuestion` and `QuestionOption` 270 | 271 | ### Get Quiz Attempt Score 272 | 273 | ```php 274 | $quiz_attempt->calculate_score() 275 | ``` 276 | 277 | In case of no answer found for a quiz question which is not optional, a negative score will be applied if any. 278 | 279 | ### Get Correct Option Of A Question 280 | 281 | ```php 282 | $question->correct_options 283 | ``` 284 | 285 | Return a collection of `QuestionOption`. 286 | 287 | ```php 288 | public function correct_options(): Collection 289 | { 290 | return $this->options()->where('is_correct', 1)->get(); 291 | } 292 | ``` 293 | 294 | Please refer to unit and features tests for more understanding. 295 | 296 | ### Validate A Quiz Question 297 | 298 | Instead of getting total score for the quiz attempt, you can use `QuizAttempt` model's `validate()` method. This method will return an array with a QuizQuestion model's 299 | `id` as the key for the assoc array that will be returned. 300 | **Example:** 301 | 302 | ```injectablephp 303 | $quizAttempt->validate($quizQuestion->id); //For a particular question 304 | $quizAttempt->validate(); //For all the questions in the quiz attempt 305 | $quizAttempt->validate($quizQuestion->id,$data); //$data can any type 306 | ``` 307 | 308 | ```php 309 | [ 310 | 1 => [ 311 | 'score' => 10, 312 | 'is_correct' => true, 313 | 'correct_answer' => ['One','Five','Seven'], 314 | 'user_answer' => ['Five','One','Seven'] 315 | ], 316 | 2 => [ 317 | 'score' => 0, 318 | 'is_correct' => false, 319 | 'correct_answer' => 'Hello There', 320 | 'user_answer' => 'Hello World' 321 | ] 322 | ] 323 | ``` 324 | 325 | To be able to render the user answer and correct answer for different types of question types other than the 3 types supported by the package, a new config option has been added. 326 | 327 | ```php 328 | 'render_answers_responses' => [ 329 | 1 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType1Answers', 330 | 2 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType2Answers', 331 | 3 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType3Answers', 332 | ] 333 | ``` 334 | 335 | By keeping the question type id as the key, you can put the path to your custom function to handle the question type. This custom method will be called from inside the 336 | `validate()` method by passing the `QuizQuestion` object as the argument for your custom method as defined in the config. 337 | **Example:** 338 | 339 | ```php 340 | public static function renderQuestionType1Answers(QuizQuestion $quizQuestion,QuizAttempt $quizAttempt,mixed $data=null) 341 | { 342 | /** 343 | * @var Question $actualQuestion 344 | */ 345 | $actualQuestion = $quizQuestion->question; 346 | $answers = $quizQuestion->answers->where('quiz_attempt_id', $quizAttempt->id); 347 | $questionOptions = $actualQuestion->options; 348 | $correctAnswer = $actualQuestion->correct_options()->first()?->option; 349 | $givenAnswer = $answers->first()?->question_option_id; 350 | foreach ($questionOptions as $questionOption) { 351 | if ($questionOption->id == $givenAnswer) { 352 | $givenAnswer = $questionOption->option; 353 | break; 354 | } 355 | } 356 | return [$correctAnswer, $givenAnswer]; 357 | } 358 | ``` 359 | 360 | As shown in the example your customer method should return an array with two elements the first one being the correct answer and the second element being the user's answer for the question. 361 | And whatever the `$data` you send to the `validate()` will be sent to these custom methods so that you can send additional data for rendering the answers. 362 | 363 | ### Testing 364 | 365 | ```bash 366 | composer test 367 | ``` 368 | 369 | ### Changelog 370 | 371 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 372 | 373 | ## Contributing 374 | 375 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 376 | 377 | ### Security 378 | 379 | If you discover any security-related issues, please email durgaharish5@gmail.com instead of using the issue tracker. 380 | 381 | ## Credits 382 | 383 | - [Harish Durga](https://github.com/harishdurga) 384 | - [All Contributors](../../contributors) 385 | 386 | ## License 387 | 388 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 389 | 390 | ## Laravel Package Boilerplate 391 | 392 | This package was generated using the [Laravel Package Boilerplate](https://laravelpackageboilerplate.com). 393 | -------------------------------------------------------------------------------- /tests/Unit/QuizAttemptTest.php: -------------------------------------------------------------------------------- 1 | "John Doe"] 26 | ); 27 | $options = []; 28 | //Question Types 29 | QuestionType::insert( 30 | [ 31 | [ 32 | 'name' => 'multiple_choice_single_answer', 33 | ], 34 | [ 35 | 'name' => 'multiple_choice_multiple_answer', 36 | ], 37 | [ 38 | 'name' => 'fill_the_blank', 39 | ], 40 | ] 41 | ); 42 | if ($questionType == 1) { 43 | $question = Question::factory()->create([ 44 | 'name' => 'How many layers in OSI model?', 45 | 'question_type_id' => 1, 46 | 'is_active' => false, 47 | ]); 48 | $question_option_one = QuestionOption::factory()->create([ 49 | 'question_id' => $question->id, 50 | 'name' => '5', 51 | 'is_correct' => false, 52 | ]); 53 | $question_option_two = QuestionOption::factory()->create([ 54 | 'question_id' => $question->id, 55 | 'name' => '8', 56 | 'is_correct' => false, 57 | ]); 58 | $question_option_three = QuestionOption::factory()->create([ 59 | 'question_id' => $question->id, 60 | 'name' => '10', 61 | 'is_correct' => false, 62 | ]); 63 | $question_option_four = QuestionOption::factory()->create([ 64 | 'question_id' => $question->id, 65 | 'name' => '7', 66 | 'is_correct' => true, 67 | ]); 68 | $options = [$question_option_one, $question_option_two, $question_option_three, $question_option_four]; 69 | } elseif ($questionType == 2) { 70 | $question = Question::factory()->create([ 71 | 'name' => 'Which of the below is a data structure?', 72 | 'question_type_id' => 2, 73 | 'is_active' => true, 74 | ]); 75 | $question_option_one = QuestionOption::factory()->create([ 76 | 'question_id' => $question->id, 77 | 'name' => 'array', 78 | 'is_correct' => true, 79 | ]); 80 | $question_option_two = QuestionOption::factory()->create([ 81 | 'question_id' => $question->id, 82 | 'name' => 'object', 83 | 'is_correct' => true, 84 | ]); 85 | $question_option_three = QuestionOption::factory()->create([ 86 | 'question_id' => $question->id, 87 | 'name' => 'for loop', 88 | 'is_correct' => false, 89 | ]); 90 | $question_option_four = QuestionOption::factory()->create([ 91 | 'question_id' => $question->id, 92 | 'name' => 'method', 93 | 'is_correct' => false, 94 | ]); 95 | $options = [$question_option_one, $question_option_two, $question_option_three, $question_option_four]; 96 | } else { 97 | $question = Question::factory()->create([ 98 | 'name' => 'Full Form Of CPU', 99 | 'question_type_id' => 3, 100 | 'is_active' => true, 101 | ]); 102 | $question_option_one = QuestionOption::factory()->create([ 103 | 'question_id' => $question->id, 104 | 'name' => 'central processing unit', 105 | 'is_correct' => true, 106 | ]); 107 | $options = [$question_option_one]; 108 | } 109 | $quiz = Quiz::factory()->make()->create([ 110 | 'name' => 'Sample Quiz', 111 | 'slug' => 'sample-quiz', 112 | 'negative_marking_settings' => [ 113 | 'enable_negative_marks' => $enableNegativeMarks, 114 | 'negative_marking_type' => $negativeMarkingType, 115 | 'negative_mark_value' => $quizNegativemarkValue, 116 | ], 117 | ]); 118 | $quizQuestion = QuizQuestion::factory()->create([ 119 | 'quiz_id' => $quiz->id, 120 | 'question_id' => $question->id, 121 | 'marks' => $marks, 122 | 'order' => 1, 123 | 'negative_marks' => $negativeMarks, 124 | ]); 125 | $quizAttempt = QuizAttempt::create([ 126 | 'quiz_id' => $quiz->id, 127 | 'participant_id' => $user->id, 128 | 'participant_type' => get_class($user), 129 | ]); 130 | return [$user, $question, $options, $quiz, $quizQuestion, $quizAttempt]; 131 | } 132 | 133 | #[Test] 134 | public function get_score_for_type_1_question_no_negative_marks() 135 | { 136 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(1, false, Quiz::PERCENTAGE_NEGATIVE_TYPE, 0, 5, 0); 137 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 138 | //Quiz Attempt And Answers 139 | $quiz_attempt = QuizAttempt::create([ 140 | 'quiz_id' => $quiz->id, 141 | 'participant_id' => $user->id, 142 | 'participant_type' => get_class($user), 143 | ]); 144 | $quiz_attempt_answer = QuizAttemptAnswer::create( 145 | [ 146 | 'quiz_attempt_id' => $quiz_attempt->id, 147 | 'quiz_question_id' => $quiz_question->id, 148 | 'question_option_id' => $question_option_four->id, 149 | ] 150 | ); 151 | $this->assertEquals(5, QuizAttempt::get_score_for_type_1_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer])); 152 | } 153 | 154 | #[Test] 155 | public function get_score_for_type_1_question_with_negative_marks_question_fixed() 156 | { 157 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(1, true, Quiz::FIXED_NEGATIVE_TYPE, 0, 5, 1); 158 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 159 | $quiz_attempt_answer = QuizAttemptAnswer::create( 160 | [ 161 | 'quiz_attempt_id' => $quiz_attempt->id, 162 | 'quiz_question_id' => $quiz_question->id, 163 | 'question_option_id' => $question_option_three->id, 164 | ] 165 | ); 166 | $this->assertEquals(-1, QuizAttempt::get_score_for_type_1_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer])); 167 | } 168 | 169 | #[Test] 170 | public function get_score_for_type_1_question_with_negative_marks_question_percentage() 171 | { 172 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(1, true, Quiz::PERCENTAGE_NEGATIVE_TYPE, 0, 5, 10); 173 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 174 | $quiz_attempt_answer = QuizAttemptAnswer::create( 175 | [ 176 | 'quiz_attempt_id' => $quiz_attempt->id, 177 | 'quiz_question_id' => $quiz_question->id, 178 | 'question_option_id' => $question_option_three->id, 179 | ] 180 | ); 181 | $this->assertEquals(-0.5, QuizAttempt::get_score_for_type_1_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer])); 182 | } 183 | 184 | #[Test] 185 | public function get_score_for_type_1_question_with_negative_marks_quiz_fixed() 186 | { 187 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(1, true, Quiz::FIXED_NEGATIVE_TYPE, 2, 10, 0); 188 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 189 | 190 | $quiz_attempt_answer = QuizAttemptAnswer::create( 191 | [ 192 | 'quiz_attempt_id' => $quiz_attempt->id, 193 | 'quiz_question_id' => $quiz_question->id, 194 | 'question_option_id' => $question_option_three->id, 195 | ] 196 | ); 197 | $this->assertEquals(-2, QuizAttempt::get_score_for_type_1_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer])); 198 | } 199 | 200 | #[Test] 201 | public function get_score_for_type_1_question_with_negative_marks_quiz_percentage() 202 | { 203 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(1, true, Quiz::PERCENTAGE_NEGATIVE_TYPE, 5, 10, 0); 204 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 205 | $quiz_attempt_answer = QuizAttemptAnswer::create( 206 | [ 207 | 'quiz_attempt_id' => $quiz_attempt->id, 208 | 'quiz_question_id' => $quiz_question->id, 209 | 'question_option_id' => $question_option_three->id, 210 | ] 211 | ); 212 | $this->assertEquals(-0.5, QuizAttempt::get_score_for_type_1_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer])); 213 | } 214 | 215 | #[Test] 216 | public function get_score_for_type_2_question_no_negative_marks() 217 | { 218 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(2, false, Quiz::PERCENTAGE_NEGATIVE_TYPE, 0, 8, 2); 219 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 220 | 221 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 222 | [ 223 | 'quiz_attempt_id' => $quiz_attempt->id, 224 | 'quiz_question_id' => $quiz_question->id, 225 | 'question_option_id' => $question_option_one->id, 226 | ] 227 | ); 228 | $quiz_attempt_answer_two = QuizAttemptAnswer::create( 229 | [ 230 | 'quiz_attempt_id' => $quiz_attempt->id, 231 | 'quiz_question_id' => $quiz_question->id, 232 | 'question_option_id' => $question_option_two->id, 233 | ] 234 | ); 235 | $this->assertEquals(8, QuizAttempt::get_score_for_type_2_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one, $quiz_attempt_answer_two])); 236 | } 237 | 238 | #[Test] 239 | public function get_score_for_type_2_question_with_negative_marks_question_fixed() 240 | { 241 | 242 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(2, true, Quiz::FIXED_NEGATIVE_TYPE, 0, 8, 2); 243 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 244 | 245 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 246 | [ 247 | 'quiz_attempt_id' => $quiz_attempt->id, 248 | 'quiz_question_id' => $quiz_question->id, 249 | 'question_option_id' => $question_option_one->id, 250 | ] 251 | ); 252 | $this->assertEquals(-2, QuizAttempt::get_score_for_type_2_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 253 | } 254 | 255 | #[Test] 256 | public function get_score_for_type_2_question_with_negative_marks_question_percentage() 257 | { 258 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(2, true, Quiz::PERCENTAGE_NEGATIVE_TYPE, 0, 8, 2); 259 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 260 | 261 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 262 | [ 263 | 'quiz_attempt_id' => $quiz_attempt->id, 264 | 'quiz_question_id' => $quiz_question->id, 265 | 'question_option_id' => $question_option_one->id, 266 | ] 267 | ); 268 | $this->assertEquals(-0.16, QuizAttempt::get_score_for_type_2_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 269 | } 270 | 271 | #[Test] 272 | public function get_score_for_type_2_question_with_negative_marks_quiz_fixed() 273 | { 274 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(2, true, Quiz::FIXED_NEGATIVE_TYPE, 1, 5, 0); 275 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 276 | 277 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 278 | [ 279 | 'quiz_attempt_id' => $quiz_attempt->id, 280 | 'quiz_question_id' => $quiz_question->id, 281 | 'question_option_id' => $question_option_one->id, 282 | ] 283 | ); 284 | $this->assertEquals(-1, QuizAttempt::get_score_for_type_2_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 285 | } 286 | 287 | #[Test] 288 | public function get_score_for_type_2_question_with_negative_marks_quiz_percentage() 289 | { 290 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(2, true, Quiz::PERCENTAGE_NEGATIVE_TYPE, 10, 5, 0); 291 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 292 | 293 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 294 | [ 295 | 'quiz_attempt_id' => $quiz_attempt->id, 296 | 'quiz_question_id' => $quiz_question->id, 297 | 'question_option_id' => $question_option_one->id, 298 | ] 299 | ); 300 | $this->assertEquals(-0.5, QuizAttempt::get_score_for_type_2_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 301 | } 302 | 303 | #[Test] 304 | public function get_score_for_type_3_question_no_negative_marks() 305 | { 306 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(3, false, Quiz::PERCENTAGE_NEGATIVE_TYPE, 0, 5, 0); 307 | [$question_option_one] = $options; 308 | 309 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 310 | [ 311 | 'quiz_attempt_id' => $quiz_attempt->id, 312 | 'quiz_question_id' => $quiz_question->id, 313 | 'question_option_id' => $question_option_one->id, 314 | 'answer' => 'central processing unit', 315 | ] 316 | ); 317 | $this->assertEquals(5, QuizAttempt::get_score_for_type_3_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 318 | } 319 | 320 | #[Test] 321 | public function get_score_for_type_3_question_with_negative_marks_question_fixed() 322 | { 323 | 324 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(3, true, Quiz::FIXED_NEGATIVE_TYPE, 0, 5, 1); 325 | [$question_option_one] = $options; 326 | 327 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 328 | [ 329 | 'quiz_attempt_id' => $quiz_attempt->id, 330 | 'quiz_question_id' => $quiz_question->id, 331 | 'question_option_id' => $question_option_one->id, 332 | 'answer' => 'cpu', 333 | ] 334 | ); 335 | $this->assertEquals(-1, QuizAttempt::get_score_for_type_3_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 336 | } 337 | 338 | #[Test] 339 | public function get_score_for_type_3_question_with_negative_marks_question_percentage() 340 | { 341 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(3, true, Quiz::PERCENTAGE_NEGATIVE_TYPE, 0, 10, 10); 342 | [$question_option_one] = $options; 343 | 344 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 345 | [ 346 | 'quiz_attempt_id' => $quiz_attempt->id, 347 | 'quiz_question_id' => $quiz_question->id, 348 | 'question_option_id' => $question_option_one->id, 349 | 'answer' => 'cpu', 350 | ] 351 | ); 352 | $this->assertEquals(-1, QuizAttempt::get_score_for_type_3_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 353 | } 354 | 355 | #[Test] 356 | public function get_score_for_type_3_question_with_negative_marks_quiz_fixed() 357 | { 358 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(3, true, Quiz::FIXED_NEGATIVE_TYPE, 1, 5, 0); 359 | [$question_option_one] = $options; 360 | 361 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 362 | [ 363 | 'quiz_attempt_id' => $quiz_attempt->id, 364 | 'quiz_question_id' => $quiz_question->id, 365 | 'question_option_id' => $question_option_one->id, 366 | 'answer' => 'cpu', 367 | ] 368 | ); 369 | $this->assertEquals(-1, QuizAttempt::get_score_for_type_3_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 370 | } 371 | 372 | #[Test] 373 | public function get_score_for_type_3_question_with_negative_marks_quiz_percentage() 374 | { 375 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init(3, true, Quiz::PERCENTAGE_NEGATIVE_TYPE, 10, 5, 0); 376 | [$question_option_one] = $options; 377 | 378 | $quiz_attempt_answer_one = QuizAttemptAnswer::create( 379 | [ 380 | 'quiz_attempt_id' => $quiz_attempt->id, 381 | 'quiz_question_id' => $quiz_question->id, 382 | 'question_option_id' => $question_option_one->id, 383 | 'answer' => 'cpu', 384 | ] 385 | ); 386 | $this->assertEquals(-0.5, QuizAttempt::get_score_for_type_3_question($quiz_attempt, $quiz_question, [$quiz_attempt_answer_one])); 387 | } 388 | 389 | #[Test] 390 | public function get_negative_marks_for_question() 391 | { 392 | $testCases = [ 393 | [ 394 | 'enable_negative_marks' => false, 395 | 'quiz_negative_marks_type' => Quiz::FIXED_NEGATIVE_TYPE, 396 | 'quiz_negative_marks' => 0, 397 | 'question_marks' => 5, 398 | 'question_negative_marks' => 0, 399 | 'expected_negative_marks' => 0, 400 | ], 401 | [ 402 | 'enable_negative_marks' => true, 403 | 'quiz_negative_marks_type' => Quiz::FIXED_NEGATIVE_TYPE, 404 | 'quiz_negative_marks' => 0, 405 | 'question_marks' => 5, 406 | 'question_negative_marks' => 1, 407 | 'expected_negative_marks' => 1, 408 | ], 409 | [ 410 | 'enable_negative_marks' => true, 411 | 'quiz_negative_marks_type' => Quiz::PERCENTAGE_NEGATIVE_TYPE, 412 | 'quiz_negative_marks' => 0, 413 | 'question_marks' => 5, 414 | 'question_negative_marks' => 30, 415 | 'expected_negative_marks' => 1.5, 416 | ], 417 | [ 418 | 'enable_negative_marks' => true, 419 | 'quiz_negative_marks_type' => Quiz::FIXED_NEGATIVE_TYPE, 420 | 'quiz_negative_marks' => 1, 421 | 'question_marks' => 5, 422 | 'question_negative_marks' => 0, 423 | 'expected_negative_marks' => 1, 424 | ], 425 | [ 426 | 'enable_negative_marks' => true, 427 | 'quiz_negative_marks_type' => Quiz::PERCENTAGE_NEGATIVE_TYPE, 428 | 'quiz_negative_marks' => 40, 429 | 'question_marks' => 5, 430 | 'question_negative_marks' => 0, 431 | 'expected_negative_marks' => 2, 432 | ], 433 | ]; 434 | $question = Question::factory()->create([ 435 | 'name' => 'Full Form Of CPU', 436 | 'question_type_id' => 3, 437 | 'is_active' => true, 438 | ]); 439 | foreach ($testCases as $key => $testCase) { 440 | $quiz = Quiz::factory()->make()->create([ 441 | 'name' => 'Sample Quiz', 442 | 'slug' => 'sample-quiz-' . $key, 443 | 'negative_marking_settings' => [ 444 | 'enable_negative_marks' => $testCase['enable_negative_marks'], 445 | 'negative_marking_type' => $testCase['quiz_negative_marks_type'], 446 | 'negative_mark_value' => $testCase['quiz_negative_marks'], 447 | ], 448 | ]); 449 | $quizQuestion = QuizQuestion::factory()->create([ 450 | 'quiz_id' => $quiz->id, 451 | 'question_id' => $question->id, 452 | 'marks' => $testCase['question_marks'], 453 | 'order' => 1, 454 | 'negative_marks' => $testCase['question_negative_marks'], 455 | ]); 456 | $this->assertEquals($testCase['expected_negative_marks'], QuizAttempt::get_negative_marks_for_question($quiz, $quizQuestion)); 457 | } 458 | } 459 | 460 | #[Test] 461 | public function get_quiz_attempt_result_with_and_without_quiz_question() 462 | { 463 | 464 | $testCases = [ 465 | [ 466 | 'name' => 'Question type 1 with no negative marks', 467 | 'question_type' => 1, 468 | 'enable_negative_marks' => false, 469 | 'negative_marking_type' => Quiz::PERCENTAGE_NEGATIVE_TYPE, 470 | 'quiz_negative_mark_value' => 0, 471 | 'marks' => 5, 472 | 'negative_marks' => 0, 473 | 'expected' => [1 => ['score' => 5, 'is_correct' => true, 'correct_answer' => '7', 'user_answer' => 7]], 474 | ], 475 | [ 476 | 'name' => 'Question type 2 with no negative marks', 477 | 'question_type' => 2, 478 | 'enable_negative_marks' => false, 479 | 'negative_marking_type' => Quiz::PERCENTAGE_NEGATIVE_TYPE, 480 | 'quiz_negative_mark_value' => 0, 481 | 'marks' => 5, 482 | 'negative_marks' => 0, 483 | 'expected' => [1 => ['score' => 5, 'is_correct' => true, 'correct_answer' => ['array', 'object'], 'user_answer' => ['array', 'object']]], 484 | ], 485 | [ 486 | 'name' => 'Question type 3 with no negative marks', 487 | 'question_type' => 3, 488 | 'enable_negative_marks' => false, 489 | 'negative_marking_type' => Quiz::PERCENTAGE_NEGATIVE_TYPE, 490 | 'quiz_negative_mark_value' => 0, 491 | 'marks' => 5, 492 | 'negative_marks' => 0, 493 | 'expected' => [1 => ['score' => 5, 'is_correct' => true, 'correct_answer' => 'central processing unit', 'user_answer' => 'central processing unit']], 494 | ], 495 | ]; 496 | foreach ($testCases as $testCase) { 497 | [$user, $question, $options, $quiz, $quiz_question, $quiz_attempt] = $this->init($testCase['question_type'], $testCase['enable_negative_marks'], $testCase['negative_marking_type'], $testCase['quiz_negative_mark_value'], $testCase['marks'], $testCase['negative_marks']); 498 | if ($testCase['question_type'] == 1 || $testCase['question_type'] == 2) { 499 | [$question_option_one, $question_option_two, $question_option_three, $question_option_four] = $options; 500 | } else { 501 | [$question_option_one] = $options; 502 | } 503 | if ($testCase['question_type'] == 1) { 504 | QuizAttemptAnswer::create( 505 | [ 506 | 'quiz_attempt_id' => $quiz_attempt->id, 507 | 'quiz_question_id' => $quiz_question->id, 508 | 'question_option_id' => $question_option_four->id, 509 | ] 510 | ); 511 | } elseif ($testCase['question_type'] == 2) { 512 | QuizAttemptAnswer::create( 513 | [ 514 | 'quiz_attempt_id' => $quiz_attempt->id, 515 | 'quiz_question_id' => $quiz_question->id, 516 | 'question_option_id' => $question_option_one->id, 517 | ] 518 | ); 519 | QuizAttemptAnswer::create( 520 | [ 521 | 'quiz_attempt_id' => $quiz_attempt->id, 522 | 'quiz_question_id' => $quiz_question->id, 523 | 'question_option_id' => $question_option_two->id, 524 | ] 525 | ); 526 | } else { 527 | QuizAttemptAnswer::create( 528 | [ 529 | 'quiz_attempt_id' => $quiz_attempt->id, 530 | 'quiz_question_id' => $quiz_question->id, 531 | 'question_option_id' => $question_option_one->id, 532 | 'answer' => 'central processing unit', 533 | ] 534 | ); 535 | } 536 | $this->assertEquals($testCase['expected'], $quiz_attempt->validate($quiz_question->id)); 537 | $this->assertEquals($testCase['expected'], $quiz_attempt->validate()); 538 | DB::raw("SET foreign_key_checks=0"); 539 | $databaseName = DB::getDatabaseName(); 540 | $tables = config('laravel-quiz.table_names'); 541 | foreach ($tables as $key => $name) { 542 | //if you don't want to truncate migrations 543 | if ($name == 'migrations') { 544 | continue; 545 | } 546 | DB::table($name)->truncate(); 547 | } 548 | DB::raw("SET foreign_key_checks=1"); 549 | } 550 | } 551 | 552 | #[Test] 553 | public function test_quiz_attempt_validate() 554 | { 555 | 556 | $user = Author::create( 557 | ['name' => "John Doe"] 558 | ); 559 | QuestionType::insert( 560 | [ 561 | [ 562 | 'name' => 'multiple_choice_single_answer', 563 | ], 564 | [ 565 | 'name' => 'multiple_choice_multiple_answer', 566 | ], 567 | [ 568 | 'name' => 'fill_the_blank', 569 | ], 570 | ] 571 | ); 572 | $questionsWithOptions = [ 573 | [ 574 | 'question' => 'How many world wonders are there?', 575 | 'options' => [ 576 | [1, 7, true], 577 | ], 578 | 'id' => 1, 579 | 'question_type' => 3, 580 | ], 581 | [ 582 | 'question' => 'What is the biggest desert in the world?', 583 | 'options' => [[2, 'Sahara', true]], 584 | 'id' => 2, 585 | 'question_type' => 3, 586 | ], 587 | [ 588 | 'question' => 'What is the biggest bird?', 589 | 'options' => [[3, 'Ostrich', true]], 590 | 'id' => 3, 591 | 'question_type' => 3, 592 | ], 593 | [ 594 | 'question' => 'Which One of these is not a continent?', 595 | 'options' => [ 596 | [4, 'US', true], [5, 'Asia', false], [6, 'Europe', false], [7, 'Australia', false], 597 | ], 598 | 'id' => 4, 599 | 'question_type' => 1, 600 | ], 601 | [ 602 | 'question' => 'Which of the following is a non metal that remains liquid at room temperature?', 603 | 'options' => [ 604 | [8, 'Phosphorous', false], [9, 'Bromine', true], [10, 'Chlorine', false], [11, 'Helium', false], 605 | ], 606 | 'id' => 5, 607 | 'question_type' => 1, 608 | ], 609 | [ 610 | 'question' => 'Select All The Mammals', 611 | 'options' => [ 612 | [12, 'cats', true], [13, 'Dogs', true], [14, 'apes', true], [15, 'None of the above', false], 613 | ], 614 | 'id' => 6, 615 | 'question_type' => 2, 616 | ], 617 | [ 618 | 'question' => 'Select All The Amphibians', 619 | 'options' => [ 620 | [16, 'frogs', true], [17, 'Dogs', false], [18, 'salamanders', true], [19, 'None of the above', false], 621 | ], 622 | 'id' => 7, 623 | 'question_type' => 2, 624 | ], 625 | 626 | ]; 627 | $quiz = Quiz::factory()->make()->create([ 628 | 'name' => 'Sample Quiz', 629 | 'slug' => 'sample-quiz', 630 | 'negative_marking_settings' => [ 631 | 'enable_negative_marks' => false, 632 | 'negative_marking_type' => Quiz::FIXED_NEGATIVE_TYPE, 633 | 'negative_mark_value' => 1, 634 | ], 635 | ]); 636 | $quizAttemptOne = QuizAttempt::create([ 637 | 'quiz_id' => $quiz->id, 638 | 'participant_id' => $user->id, 639 | 'participant_type' => get_class($user), 640 | ]); 641 | $quizAttemptTwo = QuizAttempt::create([ 642 | 'quiz_id' => $quiz->id, 643 | 'participant_id' => $user->id, 644 | 'participant_type' => get_class($user), 645 | ]); 646 | foreach ($questionsWithOptions as $questionsWithOption) { 647 | Question::factory()->create([ 648 | 'name' => $questionsWithOption['question'], 649 | 'question_type_id' => $questionsWithOption['question_type'], 650 | 'is_active' => true, 651 | 'id' => $questionsWithOption['id'], 652 | ]); 653 | foreach ($questionsWithOption['options'] as $option) { 654 | QuestionOption::factory()->create([ 655 | 'question_id' => $questionsWithOption['id'], 656 | 'name' => $option[1], 657 | 'is_correct' => $option[2], 658 | 'id' => $option[0], 659 | ]); 660 | } 661 | 662 | QuizQuestion::factory()->create([ 663 | 'quiz_id' => $quiz->id, 664 | 'question_id' => $questionsWithOption['id'], 665 | 'marks' => 1, 666 | 'order' => 1, 667 | 'negative_marks' => 0.5, 668 | 'id' => $questionsWithOption['id'], 669 | ]); 670 | foreach ($questionsWithOption['options'] as $option) { 671 | if ($option[2]) { 672 | QuizAttemptAnswer::create( 673 | [ 674 | 'quiz_attempt_id' => $quizAttemptOne->id, 675 | 'quiz_question_id' => $questionsWithOption['id'], 676 | 'question_option_id' => $option[0], 677 | 'answer' => $option[1], 678 | ] 679 | ); 680 | if ($questionsWithOption['id'] != 3) { //Skipping the third question being attempted 681 | QuizAttemptAnswer::create( 682 | [ 683 | 'quiz_attempt_id' => $quizAttemptTwo->id, 684 | 'quiz_question_id' => $questionsWithOption['id'], 685 | 'question_option_id' => $option[0], 686 | 'answer' => $option[1] . 's', 687 | ] 688 | ); 689 | } 690 | 691 | } 692 | 693 | } 694 | 695 | } 696 | 697 | $this->assertEquals([ 698 | 1 => [ 699 | 'score' => 1.0, 700 | "is_correct" => true, 701 | 'correct_answer' => 7, 702 | 'user_answer' => 7, 703 | ], 704 | 705 | 2 => [ 706 | 'score' => 1.0, 707 | 'is_correct' => true, 708 | 'correct_answer' => 'Sahara', 709 | 'user_answer' => 'Sahara', 710 | ], 711 | 712 | 3 => [ 713 | 'score' => 1.0, 714 | 'is_correct' => true, 715 | 'correct_answer' => 'Ostrich', 716 | 'user_answer' => 'Ostrich', 717 | ], 718 | 719 | 4 => [ 720 | 721 | 'score' => 1, 722 | 'is_correct' => true, 723 | 'correct_answer' => 'US', 724 | 'user_answer' => 'US', 725 | ], 726 | 727 | 5 => [ 728 | 'score' => 1, 729 | 'is_correct' => true, 730 | 'correct_answer' => 'Bromine', 731 | 'user_answer' => 'Bromine', 732 | ], 733 | 734 | 6 => [ 735 | 'score' => 1, 736 | 'is_correct' => true, 737 | 'correct_answer' => [ 738 | 0 => 'cats', 739 | 1 => 'Dogs', 740 | 2 => 'apes', 741 | ], 742 | 743 | 'user_answer' => [ 744 | 0 => 'cats', 745 | 1 => 'Dogs', 746 | 2 => 'apes', 747 | ], 748 | 749 | ], 750 | 751 | 7 => [ 752 | 'score' => 1, 753 | 'is_correct' => true, 754 | 'correct_answer' => [ 755 | 0 => 'frogs', 756 | 1 => 'salamanders', 757 | ], 758 | 759 | 'user_answer' => [ 760 | 0 => 'frogs', 761 | 1 => 'salamanders', 762 | ], 763 | ], 764 | 765 | ], $quizAttemptOne->validate(), 'Quiz Attempt With Correct Answers'); 766 | 767 | $this->assertEquals(array( 768 | 1 => array( 769 | 'score' => -0, 770 | 'is_correct' => false, 771 | 'correct_answer' => 7, 772 | 'user_answer' => '7s', 773 | ), 774 | 2 => array( 775 | 'score' => -0, 776 | 'is_correct' => false, 777 | 'correct_answer' => 'Sahara', 778 | 'user_answer' => 'Saharas', 779 | ), 780 | 3 => array( 781 | 'score' => -0, 782 | 'is_correct' => false, 783 | 'correct_answer' => 'Ostrich', 784 | 'user_answer' => '', 785 | ), 786 | 4 => array( 787 | 'score' => 1, 788 | 'is_correct' => true, 789 | 'correct_answer' => 'US', 790 | 'user_answer' => 'US', 791 | ), 792 | 5 => array( 793 | 'score' => 1, 794 | 'is_correct' => true, 795 | 'correct_answer' => 'Bromine', 796 | 'user_answer' => 'Bromine', 797 | ), 798 | 6 => array( 799 | 'score' => 1, 800 | 'is_correct' => true, 801 | 'correct_answer' => array( 802 | 0 => 'cats', 803 | 1 => 'Dogs', 804 | 2 => 'apes', 805 | ), 806 | 'user_answer' => array( 807 | 0 => 'cats', 808 | 1 => 'Dogs', 809 | 2 => 'apes', 810 | ), 811 | ), 812 | 7 => array( 813 | 'score' => 1, 814 | 'is_correct' => true, 815 | 'correct_answer' => array( 816 | 0 => 'frogs', 817 | 1 => 'salamanders', 818 | ), 819 | 'user_answer' => array( 820 | 0 => 'frogs', 821 | 1 => 'salamanders', 822 | ), 823 | ), 824 | ), $quizAttemptTwo->validate(), 'Quiz Attempt With Wrong Answers'); 825 | 826 | } 827 | } 828 | -------------------------------------------------------------------------------- /tests/Feature/QuizTest.php: -------------------------------------------------------------------------------- 1 | create([ 26 | 'name' => 'Computer Science', 27 | 'slug' => 'computer-science', 28 | ]); 29 | $algorithms = Topic::factory()->create([ 30 | 'name' => 'Algorithms', 31 | 'slug' => 'algorithms' 32 | ]); 33 | $data_structures = Topic::factory()->create([ 34 | 'name' => 'Data Structures', 35 | 'slug' => 'data-structures' 36 | ]); 37 | $computer_networks = Topic::factory()->create([ 38 | 'name' => 'Computer Networks', 39 | 'slug' => 'computer-networks' 40 | ]); 41 | $computer_science->children()->save($algorithms); 42 | $computer_science->children()->save($data_structures); 43 | $computer_science->children()->save($computer_networks); 44 | $this->assertEquals(3, $computer_science->children()->count()); 45 | 46 | // Question Types 47 | QuestionType::insert( 48 | [ 49 | [ 50 | 'name' => 'multiple_choice_single_answer', 51 | ], 52 | [ 53 | 'name' => 'multiple_choice_multiple_answer', 54 | ], 55 | [ 56 | 'name' => 'fill_the_blank', 57 | ] 58 | ] 59 | ); 60 | 61 | // Question One And Options 62 | $question_one = Question::factory()->create([ 63 | 'name' => 'What is an algorithm?', 64 | 'question_type_id' => 1, 65 | 'is_active' => true, 66 | ]); 67 | $question_one_option_one = QuestionOption::factory()->create([ 68 | 'question_id' => $question_one->id, 69 | 'name' => 'A computer program that solves a problem.', 70 | 'is_correct' => false, 71 | ]); 72 | $question_one_option_two = QuestionOption::factory()->create([ 73 | 'question_id' => $question_one->id, 74 | 'name' => 'A set of rules that define the behavior of a computer program.', 75 | 'is_correct' => false, 76 | ]); 77 | $question_one_option_three = QuestionOption::factory()->create([ 78 | 'question_id' => $question_one->id, 79 | 'name' => 'A set of instructions that tell a computer what to do.', 80 | 'is_correct' => true, 81 | ]); 82 | $question_one_option_four = QuestionOption::factory()->create([ 83 | 'question_id' => $question_one->id, 84 | 'name' => 'None of the above.', 85 | 'is_correct' => false, 86 | ]); 87 | $question_one->topics()->attach([$computer_science->id, $algorithms->id]); 88 | $this->assertEquals(2, $question_one->topics->count()); 89 | $this->assertEquals(4, $question_one->options->count()); 90 | 91 | // Question Two And Options 92 | $question_two = Question::factory()->create([ 93 | 'name' => 'Which of the below is a data structure?', 94 | 'question_type_id' => 1, 95 | 'is_active' => true, 96 | ]); 97 | 98 | $question_two_option_one = QuestionOption::factory()->create([ 99 | 'question_id' => $question_two->id, 100 | 'name' => 'array', 101 | 'is_correct' => true, 102 | ]); 103 | $question_two_option_two = QuestionOption::factory()->create([ 104 | 'question_id' => $question_two->id, 105 | 'name' => 'if', 106 | 'is_correct' => false, 107 | ]); 108 | $question_two_option_three = QuestionOption::factory()->create([ 109 | 'question_id' => $question_two->id, 110 | 'name' => 'for loop', 111 | 'is_correct' => false, 112 | ]); 113 | $question_two_option_four = QuestionOption::factory()->create([ 114 | 'question_id' => $question_two->id, 115 | 'name' => 'method', 116 | 'is_correct' => false, 117 | ]); 118 | $question_two->topics()->attach([$computer_science->id, $data_structures->id]); 119 | $this->assertEquals(2, $question_two->topics->count()); 120 | $this->assertEquals(4, $question_two->options->count()); 121 | 122 | // Question Three And Options 123 | $question_three = Question::factory()->create([ 124 | 'name' => 'How many layers in OSI model?', 125 | 'question_type_id' => 1, 126 | 'is_active' => false, 127 | ]); 128 | 129 | $question_three_option_one = QuestionOption::factory()->create([ 130 | 'question_id' => $question_three->id, 131 | 'name' => '5', 132 | 'is_correct' => false, 133 | ]); 134 | $question_three_option_two = QuestionOption::factory()->create([ 135 | 'question_id' => $question_three->id, 136 | 'name' => '8', 137 | 'is_correct' => false, 138 | ]); 139 | $question_three_option_three = QuestionOption::factory()->create([ 140 | 'question_id' => $question_three->id, 141 | 'name' => '10', 142 | 'is_correct' => false, 143 | ]); 144 | $question_three_option_four = QuestionOption::factory()->create([ 145 | 'question_id' => $question_three->id, 146 | 'name' => '7', 147 | 'is_correct' => true, 148 | ]); 149 | $question_three->topics()->attach([$computer_science->id, $computer_networks->id]); 150 | $this->assertEquals(2, $question_three->topics->count()); 151 | $this->assertEquals(4, $question_three->options->count()); 152 | 153 | $this->assertEquals(3, $computer_science->questions()->count()); 154 | 155 | // Quiz 156 | $quiz = Quiz::factory()->create([ 157 | 'name' => 'Computer Sceince Quiz', 158 | 'description' => 'Test your knowledge of computer science', 159 | 'slug' => 'computer-science-quiz', 160 | 'total_marks' => 10, 161 | 'pass_marks' => 6, 162 | 'max_attempts' => 1, 163 | 'is_published' => 1, 164 | 'valid_from' => now(), 165 | 'valid_upto' => now()->addDay(5), 166 | 'time_between_attempts' => 0, 167 | ]); 168 | 169 | // Add Question to Quiz 170 | $quiz_question_one = QuizQuestion::factory()->create([ 171 | 'quiz_id' => $quiz->id, 172 | 'question_id' => $question_one->id, 173 | 'marks' => 3, 174 | 'order' => 1, 175 | ]); 176 | $quiz_question_two = QuizQuestion::factory()->create([ 177 | 'quiz_id' => $quiz->id, 178 | 'question_id' => $question_two->id, 179 | 'marks' => 3, 180 | 'order' => 2, 181 | ]); 182 | $quiz_question_three = QuizQuestion::factory()->create([ 183 | 'quiz_id' => $quiz->id, 184 | 'question_id' => $question_three->id, 185 | 'marks' => 4, 186 | 'order' => 2, 187 | 'negative_marks' => 2, 188 | ]); 189 | 190 | $this->assertEquals(3, $quiz->questions->count()); 191 | $this->assertEquals(10, $quiz->questions->sum('marks')); 192 | 193 | // Participants 194 | $participant_one = Author::create([ 195 | 'name' => 'Bravo' 196 | ]); 197 | $participant_two = Author::create([ 198 | 'name' => 'Charlie' 199 | ]); 200 | 201 | // Quiz Attempt One And Answers 202 | $quiz_attempt_one = QuizAttempt::create([ 203 | 'quiz_id' => $quiz->id, 204 | 'participant_id' => $participant_one->id, 205 | 'participant_type' => get_class($participant_one) 206 | ]); 207 | QuizAttemptAnswer::create( 208 | [ 209 | 'quiz_attempt_id' => $quiz_attempt_one->id, 210 | 'quiz_question_id' => $quiz_question_one->id, 211 | 'question_option_id' => $question_one_option_three->id, 212 | ] 213 | ); 214 | QuizAttemptAnswer::create( 215 | [ 216 | 'quiz_attempt_id' => $quiz_attempt_one->id, 217 | 'quiz_question_id' => $quiz_question_two->id, 218 | 'question_option_id' => $question_two_option_one->id, 219 | ] 220 | ); 221 | QuizAttemptAnswer::create( 222 | [ 223 | 'quiz_attempt_id' => $quiz_attempt_one->id, 224 | 'quiz_question_id' => $quiz_question_three->id, 225 | 'question_option_id' => $question_three_option_four->id, 226 | ] 227 | ); 228 | 229 | $this->assertEquals(3, $quiz_attempt_one->answers->count()); 230 | //Calculate Obtained marks 231 | $this->assertEquals(10, $quiz_attempt_one->calculate_score()); 232 | } 233 | 234 | /** @test- */ 235 | function quiz_multiple_choice_single_answer_few_worng_answers() 236 | { 237 | $computer_science = Topic::factory()->create([ 238 | 'name' => 'Computer Science', 239 | 'slug' => 'computer-science', 240 | ]); 241 | $algorithms = Topic::factory()->create([ 242 | 'name' => 'Algorithms', 243 | 'slug' => 'algorithms' 244 | ]); 245 | $data_structures = Topic::factory()->create([ 246 | 'name' => 'Data Structures', 247 | 'slug' => 'data-structures' 248 | ]); 249 | $computer_networks = Topic::factory()->create([ 250 | 'name' => 'Computer Networks', 251 | 'slug' => 'computer-networks' 252 | ]); 253 | $computer_science->children()->save($algorithms); 254 | $computer_science->children()->save($data_structures); 255 | $computer_science->children()->save($computer_networks); 256 | $this->assertEquals(3, $computer_science->children()->count()); 257 | 258 | // Question Types 259 | QuestionType::insert( 260 | [ 261 | [ 262 | 'name' => 'multiple_choice_single_answer', 263 | ], 264 | [ 265 | 'name' => 'multiple_choice_multiple_answer', 266 | ], 267 | [ 268 | 'name' => 'fill_the_blank', 269 | ] 270 | ] 271 | ); 272 | 273 | // Question One And Options 274 | $question_one = Question::factory()->create([ 275 | 'name' => 'What is an algorithm?', 276 | 'question_type_id' => 1, 277 | 'is_active' => true, 278 | ]); 279 | $question_one_option_one = QuestionOption::factory()->create([ 280 | 'question_id' => $question_one->id, 281 | 'name' => 'A computer program that solves a problem.', 282 | 'is_correct' => false, 283 | ]); 284 | $question_one_option_two = QuestionOption::factory()->create([ 285 | 'question_id' => $question_one->id, 286 | 'name' => 'A set of rules that define the behavior of a computer program.', 287 | 'is_correct' => false, 288 | ]); 289 | $question_one_option_three = QuestionOption::factory()->create([ 290 | 'question_id' => $question_one->id, 291 | 'name' => 'A set of instructions that tell a computer what to do.', 292 | 'is_correct' => true, 293 | ]); 294 | $question_one_option_four = QuestionOption::factory()->create([ 295 | 'question_id' => $question_one->id, 296 | 'name' => 'None of the above.', 297 | 'is_correct' => false, 298 | ]); 299 | $question_one->topics()->attach([$computer_science->id, $algorithms->id]); 300 | $this->assertEquals(2, $question_one->topics->count()); 301 | $this->assertEquals(4, $question_one->options->count()); 302 | 303 | // Question Two And Options 304 | $question_two = Question::factory()->create([ 305 | 'name' => 'Which of the below is a data structure?', 306 | 'question_type_id' => 1, 307 | 'is_active' => true, 308 | ]); 309 | 310 | $question_two_option_one = QuestionOption::factory()->create([ 311 | 'question_id' => $question_two->id, 312 | 'name' => 'array', 313 | 'is_correct' => true, 314 | ]); 315 | $question_two_option_two = QuestionOption::factory()->create([ 316 | 'question_id' => $question_two->id, 317 | 'name' => 'if', 318 | 'is_correct' => false, 319 | ]); 320 | $question_two_option_three = QuestionOption::factory()->create([ 321 | 'question_id' => $question_two->id, 322 | 'name' => 'for loop', 323 | 'is_correct' => false, 324 | ]); 325 | $question_two_option_four = QuestionOption::factory()->create([ 326 | 'question_id' => $question_two->id, 327 | 'name' => 'method', 328 | 'is_correct' => false, 329 | ]); 330 | $question_two->topics()->attach([$computer_science->id, $data_structures->id]); 331 | $this->assertEquals(2, $question_two->topics->count()); 332 | $this->assertEquals(4, $question_two->options->count()); 333 | 334 | // Question Three And Options 335 | $question_three = Question::factory()->create([ 336 | 'name' => 'How many layers in OSI model?', 337 | 'question_type_id' => 1, 338 | 'is_active' => false, 339 | ]); 340 | 341 | $question_three_option_one = QuestionOption::factory()->create([ 342 | 'question_id' => $question_three->id, 343 | 'name' => '5', 344 | 'is_correct' => false, 345 | ]); 346 | $question_three_option_two = QuestionOption::factory()->create([ 347 | 'question_id' => $question_three->id, 348 | 'name' => '8', 349 | 'is_correct' => false, 350 | ]); 351 | $question_three_option_three = QuestionOption::factory()->create([ 352 | 'question_id' => $question_three->id, 353 | 'name' => '10', 354 | 'is_correct' => false, 355 | ]); 356 | $question_three_option_four = QuestionOption::factory()->create([ 357 | 'question_id' => $question_three->id, 358 | 'name' => '7', 359 | 'is_correct' => true, 360 | ]); 361 | $question_three->topics()->attach([$computer_science->id, $computer_networks->id]); 362 | $this->assertEquals(2, $question_three->topics->count()); 363 | $this->assertEquals(4, $question_three->options->count()); 364 | 365 | $this->assertEquals(3, $computer_science->questions()->count()); 366 | 367 | // Quiz 368 | $quiz = Quiz::factory()->create([ 369 | 'name' => 'Computer Sceince Quiz', 370 | 'description' => 'Test your knowledge of computer science', 371 | 'slug' => 'computer-science-quiz', 372 | 'total_marks' => 10, 373 | 'pass_marks' => 6, 374 | 'max_attempts' => 1, 375 | 'is_published' => 1, 376 | 'valid_from' => now(), 377 | 'valid_upto' => now()->addDay(5), 378 | 'time_between_attempts' => 0, 379 | ]); 380 | 381 | // Add Question to Quiz 382 | $quiz_question_one = QuizQuestion::factory()->create([ 383 | 'quiz_id' => $quiz->id, 384 | 'question_id' => $question_one->id, 385 | 'marks' => 3, 386 | 'order' => 1, 387 | ]); 388 | $quiz_question_two = QuizQuestion::factory()->create([ 389 | 'quiz_id' => $quiz->id, 390 | 'question_id' => $question_two->id, 391 | 'marks' => 3, 392 | 'order' => 2, 393 | ]); 394 | $quiz_question_three = QuizQuestion::factory()->create([ 395 | 'quiz_id' => $quiz->id, 396 | 'question_id' => $question_three->id, 397 | 'marks' => 4, 398 | 'order' => 2, 399 | 'negative_marks' => 2, 400 | ]); 401 | 402 | $this->assertEquals(3, $quiz->questions->count()); 403 | $this->assertEquals(10, $quiz->questions->sum('marks')); 404 | 405 | // Participants 406 | $participant_one = Author::create([ 407 | 'name' => 'Bravo' 408 | ]); 409 | $participant_two = Author::create([ 410 | 'name' => 'Charlie' 411 | ]); 412 | 413 | // Quiz Attempt One And Answers 414 | $quiz_attempt_one = QuizAttempt::create([ 415 | 'quiz_id' => $quiz->id, 416 | 'participant_id' => $participant_one->id, 417 | 'participant_type' => get_class($participant_one) 418 | ]); 419 | QuizAttemptAnswer::create( 420 | [ 421 | 'quiz_attempt_id' => $quiz_attempt_one->id, 422 | 'quiz_question_id' => $quiz_question_one->id, 423 | 'question_option_id' => $question_one_option_three->id, 424 | ] 425 | ); 426 | QuizAttemptAnswer::create( 427 | [ 428 | 'quiz_attempt_id' => $quiz_attempt_one->id, 429 | 'quiz_question_id' => $quiz_question_two->id, 430 | 'question_option_id' => $question_two_option_one->id, 431 | ] 432 | ); 433 | QuizAttemptAnswer::create( 434 | [ 435 | 'quiz_attempt_id' => $quiz_attempt_one->id, 436 | 'quiz_question_id' => $quiz_question_three->id, 437 | 'question_option_id' => $question_three_option_three->id, 438 | ] 439 | ); 440 | 441 | $this->assertEquals(3, $quiz_attempt_one->answers->count()); 442 | 443 | // Calculate Obtained marks 444 | $this->assertEquals(4, $quiz_attempt_one->calculate_score()); 445 | } 446 | 447 | /** @test- */ 448 | function quiz_multiple_choice_multiple_answer_all_correct_answers() 449 | { 450 | $computer_science = Topic::factory()->create([ 451 | 'name' => 'Computer Science', 452 | 'slug' => 'computer-science', 453 | ]); 454 | $algorithms = Topic::factory()->create([ 455 | 'name' => 'Algorithms', 456 | 'slug' => 'algorithms' 457 | ]); 458 | $data_structures = Topic::factory()->create([ 459 | 'name' => 'Data Structures', 460 | 'slug' => 'data-structures' 461 | ]); 462 | $computer_networks = Topic::factory()->create([ 463 | 'name' => 'Computer Networks', 464 | 'slug' => 'computer-networks' 465 | ]); 466 | $computer_science->children()->save($algorithms); 467 | $computer_science->children()->save($data_structures); 468 | $computer_science->children()->save($computer_networks); 469 | $this->assertEquals(3, $computer_science->children()->count()); 470 | 471 | // Question Types 472 | QuestionType::insert( 473 | [ 474 | [ 475 | 'name' => 'multiple_choice_single_answer', 476 | ], 477 | [ 478 | 'name' => 'multiple_choice_multiple_answer', 479 | ], 480 | [ 481 | 'name' => 'fill_the_blank', 482 | ] 483 | ] 484 | ); 485 | 486 | // Question One And Options 487 | $question_one = Question::factory()->create([ 488 | 'name' => 'What is an algorithm?', 489 | 'question_type_id' => 1, 490 | 'is_active' => true, 491 | ]); 492 | $question_one_option_one = QuestionOption::factory()->create([ 493 | 'question_id' => $question_one->id, 494 | 'name' => 'A computer program that solves a problem.', 495 | 'is_correct' => false, 496 | ]); 497 | $question_one_option_two = QuestionOption::factory()->create([ 498 | 'question_id' => $question_one->id, 499 | 'name' => 'A set of rules that define the behavior of a computer program.', 500 | 'is_correct' => false, 501 | ]); 502 | $question_one_option_three = QuestionOption::factory()->create([ 503 | 'question_id' => $question_one->id, 504 | 'name' => 'A set of instructions that tell a computer what to do.', 505 | 'is_correct' => true, 506 | ]); 507 | $question_one_option_four = QuestionOption::factory()->create([ 508 | 'question_id' => $question_one->id, 509 | 'name' => 'None of the above.', 510 | 'is_correct' => false, 511 | ]); 512 | $question_one->topics()->attach([$computer_science->id, $algorithms->id]); 513 | $this->assertEquals(2, $question_one->topics->count()); 514 | $this->assertEquals(4, $question_one->options->count()); 515 | 516 | // Question Two And Options 517 | $question_two = Question::factory()->create([ 518 | 'name' => 'Which of the below is a data structure?', 519 | 'question_type_id' => 1, 520 | 'is_active' => true, 521 | ]); 522 | 523 | $question_two_option_one = QuestionOption::factory()->create([ 524 | 'question_id' => $question_two->id, 525 | 'name' => 'array', 526 | 'is_correct' => true, 527 | ]); 528 | $question_two_option_two = QuestionOption::factory()->create([ 529 | 'question_id' => $question_two->id, 530 | 'name' => 'string', 531 | 'is_correct' => true, 532 | ]); 533 | $question_two_option_three = QuestionOption::factory()->create([ 534 | 'question_id' => $question_two->id, 535 | 'name' => 'object', 536 | 'is_correct' => true, 537 | ]); 538 | $question_two_option_four = QuestionOption::factory()->create([ 539 | 'question_id' => $question_two->id, 540 | 'name' => 'method', 541 | 'is_correct' => false, 542 | ]); 543 | $question_two->topics()->attach([$computer_science->id, $data_structures->id]); 544 | $this->assertEquals(2, $question_two->topics->count()); 545 | $this->assertEquals(4, $question_two->options->count()); 546 | 547 | // Question Three And Options 548 | $question_three = Question::factory()->create([ 549 | 'name' => 'How many layers in OSI model?', 550 | 'question_type_id' => 1, 551 | 'is_active' => false, 552 | ]); 553 | 554 | $question_three_option_one = QuestionOption::factory()->create([ 555 | 'question_id' => $question_three->id, 556 | 'name' => '5', 557 | 'is_correct' => false, 558 | ]); 559 | $question_three_option_two = QuestionOption::factory()->create([ 560 | 'question_id' => $question_three->id, 561 | 'name' => '8', 562 | 'is_correct' => false, 563 | ]); 564 | $question_three_option_three = QuestionOption::factory()->create([ 565 | 'question_id' => $question_three->id, 566 | 'name' => '10', 567 | 'is_correct' => false, 568 | ]); 569 | $question_three_option_four = QuestionOption::factory()->create([ 570 | 'question_id' => $question_three->id, 571 | 'name' => '7', 572 | 'is_correct' => true, 573 | ]); 574 | $question_three->topics()->attach([$computer_science->id, $computer_networks->id]); 575 | $this->assertEquals(2, $question_three->topics->count()); 576 | $this->assertEquals(4, $question_three->options->count()); 577 | 578 | $this->assertEquals(3, $computer_science->questions()->count()); 579 | 580 | // Quiz 581 | $quiz = Quiz::factory()->create([ 582 | 'name' => 'Computer Sceince Quiz', 583 | 'description' => 'Test your knowledge of computer science', 584 | 'slug' => 'computer-science-quiz', 585 | 'total_marks' => 10, 586 | 'pass_marks' => 6, 587 | 'max_attempts' => 1, 588 | 'is_published' => 1, 589 | 'valid_from' => now(), 590 | 'valid_upto' => now()->addDay(5), 591 | 'time_between_attempts' => 0, 592 | ]); 593 | 594 | // Add Question to Quiz 595 | $quiz_question_one = QuizQuestion::factory()->create([ 596 | 'quiz_id' => $quiz->id, 597 | 'question_id' => $question_one->id, 598 | 'marks' => 3, 599 | 'order' => 1, 600 | ]); 601 | $quiz_question_two = QuizQuestion::factory()->create([ 602 | 'quiz_id' => $quiz->id, 603 | 'question_id' => $question_two->id, 604 | 'marks' => 3, 605 | 'order' => 2, 606 | 'negative_marks' => 0 607 | ]); 608 | $quiz_question_three = QuizQuestion::factory()->create([ 609 | 'quiz_id' => $quiz->id, 610 | 'question_id' => $question_three->id, 611 | 'marks' => 4, 612 | 'order' => 3, 613 | 'negative_marks' => 2, 614 | ]); 615 | 616 | $this->assertEquals(3, $quiz->questions->count()); 617 | $this->assertEquals(10, $quiz->questions->sum('marks')); 618 | 619 | // Participants 620 | $participant_one = Author::create([ 621 | 'name' => 'Bravo' 622 | ]); 623 | 624 | // Quiz Attempt One And Answers 625 | $quiz_attempt_one = QuizAttempt::create([ 626 | 'quiz_id' => $quiz->id, 627 | 'participant_id' => $participant_one->id, 628 | 'participant_type' => get_class($participant_one) 629 | ]); 630 | QuizAttemptAnswer::create( 631 | [ 632 | 'quiz_attempt_id' => $quiz_attempt_one->id, 633 | 'quiz_question_id' => $quiz_question_one->id, 634 | 'question_option_id' => $question_one_option_three->id, 635 | ] 636 | ); 637 | QuizAttemptAnswer::create( 638 | [ 639 | 'quiz_attempt_id' => $quiz_attempt_one->id, 640 | 'quiz_question_id' => $quiz_question_two->id, 641 | 'question_option_id' => $question_two_option_one->id, 642 | ] 643 | ); 644 | QuizAttemptAnswer::create( 645 | [ 646 | 'quiz_attempt_id' => $quiz_attempt_one->id, 647 | 'quiz_question_id' => $quiz_question_two->id, 648 | 'question_option_id' => $question_two_option_two->id, 649 | ] 650 | ); 651 | QuizAttemptAnswer::create( 652 | [ 653 | 'quiz_attempt_id' => $quiz_attempt_one->id, 654 | 'quiz_question_id' => $quiz_question_two->id, 655 | 'question_option_id' => $question_two_option_three->id, 656 | ] 657 | ); 658 | 659 | QuizAttemptAnswer::create( 660 | [ 661 | 'quiz_attempt_id' => $quiz_attempt_one->id, 662 | 'quiz_question_id' => $quiz_question_three->id, 663 | 'question_option_id' => $question_three_option_four->id, 664 | ] 665 | ); 666 | 667 | // Calculate Obtained marks 668 | $this->assertEquals(10, $quiz_attempt_one->calculate_score()); 669 | } 670 | 671 | /** @test- */ 672 | function quiz_multiple_choice_multiple_answer_all_few_wrong_answers() 673 | { 674 | $computer_science = Topic::factory()->create([ 675 | 'name' => 'Computer Science', 676 | 'slug' => 'computer-science', 677 | ]); 678 | $algorithms = Topic::factory()->create([ 679 | 'name' => 'Algorithms', 680 | 'slug' => 'algorithms' 681 | ]); 682 | $data_structures = Topic::factory()->create([ 683 | 'name' => 'Data Structures', 684 | 'slug' => 'data-structures' 685 | ]); 686 | $computer_networks = Topic::factory()->create([ 687 | 'name' => 'Computer Networks', 688 | 'slug' => 'computer-networks' 689 | ]); 690 | $computer_science->children()->save($algorithms); 691 | $computer_science->children()->save($data_structures); 692 | $computer_science->children()->save($computer_networks); 693 | $this->assertEquals(3, $computer_science->children()->count()); 694 | 695 | // Question Types 696 | QuestionType::insert( 697 | [ 698 | [ 699 | 'name' => 'multiple_choice_single_answer', 700 | ], 701 | [ 702 | 'name' => 'multiple_choice_multiple_answer', 703 | ], 704 | [ 705 | 'name' => 'fill_the_blank', 706 | ] 707 | ] 708 | ); 709 | 710 | // Question One And Options 711 | $question_one = Question::factory()->create([ 712 | 'name' => 'What is an algorithm?', 713 | 'question_type_id' => 1, 714 | 'is_active' => true, 715 | ]); 716 | $question_one_option_one = QuestionOption::factory()->create([ 717 | 'question_id' => $question_one->id, 718 | 'name' => 'A computer program that solves a problem.', 719 | 'is_correct' => false, 720 | ]); 721 | $question_one_option_two = QuestionOption::factory()->create([ 722 | 'question_id' => $question_one->id, 723 | 'name' => 'A set of rules that define the behavior of a computer program.', 724 | 'is_correct' => false, 725 | ]); 726 | $question_one_option_three = QuestionOption::factory()->create([ 727 | 'question_id' => $question_one->id, 728 | 'name' => 'A set of instructions that tell a computer what to do.', 729 | 'is_correct' => true, 730 | ]); 731 | $question_one_option_four = QuestionOption::factory()->create([ 732 | 'question_id' => $question_one->id, 733 | 'name' => 'None of the above.', 734 | 'is_correct' => false, 735 | ]); 736 | $question_one->topics()->attach([$computer_science->id, $algorithms->id]); 737 | $this->assertEquals(2, $question_one->topics->count()); 738 | $this->assertEquals(4, $question_one->options->count()); 739 | 740 | // Question Two And Options 741 | $question_two = Question::factory()->create([ 742 | 'name' => 'Which of the below is a data structure?', 743 | 'question_type_id' => 2, 744 | 'is_active' => true, 745 | ]); 746 | 747 | $question_two_option_one = QuestionOption::factory()->create([ 748 | 'question_id' => $question_two->id, 749 | 'name' => 'array', 750 | 'is_correct' => true, 751 | ]); 752 | $question_two_option_two = QuestionOption::factory()->create([ 753 | 'question_id' => $question_two->id, 754 | 'name' => 'string', 755 | 'is_correct' => true, 756 | ]); 757 | $question_two_option_three = QuestionOption::factory()->create([ 758 | 'question_id' => $question_two->id, 759 | 'name' => 'object', 760 | 'is_correct' => true, 761 | ]); 762 | $question_two_option_four = QuestionOption::factory()->create([ 763 | 'question_id' => $question_two->id, 764 | 'name' => 'method', 765 | 'is_correct' => false, 766 | ]); 767 | $question_two->topics()->attach([$computer_science->id, $data_structures->id]); 768 | $this->assertEquals(2, $question_two->topics->count()); 769 | $this->assertEquals(4, $question_two->options->count()); 770 | 771 | // Question Three And Options 772 | $question_three = Question::factory()->create([ 773 | 'name' => 'How many layers in OSI model?', 774 | 'question_type_id' => 1, 775 | 'is_active' => false, 776 | ]); 777 | 778 | $question_three_option_one = QuestionOption::factory()->create([ 779 | 'question_id' => $question_three->id, 780 | 'name' => '5', 781 | 'is_correct' => false, 782 | ]); 783 | $question_three_option_two = QuestionOption::factory()->create([ 784 | 'question_id' => $question_three->id, 785 | 'name' => '8', 786 | 'is_correct' => false, 787 | ]); 788 | $question_three_option_three = QuestionOption::factory()->create([ 789 | 'question_id' => $question_three->id, 790 | 'name' => '10', 791 | 'is_correct' => false, 792 | ]); 793 | $question_three_option_four = QuestionOption::factory()->create([ 794 | 'question_id' => $question_three->id, 795 | 'name' => '7', 796 | 'is_correct' => true, 797 | ]); 798 | $question_three->topics()->attach([$computer_science->id, $computer_networks->id]); 799 | $this->assertEquals(2, $question_three->topics->count()); 800 | $this->assertEquals(4, $question_three->options->count()); 801 | 802 | $this->assertEquals(3, $computer_science->questions()->count()); 803 | 804 | // Quiz 805 | $quiz = Quiz::factory()->create([ 806 | 'name' => 'Computer Sceince Quiz', 807 | 'description' => 'Test your knowledge of computer science', 808 | 'slug' => 'computer-science-quiz', 809 | 'total_marks' => 10, 810 | 'pass_marks' => 6, 811 | 'max_attempts' => 1, 812 | 'is_published' => 1, 813 | 'valid_from' => now(), 814 | 'valid_upto' => now()->addDay(5), 815 | 'time_between_attempts' => 0, 816 | ]); 817 | 818 | //Add Question to Quiz 819 | $quiz_question_one = QuizQuestion::factory()->create([ 820 | 'quiz_id' => $quiz->id, 821 | 'question_id' => $question_one->id, 822 | 'marks' => 3, 823 | 'order' => 1, 824 | ]); 825 | $quiz_question_two = QuizQuestion::factory()->create([ 826 | 'quiz_id' => $quiz->id, 827 | 'question_id' => $question_two->id, 828 | 'marks' => 3, 829 | 'order' => 2, 830 | 'negative_marks' => 0 831 | ]); 832 | $quiz_question_three = QuizQuestion::factory()->create([ 833 | 'quiz_id' => $quiz->id, 834 | 'question_id' => $question_three->id, 835 | 'marks' => 4, 836 | 'order' => 3, 837 | 'negative_marks' => 2, 838 | ]); 839 | 840 | $this->assertEquals(3, $quiz->questions->count()); 841 | $this->assertEquals(10, $quiz->questions->sum('marks')); 842 | 843 | // Participants 844 | $participant_one = Author::create([ 845 | 'name' => 'Bravo' 846 | ]); 847 | 848 | // Quiz Attempt One And Answers 849 | $quiz_attempt_one = QuizAttempt::create([ 850 | 'quiz_id' => $quiz->id, 851 | 'participant_id' => $participant_one->id, 852 | 'participant_type' => get_class($participant_one) 853 | ]); 854 | QuizAttemptAnswer::create( 855 | [ 856 | 'quiz_attempt_id' => $quiz_attempt_one->id, 857 | 'quiz_question_id' => $quiz_question_one->id, 858 | 'question_option_id' => $question_one_option_three->id, 859 | ] 860 | ); 861 | 862 | QuizAttemptAnswer::create( 863 | [ 864 | 'quiz_attempt_id' => $quiz_attempt_one->id, 865 | 'quiz_question_id' => $quiz_question_two->id, 866 | 'question_option_id' => $question_two_option_one->id, 867 | ] 868 | ); 869 | QuizAttemptAnswer::create( 870 | [ 871 | 'quiz_attempt_id' => $quiz_attempt_one->id, 872 | 'quiz_question_id' => $quiz_question_two->id, 873 | 'question_option_id' => $question_two_option_two->id, 874 | ] 875 | ); 876 | // QuizAttemptAnswer::create( 877 | // [ 878 | // 'quiz_attempt_id' => $quiz_attempt_one->id, 879 | // 'quiz_question_id' => $quiz_question_two->id, 880 | // 'question_option_id' => $question_two_option_three->id, 881 | // ] 882 | // ); 883 | 884 | QuizAttemptAnswer::create( 885 | [ 886 | 'quiz_attempt_id' => $quiz_attempt_one->id, 887 | 'quiz_question_id' => $quiz_question_three->id, 888 | 'question_option_id' => $question_three_option_four->id, 889 | ] 890 | ); 891 | 892 | // Calculate Obtained marks 893 | $this->assertEquals(7, $quiz_attempt_one->calculate_score()); 894 | } 895 | 896 | /** @test- */ 897 | function quiz_fill_the_blank_all_correct_answers() 898 | { 899 | $computer_science = Topic::factory()->create([ 900 | 'name' => 'Computer Science', 901 | 'slug' => 'computer-science', 902 | ]); 903 | $algorithms = Topic::factory()->create([ 904 | 'name' => 'Algorithms', 905 | 'slug' => 'algorithms' 906 | ]); 907 | $data_structures = Topic::factory()->create([ 908 | 'name' => 'Data Structures', 909 | 'slug' => 'data-structures' 910 | ]); 911 | $computer_networks = Topic::factory()->create([ 912 | 'name' => 'Computer Networks', 913 | 'slug' => 'computer-networks' 914 | ]); 915 | $computer_science->children()->save($algorithms); 916 | $computer_science->children()->save($data_structures); 917 | $computer_science->children()->save($computer_networks); 918 | $this->assertEquals(3, $computer_science->children()->count()); 919 | 920 | // Question Types 921 | QuestionType::insert( 922 | [ 923 | [ 924 | 'name' => 'multiple_choice_single_answer', 925 | ], 926 | [ 927 | 'name' => 'multiple_choice_multiple_answer', 928 | ], 929 | [ 930 | 'name' => 'fill_the_blank', 931 | ] 932 | ] 933 | ); 934 | 935 | // Question One And Options 936 | $question_one = Question::factory()->create([ 937 | 'name' => 'Full Form Of CPU', 938 | 'question_type_id' => 1, 939 | 'is_active' => true, 940 | ]); 941 | $question_one_option_one = QuestionOption::factory()->create([ 942 | 'question_id' => $question_one->id, 943 | 'name' => 'central processing unit', 944 | 'is_correct' => true, 945 | ]); 946 | $question_one->topics()->attach([$computer_science->id, $algorithms->id]); 947 | $this->assertEquals(2, $question_one->topics->count()); 948 | $this->assertEquals(1, $question_one->options->count()); 949 | 950 | // Question Two And Options 951 | $question_two = Question::factory()->create([ 952 | 'name' => 'Which of the below is a data structure?', 953 | 'question_type_id' => 1, 954 | 'is_active' => true, 955 | ]); 956 | 957 | $question_two_option_one = QuestionOption::factory()->create([ 958 | 'question_id' => $question_two->id, 959 | 'name' => 'array', 960 | 'is_correct' => true, 961 | ]); 962 | $question_two_option_two = QuestionOption::factory()->create([ 963 | 'question_id' => $question_two->id, 964 | 'name' => 'if', 965 | 'is_correct' => false, 966 | ]); 967 | $question_two_option_three = QuestionOption::factory()->create([ 968 | 'question_id' => $question_two->id, 969 | 'name' => 'for loop', 970 | 'is_correct' => false, 971 | ]); 972 | $question_two_option_four = QuestionOption::factory()->create([ 973 | 'question_id' => $question_two->id, 974 | 'name' => 'method', 975 | 'is_correct' => false, 976 | ]); 977 | $question_two->topics()->attach([$computer_science->id, $data_structures->id]); 978 | $this->assertEquals(2, $question_two->topics->count()); 979 | $this->assertEquals(4, $question_two->options->count()); 980 | 981 | // Question Three And Options 982 | $question_three = Question::factory()->create([ 983 | 'name' => 'How many layers in OSI model?', 984 | 'question_type_id' => 1, 985 | 'is_active' => false, 986 | ]); 987 | 988 | $question_three_option_one = QuestionOption::factory()->create([ 989 | 'question_id' => $question_three->id, 990 | 'name' => '5', 991 | 'is_correct' => false, 992 | ]); 993 | $question_three_option_two = QuestionOption::factory()->create([ 994 | 'question_id' => $question_three->id, 995 | 'name' => '8', 996 | 'is_correct' => false, 997 | ]); 998 | $question_three_option_three = QuestionOption::factory()->create([ 999 | 'question_id' => $question_three->id, 1000 | 'name' => '10', 1001 | 'is_correct' => false, 1002 | ]); 1003 | $question_three_option_four = QuestionOption::factory()->create([ 1004 | 'question_id' => $question_three->id, 1005 | 'name' => '7', 1006 | 'is_correct' => true, 1007 | ]); 1008 | $question_three->topics()->attach([$computer_science->id, $computer_networks->id]); 1009 | $this->assertEquals(2, $question_three->topics->count()); 1010 | $this->assertEquals(4, $question_three->options->count()); 1011 | $this->assertEquals(3, $computer_science->questions()->count()); 1012 | 1013 | // Quiz 1014 | $quiz = Quiz::factory()->create([ 1015 | 'name' => 'Computer Sceince Quiz', 1016 | 'description' => 'Test your knowledge of computer science', 1017 | 'slug' => 'computer-science-quiz', 1018 | 'total_marks' => 10, 1019 | 'pass_marks' => 6, 1020 | 'max_attempts' => 1, 1021 | 'is_published' => 1, 1022 | 'valid_from' => now(), 1023 | 'valid_upto' => now()->addDay(5), 1024 | 'time_between_attempts' => 0, 1025 | ]); 1026 | 1027 | //Add Question to Quiz 1028 | $quiz_question_one = QuizQuestion::factory()->create([ 1029 | 'quiz_id' => $quiz->id, 1030 | 'question_id' => $question_one->id, 1031 | 'marks' => 3, 1032 | 'order' => 1, 1033 | ]); 1034 | $quiz_question_two = QuizQuestion::factory()->create([ 1035 | 'quiz_id' => $quiz->id, 1036 | 'question_id' => $question_two->id, 1037 | 'marks' => 3, 1038 | 'order' => 2, 1039 | ]); 1040 | $quiz_question_three = QuizQuestion::factory()->create([ 1041 | 'quiz_id' => $quiz->id, 1042 | 'question_id' => $question_three->id, 1043 | 'marks' => 4, 1044 | 'order' => 2, 1045 | 'negative_marks' => 2, 1046 | ]); 1047 | 1048 | $this->assertEquals(3, $quiz->questions->count()); 1049 | $this->assertEquals(10, $quiz->questions->sum('marks')); 1050 | 1051 | // Participants 1052 | $participant_one = Author::create([ 1053 | 'name' => 'Bravo' 1054 | ]); 1055 | $participant_two = Author::create([ 1056 | 'name' => 'Charlie' 1057 | ]); 1058 | 1059 | // Quiz Attempt One And Answers 1060 | $quiz_attempt_one = QuizAttempt::create([ 1061 | 'quiz_id' => $quiz->id, 1062 | 'participant_id' => $participant_one->id, 1063 | 'participant_type' => get_class($participant_one) 1064 | ]); 1065 | QuizAttemptAnswer::create( 1066 | [ 1067 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1068 | 'quiz_question_id' => $quiz_question_one->id, 1069 | 'question_option_id' => $question_one_option_one->id, 1070 | 'answer' => 'central processing unit' 1071 | ] 1072 | ); 1073 | QuizAttemptAnswer::create( 1074 | [ 1075 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1076 | 'quiz_question_id' => $quiz_question_two->id, 1077 | 'question_option_id' => $question_two_option_one->id, 1078 | ] 1079 | ); 1080 | QuizAttemptAnswer::create( 1081 | [ 1082 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1083 | 'quiz_question_id' => $quiz_question_three->id, 1084 | 'question_option_id' => $question_three_option_four->id, 1085 | ] 1086 | ); 1087 | 1088 | $this->assertEquals(3, $quiz_attempt_one->answers->count()); 1089 | // Calculate Obtained marks 1090 | $this->assertEquals(10, $quiz_attempt_one->calculate_score()); 1091 | } 1092 | 1093 | /** @test- */ 1094 | function quiz_fill_the_blank_few_wrong_answers() 1095 | { 1096 | $computer_science = Topic::factory()->create([ 1097 | 'name' => 'Computer Science', 1098 | 'slug' => 'computer-science', 1099 | ]); 1100 | $algorithms = Topic::factory()->create([ 1101 | 'name' => 'Algorithms', 1102 | 'slug' => 'algorithms' 1103 | ]); 1104 | $data_structures = Topic::factory()->create([ 1105 | 'name' => 'Data Structures', 1106 | 'slug' => 'data-structures' 1107 | ]); 1108 | $computer_networks = Topic::factory()->create([ 1109 | 'name' => 'Computer Networks', 1110 | 'slug' => 'computer-networks' 1111 | ]); 1112 | $computer_science->children()->save($algorithms); 1113 | $computer_science->children()->save($data_structures); 1114 | $computer_science->children()->save($computer_networks); 1115 | $this->assertEquals(3, $computer_science->children()->count()); 1116 | 1117 | // Question Types 1118 | QuestionType::insert( 1119 | [ 1120 | [ 1121 | 'name' => 'multiple_choice_single_answer', 1122 | ], 1123 | [ 1124 | 'name' => 'multiple_choice_multiple_answer', 1125 | ], 1126 | [ 1127 | 'name' => 'fill_the_blank', 1128 | ] 1129 | ] 1130 | ); 1131 | 1132 | // Question One And Options 1133 | $question_one = Question::factory()->create([ 1134 | 'name' => 'Full Form Of CPU', 1135 | 'question_type_id' => 3, 1136 | 'is_active' => true, 1137 | ]); 1138 | $question_one_option_one = QuestionOption::factory()->create([ 1139 | 'question_id' => $question_one->id, 1140 | 'name' => 'central processing unit', 1141 | 'is_correct' => true, 1142 | ]); 1143 | $question_one->topics()->attach([$computer_science->id, $algorithms->id]); 1144 | $this->assertEquals(2, $question_one->topics->count()); 1145 | $this->assertEquals(1, $question_one->options->count()); 1146 | 1147 | //Question Two And Options 1148 | $question_two = Question::factory()->create([ 1149 | 'name' => 'Which of the below is a data structure?', 1150 | 'question_type_id' => 1, 1151 | 'is_active' => true, 1152 | ]); 1153 | 1154 | $question_two_option_one = QuestionOption::factory()->create([ 1155 | 'question_id' => $question_two->id, 1156 | 'name' => 'array', 1157 | 'is_correct' => true, 1158 | ]); 1159 | $question_two_option_two = QuestionOption::factory()->create([ 1160 | 'question_id' => $question_two->id, 1161 | 'name' => 'if', 1162 | 'is_correct' => false, 1163 | ]); 1164 | $question_two_option_three = QuestionOption::factory()->create([ 1165 | 'question_id' => $question_two->id, 1166 | 'name' => 'for loop', 1167 | 'is_correct' => false, 1168 | ]); 1169 | $question_two_option_four = QuestionOption::factory()->create([ 1170 | 'question_id' => $question_two->id, 1171 | 'name' => 'method', 1172 | 'is_correct' => false, 1173 | ]); 1174 | $question_two->topics()->attach([$computer_science->id, $data_structures->id]); 1175 | $this->assertEquals(2, $question_two->topics->count()); 1176 | $this->assertEquals(4, $question_two->options->count()); 1177 | 1178 | // Question Three And Options 1179 | $question_three = Question::factory()->create([ 1180 | 'name' => 'How many layers in OSI model?', 1181 | 'question_type_id' => 1, 1182 | 'is_active' => false, 1183 | ]); 1184 | 1185 | $question_three_option_one = QuestionOption::factory()->create([ 1186 | 'question_id' => $question_three->id, 1187 | 'name' => '5', 1188 | 'is_correct' => false, 1189 | ]); 1190 | $question_three_option_two = QuestionOption::factory()->create([ 1191 | 'question_id' => $question_three->id, 1192 | 'name' => '8', 1193 | 'is_correct' => false, 1194 | ]); 1195 | $question_three_option_three = QuestionOption::factory()->create([ 1196 | 'question_id' => $question_three->id, 1197 | 'name' => '10', 1198 | 'is_correct' => false, 1199 | ]); 1200 | $question_three_option_four = QuestionOption::factory()->create([ 1201 | 'question_id' => $question_three->id, 1202 | 'name' => '7', 1203 | 'is_correct' => true, 1204 | ]); 1205 | $question_three->topics()->attach([$computer_science->id, $computer_networks->id]); 1206 | $this->assertEquals(2, $question_three->topics->count()); 1207 | $this->assertEquals(4, $question_three->options->count()); 1208 | $this->assertEquals(3, $computer_science->questions()->count()); 1209 | 1210 | // Quiz 1211 | $quiz = Quiz::factory()->create([ 1212 | 'name' => 'Computer Sceince Quiz', 1213 | 'description' => 'Test your knowledge of computer science', 1214 | 'slug' => 'computer-science-quiz', 1215 | 'total_marks' => 10, 1216 | 'pass_marks' => 6, 1217 | 'max_attempts' => 1, 1218 | 'is_published' => 1, 1219 | 'valid_from' => now(), 1220 | 'valid_upto' => now()->addDay(5), 1221 | 'time_between_attempts' => 0, 1222 | ]); 1223 | 1224 | // Add Question to Quiz 1225 | $quiz_question_one = QuizQuestion::factory()->create([ 1226 | 'quiz_id' => $quiz->id, 1227 | 'question_id' => $question_one->id, 1228 | 'marks' => 3, 1229 | 'order' => 1, 1230 | ]); 1231 | $quiz_question_two = QuizQuestion::factory()->create([ 1232 | 'quiz_id' => $quiz->id, 1233 | 'question_id' => $question_two->id, 1234 | 'marks' => 3, 1235 | 'order' => 2, 1236 | ]); 1237 | $quiz_question_three = QuizQuestion::factory()->create([ 1238 | 'quiz_id' => $quiz->id, 1239 | 'question_id' => $question_three->id, 1240 | 'marks' => 4, 1241 | 'order' => 2, 1242 | 'negative_marks' => 2, 1243 | ]); 1244 | 1245 | $this->assertEquals(3, $quiz->questions->count()); 1246 | $this->assertEquals(10, $quiz->questions->sum('marks')); 1247 | 1248 | // Participants 1249 | $participant_one = Author::create([ 1250 | 'name' => 'Bravo' 1251 | ]); 1252 | $participant_two = Author::create([ 1253 | 'name' => 'Charlie' 1254 | ]); 1255 | 1256 | // Quiz Attempt One And Answers 1257 | $quiz_attempt_one = QuizAttempt::create([ 1258 | 'quiz_id' => $quiz->id, 1259 | 'participant_id' => $participant_one->id, 1260 | 'participant_type' => get_class($participant_one) 1261 | ]); 1262 | QuizAttemptAnswer::create( 1263 | [ 1264 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1265 | 'quiz_question_id' => $quiz_question_one->id, 1266 | 'question_option_id' => $question_one_option_one->id, 1267 | 'answer' => 'central power unit' 1268 | ] 1269 | ); 1270 | QuizAttemptAnswer::create( 1271 | [ 1272 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1273 | 'quiz_question_id' => $quiz_question_two->id, 1274 | 'question_option_id' => $question_two_option_one->id, 1275 | ] 1276 | ); 1277 | QuizAttemptAnswer::create( 1278 | [ 1279 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1280 | 'quiz_question_id' => $quiz_question_three->id, 1281 | 'question_option_id' => $question_three_option_four->id, 1282 | ] 1283 | ); 1284 | 1285 | $this->assertEquals(3, $quiz_attempt_one->answers->count()); 1286 | // Calculate Obtained marks 1287 | $this->assertEquals(7, $quiz_attempt_one->calculate_score()); 1288 | } 1289 | 1290 | #[Test] 1291 | function quiz_multi_user_attempts_multi_question_types_few_wrong_answers() 1292 | { 1293 | $computer_science = Topic::factory()->create([ 1294 | 'name' => 'Computer Science', 1295 | 'slug' => 'computer-science', 1296 | ]); 1297 | $algorithms = Topic::factory()->create([ 1298 | 'name' => 'Algorithms', 1299 | 'slug' => 'algorithms' 1300 | ]); 1301 | $data_structures = Topic::factory()->create([ 1302 | 'name' => 'Data Structures', 1303 | 'slug' => 'data-structures' 1304 | ]); 1305 | $computer_networks = Topic::factory()->create([ 1306 | 'name' => 'Computer Networks', 1307 | 'slug' => 'computer-networks' 1308 | ]); 1309 | $computer_science->children()->save($algorithms); 1310 | $computer_science->children()->save($data_structures); 1311 | $computer_science->children()->save($computer_networks); 1312 | 1313 | // Question Types 1314 | QuestionType::insert( 1315 | [ 1316 | [ 1317 | 'name' => 'multiple_choice_single_answer', 1318 | ], 1319 | [ 1320 | 'name' => 'multiple_choice_multiple_answer', 1321 | ], 1322 | [ 1323 | 'name' => 'fill_the_blank', 1324 | ] 1325 | ] 1326 | ); 1327 | 1328 | // Question One And Options 1329 | $question_one = Question::factory()->create([ 1330 | 'name' => 'Full Form Of CPU', 1331 | 'question_type_id' => 3, 1332 | 'is_active' => true, 1333 | ]); 1334 | $question_one_option_one = QuestionOption::factory()->create([ 1335 | 'question_id' => $question_one->id, 1336 | 'name' => 'central processing unit', 1337 | 'is_correct' => true, 1338 | ]); 1339 | $question_one->topics()->attach([$computer_science->id, $algorithms->id]); 1340 | 1341 | // Question Two And Options 1342 | $question_two = Question::factory()->create([ 1343 | 'name' => 'Which of the below is a data structure?', 1344 | 'question_type_id' => 2, 1345 | 'is_active' => true, 1346 | ]); 1347 | 1348 | $question_two_option_one = QuestionOption::factory()->create([ 1349 | 'question_id' => $question_two->id, 1350 | 'name' => 'array', 1351 | 'is_correct' => true, 1352 | ]); 1353 | $question_two_option_two = QuestionOption::factory()->create([ 1354 | 'question_id' => $question_two->id, 1355 | 'name' => 'object', 1356 | 'is_correct' => true, 1357 | ]); 1358 | $question_two_option_three = QuestionOption::factory()->create([ 1359 | 'question_id' => $question_two->id, 1360 | 'name' => 'for loop', 1361 | 'is_correct' => false, 1362 | ]); 1363 | $question_two_option_four = QuestionOption::factory()->create([ 1364 | 'question_id' => $question_two->id, 1365 | 'name' => 'method', 1366 | 'is_correct' => false, 1367 | ]); 1368 | $question_two->topics()->attach([$computer_science->id, $data_structures->id]); 1369 | 1370 | // Question Three And Options 1371 | $question_three = Question::factory()->create([ 1372 | 'name' => 'How many layers in OSI model?', 1373 | 'question_type_id' => 1, 1374 | 'is_active' => false, 1375 | ]); 1376 | 1377 | $question_three_option_one = QuestionOption::factory()->create([ 1378 | 'question_id' => $question_three->id, 1379 | 'name' => '5', 1380 | 'is_correct' => false, 1381 | ]); 1382 | $question_three_option_two = QuestionOption::factory()->create([ 1383 | 'question_id' => $question_three->id, 1384 | 'name' => '8', 1385 | 'is_correct' => false, 1386 | ]); 1387 | $question_three_option_three = QuestionOption::factory()->create([ 1388 | 'question_id' => $question_three->id, 1389 | 'name' => '10', 1390 | 'is_correct' => false, 1391 | ]); 1392 | $question_three_option_four = QuestionOption::factory()->create([ 1393 | 'question_id' => $question_three->id, 1394 | 'name' => '7', 1395 | 'is_correct' => true, 1396 | ]); 1397 | $question_three->topics()->attach([$computer_science->id, $computer_networks->id]); 1398 | 1399 | // Quiz 1400 | $quiz = Quiz::factory()->create([ 1401 | 'name' => 'Computer Sceince Quiz', 1402 | 'description' => 'Test your knowledge of computer science', 1403 | 'slug' => 'computer-science-quiz', 1404 | 'total_marks' => 10, 1405 | 'pass_marks' => 6, 1406 | 'max_attempts' => 1, 1407 | 'is_published' => 1, 1408 | 'valid_from' => now(), 1409 | 'valid_upto' => now()->addDay(5), 1410 | 'time_between_attempts' => 0, 1411 | ]); 1412 | 1413 | // Add Question to Quiz 1414 | $quiz_question_one = QuizQuestion::factory()->create([ 1415 | 'quiz_id' => $quiz->id, 1416 | 'question_id' => $question_one->id, 1417 | 'marks' => 5, 1418 | 'order' => 1, 1419 | 'negative_marks' => 1 1420 | ]); 1421 | $quiz_question_two = QuizQuestion::factory()->create([ 1422 | 'quiz_id' => $quiz->id, 1423 | 'question_id' => $question_two->id, 1424 | 'marks' => 5, 1425 | 'order' => 2, 1426 | 'negative_marks' => 1 1427 | ]); 1428 | $quiz_question_three = QuizQuestion::factory()->create([ 1429 | 'quiz_id' => $quiz->id, 1430 | 'question_id' => $question_three->id, 1431 | 'marks' => 5, 1432 | 'order' => 3, 1433 | 'negative_marks' => 1, 1434 | ]); 1435 | 1436 | $this->assertEquals(3, $quiz->questions->count()); 1437 | $this->assertEquals(15, $quiz->questions->sum('marks')); 1438 | 1439 | // Participants 1440 | $participant_one = Author::create([ 1441 | 'name' => 'Bravo' 1442 | ]); 1443 | $participant_two = Author::create([ 1444 | 'name' => 'Charlie' 1445 | ]); 1446 | 1447 | // Quiz Attempt One And Answers 1448 | $quiz_attempt_one = QuizAttempt::create([ 1449 | 'quiz_id' => $quiz->id, 1450 | 'participant_id' => $participant_one->id, 1451 | 'participant_type' => get_class($participant_one) 1452 | ]); 1453 | QuizAttemptAnswer::create( 1454 | [ 1455 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1456 | 'quiz_question_id' => $quiz_question_one->id, 1457 | 'question_option_id' => $question_one_option_one->id, 1458 | 'answer' => 'central processing unit' 1459 | ] 1460 | ); 1461 | QuizAttemptAnswer::create( 1462 | [ 1463 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1464 | 'quiz_question_id' => $quiz_question_two->id, 1465 | 'question_option_id' => $question_two_option_one->id, 1466 | ] 1467 | ); 1468 | QuizAttemptAnswer::create( 1469 | [ 1470 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1471 | 'quiz_question_id' => $quiz_question_two->id, 1472 | 'question_option_id' => $question_two_option_two->id, 1473 | ] 1474 | ); 1475 | QuizAttemptAnswer::create( 1476 | [ 1477 | 'quiz_attempt_id' => $quiz_attempt_one->id, 1478 | 'quiz_question_id' => $quiz_question_three->id, 1479 | 'question_option_id' => $question_three_option_four->id, 1480 | ] 1481 | ); 1482 | 1483 | // Quiz Attempt Two And Answers 1484 | $quiz_attempt_two = QuizAttempt::create([ 1485 | 'quiz_id' => $quiz->id, 1486 | 'participant_id' => $participant_two->id, 1487 | 'participant_type' => get_class($participant_two) 1488 | ]); 1489 | QuizAttemptAnswer::create( 1490 | [ 1491 | 'quiz_attempt_id' => $quiz_attempt_two->id, 1492 | 'quiz_question_id' => $quiz_question_one->id, 1493 | 'question_option_id' => $question_one_option_one->id, 1494 | 'answer' => 'central processing unit' 1495 | ] 1496 | ); 1497 | QuizAttemptAnswer::create( 1498 | [ 1499 | 'quiz_attempt_id' => $quiz_attempt_two->id, 1500 | 'quiz_question_id' => $quiz_question_two->id, 1501 | 'question_option_id' => $question_two_option_one->id, 1502 | ] 1503 | ); 1504 | 1505 | $this->assertEquals(4, $quiz_attempt_one->answers->count()); 1506 | // Calculate Obtained marks 1507 | $this->assertEquals(15, $quiz_attempt_one->calculate_score()); 1508 | 1509 | $this->assertEquals(2, $quiz_attempt_two->answers->count()); 1510 | $this->assertEquals(3, $quiz_attempt_two->calculate_score()); 1511 | } 1512 | } 1513 | --------------------------------------------------------------------------------