Merge branch 'MDL-77328-402' of https://github.com/rezaies/moodle into MOODLE_402_STABLE

This commit is contained in:
Jun Pataleta 2023-07-19 09:54:18 +08:00
commit 91430abc21
No known key found for this signature in database
GPG key ID: F83510526D99E2C7
12 changed files with 123 additions and 44 deletions

View file

@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace qtype_calculated;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/question/type/numerical/question.php');
/**
* Class to represent a calculated question answer.
*
* @package qtype_calculated
* @copyright 2023 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_calculated_answer extends \qtype_numerical_answer {
/** @var int The length of the correct answer. */
public $correctanswerlength;
/** @var int The format of the correct answer. */
public $correctanswerformat;
}

View file

@ -332,11 +332,27 @@ class qtype_calculated extends question_type {
} }
} }
/**
* Initializes calculated answers for a given question.
*
* @param question_definition $question The question definition object.
* @param stdClass $questiondata The question data object.
*/
protected function initialise_calculated_answers(question_definition $question, stdClass $questiondata) {
$question->answers = array();
if (empty($questiondata->options->answers)) {
return;
}
foreach ($questiondata->options->answers as $a) {
$question->answers[$a->id] = new \qtype_calculated\qtype_calculated_answer($a->id, $a->answer,
$a->fraction, $a->feedback, $a->feedbackformat, $a->tolerance);
}
}
protected function initialise_question_instance(question_definition $question, $questiondata) { protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata); parent::initialise_question_instance($question, $questiondata);
$this->initialise_calculated_answers($question, $questiondata);
question_bank::get_qtype('numerical')->initialise_numerical_answers(
$question, $questiondata);
foreach ($questiondata->options->answers as $a) { foreach ($questiondata->options->answers as $a) {
$question->answers[$a->id]->tolerancetype = $a->tolerancetype; $question->answers[$a->id]->tolerancetype = $a->tolerancetype;
$question->answers[$a->id]->correctanswerlength = $a->correctanswerlength; $question->answers[$a->id]->correctanswerlength = $a->correctanswerlength;

View file

@ -56,12 +56,11 @@ class qtype_calculated_test_helper extends question_test_helper {
$q->questiontext = 'What is {a} + {b}?'; $q->questiontext = 'What is {a} + {b}?';
$q->generalfeedback = 'Generalfeedback: {={a} + {b}} is the right answer.'; $q->generalfeedback = 'Generalfeedback: {={a} + {b}} is the right answer.';
$q->answers = array( $q->answers = [
13 => new qtype_numerical_answer(13, '{a} + {b}', 1.0, 'Very good.', FORMAT_HTML, 0), 13 => new \qtype_calculated\qtype_calculated_answer(13, '{a} + {b}', 1.0, 'Very good.', FORMAT_HTML, 0),
14 => new qtype_numerical_answer(14, '{a} - {b}', 0.0, 'Add. not subtract!.', 14 => new \qtype_calculated\qtype_calculated_answer(14, '{a} - {b}', 0.0, 'Add. not subtract!.', FORMAT_HTML, 0),
FORMAT_HTML, 0), 17 => new \qtype_calculated\qtype_calculated_answer(17, '*', 0.0, 'Completely wrong.', FORMAT_HTML, 0),
17 => new qtype_numerical_answer(17, '*', 0.0, 'Completely wrong.', FORMAT_HTML, 0), ];
);
foreach ($q->answers as $answer) { foreach ($q->answers as $answer) {
$answer->correctanswerlength = 2; $answer->correctanswerlength = 2;
$answer->correctanswerformat = 1; $answer->correctanswerformat = 1;
@ -106,12 +105,11 @@ class qtype_calculated_test_helper extends question_test_helper {
$qdata->options->unitsleft = 0; $qdata->options->unitsleft = 0;
$qdata->options->synchronize = 0; $qdata->options->synchronize = 0;
$qdata->options->answers = array( $qdata->options->answers = [
13 => new qtype_numerical_answer(13, '{a} + {b}', 1.0, 'Very good.', FORMAT_HTML, 0.001), 13 => new \qtype_calculated\qtype_calculated_answer(13, '{a} + {b}', 1.0, 'Very good.', FORMAT_HTML, 0.001),
14 => new qtype_numerical_answer(14, '{a} - {b}', 0.0, 'Add. not subtract!.', 14 => new \qtype_calculated\qtype_calculated_answer(14, '{a} - {b}', 0.0, 'Add. not subtract!.', FORMAT_HTML, 0.001),
FORMAT_HTML, 0.001), 17 => new \qtype_calculated\qtype_calculated_answer(17, '*', 0.0, 'Completely wrong.', FORMAT_HTML, 0),
17 => new qtype_numerical_answer(17, '*', 0.0, 'Completely wrong.', FORMAT_HTML, 0), ];
);
foreach ($qdata->options->answers as $answer) { foreach ($qdata->options->answers as $answer) {
$answer->correctanswerlength = 2; $answer->correctanswerlength = 2;
$answer->correctanswerformat = 1; $answer->correctanswerformat = 1;

View file

@ -146,7 +146,6 @@ class qtype_multianswer extends question_type {
question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped); question_bank::get_qtype($wrapped->qtype)->get_question_options($wrapped);
// For wrapped questions the maxgrade is always equal to the defaultmark, // For wrapped questions the maxgrade is always equal to the defaultmark,
// there is no entry in the question_instances table for them. // there is no entry in the question_instances table for them.
$wrapped->maxmark = $wrapped->defaultmark;
$wrapped->category = $question->categoryobject->id; $wrapped->category = $question->categoryobject->id;
$question->options->questions[$sequence[$wrapped->id]] = $wrapped; $question->options->questions[$sequence[$wrapped->id]] = $wrapped;
} }
@ -303,7 +302,7 @@ class qtype_multianswer extends question_type {
} }
} }
$question->subquestions[$key] = question_bank::make_question($subqdata); $question->subquestions[$key] = question_bank::make_question($subqdata);
$question->subquestions[$key]->maxmark = $subqdata->defaultmark; $question->subquestions[$key]->defaultmark = $subqdata->defaultmark;
if (isset($subqdata->options->layout)) { if (isset($subqdata->options->layout)) {
$question->subquestions[$key]->layout = $subqdata->options->layout; $question->subquestions[$key]->layout = $subqdata->options->layout;
} }

View file

@ -183,11 +183,11 @@ abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
} }
$subfraction = ''; $subfraction = '';
if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0 if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->defaultmark > 0
&& (!is_null($fraction) || $feedback)) { && (!is_null($fraction) || $feedback)) {
$a = new stdClass(); $a = new stdClass();
$a->mark = format_float($fraction * $subq->maxmark, $options->markdp); $a->mark = format_float($fraction * $subq->defaultmark, $options->markdp);
$a->max = format_float($subq->maxmark, $options->markdp); $a->max = format_float($subq->defaultmark, $options->markdp);
$feedback[] = get_string('markoutofmax', 'question', $a); $feedback[] = get_string('markoutofmax', 'question', $a);
} }
@ -483,10 +483,10 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_
$feedback = array(); $feedback = array();
if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX && if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
$subq->maxmark > 0) { $subq->defaultmark > 0) {
$a = new stdClass(); $a = new stdClass();
$a->mark = format_float($fraction * $subq->maxmark, $options->markdp); $a->mark = format_float($fraction * $subq->defaultmark, $options->markdp);
$a->max = format_float($subq->maxmark, $options->markdp); $a->max = format_float($subq->defaultmark, $options->markdp);
$feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a)); $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
} }
@ -675,10 +675,10 @@ class qtype_multianswer_multiresponse_vertical_renderer extends qtype_multianswe
$feedback = array(); $feedback = array();
if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX && if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
$subq->maxmark > 0) { $subq->defaultmark > 0) {
$a = new stdClass(); $a = new stdClass();
$a->mark = format_float($fraction * $subq->maxmark, $options->markdp); $a->mark = format_float($fraction * $subq->defaultmark, $options->markdp);
$a->max = format_float($subq->maxmark, $options->markdp); $a->max = format_float($subq->defaultmark, $options->markdp);
$feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a)); $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
} }

