baa76cee1da6bbfbafb5bd0039e1154026714d8d
[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         $subquestions = array();
49         foreach ($question->textfragments as $i => $fragment) {
50             if ($i > 0) {
51                 $index = $question->places[$i];
52                 $token = 'qtypemultianswer' . $i . 'marker';
53                 $token = '<span class="nolink">' . $token . '</span>';
54                 $output .= $token;
55                 $subquestions[$token] = $this->subquestion($qa, $options, $index,
56                         $question->subquestions[$index]);
57             }
58             $output .= $fragment;
59         }
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'));
68         }
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'),
75                 ));
77         return $output;
78     }
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->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
88                 $subrenderer = 'multichoice_inline';
89             } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
90                 $subrenderer = 'multichoice_horizontal';
91             } else {
92                 $subrenderer = 'multichoice_vertical';
93             }
94         } else {
95             throw new coding_exception('Unexpected subquestion type.', $subq);
96         }
97         $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer);
98         return $renderer->subquestion($qa, $options, $index, $subq);
99     }
101     public function correct_response(question_attempt $qa) {
102         return '';
103     }
107 /**
108  * Subclass for generating the bits of output specific to shortanswer
109  * subquestions.
110  *
111  * @copyright 2011 The Open University
112  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
113  */
114 abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
116     abstract public function subquestion(question_attempt $qa,
117             question_display_options $options, $index,
118             question_graded_automatically $subq);
120     /**
121      * Render the feedback pop-up contents.
122      *
123      * @param question_graded_automatically $subq the subquestion.
124      * @param float $fraction the mark the student got. null if this subq was not answered.
125      * @param string $feedbacktext the feedback text, already processed with format_text etc.
126      * @param string $rightanswer the right answer, already processed with format_text etc.
127      * @param question_display_options $options the display options.
128      * @return string the HTML for the feedback popup.
129      */
130     protected function feedback_popup(question_graded_automatically $subq,
131             $fraction, $feedbacktext, $rightanswer, question_display_options $options) {
133         $feedback = array();
134         if ($options->correctness) {
135             if (is_null($fraction)) {
136                 $state = question_state::$gaveup;
137             } else {
138                 $state = question_state::graded_state_for_fraction($fraction);
139             }
140             $feedback[] = $state->default_string(true);
141         }
143         if ($options->feedback && $feedbacktext) {
144             $feedback[] = $feedbacktext;
145         }
147         if ($options->rightanswer) {
148             $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
149         }
151         $subfraction = '';
152         if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0
153                 && (!is_null($fraction) || $feedback)) {
154             $a = new stdClass();
155             $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
156             $a->max = format_float($subq->maxmark, $options->markdp);
157             $feedback[] = get_string('markoutofmax', 'question', $a);
158         }
160         if (!$feedback) {
161             return '';
162         }
164         return html_writer::tag('span', implode('<br />', $feedback),
165                 array('class' => 'feedbackspan accesshide'));
166     }
170 /**
171  * Subclass for generating the bits of output specific to shortanswer
172  * subquestions.
173  *
174  * @copyright 2011 The Open University
175  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
176  */
177 class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base {
179     public function subquestion(question_attempt $qa, question_display_options $options,
180             $index, question_graded_automatically $subq) {
182         $fieldprefix = 'sub' . $index . '_';
183         $fieldname = $fieldprefix . 'answer';
185         $response = $qa->get_last_qt_var($fieldname);
186         if ($subq->qtype->name() == 'shortanswer') {
187             $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
188         } else if ($subq->qtype->name() == 'numerical') {
189             list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
190             $matchinganswer = $subq->get_matching_answer($value, 1);
191         } else {
192             $matchinganswer = $subq->get_matching_answer($response);
193         }
195         if (!$matchinganswer) {
196             if (is_null($response) || $response === '') {
197                 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
198             } else {
199                 $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
200             }
201         }
203         // Work out a good input field size.
204         $size = max(1, core_text::strlen(trim($response)) + 1);
205         foreach ($subq->answers as $ans) {
206             $size = max($size, core_text::strlen(trim($ans->answer)));
207         }
208         $size = min(60, round($size + rand(0, $size * 0.15)));
209         // The rand bit is to make guessing harder.
211         $inputattributes = array(
212             'type' => 'text',
213             'name' => $qa->get_qt_field_name($fieldname),
214             'value' => $response,
215             'id' => $qa->get_qt_field_name($fieldname),
216             'size' => $size,
217         );
218         if ($options->readonly) {
219             $inputattributes['readonly'] = 'readonly';
220         }
222         $feedbackimg = '';
223         if ($options->correctness) {
224             $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
225             $feedbackimg = $this->feedback_image($matchinganswer->fraction);
226         }
228         if ($subq->qtype->name() == 'shortanswer') {
229             $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
230         } else {
231             $correctanswer = $subq->get_correct_answer();
232         }
234         $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
235                 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
236                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
237                 s($correctanswer->answer), $options);
239         $output = html_writer::start_tag('span', array('class' => 'subquestion'));
240         $output .= html_writer::tag('label', get_string('answer'),
241                 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
242         $output .= html_writer::empty_tag('input', $inputattributes);
243         $output .= $feedbackimg;
244         $output .= $feedbackpopup;
245         $output .= html_writer::end_tag('span');
247         return $output;
248     }
252 /**
253  * Render an embedded multiple-choice question that is displayed as a select menu.
254  *
255  * @copyright  2011 The Open University
256  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
257  */
258 class qtype_multianswer_multichoice_inline_renderer
259         extends qtype_multianswer_subq_renderer_base {
261     public function subquestion(question_attempt $qa, question_display_options $options,
262             $index, question_graded_automatically $subq) {
264         $fieldprefix = 'sub' . $index . '_';
265         $fieldname = $fieldprefix . 'answer';
267         $response = $qa->get_last_qt_var($fieldname);
268         $choices = array();
269         $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
270         $rightanswer = null;
271         foreach ($subq->get_order($qa) as $value => $ansid) {
272             $ans = $subq->answers[$ansid];
273             $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat,
274                     $qa, 'question', 'answer', $ansid);
275             if ($subq->is_choice_selected($response, $value)) {
276                 $matchinganswer = $ans;
277             }
278         }
280         $inputattributes = array(
281             'id' => $qa->get_qt_field_name($fieldname),
282         );
283         if ($options->readonly) {
284             $inputattributes['disabled'] = 'disabled';
285         }
287         $feedbackimg = '';
288         if ($options->correctness) {
289             $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
290             $feedbackimg = $this->feedback_image($matchinganswer->fraction);
291         }
292         $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
293                 $response, array('' => ''), $inputattributes);
295         $order = $subq->get_order($qa);
296         $correctresponses = $subq->get_correct_response();
297         $rightanswer = $subq->answers[$order[reset($correctresponses)]];
298         if (!$matchinganswer) {
299             $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
300         }
301         $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
302                 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
303                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
304                 $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
305                         $qa, 'question', 'answer', $rightanswer->id), $options);
307         $output = html_writer::start_tag('span', array('class' => 'subquestion'));
308         $output .= html_writer::tag('label', get_string('answer'),
309                 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
310         $output .= $select;
311         $output .= $feedbackimg;
312         $output .= $feedbackpopup;
313         $output .= html_writer::end_tag('span');
315         return $output;
316     }
320 /**
321  * Render an embedded multiple-choice question vertically, like for a normal
322  * multiple-choice question.
323  *
324  * @copyright  2010 Pierre Pichet
325  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
326  */
327 class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base {
329     public function subquestion(question_attempt $qa, question_display_options $options,
330             $index, question_graded_automatically $subq) {
332         $fieldprefix = 'sub' . $index . '_';
333         $fieldname = $fieldprefix . 'answer';
334         $response = $qa->get_last_qt_var($fieldname);
336         $inputattributes = array(
337             'type' => 'radio',
338             'name' => $qa->get_qt_field_name($fieldname),
339         );
340         if ($options->readonly) {
341             $inputattributes['disabled'] = 'disabled';
342         }
344         $result = $this->all_choices_wrapper_start();
345         $fraction = null;
346         foreach ($subq->get_order($qa) as $value => $ansid) {
347             $ans = $subq->answers[$ansid];
349             $inputattributes['value'] = $value;
350             $inputattributes['id'] = $inputattributes['name'] . $value;
352             $isselected = $subq->is_choice_selected($response, $value);
353             if ($isselected) {
354                 $inputattributes['checked'] = 'checked';
355                 $fraction = $ans->fraction;
356             } else {
357                 unset($inputattributes['checked']);
358             }
360             $class = 'r' . ($value % 2);
361             if ($options->correctness && $isselected) {
362                 $feedbackimg = $this->feedback_image($ans->fraction);
363                 $class .= ' ' . $this->feedback_class($ans->fraction);
364             } else {
365                 $feedbackimg = '';
366             }
368             $result .= $this->choice_wrapper_start($class);
369             $result .= html_writer::empty_tag('input', $inputattributes);
370             $result .= html_writer::tag('label', $subq->format_text($ans->answer,
371                     $ans->answerformat, $qa, 'question', 'answer', $ansid),
372                     array('for' => $inputattributes['id']));
373             $result .= $feedbackimg;
375             if ($options->feedback && $isselected && trim($ans->feedback)) {
376                 $result .= html_writer::tag('div',
377                         $subq->format_text($ans->feedback, $ans->feedbackformat,
378                                 $qa, 'question', 'answerfeedback', $ansid),
379                         array('class' => 'specificfeedback'));
380             }
382             $result .= $this->choice_wrapper_end();
383         }
385         $result .= $this->all_choices_wrapper_end();
387         $feedback = array();
388         if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
389                 $subq->maxmark > 0) {
390             $a = new stdClass();
391             $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
392             $a->max = format_float($subq->maxmark, $options->markdp);
394             $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
395         }
397         if ($options->rightanswer) {
398             foreach ($subq->answers as $ans) {
399                 if (question_state::graded_state_for_fraction($ans->fraction) ==
400                         question_state::$gradedright) {
401                     $feedback[] = get_string('correctansweris', 'qtype_multichoice',
402                             $subq->format_text($ans->answer, $ans->answerformat,
403                                     $qa, 'question', 'answer', $ansid));
404                     break;
405                 }
406             }
407         }
409         $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
411         return $result;
412     }
414     /**
415      * @param string $class class attribute value.
416      * @return string HTML to go before each choice.
417      */
418     protected function choice_wrapper_start($class) {
419         return html_writer::start_tag('div', array('class' => $class));
420     }
422     /**
423      * @return string HTML to go after each choice.
424      */
425     protected function choice_wrapper_end() {
426         return html_writer::end_tag('div');
427     }
429     /**
430      * @return string HTML to go before all the choices.
431      */
432     protected function all_choices_wrapper_start() {
433         return html_writer::start_tag('div', array('class' => 'answer'));
434     }
436     /**
437      * @return string HTML to go after all the choices.
438      */
439     protected function all_choices_wrapper_end() {
440         return html_writer::end_tag('div');
441     }
445 /**
446  * Render an embedded multiple-choice question vertically, like for a normal
447  * multiple-choice question.
448  *
449  * @copyright  2010 Pierre Pichet
450  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
451  */
452 class qtype_multianswer_multichoice_horizontal_renderer
453         extends qtype_multianswer_multichoice_vertical_renderer {
455     protected function choice_wrapper_start($class) {
456         return html_writer::start_tag('td', array('class' => $class));
457     }
459     protected function choice_wrapper_end() {
460         return html_writer::end_tag('td');
461     }
463     protected function all_choices_wrapper_start() {
464         return html_writer::start_tag('table', array('class' => 'answer')) .
465                 html_writer::start_tag('tbody') . html_writer::start_tag('tr');
466     }
468     protected function all_choices_wrapper_end() {
469         return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
470                 html_writer::end_tag('table');
471     }