Skip to content

Commit

Permalink
Merge pull request #29 from harishdurga/28-quizattempt-check-if-quest…
Browse files Browse the repository at this point in the history
…ion-is-answered-correctly

28 quizattempt check if question is answered correctly
  • Loading branch information
harishdurga authored Sep 17, 2022
2 parents b681d49 + 27fadb8 commit dd5dc7e
Show file tree
Hide file tree
Showing 6 changed files with 400 additions and 140 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ vendor/
.phpunit.result.cache
logfile.txt
.vscode/
.DS_Store
.DS_Store
.idea/
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,65 @@ public function correct_options(): Collection

Please refer unit and features tests for more understanding.

### Validate A Quiz Question
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
`id` as the key for the assoc array that will be returned.
**Example:**
```injectablephp
$quizAttempt->validate($quizQuestion->id); //For a particular question
$quizAttempt->validate(); //For all the questions in the quiz attempt
$quizAttempt->validate($quizQuestion->id,$data); //$data can any type
```
```php
[
1 => [
'score' => 10,
'is_correct' => true,
'correct_answer' => ['One','Five','Seven'],
'user_answer' => ['Five','One','Seven']
],
2 => [
'score' => 0,
'is_correct' => false,
'correct_answer' => 'Hello There',
'user_answer' => 'Hello World'
]
]
```
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.
```php
'render_answers_responses' => [
1 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType1Answers',
2 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType2Answers',
3 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType3Answers',
]
```
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
`validate()` method by passing the `QuizQuestion` object as the argument for your custom method as defined in the config.
**Example:**
```php
public static function renderQuestionType1Answers(QuizQuestion $quizQuestion, mixed $data=null)
{
/**
* @var Question $actualQuestion
*/
$actualQuestion = $quizQuestion->question;
$answers = $quizQuestion->answers;
$questionOptions = $actualQuestion->options;
$correctAnswer = $actualQuestion->correct_options()->first()?->option;
$givenAnswer = $answers->first()?->question_option_id;
foreach ($questionOptions as $questionOption) {
if ($questionOption->id == $givenAnswer) {
$givenAnswer = $questionOption->option;
break;
}
}
return [$correctAnswer, $givenAnswer];
}
```
As shown in the example you 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.
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.

### Testing

