Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

28 quizattempt check if question is answered correctly #29

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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