MDL-29691 Improve numerical format in multianswer
[moodle.git] / question / type / multianswer / renderer.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
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.
8 //
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.
13 //
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/>.
17 /**
18  * Multianswer question renderer classes.
19  * Handle shortanswer, numerical and various multichoice subquestions
20  *
21  * @package    qtype
22  * @subpackage multianswer
23  * @copyright  2010 Pierre Pichet
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
28 require_once($CFG->dirroot . '/question/type/shortanswer/renderer.php');
31 /**
32  * Base class for generating the bits of output common to multianswer
33  * (Cloze) questions.
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)
37  *
38  * @copyright 2010 Pierre Pichet
39  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
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();
47         $output = '';
48         foreach ($question->textfragments as $i => $fragment) {
49             if ($i > 0) {
50                 $index = $question->places[$i];
51                 $output .= $this->subquestion($qa, $options, $index,
52                         $question->subquestions[$index]);
53             }
54             $output .= $question->format_text($fragment, $question->questiontextformat,
55                     $qa, 'question', 'questiontext', $question->id);
56         }
58         $this->page->requires->js_init_call('M.qtype_multianswer.init',
59                 array('#q' . $qa->get_slot()), false, array(
60                     'name'     => 'qtype_multianswer',
61                     'fullpath' => '/question/type/multianswer/module.js',
62                     'requires' => array('base', 'node', 'event', 'overlay'),
63                 ));
65         return $output;
66     }
68     public function subquestion(question_attempt $qa,
69             question_display_options $options, $index, question_graded_automatically $subq) {
71         $subtype = $subq->qtype->name();
72         if ($subtype == 'numerical' || $subtype == 'shortanswer') {
73             $subrenderer = 'textfield';
74         } else if ($subtype == 'multichoice') {
75             if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
76                 $subrenderer = 'multichoice_inline';
77             } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
78                 $subrenderer = 'multichoice_horizontal';
79             } else {
80                 $subrenderer = 'multichoice_vertical';
81             }
82         } else {
83             throw new coding_exception('Unexpected subquestion type.', $subq);
84         }
85         $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer);
86         return $renderer->subquestion($qa, $options, $index, $subq);
87     }
89     public function correct_response(question_attempt $qa) {
90         return '';
91     }
92 }
95 /**
96  * Subclass for generating the bits of output specific to shortanswer
97  * subquestions.
98  *
99  * @copyright 2011 The Open University
100  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
101  */
102 abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
104     abstract public function subquestion(question_attempt $qa,
105             question_display_options $options, $index,
106             question_graded_automatically $subq);
108     /**
109      * Render the feedback pop-up contents.
110      *
111      * @param question_graded_automatically $subq the subquestion.
112      * @param float $fraction the mark the student got. null if this subq was not answered.
113      * @param string $feedbacktext the feedback text, already processed with format_text etc.
114      * @param string $rightanswer the right answer, already processed with format_text etc.
115      * @param question_display_options $options the display options.
116      * @return string the HTML for the feedback popup.
117      */
118     protected function feedback_popup(question_graded_automatically $subq,
119             $fraction, $feedbacktext, $rightanswer, question_display_options $options) {
121         $feedback = array();
122         if ($options->correctness) {
123             if (is_null($fraction)) {
124                 $state = question_state::$gaveup;
125             } else {
126                 $state = question_state::graded_state_for_fraction($fraction);
127             }
128             $feedback[] = $state->default_string(true);
129         }
131         if ($options->feedback && $feedbacktext) {
132             $feedback[] = $feedbacktext;
133         }
135         if ($options->rightanswer) {
136             $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
137         }
139         $subfraction = '';
140         if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0
141                 && (!is_null($fraction) || $feedback)) {
142             $a = new stdClass();
143             $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
144             $a->max =  format_float($subq->maxmark, $options->markdp);
145             $feedback[] = get_string('markoutofmax', 'question', $a);
146         }
148         if (!$feedback) {
149             return '';
150         }
152         return html_writer::tag('span', implode('<br />', $feedback),
153                 array('class' => 'feedbackspan accesshide'));
154     }
158 /**
159  * Subclass for generating the bits of output specific to shortanswer
160  * subquestions.
161  *
162  * @copyright 2011 The Open University
163  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
164  */
165 class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base {
167     public function subquestion(question_attempt $qa, question_display_options $options,
168             $index, question_graded_automatically $subq) {
170         $fieldprefix = 'sub' . $index . '_';
171         $fieldname = $fieldprefix . 'answer';
173         $response = $qa->get_last_qt_var($fieldname);
174         if ($subq->qtype->name() == 'shortanswer') {
175             $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
176         } else if ($subq->qtype->name() == 'numerical') {
177             list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
178             $matchinganswer = $subq->get_matching_answer($value, 1);
179         } else {
180             $matchinganswer = $subq->get_matching_answer($response);
181         }
183         if (!$matchinganswer) {
184             if (is_null($response) || $response === '') {
185                 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
186             } else {
187                 $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
188             }
189         }
191         // Work out a good input field size.
192         $size = max(1, strlen(trim($response)) + 1);
193         foreach ($subq->answers as $ans) {
194             $size = max($size, strlen(trim($ans->answer)));
195         }
196         $size = min(60, round($size + rand(0, $size*0.15)));
197         // The rand bit is to make guessing harder.
199         $inputattributes = array(
200             'type' => 'text',
201             'name' => $qa->get_qt_field_name($fieldname),
202             'value' => $response,
203             'id' => $qa->get_qt_field_name($fieldname),
204             'size' => $size,
205         );
206         if ($options->readonly) {
207             $inputattributes['readonly'] = 'readonly';
208         }
210         $feedbackimg = '';
211         if ($options->correctness) {
212             $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
213             $feedbackimg = $this->feedback_image($matchinganswer->fraction);
214         }
216         if ($subq->qtype->name() == 'shortanswer') {
217             $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
218         } else {
219             $correctanswer = $subq->get_correct_answer();
220         }
222         $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
223                 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
224                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
225                 s($correctanswer->answer), $options);
227         $output = html_writer::start_tag('span', array('class' => 'subquestion'));
228         $output .= html_writer::tag('label', get_string('answer'),
229                 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
230         $output .= html_writer::empty_tag('input', $inputattributes);
231         $output .= $feedbackimg;
232         $output .= $feedbackpopup;
233         $output .= html_writer::end_tag('span');
235         return $output;
236     }
240 /**
241  * Render an embedded multiple-choice question that is displayed as a select menu.
242  *
243  * @copyright  2011 The Open University
244  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
245  */
246 class qtype_multianswer_multichoice_inline_renderer
247         extends qtype_multianswer_subq_renderer_base {
249     public function subquestion(question_attempt $qa, question_display_options $options,
250             $index, question_graded_automatically $subq) {
252         $fieldprefix = 'sub' . $index . '_';
253         $fieldname = $fieldprefix . 'answer';
255         $response = $qa->get_last_qt_var($fieldname);
256         $choices = array();
257         $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
258         $rightanswer = null;
259         foreach ($subq->get_order($qa) as $value => $ansid) {
260             $ans = $subq->answers[$ansid];
261             $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat,
262                     $qa, 'question', 'answer', $ansid);
263             if ($subq->is_choice_selected($response, $value)) {
264                 $matchinganswer = $ans;
265             }
266         }
268         $inputattributes = array(
269             'id' => $qa->get_qt_field_name($fieldname),
270         );
271         if ($options->readonly) {
272             $inputattributes['disabled'] = 'disabled';
273         }
275         $feedbackimg = '';
276         if ($options->correctness) {
277             $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
278             $feedbackimg = $this->feedback_image($matchinganswer->fraction);
279         }
280         $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
281                 $response, array('' => ''), $inputattributes);
283         $order = $subq->get_order($qa);
284         $correctresponses = $subq->get_correct_response();
285         $rightanswer = $subq->answers[$order[reset($correctresponses)]];
286         if (!$matchinganswer) {
287             $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
288         }
289         $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
290                 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
291                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
292                 $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
293                         $qa, 'question', 'answer', $rightanswer->id), $options);
295         $output = html_writer::start_tag('span', array('class' => 'subquestion'));
296         $output .= html_writer::tag('label', get_string('answer'),
297                 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
298         $output .= $select;
299         $output .= $feedbackimg;
300         $output .= $feedbackpopup;
301         $output .= html_writer::end_tag('span');
303         return $output;
304     }
308 /**
309  * Render an embedded multiple-choice question vertically, like for a normal
310  * multiple-choice question.
311  *
312  * @copyright  2010 Pierre Pichet
313  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
314  */
315 class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base {
317     public function subquestion(question_attempt $qa, question_display_options $options,
318             $index, question_graded_automatically $subq) {
320         $fieldprefix = 'sub' . $index . '_';
321         $fieldname = $fieldprefix . 'answer';
322         $response = $qa->get_last_qt_var($fieldname);
324         $inputattributes = array(
325             'type' => 'radio',
326             'name' => $qa->get_qt_field_name($fieldname),
327         );
328         if ($options->readonly) {
329             $inputattributes['disabled'] = 'disabled';
330         }
332         $result = $this->all_choices_wrapper_start();
333         $fraction = null;
334         foreach ($subq->get_order($qa) as $value => $ansid) {
335             $ans = $subq->answers[$ansid];
337             $inputattributes['value'] = $value;
338             $inputattributes['id'] = $inputattributes['name'] . $value;
340             $isselected = $subq->is_choice_selected($response, $value);
341             if ($isselected) {
342                 $inputattributes['checked'] = 'checked';
343                 $fraction = $ans->fraction;
344             } else {
345                 unset($inputattributes['checked']);
346             }
348             $class = 'r' . ($value % 2);
349             if ($options->correctness && $isselected) {
350                 $feedbackimg = $this->feedback_image($ans->fraction);
351                 $class .= ' ' . $this->feedback_class($ans->fraction);
352             } else {
353                 $feedbackimg = '';
354             }
356             $result .= $this->choice_wrapper_start($class);
357             $result .= html_writer::empty_tag('input', $inputattributes);
358             $result .= html_writer::tag('label', $subq->format_text($ans->answer,
359                     $ans->answerformat, $qa, 'question', 'answer', $ansid),
360                     array('for' => $inputattributes['id']));
361             $result .= $feedbackimg;
363             if ($options->feedback && $isselected && trim($ans->feedback)) {
364                 $result .= html_writer::tag('div',
365                         $subq->format_text($ans->feedback, $ans->feedbackformat,
366                                 $qa, 'question', 'answerfeedback', $ansid),
367                         array('class' => 'specificfeedback'));
368             }
370             $result .= $this->choice_wrapper_end();
371         }
373         $result .= $this->all_choices_wrapper_end();
375         $feedback = array();
376         if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
377                 $subq->maxmark > 0) {
378             $a = new stdClass();
379             $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
380             $a->max =  format_float($subq->maxmark, $options->markdp);
382             $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
383         }
385         if ($options->rightanswer) {
386             foreach ($subq->answers as $ans) {
387                 if (question_state::graded_state_for_fraction($ans->fraction) ==
388                         question_state::$gradedright) {
389                     $feedback[] = get_string('correctansweris', 'qtype_multichoice',
390                             $subq->format_text($ans->answer, $ans->answerformat,
391                                     $qa, 'question', 'answer', $ansid));
392                     break;
393                 }
394             }
395         }
397         $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
399         return $result;
400     }
402     /**
403      * @param string $class class attribute value.
404      * @return string HTML to go before each choice.
405      */
406     protected function choice_wrapper_start($class) {
407         return html_writer::start_tag('div', array('class' => $class));
408     }
410     /**
411      * @return string HTML to go after each choice.
412      */
413     protected function choice_wrapper_end() {
414         return html_writer::end_tag('div');
415     }
417     /**
418      * @return string HTML to go before all the choices.
419      */
420     protected function all_choices_wrapper_start() {
421         return html_writer::start_tag('div', array('class' => 'answer'));
422     }
424     /**
425      * @return string HTML to go after all the choices.
426      */
427     protected function all_choices_wrapper_end() {
428         return html_writer::end_tag('div');
429     }
433 /**
434  * Render an embedded multiple-choice question vertically, like for a normal
435  * multiple-choice question.
436  *
437  * @copyright  2010 Pierre Pichet
438  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
439  */
440 class qtype_multianswer_multichoice_horizontal_renderer
441         extends qtype_multianswer_multichoice_vertical_renderer {
443     protected function choice_wrapper_start($class) {
444         return html_writer::start_tag('td', array('class' => $class));
445     }
447     protected function choice_wrapper_end() {
448         return html_writer::end_tag('td');
449     }
451     protected function all_choices_wrapper_start() {
452         return html_writer::start_tag('table', array('class' => 'answer')) .
453                 html_writer::start_tag('tbody') . html_writer::start_tag('tr');
454     }
456     protected function all_choices_wrapper_end() {
457         return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
458                 html_writer::end_tag('table');
459     }