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 foreach ($question->textfragments as $i => $fragment) {
50 $index = $question->places[$i];
51 $output .= $this->subquestion($qa, $options, $index,
52 $question->subquestions[$index]);
54 $output .= $question->format_text($fragment, $question->questiontextformat,
55 $qa, 'question', 'questiontext', $question->id);
58 if ($qa->get_state() == question_state::$invalid) {
59 $output .= html_writer::nonempty_tag('div',
60 $question->get_validation_error($qa->get_last_qt_data()),
61 array('class' => 'validationerror'));
64 $this->page->requires->js_init_call('M.qtype_multianswer.init',
65 array('#q' . $qa->get_slot()), false, array(
66 'name' => 'qtype_multianswer',
67 'fullpath' => '/question/type/multianswer/module.js',
68 'requires' => array('base', 'node', 'event', 'overlay'),
74 public function subquestion(question_attempt $qa,
75 question_display_options $options, $index, question_graded_automatically $subq) {
77 $subtype = $subq->qtype->name();
78 if ($subtype == 'numerical' || $subtype == 'shortanswer') {
79 $subrenderer = 'textfield';
80 } else if ($subtype == 'multichoice') {
81 if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
82 $subrenderer = 'multichoice_inline';
83 } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
84 $subrenderer = 'multichoice_horizontal';
86 $subrenderer = 'multichoice_vertical';
89 throw new coding_exception('Unexpected subquestion type.', $subq);
91 $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer);
92 return $renderer->subquestion($qa, $options, $index, $subq);
95 public function correct_response(question_attempt $qa) {
102 * Subclass for generating the bits of output specific to shortanswer
105 * @copyright 2011 The Open University
106 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
108 abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
110 abstract public function subquestion(question_attempt $qa,
111 question_display_options $options, $index,
112 question_graded_automatically $subq);
115 * Render the feedback pop-up contents.
117 * @param question_graded_automatically $subq the subquestion.
118 * @param float $fraction the mark the student got. null if this subq was not answered.
119 * @param string $feedbacktext the feedback text, already processed with format_text etc.
120 * @param string $rightanswer the right answer, already processed with format_text etc.
121 * @param question_display_options $options the display options.
122 * @return string the HTML for the feedback popup.
124 protected function feedback_popup(question_graded_automatically $subq,
125 $fraction, $feedbacktext, $rightanswer, question_display_options $options) {
128 if ($options->correctness) {
129 if (is_null($fraction)) {
130 $state = question_state::$gaveup;
132 $state = question_state::graded_state_for_fraction($fraction);
134 $feedback[] = $state->default_string(true);
137 if ($options->feedback && $feedbacktext) {
138 $feedback[] = $feedbacktext;
141 if ($options->rightanswer) {
142 $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
146 if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0
147 && (!is_null($fraction) || $feedback)) {
149 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
150 $a->max = format_float($subq->maxmark, $options->markdp);
151 $feedback[] = get_string('markoutofmax', 'question', $a);
158 return html_writer::tag('span', implode('<br />', $feedback),
159 array('class' => 'feedbackspan accesshide'));
165 * Subclass for generating the bits of output specific to shortanswer
168 * @copyright 2011 The Open University
169 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
171 class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base {
173 public function subquestion(question_attempt $qa, question_display_options $options,
174 $index, question_graded_automatically $subq) {
176 $fieldprefix = 'sub' . $index . '_';
177 $fieldname = $fieldprefix . 'answer';
179 $response = $qa->get_last_qt_var($fieldname);
180 if ($subq->qtype->name() == 'shortanswer') {
181 $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
182 } else if ($subq->qtype->name() == 'numerical') {
183 list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
184 $matchinganswer = $subq->get_matching_answer($value, 1);
186 $matchinganswer = $subq->get_matching_answer($response);
189 if (!$matchinganswer) {
190 if (is_null($response) || $response === '') {
191 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
193 $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
197 // Work out a good input field size.
198 $size = max(1, core_text::strlen(trim($response)) + 1);
199 foreach ($subq->answers as $ans) {
200 $size = max($size, core_text::strlen(trim($ans->answer)));
202 $size = min(60, round($size + rand(0, $size*0.15)));
203 // The rand bit is to make guessing harder.
205 $inputattributes = array(
207 'name' => $qa->get_qt_field_name($fieldname),
208 'value' => $response,
209 'id' => $qa->get_qt_field_name($fieldname),
212 if ($options->readonly) {
213 $inputattributes['readonly'] = 'readonly';
217 if ($options->correctness) {
218 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
219 $feedbackimg = $this->feedback_image($matchinganswer->fraction);
222 if ($subq->qtype->name() == 'shortanswer') {
223 $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
225 $correctanswer = $subq->get_correct_answer();
228 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
229 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
230 $qa, 'question', 'answerfeedback', $matchinganswer->id),
231 s($correctanswer->answer), $options);
233 $output = html_writer::start_tag('span', array('class' => 'subquestion'));
234 $output .= html_writer::tag('label', get_string('answer'),
235 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
236 $output .= html_writer::empty_tag('input', $inputattributes);
237 $output .= $feedbackimg;
238 $output .= $feedbackpopup;
239 $output .= html_writer::end_tag('span');
247 * Render an embedded multiple-choice question that is displayed as a select menu.
249 * @copyright 2011 The Open University
250 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
252 class qtype_multianswer_multichoice_inline_renderer
253 extends qtype_multianswer_subq_renderer_base {
255 public function subquestion(question_attempt $qa, question_display_options $options,
256 $index, question_graded_automatically $subq) {
258 $fieldprefix = 'sub' . $index . '_';
259 $fieldname = $fieldprefix . 'answer';
261 $response = $qa->get_last_qt_var($fieldname);
263 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
265 foreach ($subq->get_order($qa) as $value => $ansid) {
266 $ans = $subq->answers[$ansid];
267 $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat,
268 $qa, 'question', 'answer', $ansid);
269 if ($subq->is_choice_selected($response, $value)) {
270 $matchinganswer = $ans;
274 $inputattributes = array(
275 'id' => $qa->get_qt_field_name($fieldname),
277 if ($options->readonly) {
278 $inputattributes['disabled'] = 'disabled';
282 if ($options->correctness) {
283 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
284 $feedbackimg = $this->feedback_image($matchinganswer->fraction);
286 $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
287 $response, array('' => ''), $inputattributes);
289 $order = $subq->get_order($qa);
290 $correctresponses = $subq->get_correct_response();
291 $rightanswer = $subq->answers[$order[reset($correctresponses)]];
292 if (!$matchinganswer) {
293 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
295 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
296 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
297 $qa, 'question', 'answerfeedback', $matchinganswer->id),
298 $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
299 $qa, 'question', 'answer', $rightanswer->id), $options);
301 $output = html_writer::start_tag('span', array('class' => 'subquestion'));
302 $output .= html_writer::tag('label', get_string('answer'),
303 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
305 $output .= $feedbackimg;
306 $output .= $feedbackpopup;
307 $output .= html_writer::end_tag('span');
315 * Render an embedded multiple-choice question vertically, like for a normal
316 * multiple-choice question.
318 * @copyright 2010 Pierre Pichet
319 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
321 class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base {
323 public function subquestion(question_attempt $qa, question_display_options $options,
324 $index, question_graded_automatically $subq) {
326 $fieldprefix = 'sub' . $index . '_';
327 $fieldname = $fieldprefix . 'answer';
328 $response = $qa->get_last_qt_var($fieldname);
330 $inputattributes = array(
332 'name' => $qa->get_qt_field_name($fieldname),
334 if ($options->readonly) {
335 $inputattributes['disabled'] = 'disabled';
338 $result = $this->all_choices_wrapper_start();
340 foreach ($subq->get_order($qa) as $value => $ansid) {
341 $ans = $subq->answers[$ansid];
343 $inputattributes['value'] = $value;
344 $inputattributes['id'] = $inputattributes['name'] . $value;
346 $isselected = $subq->is_choice_selected($response, $value);
348 $inputattributes['checked'] = 'checked';
349 $fraction = $ans->fraction;
351 unset($inputattributes['checked']);
354 $class = 'r' . ($value % 2);
355 if ($options->correctness && $isselected) {
356 $feedbackimg = $this->feedback_image($ans->fraction);
357 $class .= ' ' . $this->feedback_class($ans->fraction);
362 $result .= $this->choice_wrapper_start($class);
363 $result .= html_writer::empty_tag('input', $inputattributes);
364 $result .= html_writer::tag('label', $subq->format_text($ans->answer,
365 $ans->answerformat, $qa, 'question', 'answer', $ansid),
366 array('for' => $inputattributes['id']));
367 $result .= $feedbackimg;
369 if ($options->feedback && $isselected && trim($ans->feedback)) {
370 $result .= html_writer::tag('div',
371 $subq->format_text($ans->feedback, $ans->feedbackformat,
372 $qa, 'question', 'answerfeedback', $ansid),
373 array('class' => 'specificfeedback'));
376 $result .= $this->choice_wrapper_end();
379 $result .= $this->all_choices_wrapper_end();
382 if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
383 $subq->maxmark > 0) {
385 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
386 $a->max = format_float($subq->maxmark, $options->markdp);
388 $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
391 if ($options->rightanswer) {
392 foreach ($subq->answers as $ans) {
393 if (question_state::graded_state_for_fraction($ans->fraction) ==
394 question_state::$gradedright) {
395 $feedback[] = get_string('correctansweris', 'qtype_multichoice',
396 $subq->format_text($ans->answer, $ans->answerformat,
397 $qa, 'question', 'answer', $ansid));
403 $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
409 * @param string $class class attribute value.
410 * @return string HTML to go before each choice.
412 protected function choice_wrapper_start($class) {
413 return html_writer::start_tag('div', array('class' => $class));
417 * @return string HTML to go after each choice.
419 protected function choice_wrapper_end() {
420 return html_writer::end_tag('div');
424 * @return string HTML to go before all the choices.
426 protected function all_choices_wrapper_start() {
427 return html_writer::start_tag('div', array('class' => 'answer'));
431 * @return string HTML to go after all the choices.
433 protected function all_choices_wrapper_end() {
434 return html_writer::end_tag('div');
440 * Render an embedded multiple-choice question vertically, like for a normal
441 * multiple-choice question.
443 * @copyright 2010 Pierre Pichet
444 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
446 class qtype_multianswer_multichoice_horizontal_renderer
447 extends qtype_multianswer_multichoice_vertical_renderer {
449 protected function choice_wrapper_start($class) {
450 return html_writer::start_tag('td', array('class' => $class));
453 protected function choice_wrapper_end() {
454 return html_writer::end_tag('td');
457 protected function all_choices_wrapper_start() {
458 return html_writer::start_tag('table', array('class' => 'answer')) .
459 html_writer::start_tag('tbody') . html_writer::start_tag('tr');
462 protected function all_choices_wrapper_end() {
463 return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
464 html_writer::end_tag('table');