diff --git a/question/type/ordering/classes/output/feedback.php b/question/type/ordering/classes/output/feedback.php new file mode 100644 index 00000000000..812705c9a81 --- /dev/null +++ b/question/type/ordering/classes/output/feedback.php @@ -0,0 +1,88 @@ +. + +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 + * @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; + } +} diff --git a/question/type/ordering/renderer.php b/question/type/ordering/renderer.php index c98b96f68da..7d134f908ec 100644 --- a/question/type/ordering/renderer.php +++ b/question/type/ordering/renderer.php @@ -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. */ diff --git a/question/type/ordering/templates/feedback.mustache b/question/type/ordering/templates/feedback.mustache new file mode 100644 index 00000000000..166c2005667 --- /dev/null +++ b/question/type/ordering/templates/feedback.mustache @@ -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 . +}} +{{! + @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}} +
+ {{{.}}} +
+{{/specificfeedback}} +{{#numpartscorrect}} +
+ {{>qtype_ordering/num_parts_correct}} +
+{{/numpartscorrect}} +{{#specificgradedetailfeedback}} + {{>qtype_ordering/specific_grade_detail_feedback}} +{{/specificgradedetailfeedback}} +{{#hint}} +
+ {{{.}}} +
+{{/hint}} +{{#generalfeedback}} +
+ {{{.}}} +
+{{/generalfeedback}} +{{#rightanswer}} +
+ {{>qtype_ordering/correct_response}} +
+{{/rightanswer}} diff --git a/question/type/ordering/tests/behat/preview.feature b/question/type/ordering/tests/behat/preview.feature index 9dfa1586dab..e212d891926 100644 --- a/question/type/ordering/tests/behat/preview.feature +++ b/question/type/ordering/tests/behat/preview.feature @@ -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. diff --git a/question/type/ordering/tests/output/feedback_test.php b/question/type/ordering/tests/output/feedback_test.php new file mode 100644 index 00000000000..f162603d545 --- /dev/null +++ b/question/type/ordering/tests/output/feedback_test.php @@ -0,0 +1,366 @@ +. + +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 + * @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, + ], + ], + ], + ]; + } +}