Merge branch 'MDL-71414-401' of https://github.com/laurentdavid/moodle into MOODLE_401_STABLE

This commit is contained in:
Ilya Tregubov
2023-10-10 10:19:44 +08:00
4 changed files with 196 additions and 131 deletions
+2 -2
View File
@@ -57,7 +57,7 @@ class result implements renderable, templatable {
/** Pass answer state. */
const FAIL = 6;
/** Unkown answer state. */
/** Unknown answer state. */
const UNKNOWN = 7;
/** Text answer state. */
@@ -284,7 +284,7 @@ class result implements renderable, templatable {
self::UNCHECKED => 'unchecked',
self::PASS => 'pass',
self::FAIL => 'fail',
self::UNKNOWN => 'unkown',
self::UNKNOWN => 'unknown',
self::TEXT => 'text',
];
$state = $states[$state] ?? self::UNKNOWN;
@@ -27,7 +27,6 @@ namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
use renderer_base;
/**
* Class to display H5P matching result.
@@ -40,7 +39,7 @@ class matching extends result {
/**
* Return the options data structure.
*
* @return array of options
* @return array|null of options
*/
protected function export_options(): ?array {
// Suppose H5P choices have only list of valid answers.
@@ -50,9 +49,9 @@ class matching extends result {
// Get sources (options).
if (isset($additionals->source)) {
$options = $this->get_descriptions($additionals->source);
$sources = $this->get_descriptions($additionals->source);
} else {
$options = [];
$sources = [];
}
// Get targets.
@@ -61,55 +60,69 @@ class matching extends result {
} else {
$targets = [];
}
// Create original options array.
$options = array_map(function ($source) {
$cloneddraggable = clone $source;
$cloneddraggable->correctanswers = [];
return $cloneddraggable;
}, $sources);
// Correct answers.
// Fill options with correct answers flags if they exist.
foreach ($correctpattern as $pattern) {
if (!is_array($pattern) || count($pattern) != 2) {
continue;
}
// One pattern must be from options and the other from targets.
if (isset($options[$pattern[0]]) && isset($targets[$pattern[1]])) {
$option = $options[$pattern[0]];
$target = $targets[$pattern[1]];
} else if (isset($targets[$pattern[0]]) && isset($options[$pattern[1]])) {
$option = $options[$pattern[1]];
// We assume here that the activity is following the convention sets in:
// https://github.com/h5p/h5p-php-report/blob/master/type-processors/matching-processor.class.php
// i.e. source is index 1 and dropzone is index 0.
if (isset($sources[$pattern[1]]) && isset($targets[$pattern[0]])) {
$target = $targets[$pattern[0]];
} else {
$option = null;
}
if ($option) {
$option->correctanswer = $this->get_answer(parent::TEXT, $target->description);
$option->correctanswerid = $target->id;
$source = $sources[$pattern[1]];
$currentoption = $options[$source->id];
$currentoption->correctanswers[$target->id] = $target->description;
}
}
// User responses.
// Fill in user responses.
foreach ($this->response as $response) {
if (!is_array($response) || count($response) != 2) {
continue;
}
// One repsonse must be from options and the other from targets.
if (isset($options[$response[0]]) && isset($targets[$response[1]])) {
$option = $options[$response[0]];
$target = $targets[$response[1]];
$answer = $response[1];
} else if (isset($targets[$response[0]]) && isset($options[$response[1]])) {
$option = $options[$response[1]];
if (isset($sources[$response[1]]) && isset($targets[$response[0]])) {
$source = $sources[$response[1]];
$target = $targets[$response[0]];
$answer = $response[0];
} else {
$option = null;
}
if ($option) {
if (isset($option->correctanswerid) && $option->correctanswerid == $answer) {
$state = parent::CORRECT;
} else {
$state = parent::INCORRECT;
$option = $options[$source->id] ?? null;
if ($option) {
if (isset($option->correctanswers[$answer])) {
$state = parent::CORRECT;
} else {
$state = parent::INCORRECT;
}
$option->useranswer = $this->get_answer($state, $target->description);
}
$option->useranswer = $this->get_answer($state, $target->description);
}
}
return $options;
// Fill in unchecked options.
foreach ($options as $option) {
if (!isset($option->useranswer)) {
if (!empty($option->correctanswers)) {
$option->useranswer = $this->get_answer(parent::INCORRECT,
get_string('answer_noanswer', 'mod_h5pactivity'));
} else {
$option->useranswer = $this->get_answer(parent::CORRECT,
get_string('answer_noanswer', 'mod_h5pactivity'));
}
}
}
// Now flattern correct answers.
foreach ($options as $option) {
$option->correctanswer = $this->get_answer( parent::TEXT, join(', ', $option->correctanswers));
unset($option->correctanswers);
}
return array_values($options);
}
/**
+2 -1
View File
@@ -32,8 +32,9 @@ $string['answer_fail'] = 'Incorrect answer';
$string['answer_incorrect'] = 'Your answer is incorrect';
$string['answer_pass'] = 'Correct answer';
$string['answer_unchecked'] = 'Answer unchecked';
$string['answer_unknown'] = 'Unknown answer';
$string['answer_unknown'] = 'Unkown answer';
$string['answer_text'] = 'Answer text';
$string['answer_noanswer'] = 'None';
$string['areapackage'] = 'Package file';
$string['attempt'] = 'Attempt';
$string['attempt_average'] = 'Attempts average scored';
@@ -28,6 +28,144 @@ use mod_h5pactivity\local\manager;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class result_test extends \advanced_testcase {
/**
* Data provider for result export_options test
*
* @return array[]
*/
public static function result_data_provider(): array {
return [
'fill-in with case sensitive' => [
'result' => [
'interactiontype' => 'fill-in',
'description' => '<p>Fill in the missing words</p>
<p>Meow .... this is a __________</p>
<p>Bark... this is a __________</p>',
'correctpattern' => '["{case_matters=true}cat[,]dog"]',
'response' => 'Cat[,]dog',
'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,'
. '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":true,'
. '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}',
],
'useranswers' => [
'Gap #1 - cat' => (object) ['answer' => 'Cat', 'incorrect' => true],
'Gap #2 - dog' => (object) ['answer' => 'dog', 'correct' => true],
],
],
'fill-in with case insensitive' => [
'result' => [
'interactiontype' => 'fill-in',
'description' => '<p>Fill in the missing words</p>
<p>Meow .... this is a __________</p>
<p>Bark... this is a __________</p>',
'correctpattern' => '["{case_matters=false}cat[,]dog"]',
'response' => 'Cat[,]dog',
'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,'
. '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":false,'
. '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}',
],
'useranswers' => [
'Gap #1 - cat' => (object) ['answer' => 'Cat', 'correct' => true],
'Gap #2 - dog' => (object) ['answer' => 'dog', 'correct' => true],
],
],
'drag and drop' => [
'result' => [
'interactiontype' => 'matching',
'description' => 'Drag and Drop Test',
'correctpattern' => '["0[.]0[,]0[.]2[,]1[.]1[,]1[.]0"]',
'response' => '0[.]0[,]1[.]1[,]0[.]2[,]0[.]3',
'additionals' => '{"source":[{"id":"0","description":{"en-US":"Answer 1 (DZ1 and DZ2)\n"}},'
. '{"id":"1","description":{"en-US":"Anwser 2 (DZ2)\n"}},'
. '{"id":"2","description":{"en-US":"Anwser 3 (DZ1)\n"}},'
. '{"id":"3","description":{"en-US":"Anwser 4 (neither)\n"}}],'
. '"target":[{"id":"0","description":{"en-US":"Dropzone 1\n"}},'
. '{"id":"1","description":{"en-US":"Dropzone 2\n"}}],'
. '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":41,'
. '"http:\/\/h5p.org\/x-api\/h5p-subContentId":"59590246-f16e-4855-8dd6-c80e892ef96b"},'
. '"contextExtensions":{}}',
],
'useranswers' => [
'Answer 1 (DZ1 and DZ2) - Dropzone 1, Dropzone 2' => (object) ['answer' => 'Dropzone 1', 'correct' => true],
'Anwser 2 (DZ2) - Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true],
'Anwser 3 (DZ1) - Dropzone 1' => (object) ['answer' => 'Dropzone 1', 'correct' => true],
'Anwser 4 (neither) - ' => (object) ['answer' => 'Dropzone 1', 'incorrect' => true],
],
],
'drag and drop with no answer' => [
'result' => [
'interactiontype' => 'matching',
'description' => 'Drag and Drop Test',
'correctpattern' => '["0[.]0[,]0[.]2[,]1[.]1[,]1[.]0"]',
'response' => '1[.]0[,]1[.]1',
'additionals' => '{"source":[{"id":"0","description":{"en-US":"Answer 1 (DZ1 and DZ2)\n"}},'
. '{"id":"1","description":{"en-US":"Anwser 2 (DZ2)\n"}},'
. '{"id":"2","description":{"en-US":"Anwser 3 (DZ1)\n"}},'
. '{"id":"3","description":{"en-US":"Anwser 4 (neither)\n"}}],'
. '"target":[{"id":"0","description":{"en-US":"Dropzone 1\n"}},'
. '{"id":"1","description":{"en-US":"Dropzone 2\n"}}],'
. '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":41,'
. '"http:\/\/h5p.org\/x-api\/h5p-subContentId":"59590246-f16e-4855-8dd6-c80e892ef96b"},'
. '"contextExtensions":{}}',
],
'useranswers' => [
'Answer 1 (DZ1 and DZ2) - Dropzone 1, Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true],
'Anwser 2 (DZ2) - Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true],
'Anwser 3 (DZ1) - Dropzone 1' => (object) ['answer' => 'None', 'incorrect' => true],
'Anwser 4 (neither) - ' => (object) ['answer' => 'None', 'correct' => true],
],
],
'sort the paragraph text' => [
'result' => [
'interactiontype' => 'sequencing',
'description' => 'SortParagraphs',
'correctpattern' => '["0[,]1[,]2[,]3[,]4"]',
'response' => '0[,]1[,]3[,]2[,]4',
'additionals' => '{"choices":[{"id":0,"description":{"en":"First I wake up at 7.30 am\\n",'
. '"en-US":"First I wake up at 7.30 am\\n"}}'
. ',{"id":1,"description":{"en":"Next I get dressed\\n","en-US":"Next I get dressed\\n"}},'
. '{"id":2,"description":{"en":"Afterward I have breakfast\\n","en-US":"Afterward I have breakfast\\n"}},'
. '{"id":3,"description":{"en":"I brush my teeth\\n","en-US":"I brush my teeth\\n"}},'
. '{"id":4,"description":{"en":"Finally I go school\\n","en-US":"Finally I go school\\n"}}],'
.
'"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":39,'
. '"https:\\/\\/h5p.org\\/x-api\\/duplicates-interchangeable":1},"contextExtensions":{}}',
],
'useranswers' => [
'#1 - First I wake up at 7.30 am' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#2 - Next I get dressed' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#3 - Afterward I have breakfast' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#4 - I brush my teeth' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#5 - Finally I go school' => (object) ['answer' => 'Correct answer', 'pass' => true],
],
],
'sequencing images' => [
'result' => [
'interactiontype' => 'sequencing',
'description' => 'Order the planets from smallest to largest',
'correctpattern' => '["item_3[,]item_0[,]item_7[,]item_1[,]item_5[,]item_2[,]item_4[,]item_6"]',
'response' => 'item_0[,]item_1[,]item_3[,]item_2[,]item_5[,]item_4[,]item_6[,]item_7',
'additionals' => '{"choices":[{"id":"item_3","description":{"en-US":"Mercury"}},'
. '{"id":"item_0","description":{"en-US":"Mars"}},{"id":"item_7","description":{"en-US":"Earth"}},'
. '{"id":"item_1","description":{"en-US":"Venus"}},{"id":"item_5","description":{"en-US":"Uranus"}},'
. '{"id":"item_2","description":{"en-US":"Neptune"}},{"id":"item_4","description":{"en-US":"Saturn"}},'
. '{"id":"item_6","description":{"en-US":"Jupiter"}}],"extensions":'
. '{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":43},"contextExtensions":{}}',
],
'useranswers' => [
'#1 - Mercury' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#2 - Mars' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#3 - Earth' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#4 - Venus' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#5 - Uranus' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#6 - Neptune' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#7 - Saturn' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#8 - Jupiter' => (object) ['answer' => 'Correct answer', 'pass' => true],
],
],
];
}
/**
* Test result export_options
*
@@ -78,100 +216,13 @@ class result_test extends \advanced_testcase {
$data = $exportoptions->invoke($resultoutput);
$useranswersdata = array_map(function($item) {
return $item->useranswer;
return $item->useranswer ?? null;
}, $data);
$keys = array_map(function($item) {
return $item->description . ' - ' . ($item->correctanswer->answer ?? '');
}, $data);
$useranswersdata = array_combine($keys, $useranswersdata);
$this->assertEquals($expecteduseranswers, $useranswersdata);
}
/**
* Data provider for result export_options test
*
* @return array[]
*/
public function result_data_provider(): array {
return [
'fill-in with case sensitive' => [
'result' => [
'interactiontype' => 'fill-in',
'description' => '<p>Fill in the missing words</p>
<p>Meow .... this is a __________</p>
<p>Bark... this is a __________</p>',
'correctpattern' => '["{case_matters=true}cat[,]dog"]',
'response' => 'Cat[,]dog',
'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,'
. '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":true,'
. '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}',
],
'useranswers' => [
(object) ['answer' => 'Cat', 'incorrect' => true],
(object) ['answer' => 'dog', 'correct' => true],
],
],
'fill-in with case insensitive' => [
'result' => [
'interactiontype' => 'fill-in',
'description' => '<p>Fill in the missing words</p>
<p>Meow .... this is a __________</p>
<p>Bark... this is a __________</p>',
'correctpattern' => '["{case_matters=false}cat[,]dog"]',
'response' => 'Cat[,]dog',
'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,'
. '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":false,'
. '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}',
],
'useranswers' => [
(object) ['answer' => 'Cat', 'correct' => true],
(object) ['answer' => 'dog', 'correct' => true],
],
],
'sort the paragraph text' => [
'result' => [
'interactiontype' => 'sequencing',
'description' => 'SortParagraphs',
'correctpattern' => '["0[,]1[,]2[,]3[,]4"]',
'response' => '0[,]1[,]3[,]2[,]4',
'additionals' => '{"choices":[{"id":0,"description":{"en":"First I wake up at 7.30 am\\n",'
. '"en-US":"First I wake up at 7.30 am\\n"}}'
. ',{"id":1,"description":{"en":"Next I get dressed\\n","en-US":"Next I get dressed\\n"}},'
. '{"id":2,"description":{"en":"Afterward I have breakfast\\n","en-US":"Afterward I have breakfast\\n"}},'
. '{"id":3,"description":{"en":"I brush my teeth\\n","en-US":"I brush my teeth\\n"}},'
. '{"id":4,"description":{"en":"Finally I go school\\n","en-US":"Finally I go school\\n"}}],'
.
'"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":39,'
. '"https:\\/\\/h5p.org\\/x-api\\/duplicates-interchangeable":1},"contextExtensions":{}}',
],
'useranswers' => [
(object) ['answer' => 'Correct answer', 'pass' => true, ],
(object) ['answer' => 'Correct answer', 'pass' => true, ],
(object) ['answer' => 'Incorrect answer', 'fail' => true, ],
(object) ['answer' => 'Incorrect answer', 'fail' => true, ],
(object) ['answer' => 'Correct answer', 'pass' => true, ],
],
],
'sequencing images' => [
'result' => [
'interactiontype' => 'sequencing',
'description' => 'Order the planets from smallest to largest',
'correctpattern' => '["item_3[,]item_0[,]item_7[,]item_1[,]item_5[,]item_2[,]item_4[,]item_6"]',
'response' => 'item_0[,]item_1[,]item_3[,]item_2[,]item_5[,]item_4[,]item_6[,]item_7',
'additionals' => '{"choices":[{"id":"item_3","description":{"en-US":"Mercury"}},'
. '{"id":"item_0","description":{"en-US":"Mars"}},{"id":"item_7","description":{"en-US":"Earth"}},'
. '{"id":"item_1","description":{"en-US":"Venus"}},{"id":"item_5","description":{"en-US":"Uranus"}},'
. '{"id":"item_2","description":{"en-US":"Neptune"}},{"id":"item_4","description":{"en-US":"Saturn"}},'
. '{"id":"item_6","description":{"en-US":"Jupiter"}}],"extensions":'
. '{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":43},"contextExtensions":{}}',
],
'useranswers' => [
(object) ['answer' => 'Correct answer', 'pass' => true, ],
(object) ['answer' => 'Correct answer', 'pass' => true, ],
(object) ['answer' => 'Incorrect answer', 'fail' => true, ],
(object) ['answer' => 'Incorrect answer', 'fail' => true, ],
(object) ['answer' => 'Incorrect answer', 'fail' => true, ],
(object) ['answer' => 'Incorrect answer', 'fail' => true, ],
(object) ['answer' => 'Correct answer', 'pass' => true, ],
(object) ['answer' => 'Correct answer', 'pass' => true, ],
],
]
];
}
}