Skip to content

Commit

Permalink
Merge branch 'MDL-66259' of https://github.com/stronk7/moodle
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewnicols authored and stronk7 committed Apr 28, 2020
2 parents 594189e + a196cf5 commit 605107b
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 5 deletions.
2 changes: 1 addition & 1 deletion mod/quiz/report/responses/tests/fixtures/questions00.csv
Expand Up @@ -4,4 +4,4 @@ slot,type,which,cat,mark,overrides.hint.0.text,overrides.hint.0.format,overrides
,numerical,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
2,calculatedsimple,sumwithvariants,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
3,match,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
4,truefalse,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
4,truefalse,,maincat,1,"",0,"",0,"",0,"",0,0
5 changes: 5 additions & 0 deletions mod/quiz/tests/attempt_walkthrough_from_csv_test.php
Expand Up @@ -94,6 +94,11 @@ public function create_quiz($quizsettings, $qs) {
if ($q['type'] !== 'random') {
// Don't actually create random questions here.
$overrides = array('category' => $cat->id, 'defaultmark' => $q['mark']) + $q['overrides'];
if ($q['type'] === 'truefalse') {
// True/false question can never have hints, but sometimes we need to put them
// in the CSV file, to keep it rectangular.
unset($overrides['hint']);
}
$question = $questiongenerator->create_question($q['type'], $q['which'], $overrides);
$q['id'] = $question->id;

Expand Down
1 change: 1 addition & 0 deletions question/type/calculated/questiontype.php
Expand Up @@ -61,6 +61,7 @@ public function get_question_options($question) {
// First get the datasets and default options.
// The code is used for calculated, calculatedsimple and calculatedmulti qtypes.
global $CFG, $DB, $OUTPUT;
parent::get_question_options($question);
if (!$question->options = $DB->get_record('question_calculated_options',
array('question' => $question->id))) {
$question->options = new stdClass();
Expand Down
78 changes: 78 additions & 0 deletions question/type/calculated/tests/questiontype_test.php
Expand Up @@ -67,6 +67,84 @@ public function test_get_random_guess_score() {
$this->assertEquals(0.1, $this->qtype->get_random_guess_score($q));
}

public function test_load_question() {
$this->resetAfterTest();

$syscontext = context_system::instance();
/** @var core_question_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $generator->create_question_category(['contextid' => $syscontext->id]);

$fromform = test_question_maker::get_question_form_data('calculated');
$fromform->category = $category->id . ',' . $syscontext->id;

$question = new stdClass();
$question->category = $category->id;
$question->qtype = 'calculated';
$question->createdby = 0;

$this->qtype->save_question($question, $fromform);
$questiondata = question_bank::load_question_data($question->id);

$this->assertEquals(['id', 'category', 'parent', 'name', 'questiontext', 'questiontextformat',
'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype',
'length', 'stamp', 'version', 'hidden', 'timecreated', 'timemodified',
'createdby', 'modifiedby', 'idnumber', 'contextid', 'options', 'hints', 'categoryobject'],
array_keys(get_object_vars($questiondata)));
$this->assertEquals($category->id, $questiondata->category);
$this->assertEquals(0, $questiondata->parent);
$this->assertEquals($fromform->name, $questiondata->name);
$this->assertEquals($fromform->questiontext, $questiondata->questiontext);
$this->assertEquals($fromform->questiontextformat, $questiondata->questiontextformat);
$this->assertEquals('', $questiondata->generalfeedback);
$this->assertEquals(0, $questiondata->generalfeedbackformat);
$this->assertEquals($fromform->defaultmark, $questiondata->defaultmark);
$this->assertEquals(0, $questiondata->penalty);
$this->assertEquals('calculated', $questiondata->qtype);
$this->assertEquals(1, $questiondata->length);
$this->assertEquals(0, $questiondata->hidden);
$this->assertEquals($question->createdby, $questiondata->createdby);
$this->assertEquals($question->createdby, $questiondata->modifiedby);
$this->assertEquals('', $questiondata->idnumber);
$this->assertEquals($syscontext->id, $questiondata->contextid);
$this->assertEquals([], $questiondata->hints);

// Options.
$this->assertEquals($questiondata->id, $questiondata->options->question);
$this->assertEquals([], $questiondata->options->units);
$this->assertEquals(qtype_numerical::UNITNONE, $questiondata->options->showunits);
$this->assertEquals(0, $questiondata->options->unitgradingtype); // Unit role is none, so this is 0.
$this->assertEquals($fromform->unitpenalty, $questiondata->options->unitpenalty);
$this->assertEquals($fromform->unitsleft, $questiondata->options->unitsleft);

// Build the expected answer base.
$answerbase = [
'question' => $questiondata->id,
'answerformat' => 0,
];
$expectedanswers = [];
foreach ($fromform->answer as $key => $value) {
$answer = $answerbase + [
'answer' => $fromform->answer[$key],
'fraction' => (float)$fromform->fraction[$key],
'tolerance' => $fromform->tolerance[$key],
'tolerancetype' => $fromform->tolerancetype[$key],
'correctanswerlength' => $fromform->correctanswerlength[$key],
'correctanswerformat' => $fromform->correctanswerformat[$key],
'feedback' => $fromform->feedback[$key]['text'],
'feedbackformat' => $fromform->feedback[$key]['format'],
];
$expectedanswers[] = (object)$answer;
}
// Need to get rid of ids.
$gotanswers = array_map(function($answer) {
unset($answer->id);
return $answer;
}, $questiondata->options->answers);
// Compare answers.
$this->assertEquals($expectedanswers, array_values($gotanswers));
}

protected function get_possible_response($ans, $tolerance, $type) {
$a = new stdClass();
$a->answer = $ans;
Expand Down
1 change: 1 addition & 0 deletions question/type/multianswer/questiontype.php
Expand Up @@ -45,6 +45,7 @@ public function can_analyse_responses() {
public function get_question_options($question) {
global $DB, $OUTPUT;

parent::get_question_options($question);
// Get relevant data indexed by positionkey from the multianswers table.
$sequence = $DB->get_field('question_multianswer', 'sequence',
array('question' => $question->id), MUST_EXIST);
Expand Down
111 changes: 111 additions & 0 deletions question/type/multianswer/tests/questiontype_test.php
Expand Up @@ -115,6 +115,117 @@ public function test_get_random_guess_score() {
$this->assertEquals(0.1666667, $this->qtype->get_random_guess_score($q), '', 0.0000001);
}

public function test_load_question() {
$this->resetAfterTest();

$syscontext = context_system::instance();
/** @var core_question_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $generator->create_question_category(['contextid' => $syscontext->id]);

$fromform = test_question_maker::get_question_form_data('multianswer');
$fromform->category = $category->id . ',' . $syscontext->id;

$question = new stdClass();
$question->category = $category->id;
$question->qtype = 'multianswer';
$question->createdby = 0;

// Note, $question gets modified during save because of the way subquestions
// are extracted.
$question = $this->qtype->save_question($question, $fromform);

$questiondata = question_bank::load_question_data($question->id);

$this->assertEquals(['id', 'category', 'parent', 'name', 'questiontext', 'questiontextformat',
'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype',
'length', 'stamp', 'version', 'hidden', 'timecreated', 'timemodified',
'createdby', 'modifiedby', 'idnumber', 'contextid', 'options', 'hints', 'categoryobject'],
array_keys(get_object_vars($questiondata)));
$this->assertEquals($category->id, $questiondata->category);
$this->assertEquals(0, $questiondata->parent);
$this->assertEquals($fromform->name, $questiondata->name);
$this->assertEquals($fromform->questiontext, $questiondata->questiontext);
$this->assertEquals($fromform->questiontextformat, $questiondata->questiontextformat);
$this->assertEquals($fromform->generalfeedback['text'], $questiondata->generalfeedback);
$this->assertEquals($fromform->generalfeedback['format'], $questiondata->generalfeedbackformat);
$this->assertEquals($fromform->defaultmark, $questiondata->defaultmark);
$this->assertEquals(0, $questiondata->penalty);
$this->assertEquals('multianswer', $questiondata->qtype);
$this->assertEquals(1, $questiondata->length);
$this->assertEquals(0, $questiondata->hidden);
$this->assertEquals($question->createdby, $questiondata->createdby);
$this->assertEquals($question->createdby, $questiondata->modifiedby);
$this->assertEquals('', $questiondata->idnumber);
$this->assertEquals($syscontext->id, $questiondata->contextid);

// Build the expected hint base.
$hintbase = [
'questionid' => $questiondata->id,
'shownumcorrect' => 0,
'clearwrong' => 0,
'options' => null];
$expectedhints = [];
foreach ($fromform->hint as $key => $value) {
$hint = $hintbase + [
'hint' => $value['text'],
'hintformat' => $value['format'],
];
$expectedhints[] = (object)$hint;
}
// Need to get rid of ids.
$gothints = array_map(function($hint) {
unset($hint->id);
return $hint;
}, $questiondata->hints);
// Compare hints.
$this->assertEquals($expectedhints, array_values($gothints));

// Options.
$this->assertEquals(['answers', 'questions'], array_keys(get_object_vars($questiondata->options)));
$this->assertEquals(count($fromform->options->questions), count($questiondata->options->questions));

// Option answers.
$this->assertEquals([], $questiondata->options->answers);

// Build the expected questions. We aren't going deeper to subquestion answers, options... that's another qtype job.
$expectedquestions = [];
foreach ($fromform->options->questions as $key => $value) {
$question = [
'id' => $value->id,
'category' => $category->id,
'parent' => $questiondata->id,
'name' => $value->name,
'questiontext' => $value->questiontext,
'questiontextformat' => $value->questiontextformat,
'generalfeedback' => $value->generalfeedback,
'generalfeedbackformat' => $value->generalfeedbackformat,
'defaultmark' => (float) $value->defaultmark,
'penalty' => (float)$value->penalty,
'qtype' => $value->qtype,
'length' => $value->length,
'stamp' => $value->stamp,
'hidden' => 0,
'timecreated' => $value->timecreated,
'timemodified' => $value->timemodified,
'createdby' => $value->createdby,
'modifiedby' => $value->modifiedby,
];
$expectedquestions[] = (object)$question;
}
// Need to get rid of (version, idnumber, options, hints, maxmark). They are missing @ fromform.
$gotquestions = array_map(function($question) {
unset($question->version);
unset($question->idnumber);
unset($question->options);
unset($question->hints);
unset($question->maxmark);
return $question;
}, $questiondata->options->questions);
// Compare questions.
$this->assertEquals($expectedquestions, array_values($gotquestions));
}

public function test_question_saving_twosubq() {
$this->resetAfterTest(true);
$this->setAdminUser();
Expand Down
2 changes: 0 additions & 2 deletions question/type/numerical/db/upgradelib.php
Expand Up @@ -33,8 +33,6 @@
*
* This class is used by the code in question/engine/upgrade/upgradelib.php.
*
* TODO update for the changes in Moodle 2.0.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
Expand Down
22 changes: 20 additions & 2 deletions question/type/questiontypebase.php
Expand Up @@ -1055,9 +1055,27 @@ public function actual_number_of_questions($question) {
}

/**
* @param object $question
* Calculate the score a monkey would get on a question by clicking randomly.
*
* Some question types have significant non-zero average expected score
* of the response is just selected randomly. For example 50% for a
* true-false question. It is useful to know what this is. For example
* it gets shown in the quiz statistics report.
*
* For almost any open-ended question type (E.g. shortanswer or numerical)
* this should be 0.
*
* For selective response question types (e.g. multiple choice), you can probably compute this.
*
* For particularly complicated question types the may be impossible or very
* difficult to compute. In this case return null. (Or, if the expected score
* is very tiny even though the exact value is unknown, it may appropriate
* to return 0.)
*
* @param stdClass $questiondata data defining a question, as returned by
* question_bank::load_question_data().
* @return number|null either a fraction estimating what the student would
* score by guessing, or null, if it is not possible to estimate.
* score by guessing, or null, if it is not possible to estimate.
*/
public function get_random_guess_score($questiondata) {
return 0;
Expand Down
1 change: 1 addition & 0 deletions question/type/random/questiontype.php
Expand Up @@ -116,6 +116,7 @@ protected function init_qtype_lists() {
}

public function get_question_options($question) {
parent::get_question_options($question);
return true;
}

Expand Down
45 changes: 45 additions & 0 deletions question/type/random/tests/questiontype_test.php
Expand Up @@ -27,6 +27,7 @@
defined('MOODLE_INTERNAL') || die();

global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
require_once($CFG->dirroot . '/question/type/random/questiontype.php');


Expand Down Expand Up @@ -59,6 +60,50 @@ public function test_get_random_guess_score() {
$this->assertNull($this->qtype->get_random_guess_score(null));
}

public function test_load_question() {
$this->resetAfterTest();

$syscontext = context_system::instance();
/** @var core_question_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$category = $generator->create_question_category(['contextid' => $syscontext->id]);

$fromform = test_question_maker::get_question_form_data('random');
$fromform->category = $category->id . ',' . $syscontext->id;

$question = new stdClass();
$question->category = $category->id;
$question->qtype = 'random';
$question->createdby = 0;

$this->qtype->save_question($question, $fromform);
$questiondata = question_bank::load_question_data($question->id);

$this->assertEquals(['id', 'category', 'parent', 'name', 'questiontext', 'questiontextformat',
'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype',
'length', 'stamp', 'version', 'hidden', 'timecreated', 'timemodified',
'createdby', 'modifiedby', 'idnumber', 'contextid', 'options', 'hints', 'categoryobject'],
array_keys(get_object_vars($questiondata)));
$this->assertEquals($category->id, $questiondata->category);

// Random questions are not real questions. This is signaled by parent
// being non-zero - and in fact equal to question id.
$this->assertEquals($questiondata->id, $questiondata->parent);
$this->assertEquals('Random (' . $category->name . ')', $questiondata->name);
$this->assertEquals(0, $questiondata->questiontext); // Used to store 'Select from subcategories'.
$this->assertEquals('random', $questiondata->qtype);
$this->assertEquals(1, $questiondata->length);
$this->assertEquals(0, $questiondata->hidden);
$this->assertEquals($category->contextid, $questiondata->contextid);

// Options - not used.
$this->assertEquals(['answers'], array_keys(get_object_vars($questiondata->options)));
$this->assertEquals([], $questiondata->options->answers);

// Hints - not used.
$this->assertEquals([], $questiondata->hints);
}

public function test_get_possible_responses() {
$this->assertEquals(array(), $this->qtype->get_possible_responses(null));
}
Expand Down
1 change: 1 addition & 0 deletions question/type/truefalse/questiontype.php
Expand Up @@ -113,6 +113,7 @@ public function save_question_options($question) {
*/
public function get_question_options($question) {
global $DB, $OUTPUT;
parent::get_question_options($question);
// Get additional information from database
// and attach it to the question object.
if (!$question->options = $DB->get_record('question_truefalse',
Expand Down

0 comments on commit 605107b

Please sign in to comment.