├── .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 | 
2 |
3 | # Laravel Quiz
4 |
5 | [](https://packagist.org/packages/harishdurga/laravel-quiz)
6 | [](https://packagist.org/packages/harishdurga/laravel-quiz)
7 | 
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 | 
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 |
--------------------------------------------------------------------------------