View file

@ -78,7 +78,7 @@ class qtype_multianswer_test_helper extends question_test_helper {
15 => new question_answer(15, '*', 0.0, 'Wrong answer', FORMAT_HTML), 15 => new question_answer(15, '*', 0.0, 'Wrong answer', FORMAT_HTML),
); );
$sa->qtype = question_bank::get_qtype('shortanswer'); $sa->qtype = question_bank::get_qtype('shortanswer');
$sa->maxmark = 1; $sa->defaultmark = 1;
// Multiple-choice subquestion. // Multiple-choice subquestion.
question_bank::load_question_definition_classes('multichoice'); question_bank::load_question_definition_classes('multichoice');
@ -104,7 +104,7 @@ class qtype_multianswer_test_helper extends question_test_helper {
'Well done!', FORMAT_HTML), 'Well done!', FORMAT_HTML),
); );
$mc->qtype = question_bank::get_qtype('multichoice'); $mc->qtype = question_bank::get_qtype('multichoice');
$mc->maxmark = 1; $mc->defaultmark = 1;
$q->subquestions = array( $q->subquestions = array(
1 => $sa, 1 => $sa,
@ -342,7 +342,7 @@ class qtype_multianswer_test_helper extends question_test_helper {
$data['Arizona'], FORMAT_HTML), $data['Arizona'], FORMAT_HTML),
); );
$mc->qtype = question_bank::get_qtype('multichoice'); $mc->qtype = question_bank::get_qtype('multichoice');
$mc->maxmark = 1; $mc->defaultmark = 1;
$q->subquestions[$i] = $mc; $q->subquestions[$i] = $mc;
} }
@ -385,7 +385,7 @@ class qtype_multianswer_test_helper extends question_test_helper {
); );
$sub->qtype = question_bank::get_qtype('numerical'); $sub->qtype = question_bank::get_qtype('numerical');
$sub->ap = new qtype_numerical_answer_processor(array()); $sub->ap = new qtype_numerical_answer_processor(array());
$sub->maxmark = 1; $sub->defaultmark = 1;
$q->subquestions = array( $q->subquestions = array(
1 => $sub, 1 => $sub,
@ -444,7 +444,7 @@ class qtype_multianswer_test_helper extends question_test_helper {
'', FORMAT_HTML), '', FORMAT_HTML),
); );
$mc->qtype = question_bank::get_qtype('multichoice'); $mc->qtype = question_bank::get_qtype('multichoice');
$mc->maxmark = 1; $mc->defaultmark = 1;
// Multiple-choice subquestion. // Multiple-choice subquestion.
question_bank::load_question_definition_classes('multichoice'); question_bank::load_question_definition_classes('multichoice');
@ -474,7 +474,7 @@ class qtype_multianswer_test_helper extends question_test_helper {
'Correct', FORMAT_HTML), 'Correct', FORMAT_HTML),
); );
$mc2->qtype = question_bank::get_qtype('multichoice'); $mc2->qtype = question_bank::get_qtype('multichoice');
$mc2->maxmark = 1; $mc2->defaultmark = 1;
$q->subquestions = array( $q->subquestions = array(
1 => $mc, 1 => $mc,

View file

@ -207,10 +207,8 @@ class qtype_numerical_question extends question_graded_automatically {
} }
foreach ($this->answers as $answer) { foreach ($this->answers as $answer) {
if ($answer->within_tolerance($scaledvalue)) { if ($answer->within_tolerance($scaledvalue)) {
$answer->unitisright = !is_null($multiplier);
return $answer; return $answer;
} else if ($answer->within_tolerance($value)) { } else if ($answer->within_tolerance($value)) {
$answer->unitisright = false;
return $answer; return $answer;
} }
} }
@ -218,6 +216,23 @@ class qtype_numerical_question extends question_graded_automatically {
return null; return null;
} }
/**
* Checks if the provided $multiplier is appropriate for the unit of the given $value,
* ensuring that multiplying $value by the $multiplier yields the expected $answer.
*
* @param qtype_numerical_answer $answer The expected result when multiplying $value by the appropriate $multiplier.
* @param float $value The provided value
* @param float|null $multiplier The multiplier value for the unit of $value.
* @return bool Returns true if the $multiplier is correct for the unit of $value, false otherwise.
*/
public function is_unit_right(qtype_numerical_answer $answer, float $value, ?float $multiplier): bool {
if (is_null($multiplier)) {
return false;
}
return $answer->within_tolerance($multiplier * $value);
}
public function get_correct_answer() { public function get_correct_answer() {
foreach ($this->answers as $answer) { foreach ($this->answers as $answer) {
$state = question_state::graded_state_for_fraction($answer->fraction); $state = question_state::graded_state_for_fraction($answer->fraction);
@ -256,12 +271,14 @@ class qtype_numerical_question extends question_graded_automatically {
list($value, $unit, $multiplier) = $this->ap->apply_units( list($value, $unit, $multiplier) = $this->ap->apply_units(
$response['answer'], $selectedunit); $response['answer'], $selectedunit);
/** @var qtype_numerical_answer $answer */
$answer = $this->get_matching_answer($value, $multiplier); $answer = $this->get_matching_answer($value, $multiplier);
if (!$answer) { if (!$answer) {
return array(0, question_state::$gradedwrong); return array(0, question_state::$gradedwrong);
} }
$fraction = $this->apply_unit_penalty($answer->fraction, $answer->unitisright); $unitisright = $this->is_unit_right($answer, $value, $multiplier);
$fraction = $this->apply_unit_penalty($answer->fraction, $unitisright);
return array($fraction, question_state::graded_state_for_fraction($fraction)); return array($fraction, question_state::graded_state_for_fraction($fraction));
} }
@ -276,6 +293,7 @@ class qtype_numerical_question extends question_graded_automatically {
$selectedunit = null; $selectedunit = null;
} }
list($value, $unit, $multiplier) = $this->ap->apply_units($response['answer'], $selectedunit); list($value, $unit, $multiplier) = $this->ap->apply_units($response['answer'], $selectedunit);
/** @var qtype_numerical_answer $ans */
$ans = $this->get_matching_answer($value, $multiplier); $ans = $this->get_matching_answer($value, $multiplier);
$resp = $response['answer']; $resp = $response['answer'];
@ -291,9 +309,10 @@ class qtype_numerical_question extends question_graded_automatically {
return array($this->id => new question_classified_response(0, $resp, 0)); return array($this->id => new question_classified_response(0, $resp, 0));
} }
return array($this->id => new question_classified_response($ans->id, $unitisright = $this->is_unit_right($ans, $value, $multiplier);
$resp, return [
$this->apply_unit_penalty($ans->fraction, $ans->unitisright))); $this->id => new question_classified_response($ans->id, $resp, $this->apply_unit_penalty($ans->fraction, $unitisright))
];
} }
public function check_file_access($qa, $options, $component, $filearea, $args, public function check_file_access($qa, $options, $component, $filearea, $args,

View file

@ -62,7 +62,8 @@ class qtype_numerical_renderer extends qtype_renderer {
$currentanswer, $selectedunit); $currentanswer, $selectedunit);
$answer = $question->get_matching_answer($value, $multiplier); $answer = $question->get_matching_answer($value, $multiplier);
if ($answer) { if ($answer) {
$fraction = $question->apply_unit_penalty($answer->fraction, $answer->unitisright); $unitisright = $question->is_unit_right($answer, $value, $multiplier);
$fraction = $question->apply_unit_penalty($answer->fraction, $unitisright);
} else { } else {
$fraction = 0; $fraction = 0;
} }

View file

@ -88,7 +88,7 @@ abstract class question_definition {
/** @var integer question test format. */ /** @var integer question test format. */
public $generalfeedbackformat; public $generalfeedbackformat;
/** @var number what this quetsion is marked out of, by default. */ /** @var float what this quetsion is marked out of, by default. */
public $defaultmark = 1; public $defaultmark = 1;
/** @var integer How many question numbers this question consumes. */ /** @var integer How many question numbers this question consumes. */

View file

@ -41,6 +41,15 @@ class qtype_truefalse_question extends question_graded_automatically {
public $trueanswerid; public $trueanswerid;
public $falseanswerid; public $falseanswerid;
/** @var int the format of the true feedback. */
public $truefeedbackformat;
/** @var int the format of the false feedback. */
public $falsefeedbackformat;
/** @var bool true to show the standard instruction, otherwise hide it. */
public $showstandardinstruction;
public function get_expected_data() { public function get_expected_data() {
return array('answer' => PARAM_INT); return array('answer' => PARAM_INT);
} }

View file

@ -142,11 +142,9 @@ class qtype_truefalse extends question_type {
protected function initialise_question_instance(question_definition $question, $questiondata) { protected function initialise_question_instance(question_definition $question, $questiondata) {
parent::initialise_question_instance($question, $questiondata); parent::initialise_question_instance($question, $questiondata);
$answers = $questiondata->options->answers; $answers = $questiondata->options->answers;
if ($answers[$questiondata->options->trueanswer]->fraction > 0.99) {
$question->rightanswer = true; /** @var qtype_truefalse_question $question */
} else { $question->rightanswer = $answers[$questiondata->options->trueanswer]->fraction > 0.99;
$question->rightanswer = false;
}
$question->truefeedback = $answers[$questiondata->options->trueanswer]->feedback; $question->truefeedback = $answers[$questiondata->options->trueanswer]->feedback;
$question->falsefeedback = $answers[$questiondata->options->falseanswer]->feedback; $question->falsefeedback = $answers[$questiondata->options->falseanswer]->feedback;
$question->truefeedbackformat = $question->truefeedbackformat =

View file

@ -37,6 +37,7 @@ class qtype_truefalse_renderer extends qtype_renderer {
public function formulation_and_controls(question_attempt $qa, public function formulation_and_controls(question_attempt $qa,
question_display_options $options) { question_display_options $options) {
/** @var qtype_truefalse_question $question */
$question = $qa->get_question(); $question = $qa->get_question();
$response = $qa->get_last_qt_var('answer', ''); $response = $qa->get_last_qt_var('answer', '');