MDL-79874 qtype_ordering: Move feedback into exporter

Part of: MDL-79863
This commit is contained in:
Mathew May
2024-04-04 16:21:07 +08:00
parent e1357ee51c
commit 9d060922ae
5 changed files with 562 additions and 42 deletions
@@ -0,0 +1,88 @@
<?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/>.
namespace qtype_ordering\output;
use renderer_base;
use question_attempt;
use question_display_options;
/**
* Renderable class for the displaying the feedback.
*
* @package qtype_ordering
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class feedback extends renderable_base {
/** @var question_display_options $options The question options. */
protected $options;
/**
* The class constructor.
*
* @param question_attempt $qa The question attempt object.
* @param question_display_options $options Controls what should and should not be displayed via question_display_options but unit tests are fickle.
*/
public function __construct(question_attempt $qa, question_display_options $options) {
parent::__construct($qa);
$this->options = $options;
}
/**
* Export the data for the mustache template.
*
* @param renderer_base $output renderer to be used to render the feedback elements.
* @return array
*/
public function export_for_template(renderer_base $output): array {
global $PAGE;
$data = [];
$question = $this->qa->get_question();
$qtyperenderer = $PAGE->get_renderer('qtype_ordering');
if ($this->options->feedback) {
// Literal render out but we trust the teacher.
$data['specificfeedback'] = $qtyperenderer->specific_feedback($this->qa);
$specificgradedetailfeedback = new specific_grade_detail_feedback($this->qa);
$data['specificgradedetailfeedback'] = $specificgradedetailfeedback->export_for_template($output);
if ($hint = $this->qa->get_applicable_hint()) {
$data['hint'] = $question->format_hint($hint, $this->qa);
}
}
if ($this->options->numpartscorrect) {
$numpartscorrect = new num_parts_correct($this->qa);
$data['numpartscorrect'] = $numpartscorrect->export_for_template($output);
}
if ($this->options->generalfeedback) {
// Literal render out but we trust the teacher.
$data['generalfeedback'] = $question->format_generalfeedback($this->qa);
}
if ($this->options->rightanswer) {
$correctresponse = new correct_response($this->qa);
$data['rightanswer'] = $correctresponse->export_for_template($output);
}
return $data;
}
}
+5 -39
View File
@@ -200,50 +200,15 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
* area that contains the various forms of feedback. This function generates
* the content of this area belonging to the question type.
*
* @codeCoverageIgnore This is tested by the feedback exporter.
* @param question_attempt $qa The question attempt to display.
* @param question_display_options $options Controls what should and should not be displayed.
* @return string HTML fragment.
*/
public function feedback(question_attempt $qa, question_display_options $options) {
$output = '';
$hint = null;
$isshownumpartscorrect = true;
if ($options->feedback) {
$output .= html_writer::nonempty_tag('div', $this->specific_feedback($qa),
array('class' => 'specificfeedback'));
if ($options->numpartscorrect) {
$output .= html_writer::nonempty_tag('div', $this->num_parts_correct($qa),
array('class' => 'numpartscorrect'));
$isshownumpartscorrect = false;
}
$output .= $this->specific_grade_detail_feedback($qa);
$hint = $qa->get_applicable_hint();
}
if ($options->numpartscorrect && $isshownumpartscorrect) {
$output .= html_writer::nonempty_tag('div', $this->num_parts_correct($qa),
array('class' => 'numpartscorrect'));
}
if ($hint) {
$output .= $this->hint($qa, $hint);
}
if ($options->generalfeedback) {
$output .= html_writer::nonempty_tag('div', $this->general_feedback($qa),
array('class' => 'generalfeedback'));
}
if ($options->rightanswer) {
$output .= html_writer::nonempty_tag('div', $this->correct_response($qa),
array('class' => 'rightanswer'));
}
return $output;
$feedback = new \qtype_ordering\output\feedback($qa, $options);
return $this->output->render_from_template('qtype_ordering/feedback',
$feedback->export_for_template($this->output));
}
/**
@@ -262,6 +227,7 @@ class qtype_ordering_renderer extends qtype_with_combined_feedback_renderer {
* Generate the specific feedback. This is feedback that varies according to
* the response the student gave.
*
* @codeCoverageIgnore This is tested by the feedback exporter.
* @param question_attempt $qa The question attempt to display.
* @return string HTML fragment.
*/
@@ -0,0 +1,100 @@
{{!
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/>.
}}
{{!
@template qtype_ordering/feedback
Renders the feedback for the question.
Context variables required for this template:
* specificfeedback - Any specific feedback for the question.
* numpartscorrect - The number of parts correct.
* specificgradedetailfeedback - The specific grade detail feedback.
* hint - The hint for the question.
* generalfeedback - The general feedback for the question.
* rightanswer - The right answer for the question.
Example context (json):
{
"specificfeedback": "Maybe you should try again.",
"numpartscorrect": {
"numcorrect": 1,
"numpartial": 3,
"numincorrect": 0
},
"specificgradedetailfeedback": {
"showpartialwrong": true,
"gradingtype": "Grading type: Absolute position",
"gradedetails": "93",
"orderinglayoutclass": "vertical",
"scoredetails": [
{
"score": "1",
"maxscore": "1",
"percent": "100"
},
{
"score": "0",
"maxscore": "1",
"percent": "0"
}
]
},
"hint": "The cake is a lie.",
"generalfeedback": "Here is some general feedback.",
"rightanswer": {
"hascorrectresponse": true,
"showcorrect": "true",
"orderinglayoutclass": "vertical",
"correctanswers": [
{
"answertext": "Correct answer 1"
},
{
"answertext": "Correct answer 2"
}
]
}
}
}}
{{#specificfeedback}}
<div class="specificfeedback">
{{{.}}}
</div>
{{/specificfeedback}}
{{#numpartscorrect}}
<div class="numpartscorrect">
{{>qtype_ordering/num_parts_correct}}
</div>
{{/numpartscorrect}}
{{#specificgradedetailfeedback}}
{{>qtype_ordering/specific_grade_detail_feedback}}
{{/specificgradedetailfeedback}}
{{#hint}}
<div class="hint">
{{{.}}}
</div>
{{/hint}}
{{#generalfeedback}}
<div class="generalfeedback">
{{{.}}}
</div>
{{/generalfeedback}}
{{#rightanswer}}
<div class="rightanswer">
{{>qtype_ordering/correct_response}}
</div>
{{/rightanswer}}
@@ -33,7 +33,7 @@ Feature: Preview an Ordering question
And I drag "Dynamic" to space "4" in the ordering question
And I drag "Learning" to space "5" in the ordering question
And I press "Submit and finish"
Then the state of "Put these words in order." question is shown as "Correct"
Then I should see "Correct items: 6"
And I should see "Mark 1.00 out of 1.00"
@javascript
@@ -48,8 +48,8 @@ Feature: Preview an Ordering question
And I drag "Learning" to space "5" in the ordering question
And I drag "Environment" to space "2" in the ordering question
And I press "Submit and finish"
And I should see "You have 1 item correct."
And I should see "You have 5 items partially correct."
And I should see "Correct items: 1"
And I should see "Partially correct items: 5"
@javascript
Scenario: Preview an Ordering question with no show number of correct option.
@@ -0,0 +1,366 @@
<?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/>.
namespace qtype_ordering\output;
use qbehaviour_walkthrough_test_base;
use qtype_ordering\question_hint_ordering;
use test_question_maker;
use qtype_ordering_question;
use qtype_ordering_test_helper;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Test the feedback exporter.
*
* @package qtype_ordering
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class feedback_test extends qbehaviour_walkthrough_test_base {
/** @var array $correctanswers The correct answers for the question, added to quickly reference. */
private $correctanswers = [
0 => [
'answertext' => 'Modular'
],
1 => [
'answertext' => 'Object'
],
2 => [
'answertext' => 'Oriented'
],
3 => [
'answertext' => 'Dynamic'
],
4 => [
'answertext' => 'Learning'
],
5 => [
'answertext' => 'Environment'
],
];
/**
* Test the exported data for the template that renders the feedback test to a given question attempt.
*
* @covers \qtype_ordering\output\feedback::export_for_template()
* @dataProvider export_for_template_provider
* @param array $answeritems The array of ordered answers.
* @param int $gradingtype Grading type.
* @param array $testoptions Do we want to change direction, is it in progress and do we want feedback.
* @param array $expected The expected exported data.
* @return void
*/
public function test_export_for_template(array $answeritems, int $gradingtype, array $testoptions, array $expected): void {
global $PAGE;
$question = test_question_maker::make_question('ordering');
$question->hints = [
new question_hint_ordering(13, 'This is the first hint.', FORMAT_HTML, true, false, true),
new question_hint_ordering(14, 'This is the second hint.', FORMAT_HTML, false, false, false),
];
$question->options->layouttype = $testoptions['rot'] === 'horizontal' ? qtype_ordering_question::LAYOUT_HORIZONTAL :
qtype_ordering_question::LAYOUT_VERTICAL;
// If we need to access the attempt midway through, we need a flow where we don't grade instantly.
if (!$testoptions['inprogress']) {
$qa = new \testable_question_attempt($question, 0);
$step = new \question_attempt_step();
$qa->add_step($step);
$qa->set_behaviour($question->make_behaviour($qa, 'interactive'));
$question->options->gradingtype = $gradingtype;
$question->start_attempt($step, 1);
// Process a response and check the expected result.
$keys = implode(',', array_keys($answeritems));
$values = array_values($answeritems);
$step->set_qt_var('_currentresponse', $keys);
list($fraction, $state) = $question->grade_response(qtype_ordering_test_helper::get_response($question, $values));
$qa->get_last_step()->set_state($state);
$attempt = $qa;
} else {
$this->start_attempt_at_question($question, 'interactive');
$this->process_submission(array_merge(['-submit' => 1], ['answers' => array_values($answeritems)]));
$attempt = $this->get_question_attempt();
// Omit the numparts as we are not testing it here, and it can be a bit flaky when manually processing an attempt.
$this->displayoptions->numpartscorrect = false;
}
if (!$testoptions['feedback']) {
$this->displayoptions->feedback = false;
}
$renderer = $PAGE->get_renderer('core');
$feedbackobj = new feedback($attempt, $this->displayoptions);
$actual = $feedbackobj->export_for_template($renderer);
$this->assertEquals($expected, $actual);
}
/**
* Data provider for the test_export_for_template test.
*
* @return array
*/
public function export_for_template_provider(): array {
global $CFG;
require_once($CFG->dirroot . '/question/type/ordering/question.php');
return [
'Do not show partial or wrong' => [
[13 => 'Modular', 14 => 'Object', 15 => 'Oriented', 16 => 'Dynamic', 17 => 'Learning', 18 => 'Environment'],
qtype_ordering_question::GRADING_RELATIVE_NEXT_EXCLUDE_LAST,
['rot' => 'horizontal', 'inprogress' => false, 'feedback' => true],
[
'specificfeedback' => 'Well done!',
'numpartscorrect' => [
'numcorrect' => 5,
'numpartial' => 0,
'numincorrect' => 0,
],
'specificgradedetailfeedback' => [
'showpartialwrong' => 0
],
'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".',
'rightanswer' => [
'hascorrectresponse' => true,
'showcorrect' => false
],
],
],
'Partially correct question attempt (horizontal layout). Relative to ALL the previous and next items' => [
[13 => 'Modular', 14 => 'Object', 15 => 'Oriented', 17 => 'Learning', 16 => 'Dynamic', 18 => 'Environment'],
qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT,
['rot' => 'horizontal', 'inprogress' => false, 'feedback' => true],
[
'specificfeedback' => 'Parts, but only parts, of your response are correct.',
'numpartscorrect' => [
'numcorrect' => 4,
'numpartial' => 2,
'numincorrect' => 0,
],
'specificgradedetailfeedback' => [
'showpartialwrong' => 1,
'gradingtype' => 'Grading type: Relative to ALL the previous and next items',
'orderinglayoutclass' => 'horizontal',
'scoredetails' => [
0 => [
'score' => 5,
'maxscore' => 5,
'percent' => 100.00,
],
1 => [
'score' => 5,
'maxscore' => 5,
'percent' => 100.00,
],
2 => [
'score' => 5,
'maxscore' => 5,
'percent' => 100.00,
],
3 => [
'score' => 4,
'maxscore' => 5,
'percent' => 80.00,
],
4 => [
'score' => 4,
'maxscore' => 5,
'percent' => 80.00,
],
5 => [
'score' => 5,
'maxscore' => 5,
'percent' => 100.00,
],
],
'gradedetails' => 93.0,
'totalscore' => 28,
'totalmaxscore' => 30,
],
'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".',
'rightanswer' => [
'hascorrectresponse' => true,
'showcorrect' => true,
'orderinglayoutclass' => 'horizontal',
'correctanswers' => $this->correctanswers,
],
],
],
'Partially correct question attempt in progress (horizontal layout). Relative to ALL the previous and next items with hints' => [
[13 => 'Modular', 14 => 'Object', 15 => 'Oriented', 17 => 'Learning', 16 => 'Dynamic', 18 => 'Environment'],
qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT,
['rot' => 'horizontal', 'inprogress' => true, 'feedback' => true],
[
'specificfeedback' => 'Parts, but only parts, of your response are correct.',
'specificgradedetailfeedback' => [
'showpartialwrong' => 0,
],
'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".',
'rightanswer' => [
'hascorrectresponse' => true,
'showcorrect' => false,
],
'hint' => 'This is the first hint.',
],
],
'Partially correct question attempt in progress (No feedback). Relative to ALL the previous and next items' => [
[13 => 'Modular', 14 => 'Object', 15 => 'Oriented', 17 => 'Learning', 16 => 'Dynamic', 18 => 'Environment'],
qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT,
['rot' => 'horizontal', 'inprogress' => false, 'feedback' => false],
[
'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".',
'rightanswer' => [
'hascorrectresponse' => true,
'showcorrect' => true,
'orderinglayoutclass' => 'horizontal',
'correctanswers' => $this->correctanswers,
],
'numpartscorrect' => [
'numcorrect' => 4,
'numpartial' => 2,
'numincorrect' => 0,
],
],
],
'Incorrect question attempt (horizontal layout). Relative to ALL the previous and next items' => [
[14 => 'Object', 16 => 'Dynamic', 13 => 'Modular', 17 => 'Learning', 18 => 'Environment', 15 => 'Oriented'],
qtype_ordering_question::GRADING_RELATIVE_ALL_PREVIOUS_AND_NEXT,
['rot' => 'horizontal', 'inprogress' => false, 'feedback' => true],
[
'specificfeedback' => 'Parts, but only parts, of your response are correct.',
'numpartscorrect' => [
'numcorrect' => 0,
'numpartial' => 6,
'numincorrect' => 0,
],
'specificgradedetailfeedback' => [
'showpartialwrong' => 1,
'gradingtype' => 'Grading type: Relative to ALL the previous and next items',
'orderinglayoutclass' => 'horizontal',
'scoredetails' => [
0 => [
'score' => 4,
'maxscore' => 5,
'percent' => 80.00,
],
1 => [
'score' => 3,
'maxscore' => 5,
'percent' => 60.00,
],
2 => [
'score' => 3,
'maxscore' => 5,
'percent' => 60.00,
],
3 => [
'score' => 4,
'maxscore' => 5,
'percent' => 80.0,
],
4 => [
'score' => 4,
'maxscore' => 5,
'percent' => 80.00,
],
5 => [
'score' => 2,
'maxscore' => 5,
'percent' => 40.0,
],
],
'gradedetails' => 67.0,
'totalscore' => 20,
'totalmaxscore' => 30,
],
'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".',
'rightanswer' => [
'hascorrectresponse' => true,
'showcorrect' => true,
'orderinglayoutclass' => 'horizontal',
'correctanswers' => $this->correctanswers,
],
],
],
'Incorrect question attempt (vertical layout). Grading type: Relative to the next item (excluding last)' => [
[14 => 'Object', 16 => 'Dynamic', 13 => 'Modular', 17 => 'Learning', 18 => 'Environment', 15 => 'Oriented'],
qtype_ordering_question::GRADING_RELATIVE_NEXT_EXCLUDE_LAST,
['rot' => 'vertical', 'inprogress' => false, 'feedback' => true],
[
'specificfeedback' => 'Parts, but only parts, of your response are correct.',
'numpartscorrect' => [
'numcorrect' => 1,
'numpartial' => 0,
'numincorrect' => 4,
],
'specificgradedetailfeedback' => [
'showpartialwrong' => 1,
'gradingtype' => 'Grading type: Relative to the next item (excluding last)',
'orderinglayoutclass' => 'vertical',
'scoredetails' => [
0 => [
'score' => 0,
'maxscore' => 1,
'percent' => 0.0,
],
1 => [
'score' => 0,
'maxscore' => 1,
'percent' => 0.0,
],
2 => [
'score' => 0,
'maxscore' => 1,
'percent' => 0.0,
],
3 => [
'score' => 1,
'maxscore' => 1,
'percent' => 100.0,
],
4 => [
'score' => 'No score',
'maxscore' => null,
'percent' => 0,
],
5 => [
'score' => 0,
'maxscore' => 1,
'percent' => 0.0,
],
],
'gradedetails' => 20.0,
'totalscore' => 1,
'totalmaxscore' => 5,
],
'generalfeedback' => 'The correct answer is "Modular Object Oriented Dynamic Learning Environment".',
'rightanswer' => [
'hascorrectresponse' => true,
'showcorrect' => true,
'orderinglayoutclass' => 'vertical',
'correctanswers' => $this->correctanswers,
],
],
],
];
}
}