```bash
Expand Down
32 changes: 23 additions & 9 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@
*/

'table_names' => [
'topics' => 'topics',
'question_types' => 'question_types',
'questions' => 'questions',
'topicables' => 'topicables',
'question_options' => 'question_options',
'quizzes' => 'quizzes',
'quiz_questions' => 'quiz_questions',
'quiz_attempts' => 'quiz_attempts',
'topics' => 'topics',
'question_types' => 'question_types',
'questions' => 'questions',
'topicables' => 'topicables',
'question_options' => 'question_options',
'quizzes' => 'quizzes',
'quiz_questions' => 'quiz_questions',
'quiz_attempts' => 'quiz_attempts',
'quiz_attempt_answers' => 'quiz_attempt_answers',
'quiz_authors' => 'quiz_authors'
'quiz_authors' => 'quiz_authors'
],

/*
Expand Down Expand Up @@ -103,6 +103,20 @@
1 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_1_question',
2 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_2_question',
3 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::get_score_for_type_3_question',
],

/*
|--------------------------------------------------------------------------
| Question type answer/solution render
|--------------------------------------------------------------------------
|
| Render correct answer and given response for different question types
|
*/
'render_answers_responses' => [
1 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType1Answers',
2 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType2Answers',
3 => '\Harishdurga\LaravelQuiz\Models\QuizAttempt::renderQuestionType3Answers',
]

];
2 changes: 2 additions & 0 deletions src/Models/QuestionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class QuestionType extends Model
{
use HasFactory, SoftDeletes;

public const MULTIPLE_CHOICE_SINGLE_ANSWER = 1,MULTIPLE_CHOICE_MULTI_ANSWER=2,FILL_IN_BLANK=3;

/**
* The attributes that aren't mass assignable.
*
Expand Down
115 changes: 103 additions & 12 deletions src/Models/QuizAttempt.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Harishdurga\LaravelQuiz\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

Expand Down Expand Up @@ -56,7 +56,7 @@ public function calculate_score($data = null): float
$score = 0;
$quiz_questions_collection = $this->quiz->questions()->with('question')->orderBy('id', 'ASC')->get();
$quiz_attempt_answers = [];
foreach ($this->answers as $key => $quiz_attempt_answer) {
foreach ($this->answers as $quiz_attempt_answer) {
$quiz_attempt_answers[$quiz_attempt_answer->quiz_question_id][] = $quiz_attempt_answer;
}
foreach ($quiz_questions_collection as $quiz_question) {
Expand All @@ -69,25 +69,25 @@ public function calculate_score($data = null): float
/**
* @param QuizAttemptAnswer[] $quizQuestionAnswers All the answers of the quiz question
*/
public static function get_score_for_type_1_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array $quizQuestionAnswers, $data = null): float
public static function get_score_for_type_1_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array|Collection $quizQuestionAnswers, $data = null): float
{
$quiz = $quizAttempt->quiz;
$question = $quizQuestion->question;
$correct_answer = ($question->correct_options())->first()->id;
$negative_marks = self::get_negative_marks_for_question($quiz, $quizQuestion);
if (!empty($correct_answer)) {
if (count($quizQuestionAnswers)) {
return $quizQuestionAnswers[0]->question_option_id == $correct_answer ? $quizQuestion->marks : - ($negative_marks); // Return marks in case of correct answer else negative marks
return $quizQuestionAnswers[0]->question_option_id == $correct_answer ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks
}
return $quizQuestion->is_optional ? 0 : -$negative_marks; // If the question is optional, then the negative marks will be 0
}
return count($quizQuestionAnswers) ? (float) $quizQuestion->marks : 0; // Incase of no correct answer, if there is any answer then give full marks
return count($quizQuestionAnswers) ? (float)$quizQuestion->marks : 0; // Incase of no correct answer, if there is any answer then give full marks
}

/**
* @param QuizAttemptAnswer[] $quizQuestionAnswers All the answers of the quiz question
*/
public static function get_score_for_type_2_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array $quizQuestionAnswers, $data = null): float
public static function get_score_for_type_2_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array|Collection $quizQuestionAnswers, $data = null): float
{
$quiz = $quizAttempt->quiz;
$question = $quizQuestion->question;
Expand All @@ -96,28 +96,28 @@ public static function get_score_for_type_2_question(QuizAttempt $quizAttempt, Q
if (!empty($correct_answer)) {
if (count($quizQuestionAnswers)) {
$temp_arr = [];
foreach ($quizQuestionAnswers as $answer) {
foreach ($quizQuestionAnswers as $answer) {
$temp_arr[] = $answer->question_option_id;
}
return $correct_answer->toArray() == $temp_arr ? $quizQuestion->marks : - ($negative_marks); // Return marks in case of correct answer else negative marks
return $correct_answer->toArray() == $temp_arr ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks
}
return $quizQuestion->is_optional ? 0 : -$negative_marks; // If the question is optional, then the negative marks will be 0
}
return count($quizQuestionAnswers) ? (float) $quizQuestion->marks : 0; // Incase of no correct answer, if there is any answer then give full marks
return count($quizQuestionAnswers) ? (float)$quizQuestion->marks : 0; // Incase of no correct answer, if there is any answer then give full marks
}

/**
* @param QuizAttemptAnswer[] $quizQuestionAnswers All the answers of the quiz question
*/
public static function get_score_for_type_3_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array $quizQuestionAnswers, $data = null): float
public static function get_score_for_type_3_question(QuizAttempt $quizAttempt, QuizQuestion $quizQuestion, array|Collection $quizQuestionAnswers, $data = null): float
{
$quiz = $quizAttempt->quiz;
$question = $quizQuestion->question;
$correct_answer = ($question->correct_options())->first()->option;
$negative_marks = self::get_negative_marks_for_question($quiz, $quizQuestion);
if (!empty($correct_answer)) {
if (count($quizQuestionAnswers)) {
return $quizQuestionAnswers[0]->answer == $correct_answer ? $quizQuestion->marks : - ($negative_marks); // Return marks in case of correct answer else negative marks
return $quizQuestionAnswers[0]->answer == $correct_answer ? $quizQuestion->marks : -($negative_marks); // Return marks in case of correct answer else negative marks
}
return $quizQuestion->is_optional ? 0 : -$negative_marks; // If the question is optional, then the negative marks will be 0
}
Expand All @@ -129,7 +129,7 @@ public static function get_negative_marks_for_question(Quiz $quiz, QuizQuestion
$negative_marking_settings = $quiz->negative_marking_settings ?? [
'enable_negative_marks' => true,
'negative_marking_type' => 'fixed',
'negative_mark_value' => 0,
'negative_mark_value' => 0,
];
if (!$negative_marking_settings['enable_negative_marks']) { // If negative marking is disabled
return 0;
Expand All @@ -140,4 +140,95 @@ public static function get_negative_marks_for_question(Quiz $quiz, QuizQuestion
}
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));
}

private function validateQuizQuestion(QuizQuestion $quizQuestion, mixed $data = null): array
{
$isCorrect = true;
$actualQuestion = $quizQuestion->question;
$answers = $quizQuestion->answers;
$questionType = $actualQuestion->question_type;
$score = call_user_func_array(config('laravel-quiz.get_score_for_question_type')[$questionType->id], [$this, $quizQuestion, $answers ?? [], $data]);
if ($score <= 0) {
$isCorrect = false;
}
list($correctAnswer, $userAnswer) = config('laravel-quiz.render_answers_responses')[$questionType->id]($quizQuestion, $data);
return [
'score' => $score,
'is_correct' => $isCorrect,
'correct_answer' => $correctAnswer,
'user_answer' => $userAnswer
];
}

/**
* @param int|null $quizQuestionId
* @param $data mixed|null data to be passed to the user defined function to evaluate different question types
* @return array|null [1=>['score'=>10,'is_correct'=>true,'correct_answer'=>'a','user_answer'=>'a']]
*/
public function validate(int|null $quizQuestionId = null, mixed $data = null): array|null
{
if ($quizQuestionId) {
$quizQuestion = QuizQuestion::where(['quiz_id' => $this->quiz_id, 'id' => $quizQuestionId])
->with('question')
->with('answers')
->with('question.options')->first();
if ($quizQuestion) {
return [$quizQuestionId => $this->validateQuizQuestion($quizQuestion, $data)];
}
return null; //QuizQuestion is empty
}
$quizQuestions = QuizQuestion::where(['quiz_id' => $this->quiz_id])->get();
if ($quizQuestions->count() == 0) {
return null;
}
$result = [];
foreach ($quizQuestions as $quizQuestion) {
$result[$quizQuestion->id] = $this->validateQuizQuestion($quizQuestion);
}
return $result;
}

public static function renderQuestionType1Answers(QuizQuestion $quizQuestion, mixed $data = null)
{
/**
* @var Question $actualQuestion
*/
$actualQuestion = $quizQuestion->question;
$answers = $quizQuestion->answers;
$questionOptions = $actualQuestion->options;
$correctAnswer = $actualQuestion->correct_options()->first()?->option;
$givenAnswer = $answers->first()?->question_option_id;
foreach ($questionOptions as $questionOption) {
if ($questionOption->id == $givenAnswer) {
$givenAnswer = $questionOption->option;
break;
}
}
return [$correctAnswer, $givenAnswer];
}

public static function renderQuestionType2Answers(QuizQuestion $quizQuestion, mixed $data = null)
{
$actualQuestion = $quizQuestion->question;
$userAnswersCollection = $quizQuestion->answers;
$correctAnswersCollection = $actualQuestion->correct_options();
$correctAnswers = $userAnswers = [];
foreach ($correctAnswersCollection as $correctAnswer) {
$correctAnswers[] = $correctAnswer->option;
}
foreach ($userAnswersCollection as $userAnswer) {
$userAnswers[] = $userAnswer?->question_option?->option;
}
return [$correctAnswers, $userAnswers];
}

public static function renderQuestionType3Answers(QuizQuestion $quizQuestion, mixed $data = null)
{
$actualQuestion = $quizQuestion->question;
$userAnswersCollection = $quizQuestion->answers;
$correctAnswersCollection = $actualQuestion->correct_options();
$userAnswer = $userAnswersCollection->first()?->answer;
$correctAnswer = $correctAnswersCollection->first()?->option;
return [$correctAnswer, $userAnswer];
}
}
Loading

0 comments on commit dd5dc7e

Please sign in to comment.