weekly release 2.4dev
[moodle.git] / question / type / multianswer / renderer.php
CommitLineData
12c6e008 1<?php
ab50232b
TH
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/>.
16
ab50232b
TH
17/**
18 * Multianswer question renderer classes.
19 * Handle shortanswer, numerical and various multichoice subquestions
20 *
42a5b055
TH
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
ab50232b
TH
25 */
26
42a5b055
TH
27
28require_once($CFG->dirroot . '/question/type/shortanswer/renderer.php');
29
30
ab50232b
TH
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
12c6e008 35 * the task of display their input elements and status
ab50232b
TH
36 * feedback, grade, correct answer(s)
37 *
fa6c8620 38 * @copyright 2010 Pierre Pichet
ab50232b
TH
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 */
12c6e008 41class qtype_multianswer_renderer extends qtype_renderer {
ab50232b
TH
42
43 public function formulation_and_controls(question_attempt $qa,
44 question_display_options $options) {
ab50232b
TH
45 $question = $qa->get_question();
46
42a5b055
TH
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 }
ab50232b 57
42a5b055
TH
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 ));
ab50232b 64
42a5b055
TH
65 return $output;
66 }
ab50232b 67
42a5b055
TH
68 public function subquestion(question_attempt $qa,
69 question_display_options $options, $index, question_graded_automatically $subq) {
7ac7977c 70
42a5b055
TH
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';
ab50232b 79 } else {
42a5b055
TH
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);
ab50232b
TH
87 }
88
ab50232b 89 public function correct_response(question_attempt $qa) {
fa6c8620 90 return '';
ab50232b 91 }
ab50232b
TH
92}
93
94
95/**
96 * Subclass for generating the bits of output specific to shortanswer
97 * subquestions.
98 *
42a5b055 99 * @copyright 2011 The Open University
ab50232b
TH
100 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
101 */
7ac7977c
TH
102abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
103
104 abstract public function subquestion(question_attempt $qa,
105 question_display_options $options, $index,
106 question_graded_automatically $subq);
107
108 /**
109 * Render the feedback pop-up contents.
36e91f1d 110 *
7ac7977c
TH
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) {
120
121 if (!$options->feedback) {
122 return '';
123 }
124
125 $feedback = array();
126 if ($options->correctness) {
127 if (is_null($fraction)) {
128 $state = question_state::$gaveup;
129 } else {
130 $state = question_state::graded_state_for_fraction($fraction);
131 }
132 $feedback[] = $state->default_string(true);
133 }
134
bca80658
TH
135 if ($feedbacktext) { // Note $options->feedback is already checked above.
136 $feedback[] = $feedbacktext;
137 }
138
7ac7977c
TH
139 if ($options->rightanswer) {
140 $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
141 }
142
143 $subfraction = '';
144 if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0) {
145 $a = new stdClass();
146 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
147 $a->max = format_float($subq->maxmark, $options->markdp);
148 $feedback[] = get_string('markoutofmax', 'question', $a);
149 }
150
bca80658
TH
151 if (!$feedback) {
152 return '';
153 }
154
7ac7977c
TH
155 return html_writer::tag('span', implode('<br />', $feedback),
156 array('class' => 'feedbackspan accesshide'));
157 }
158}
159
160
161/**
162 * Subclass for generating the bits of output specific to shortanswer
163 * subquestions.
164 *
165 * @copyright 2011 The Open University
166 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
167 */
168class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base {
ab50232b 169
42a5b055
TH
170 public function subquestion(question_attempt $qa, question_display_options $options,
171 $index, question_graded_automatically $subq) {
172
173 $fieldprefix = 'sub' . $index . '_';
174 $fieldname = $fieldprefix . 'answer';
7ac7977c 175
42a5b055 176 $response = $qa->get_last_qt_var($fieldname);
7ac7977c
TH
177 if ($subq->qtype->name() == 'shortanswer') {
178 $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
3a6eb8ef
TH
179 } else if ($subq->qtype->name() == 'numerical') {
180 $matchinganswer = $subq->get_matching_answer($response, 1);
7ac7977c
TH
181 } else {
182 $matchinganswer = $subq->get_matching_answer($response);
183 }
184
42a5b055 185 if (!$matchinganswer) {
7ac7977c 186 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
ab50232b 187 }
42a5b055
TH
188
189 // Work out a good input field size.
190 $size = max(1, strlen(trim($response)) + 1);
191 foreach ($subq->answers as $ans) {
192 $size = max($size, strlen(trim($ans->answer)));
ab50232b 193 }
42a5b055
TH
194 $size = min(60, round($size + rand(0, $size*0.15)));
195 // The rand bit is to make guessing harder
196
ab50232b
TH
197 $inputattributes = array(
198 'type' => 'text',
42a5b055 199 'name' => $qa->get_qt_field_name($fieldname),
ab50232b 200 'value' => $response,
42a5b055 201 'id' => $qa->get_qt_field_name($fieldname),
ab50232b
TH
202 'size' => $size,
203 );
ab50232b
TH
204 if ($options->readonly) {
205 $inputattributes['readonly'] = 'readonly';
ab50232b 206 }
ab50232b 207
42a5b055
TH
208 $feedbackimg = '';
209 if ($options->correctness) {
7ac7977c
TH
210 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
211 $feedbackimg = $this->feedback_image($matchinganswer->fraction);
ab50232b 212 }
42a5b055 213
7ac7977c
TH
214 if ($subq->qtype->name() == 'shortanswer') {
215 $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
216 } else {
217 $correctanswer = $subq->get_correct_answer();
218 }
42a5b055 219
7ac7977c
TH
220 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
221 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
222 $qa, 'question', 'answerfeedback', $matchinganswer->id),
223 s($correctanswer->answer), $options);
224
225 $output = '';
3211569a 226 $output .= html_writer::start_tag('label', array('class' => 'subq'));
7ac7977c
TH
227 $output .= html_writer::empty_tag('input', $inputattributes);
228 $output .= $feedbackimg;
229 $output .= $feedbackpopup;
230 $output .= html_writer::end_tag('label');
42a5b055 231
7ac7977c
TH
232 return $output;
233 }
234}
235
236
237/**
238 * Render an embedded multiple-choice question that is displayed as a select menu.
239 *
dcedbb0e 240 * @copyright 2011 The Open University
7ac7977c
TH
241 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
242 */
243class qtype_multianswer_multichoice_inline_renderer
244 extends qtype_multianswer_subq_renderer_base {
245
246 public function subquestion(question_attempt $qa, question_display_options $options,
247 $index, question_graded_automatically $subq) {
248
249 $fieldprefix = 'sub' . $index . '_';
250 $fieldname = $fieldprefix . 'answer';
251
252 $response = $qa->get_last_qt_var($fieldname);
253 $choices = array();
254 $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
255 $rightanswer = null;
256 foreach ($subq->get_order($qa) as $value => $ansid) {
257 $ans = $subq->answers[$ansid];
258 $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat,
259 $qa, 'question', 'answer', $ansid);
260 if ($subq->is_choice_selected($response, $value)) {
261 $matchinganswer = $ans;
ab50232b 262 }
7ac7977c 263 }
42a5b055 264
7ac7977c
TH
265 $inputattributes = array(
266 'id' => $qa->get_qt_field_name($fieldname),
267 );
268 if ($options->readonly) {
269 $inputattributes['disabled'] = 'disabled';
ab50232b 270 }
ab50232b 271
7ac7977c
TH
272 $feedbackimg = '';
273 if ($options->correctness) {
274 $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
275 $feedbackimg = $this->feedback_image($matchinganswer->fraction);
276 }
3211569a
SH
277
278 $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
7ac7977c
TH
279 $response, array('' => ''), $inputattributes);
280
281 $order = $subq->get_order($qa);
b8447700
TH
282 $correctresponses = $subq->get_correct_response();
283 $rightanswer = $subq->answers[$order[reset($correctresponses)]];
7ac7977c
TH
284 $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
285 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
286 $qa, 'question', 'answerfeedback', $matchinganswer->id),
287 $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
288 $qa, 'question', 'answer', $rightanswer->id), $options);
289
42a5b055
TH
290 $output = '';
291 $output .= html_writer::start_tag('label', array('class' => 'subq'));
7ac7977c 292 $output .= $select;
42a5b055
TH
293 $output .= $feedbackimg;
294 $output .= $feedbackpopup;
295 $output .= html_writer::end_tag('label');
ab50232b 296
42a5b055
TH
297 return $output;
298 }
ab50232b
TH
299}
300
12c6e008 301
ab50232b 302/**
dcedbb0e
TH
303 * Render an embedded multiple-choice question vertically, like for a normal
304 * multiple-choice question.
12c6e008
TH
305 *
306 * @copyright 2010 Pierre Pichet
307 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
ab50232b 308 */
dcedbb0e 309class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base {
ab50232b 310
dcedbb0e
TH
311 public function subquestion(question_attempt $qa, question_display_options $options,
312 $index, question_graded_automatically $subq) {
12c6e008 313
dcedbb0e
TH
314 $fieldprefix = 'sub' . $index . '_';
315 $fieldname = $fieldprefix . 'answer';
316 $response = $qa->get_last_qt_var($fieldname);
12c6e008 317
ab50232b 318 $inputattributes = array(
dcedbb0e
TH
319 'type' => 'radio',
320 'name' => $qa->get_qt_field_name($fieldname),
12c6e008 321 );
ab50232b
TH
322 if ($options->readonly) {
323 $inputattributes['disabled'] = 'disabled';
324 }
dcedbb0e
TH
325
326 $result = $this->all_choices_wrapper_start();
327 $fraction = null;
328 foreach ($subq->get_order($qa) as $value => $ansid) {
329 $ans = $subq->answers[$ansid];
330
331 $inputattributes['value'] = $value;
332 $inputattributes['id'] = $inputattributes['name'] . $value;
333
334 $isselected = $subq->is_choice_selected($response, $value);
ab50232b
TH
335 if ($isselected) {
336 $inputattributes['checked'] = 'checked';
dcedbb0e 337 $fraction = $ans->fraction;
ab50232b
TH
338 } else {
339 unset($inputattributes['checked']);
340 }
ab50232b 341
ab50232b 342 $class = 'r' . ($value % 2);
dcedbb0e
TH
343 if ($options->correctness && $isselected) {
344 $feedbackimg = $this->feedback_image($ans->fraction);
345 $class .= ' ' . $this->feedback_class($ans->fraction);
346 } else {
347 $feedbackimg = '';
ab50232b 348 }
12c6e008 349
dcedbb0e
TH
350 $result .= $this->choice_wrapper_start($class);
351 $result .= html_writer::empty_tag('input', $inputattributes);
352 $result .= html_writer::tag('label', $subq->format_text($ans->answer,
353 $ans->answerformat, $qa, 'question', 'answer', $ansid),
354 array('for' => $inputattributes['id']));
355 $result .= $feedbackimg;
356
357 if ($options->feedback && $isselected && trim($ans->feedback)) {
358 $result .= html_writer::tag('div',
359 $subq->format_text($ans->feedback, $ans->feedbackformat,
360 $qa, 'question', 'answerfeedback', $ansid),
361 array('class' => 'specificfeedback'));
ab50232b 362 }
12c6e008 363
dcedbb0e 364 $result .= $this->choice_wrapper_end();
12c6e008
TH
365 }
366
dcedbb0e 367 $result .= $this->all_choices_wrapper_end();
12c6e008 368
dcedbb0e
TH
369 if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
370 $subq->maxmark > 0) {
371 $a = new stdClass();
372 $a->mark = format_float($fraction * $subq->maxmark, $options->markdp);
373 $a->max = format_float($subq->maxmark, $options->markdp);
ab50232b 374
dcedbb0e
TH
375 $result .= html_writer::tag('div', get_string('markoutofmax', 'question', $a),
376 array('class' => 'outcome'));
12c6e008 377 }
ab50232b 378
dcedbb0e 379 return $result;
12c6e008
TH
380 }
381
dcedbb0e
TH
382 /**
383 * @param string $class class attribute value.
384 * @return string HTML to go before each choice.
385 */
386 protected function choice_wrapper_start($class) {
387 return html_writer::start_tag('div', array('class' => $class));
12c6e008
TH
388 }
389
dcedbb0e
TH
390 /**
391 * @return string HTML to go after each choice.
392 */
393 protected function choice_wrapper_end() {
394 return html_writer::end_tag('div');
12c6e008
TH
395 }
396
dcedbb0e
TH
397 /**
398 * @return string HTML to go before all the choices.
399 */
400 protected function all_choices_wrapper_start() {
401 return html_writer::start_tag('div', array('class' => 'answer'));
12c6e008
TH
402 }
403
dcedbb0e
TH
404 /**
405 * @return string HTML to go after all the choices.
406 */
407 protected function all_choices_wrapper_end() {
408 return html_writer::end_tag('div');
12c6e008 409 }
dcedbb0e 410}
12c6e008 411
ab50232b 412
dcedbb0e
TH
413/**
414 * Render an embedded multiple-choice question vertically, like for a normal
415 * multiple-choice question.
416 *
417 * @copyright 2010 Pierre Pichet
418 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
419 */
420class qtype_multianswer_multichoice_horizontal_renderer
421 extends qtype_multianswer_multichoice_vertical_renderer {
12c6e008 422
dcedbb0e
TH
423 protected function choice_wrapper_start($class) {
424 return html_writer::start_tag('td', array('class' => $class));
ab50232b 425 }
12c6e008 426
dcedbb0e
TH
427 protected function choice_wrapper_end() {
428 return html_writer::end_tag('td');
429 }
12c6e008 430
dcedbb0e
TH
431 protected function all_choices_wrapper_start() {
432 return html_writer::start_tag('table', array('class' => 'answer')) .
433 html_writer::start_tag('tbody') . html_writer::start_tag('tr');
434 }
ab50232b 435
dcedbb0e
TH
436 protected function all_choices_wrapper_end() {
437 return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
438 html_writer::end_tag('table');
ab50232b 439 }
ab50232b 440}