2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Multianswer question renderer classes.
19 * Handle shortanswer, numerical and various multichoice subquestions
22 * @subpackage multianswer
23 * @copyright 2010 Pierre Pichet
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 require_once($CFG->dirroot
. '/question/type/shortanswer/renderer.php');
32 * Base class for generating the bits of output common to multianswer
34 * This render the main question text and transfer to the subquestions
35 * the task of display their input elements and status
36 * feedback, grade, correct answer(s)
38 * @copyright 2010 Pierre Pichet
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 class qtype_multianswer_renderer
extends qtype_renderer
{
43 public function formulation_and_controls(question_attempt
$qa,
44 question_display_options
$options) {
45 $question = $qa->get_question();
48 $subquestions = array();
49 foreach ($question->textfragments
as $i => $fragment) {
51 $index = $question->places
[$i];
52 $token = 'qtypemultianswer' . $i . 'marker';
53 $token = '<span class="nolink">' . $token . '</span>';
55 $subquestions[$token] = $this->subquestion($qa, $options, $index,
56 $question->subquestions
[$index]);
60 $output = $question->format_text($output, $question->questiontextformat
,
61 $qa, 'question', 'questiontext', $question->id
);
62 $output = str_replace(array_keys($subquestions), array_values($subquestions), $output);
64 if ($qa->get_state() == question_state
::$invalid) {
65 $output .= html_writer
::nonempty_tag('div',
66 $question->get_validation_error($qa->get_last_qt_data()),
67 array('class' => 'validationerror'));
70 $this->page
->requires
->js_init_call('M.qtype_multianswer.init',
71 array('#q' . $qa->get_slot()), false, array(
72 'name' => 'qtype_multianswer',
73 'fullpath' => '/question/type/multianswer/module.js',
74 'requires' => array('base', 'node', 'event', 'overlay'),
80 public function subquestion(question_attempt
$qa,
81 question_display_options
$options, $index, question_graded_automatically
$subq) {
83 $subtype = $subq->qtype
->name();
84 if ($subtype == 'numerical' ||
$subtype == 'shortanswer') {
85 $subrenderer = 'textfield';
86 } else if ($subtype == 'multichoice') {
87 if ($subq instanceof qtype_multichoice_multi_question
) {
88 if ($subq->layout
== qtype_multichoice_base
::LAYOUT_VERTICAL
) {
89 $subrenderer = 'multiresponse_vertical';
91 $subrenderer = 'multiresponse_horizontal';
94 if ($subq->layout
== qtype_multichoice_base
::LAYOUT_DROPDOWN
) {
95 $subrenderer = 'multichoice_inline';
96 } else if ($subq->layout
== qtype_multichoice_base
::LAYOUT_HORIZONTAL
) {
97 $subrenderer = 'multichoice_horizontal';
99 $subrenderer = 'multichoice_vertical';
103 throw new coding_exception('Unexpected subquestion type.', $subq);
105 $renderer = $this->page
->get_renderer('qtype_multianswer', $subrenderer);
106 return $renderer->subquestion($qa, $options, $index, $subq);
109 public function correct_response(question_attempt
$qa) {
116 * Subclass for generating the bits of output specific to shortanswer
119 * @copyright 2011 The Open University
120 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
122 abstract class qtype_multianswer_subq_renderer_base
extends qtype_renderer
{
124 abstract public function subquestion(question_attempt
$qa,
125 question_display_options
$options, $index,
126 question_graded_automatically
$subq);
129 * Render the feedback pop-up contents.
131 * @param question_graded_automatically $subq the subquestion.
132 * @param float $fraction the mark the student got. null if this subq was not answered.
133 * @param string $feedbacktext the feedback text, already processed with format_text etc.
134 * @param string $rightanswer the right answer, already processed with format_text etc.
135 * @param question_display_options $options the display options.
136 * @return string the HTML for the feedback popup.
138 protected function feedback_popup(question_graded_automatically
$subq,
139 $fraction, $feedbacktext, $rightanswer, question_display_options
$options) {
142 if ($options->correctness
) {
143 if (is_null($fraction)) {
144 $state = question_state
::$gaveup;
146 $state = question_state
::graded_state_for_fraction($fraction);
148 $feedback[] = $state->default_string(true);
151 if ($options->feedback
&& $feedbacktext) {
152 $feedback[] = $feedbacktext;
155 if ($options->rightanswer
) {
156 $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
160 if ($options->marks
>= question_display_options
::MARK_AND_MAX
&& $subq->maxmark
> 0
161 && (!is_null($fraction) ||
$feedback)) {
163 $a->mark
= format_float($fraction * $subq->maxmark
, $options->markdp
);
164 $a->max
= format_float($subq->maxmark
, $options->markdp
);
165 $feedback[] = get_string('markoutofmax', 'question', $a);
172 return html_writer
::tag('span', implode('<br />', $feedback),
173 array('class' => 'feedbackspan accesshide'));
179 * Subclass for generating the bits of output specific to shortanswer
182 * @copyright 2011 The Open University
183 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
185 class qtype_multianswer_textfield_renderer
extends qtype_multianswer_subq_renderer_base
{
187 public function subquestion(question_attempt
$qa, question_display_options
$options,
188 $index, question_graded_automatically
$subq) {
190 $fieldprefix = 'sub' . $index . '_';
191 $fieldname = $fieldprefix . 'answer';
193 $response = $qa->get_last_qt_var($fieldname);
194 if ($subq->qtype
->name() == 'shortanswer') {
195 $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
196 } else if ($subq->qtype
->name() == 'numerical') {
197 list($value, $unit, $multiplier) = $subq->ap
->apply_units($response, '');
198 $matchinganswer = $subq->get_matching_answer($value, 1);
200 $matchinganswer = $subq->get_matching_answer($response);
203 if (!$matchinganswer) {
204 if (is_null($response) ||
$response === '') {
205 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML
);
207 $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML
);
211 // Work out a good input field size.
212 $size = max(1, core_text
::strlen(trim($response)) +
1);
213 foreach ($subq->answers
as $ans) {
214 $size = max($size, core_text
::strlen(trim($ans->answer
)));
216 $size = min(60, round($size +
rand(0, $size * 0.15)));
217 // The rand bit is to make guessing harder.
219 $inputattributes = array(
221 'name' => $qa->get_qt_field_name($fieldname),
222 'value' => $response,
223 'id' => $qa->get_qt_field_name($fieldname),
225 'class' => 'form-control',
227 if ($options->readonly
) {
228 $inputattributes['readonly'] = 'readonly';
232 if ($options->correctness
) {
233 $inputattributes['class'] .= ' ' . $this->feedback_class($matchinganswer->fraction
);
234 $feedbackimg = $this->feedback_image($matchinganswer->fraction
);
237 if ($subq->qtype
->name() == 'shortanswer') {
238 $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
240 $correctanswer = $subq->get_correct_answer();
243 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction
,
244 $subq->format_text($matchinganswer->feedback
, $matchinganswer->feedbackformat
,
245 $qa, 'question', 'answerfeedback', $matchinganswer->id
),
246 s($correctanswer->answer
), $options);
248 $output = html_writer
::start_tag('span', array('class' => 'subquestion form-inline'));
249 $output .= html_writer
::tag('label', get_string('answer'),
250 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
251 $output .= html_writer
::empty_tag('input', $inputattributes);
252 $output .= $feedbackimg;
253 $output .= $feedbackpopup;
254 $output .= html_writer
::end_tag('span');
262 * Render an embedded multiple-choice question that is displayed as a select menu.
264 * @copyright 2011 The Open University
265 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
267 class qtype_multianswer_multichoice_inline_renderer
268 extends qtype_multianswer_subq_renderer_base
{
270 public function subquestion(question_attempt
$qa, question_display_options
$options,
271 $index, question_graded_automatically
$subq) {
273 $fieldprefix = 'sub' . $index . '_';
274 $fieldname = $fieldprefix . 'answer';
276 $response = $qa->get_last_qt_var($fieldname);
278 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML
);
280 foreach ($subq->get_order($qa) as $value => $ansid) {
281 $ans = $subq->answers
[$ansid];
282 $choices[$value] = $subq->format_text($ans->answer
, $ans->answerformat
,
283 $qa, 'question', 'answer', $ansid);
284 if ($subq->is_choice_selected($response, $value)) {
285 $matchinganswer = $ans;
289 $inputattributes = array(
290 'id' => $qa->get_qt_field_name($fieldname),
292 if ($options->readonly
) {
293 $inputattributes['disabled'] = 'disabled';
297 if ($options->correctness
) {
298 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction
);
299 $feedbackimg = $this->feedback_image($matchinganswer->fraction
);
301 $select = html_writer
::select($choices, $qa->get_qt_field_name($fieldname),
302 $response, array('' => ''), $inputattributes);
304 $order = $subq->get_order($qa);
305 $correctresponses = $subq->get_correct_response();
306 $rightanswer = $subq->answers
[$order[reset($correctresponses)]];
307 if (!$matchinganswer) {
308 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML
);
310 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction
,
311 $subq->format_text($matchinganswer->feedback
, $matchinganswer->feedbackformat
,
312 $qa, 'question', 'answerfeedback', $matchinganswer->id
),
313 $subq->format_text($rightanswer->answer
, $rightanswer->answerformat
,
314 $qa, 'question', 'answer', $rightanswer->id
), $options);
316 $output = html_writer
::start_tag('span', array('class' => 'subquestion'));
317 $output .= html_writer
::tag('label', get_string('answer'),
318 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
320 $output .= $feedbackimg;
321 $output .= $feedbackpopup;
322 $output .= html_writer
::end_tag('span');
330 * Render an embedded multiple-choice question vertically, like for a normal
331 * multiple-choice question.
333 * @copyright 2010 Pierre Pichet
334 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
336 class qtype_multianswer_multichoice_vertical_renderer
extends qtype_multianswer_subq_renderer_base
{
338 public function subquestion(question_attempt
$qa, question_display_options
$options,
339 $index, question_graded_automatically
$subq) {
341 $fieldprefix = 'sub' . $index . '_';
342 $fieldname = $fieldprefix . 'answer';
343 $response = $qa->get_last_qt_var($fieldname);
345 $inputattributes = array(
347 'name' => $qa->get_qt_field_name($fieldname),
349 if ($options->readonly
) {
350 $inputattributes['disabled'] = 'disabled';
353 $result = $this->all_choices_wrapper_start();
355 foreach ($subq->get_order($qa) as $value => $ansid) {
356 $ans = $subq->answers
[$ansid];
358 $inputattributes['value'] = $value;
359 $inputattributes['id'] = $inputattributes['name'] . $value;
361 $isselected = $subq->is_choice_selected($response, $value);
363 $inputattributes['checked'] = 'checked';
364 $fraction = $ans->fraction
;
366 unset($inputattributes['checked']);
369 $class = 'r' . ($value %
2);
370 if ($options->correctness
&& $isselected) {
371 $feedbackimg = $this->feedback_image($ans->fraction
);
372 $class .= ' ' . $this->feedback_class($ans->fraction
);
377 $result .= $this->choice_wrapper_start($class);
378 $result .= html_writer
::empty_tag('input', $inputattributes);
379 $result .= html_writer
::tag('label', $subq->format_text($ans->answer
,
380 $ans->answerformat
, $qa, 'question', 'answer', $ansid),
381 array('for' => $inputattributes['id']));
382 $result .= $feedbackimg;
384 if ($options->feedback
&& $isselected && trim($ans->feedback
)) {
385 $result .= html_writer
::tag('div',
386 $subq->format_text($ans->feedback
, $ans->feedbackformat
,
387 $qa, 'question', 'answerfeedback', $ansid),
388 array('class' => 'specificfeedback'));
391 $result .= $this->choice_wrapper_end();
394 $result .= $this->all_choices_wrapper_end();
397 if ($options->feedback
&& $options->marks
>= question_display_options
::MARK_AND_MAX
&&
398 $subq->maxmark
> 0) {
400 $a->mark
= format_float($fraction * $subq->maxmark
, $options->markdp
);
401 $a->max
= format_float($subq->maxmark
, $options->markdp
);
403 $feedback[] = html_writer
::tag('div', get_string('markoutofmax', 'question', $a));
406 if ($options->rightanswer
) {
407 foreach ($subq->answers
as $ans) {
408 if (question_state
::graded_state_for_fraction($ans->fraction
) ==
409 question_state
::$gradedright) {
410 $feedback[] = get_string('correctansweris', 'qtype_multichoice',
411 $subq->format_text($ans->answer
, $ans->answerformat
,
412 $qa, 'question', 'answer', $ansid));
418 $result .= html_writer
::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
424 * @param string $class class attribute value.
425 * @return string HTML to go before each choice.
427 protected function choice_wrapper_start($class) {
428 return html_writer
::start_tag('div', array('class' => $class));
432 * @return string HTML to go after each choice.
434 protected function choice_wrapper_end() {
435 return html_writer
::end_tag('div');
439 * @return string HTML to go before all the choices.
441 protected function all_choices_wrapper_start() {
442 return html_writer
::start_tag('div', array('class' => 'answer'));
446 * @return string HTML to go after all the choices.
448 protected function all_choices_wrapper_end() {
449 return html_writer
::end_tag('div');
455 * Render an embedded multiple-choice question vertically, like for a normal
456 * multiple-choice question.
458 * @copyright 2010 Pierre Pichet
459 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
461 class qtype_multianswer_multichoice_horizontal_renderer
462 extends qtype_multianswer_multichoice_vertical_renderer
{
464 protected function choice_wrapper_start($class) {
465 return html_writer
::start_tag('td', array('class' => $class));
468 protected function choice_wrapper_end() {
469 return html_writer
::end_tag('td');
472 protected function all_choices_wrapper_start() {
473 return html_writer
::start_tag('table', array('class' => 'answer')) .
474 html_writer
::start_tag('tbody') . html_writer
::start_tag('tr');
477 protected function all_choices_wrapper_end() {
478 return html_writer
::end_tag('tr') . html_writer
::end_tag('tbody') .
479 html_writer
::end_tag('table');
484 * Class qtype_multianswer_multiresponse_renderer
486 * @copyright 2016 Davo Smith, Synergy Learning
487 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
489 class qtype_multianswer_multiresponse_vertical_renderer
extends qtype_multianswer_subq_renderer_base
{
492 * Output the content of the subquestion.
494 * @param question_attempt $qa
495 * @param question_display_options $options
497 * @param question_graded_automatically $subq
500 public function subquestion(question_attempt
$qa, question_display_options
$options,
501 $index, question_graded_automatically
$subq) {
503 if (!$subq instanceof qtype_multichoice_multi_question
) {
504 throw new coding_exception('Expecting subquestion of type qtype_multichoice_multi_question');
507 $fieldprefix = 'sub' . $index . '_';
508 $fieldname = $fieldprefix . 'choice';
510 // Extract the responses that related to this question + strip off the prefix.
511 $fieldprefixlen = strlen($fieldprefix);
513 foreach ($qa->get_last_qt_data() as $name => $val) {
514 if (substr($name, 0, $fieldprefixlen) == $fieldprefix) {
515 $name = substr($name, $fieldprefixlen);
516 $response[$name] = $val;
520 $basename = $qa->get_qt_field_name($fieldname);
521 $inputattributes = array(
522 'type' => 'checkbox',
525 if ($options->readonly
) {
526 $inputattributes['disabled'] = 'disabled';
529 $result = $this->all_choices_wrapper_start();
531 // Calculate the total score (as we need to know if choices should be marked as 'correct' or 'partial').
533 foreach ($subq->get_order($qa) as $value => $ansid) {
534 $ans = $subq->answers
[$ansid];
535 if ($subq->is_choice_selected($response, $value)) {
536 $fraction +
= $ans->fraction
;
539 // Display 'correct' answers as correct, if we are at 100%, otherwise mark them as 'partial'.
540 $answerfraction = ($fraction > 0.999) ?
1.0 : 0.5;
542 foreach ($subq->get_order($qa) as $value => $ansid) {
543 $ans = $subq->answers
[$ansid];
545 $name = $basename.$value;
546 $inputattributes['name'] = $name;
547 $inputattributes['id'] = $name;
549 $isselected = $subq->is_choice_selected($response, $value);
551 $inputattributes['checked'] = 'checked';
553 unset($inputattributes['checked']);
556 $class = 'r' . ($value %
2);
557 if ($options->correctness
&& $isselected) {
558 $thisfrac = ($ans->fraction
> 0) ?
$answerfraction : 0;
559 $feedbackimg = $this->feedback_image($thisfrac);
560 $class .= ' ' . $this->feedback_class($thisfrac);
565 $result .= $this->choice_wrapper_start($class);
566 $result .= html_writer
::empty_tag('input', $inputattributes);
567 $result .= html_writer
::tag('label', $subq->format_text($ans->answer
,
568 $ans->answerformat
, $qa, 'question', 'answer', $ansid),
569 array('for' => $inputattributes['id']));
570 $result .= $feedbackimg;
572 if ($options->feedback
&& $isselected && trim($ans->feedback
)) {
573 $result .= html_writer
::tag('div',
574 $subq->format_text($ans->feedback
, $ans->feedbackformat
,
575 $qa, 'question', 'answerfeedback', $ansid),
576 array('class' => 'specificfeedback'));
579 $result .= $this->choice_wrapper_end();
582 $result .= $this->all_choices_wrapper_end();
585 if ($options->feedback
&& $options->marks
>= question_display_options
::MARK_AND_MAX
&&
586 $subq->maxmark
> 0) {
588 $a->mark
= format_float($fraction * $subq->maxmark
, $options->markdp
);
589 $a->max
= format_float($subq->maxmark
, $options->markdp
);
591 $feedback[] = html_writer
::tag('div', get_string('markoutofmax', 'question', $a));
594 if ($options->rightanswer
) {
596 foreach ($subq->answers
as $ans) {
597 if (question_state
::graded_state_for_fraction($ans->fraction
) != question_state
::$gradedwrong) {
598 $correct[] = $subq->format_text($ans->answer
, $ans->answerformat
, $qa, 'question', 'answer', $ans->id
);
601 $correct = '<ul><li>'.implode('</li><li>', $correct).'</li></ul>';
602 $feedback[] = get_string('correctansweris', 'qtype_multichoice', $correct);
605 $result .= html_writer
::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
611 * @param string $class class attribute value.
612 * @return string HTML to go before each choice.
614 protected function choice_wrapper_start($class) {
615 return html_writer
::start_tag('div', array('class' => $class));
619 * @return string HTML to go after each choice.
621 protected function choice_wrapper_end() {
622 return html_writer
::end_tag('div');
626 * @return string HTML to go before all the choices.
628 protected function all_choices_wrapper_start() {
629 return html_writer
::start_tag('div', array('class' => 'answer'));
633 * @return string HTML to go after all the choices.
635 protected function all_choices_wrapper_end() {
636 return html_writer
::end_tag('div');
641 * Render an embedded multiple-response question horizontally.
643 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
645 class qtype_multianswer_multiresponse_horizontal_renderer
646 extends qtype_multianswer_multiresponse_vertical_renderer
{
648 protected function choice_wrapper_start($class) {
649 return html_writer
::start_tag('td', array('class' => $class));
652 protected function choice_wrapper_end() {
653 return html_writer
::end_tag('td');
656 protected function all_choices_wrapper_start() {
657 return html_writer
::start_tag('table', array('class' => 'answer')) .
658 html_writer
::start_tag('tbody') . html_writer
::start_tag('tr');
661 protected function all_choices_wrapper_end() {
662 return html_writer
::end_tag('tr') . html_writer
::end_tag('tbody') .
663 html_writer
::end_tag('table');