Commit | Line | Data |
---|---|---|
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 | |
28 | require_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 | 41 | class 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 | |
86969816 TH |
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')); | |
62 | } | |
63 | ||
42a5b055 TH |
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'), | |
69 | )); | |
ab50232b | 70 | |
42a5b055 TH |
71 | return $output; |
72 | } | |
ab50232b | 73 | |
42a5b055 TH |
74 | public function subquestion(question_attempt $qa, |
75 | question_display_options $options, $index, question_graded_automatically $subq) { | |
7ac7977c | 76 | |
42a5b055 TH |
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'; | |
ab50232b | 85 | } else { |
42a5b055 TH |
86 | $subrenderer = 'multichoice_vertical'; |
87 | } | |
88 | } else { | |
89 | throw new coding_exception('Unexpected subquestion type.', $subq); | |
90 | } | |
91 | $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer); | |
92 | return $renderer->subquestion($qa, $options, $index, $subq); | |
ab50232b TH |
93 | } |
94 | ||
ab50232b | 95 | public function correct_response(question_attempt $qa) { |
fa6c8620 | 96 | return ''; |
ab50232b | 97 | } |
ab50232b TH |
98 | } |
99 | ||
100 | ||
101 | /** | |
102 | * Subclass for generating the bits of output specific to shortanswer | |
103 | * subquestions. | |
104 | * | |
42a5b055 | 105 | * @copyright 2011 The Open University |
ab50232b TH |
106 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
107 | */ | |
7ac7977c TH |
108 | abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer { |
109 | ||
110 | abstract public function subquestion(question_attempt $qa, | |
111 | question_display_options $options, $index, | |
112 | question_graded_automatically $subq); | |
113 | ||
114 | /** | |
115 | * Render the feedback pop-up contents. | |
36e91f1d | 116 | * |
7ac7977c TH |
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. | |
123 | */ | |
124 | protected function feedback_popup(question_graded_automatically $subq, | |
125 | $fraction, $feedbacktext, $rightanswer, question_display_options $options) { | |
126 | ||
7ac7977c TH |
127 | $feedback = array(); |
128 | if ($options->correctness) { | |
129 | if (is_null($fraction)) { | |
130 | $state = question_state::$gaveup; | |
131 | } else { | |
132 | $state = question_state::graded_state_for_fraction($fraction); | |
133 | } | |
134 | $feedback[] = $state->default_string(true); | |
135 | } | |
136 | ||
6b290a49 | 137 | if ($options->feedback && $feedbacktext) { |
bca80658 TH |
138 | $feedback[] = $feedbacktext; |
139 | } | |
140 | ||
7ac7977c TH |
141 | if ($options->rightanswer) { |
142 | $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer); | |
143 | } | |
144 | ||
145 | $subfraction = ''; | |
6b290a49 TH |
146 | if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->maxmark > 0 |
147 | && (!is_null($fraction) || $feedback)) { | |
7ac7977c TH |
148 | $a = new stdClass(); |
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); | |
152 | } | |
153 | ||
bca80658 TH |
154 | if (!$feedback) { |
155 | return ''; | |
156 | } | |
157 | ||
7ac7977c TH |
158 | return html_writer::tag('span', implode('<br />', $feedback), |
159 | array('class' => 'feedbackspan accesshide')); | |
160 | } | |
161 | } | |
162 | ||
163 | ||
164 | /** | |
165 | * Subclass for generating the bits of output specific to shortanswer | |
166 | * subquestions. | |
167 | * | |
168 | * @copyright 2011 The Open University | |
169 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
170 | */ | |
171 | class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base { | |
ab50232b | 172 | |
42a5b055 TH |
173 | public function subquestion(question_attempt $qa, question_display_options $options, |
174 | $index, question_graded_automatically $subq) { | |
175 | ||
176 | $fieldprefix = 'sub' . $index . '_'; | |
177 | $fieldname = $fieldprefix . 'answer'; | |
7ac7977c | 178 | |
42a5b055 | 179 | $response = $qa->get_last_qt_var($fieldname); |
7ac7977c TH |
180 | if ($subq->qtype->name() == 'shortanswer') { |
181 | $matchinganswer = $subq->get_matching_answer(array('answer' => $response)); | |
3a6eb8ef | 182 | } else if ($subq->qtype->name() == 'numerical') { |
b2a79cc1 TH |
183 | list($value, $unit, $multiplier) = $subq->ap->apply_units($response, ''); |
184 | $matchinganswer = $subq->get_matching_answer($value, 1); | |
7ac7977c TH |
185 | } else { |
186 | $matchinganswer = $subq->get_matching_answer($response); | |
187 | } | |
188 | ||
42a5b055 | 189 | if (!$matchinganswer) { |
b2a79cc1 TH |
190 | if (is_null($response) || $response === '') { |
191 | $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); | |
192 | } else { | |
193 | $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML); | |
194 | } | |
ab50232b | 195 | } |
42a5b055 TH |
196 | |
197 | // Work out a good input field size. | |
2f1e464a | 198 | $size = max(1, core_text::strlen(trim($response)) + 1); |
42a5b055 | 199 | foreach ($subq->answers as $ans) { |
2f1e464a | 200 | $size = max($size, core_text::strlen(trim($ans->answer))); |
ab50232b | 201 | } |
42a5b055 | 202 | $size = min(60, round($size + rand(0, $size*0.15))); |
1649a4f7 | 203 | // The rand bit is to make guessing harder. |
42a5b055 | 204 | |
ab50232b TH |
205 | $inputattributes = array( |
206 | 'type' => 'text', | |
42a5b055 | 207 | 'name' => $qa->get_qt_field_name($fieldname), |
ab50232b | 208 | 'value' => $response, |
42a5b055 | 209 | 'id' => $qa->get_qt_field_name($fieldname), |
ab50232b TH |
210 | 'size' => $size, |
211 | ); | |
ab50232b TH |
212 | if ($options->readonly) { |
213 | $inputattributes['readonly'] = 'readonly'; | |
ab50232b | 214 | } |
ab50232b | 215 | |
42a5b055 TH |
216 | $feedbackimg = ''; |
217 | if ($options->correctness) { | |
7ac7977c TH |
218 | $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction); |
219 | $feedbackimg = $this->feedback_image($matchinganswer->fraction); | |
ab50232b | 220 | } |
42a5b055 | 221 | |
7ac7977c TH |
222 | if ($subq->qtype->name() == 'shortanswer') { |
223 | $correctanswer = $subq->get_matching_answer($subq->get_correct_response()); | |
224 | } else { | |
225 | $correctanswer = $subq->get_correct_answer(); | |
226 | } | |
42a5b055 | 227 | |
7ac7977c TH |
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); | |
232 | ||
588d1b59 | 233 | $output = html_writer::start_tag('span', array('class' => 'subquestion')); |
c3cdf1e4 FM |
234 | $output .= html_writer::tag('label', get_string('answer'), |
235 | array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); | |
7ac7977c TH |
236 | $output .= html_writer::empty_tag('input', $inputattributes); |
237 | $output .= $feedbackimg; | |
238 | $output .= $feedbackpopup; | |
588d1b59 | 239 | $output .= html_writer::end_tag('span'); |
42a5b055 | 240 | |
7ac7977c TH |
241 | return $output; |
242 | } | |
243 | } | |
244 | ||
245 | ||
246 | /** | |
247 | * Render an embedded multiple-choice question that is displayed as a select menu. | |
248 | * | |
dcedbb0e | 249 | * @copyright 2011 The Open University |
7ac7977c TH |
250 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later |
251 | */ | |
252 | class qtype_multianswer_multichoice_inline_renderer | |
253 | extends qtype_multianswer_subq_renderer_base { | |
254 | ||
255 | public function subquestion(question_attempt $qa, question_display_options $options, | |
256 | $index, question_graded_automatically $subq) { | |
257 | ||
258 | $fieldprefix = 'sub' . $index . '_'; | |
259 | $fieldname = $fieldprefix . 'answer'; | |
260 | ||
261 | $response = $qa->get_last_qt_var($fieldname); | |
262 | $choices = array(); | |
263 | $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); | |
264 | $rightanswer = null; | |
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; | |
ab50232b | 271 | } |
7ac7977c | 272 | } |
42a5b055 | 273 | |
7ac7977c TH |
274 | $inputattributes = array( |
275 | 'id' => $qa->get_qt_field_name($fieldname), | |
276 | ); | |
277 | if ($options->readonly) { | |
278 | $inputattributes['disabled'] = 'disabled'; | |
ab50232b | 279 | } |
ab50232b | 280 | |
7ac7977c TH |
281 | $feedbackimg = ''; |
282 | if ($options->correctness) { | |
283 | $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction); | |
284 | $feedbackimg = $this->feedback_image($matchinganswer->fraction); | |
285 | } | |
3211569a | 286 | $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname), |
7ac7977c TH |
287 | $response, array('' => ''), $inputattributes); |
288 | ||
289 | $order = $subq->get_order($qa); | |
b8447700 TH |
290 | $correctresponses = $subq->get_correct_response(); |
291 | $rightanswer = $subq->answers[$order[reset($correctresponses)]]; | |
b2a79cc1 TH |
292 | if (!$matchinganswer) { |
293 | $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML); | |
294 | } | |
7ac7977c TH |
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); | |
300 | ||
588d1b59 | 301 | $output = html_writer::start_tag('span', array('class' => 'subquestion')); |
c3cdf1e4 FM |
302 | $output .= html_writer::tag('label', get_string('answer'), |
303 | array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); | |
7ac7977c | 304 | $output .= $select; |
42a5b055 TH |
305 | $output .= $feedbackimg; |
306 | $output .= $feedbackpopup; | |
588d1b59 | 307 | $output .= html_writer::end_tag('span'); |
ab50232b | 308 | |
42a5b055 TH |
309 | return $output; |
310 | } | |
ab50232b TH |
311 | } |
312 | ||
12c6e008 | 313 | |
ab50232b | 314 | /** |
dcedbb0e TH |
315 | * Render an embedded multiple-choice question vertically, like for a normal |
316 | * multiple-choice question. | |
12c6e008 TH |
317 | * |
318 | * @copyright 2010 Pierre Pichet | |
319 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
ab50232b | 320 | */ |
dcedbb0e | 321 | class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base { |
ab50232b | 322 | |
dcedbb0e TH |
323 | public function subquestion(question_attempt $qa, question_display_options $options, |
324 | $index, question_graded_automatically $subq) { | |
12c6e008 | 325 | |
dcedbb0e TH |
326 | $fieldprefix = 'sub' . $index . '_'; |
327 | $fieldname = $fieldprefix . 'answer'; | |
328 | $response = $qa->get_last_qt_var($fieldname); | |
12c6e008 | 329 | |
ab50232b | 330 | $inputattributes = array( |
dcedbb0e TH |
331 | 'type' => 'radio', |
332 | 'name' => $qa->get_qt_field_name($fieldname), | |
12c6e008 | 333 | ); |
ab50232b TH |
334 | if ($options->readonly) { |
335 | $inputattributes['disabled'] = 'disabled'; | |
336 | } | |
dcedbb0e TH |
337 | |
338 | $result = $this->all_choices_wrapper_start(); | |
339 | $fraction = null; | |
340 | foreach ($subq->get_order($qa) as $value => $ansid) { | |
341 | $ans = $subq->answers[$ansid]; | |
342 | ||
343 | $inputattributes['value'] = $value; | |
344 | $inputattributes['id'] = $inputattributes['name'] . $value; | |
345 | ||
346 | $isselected = $subq->is_choice_selected($response, $value); | |
ab50232b TH |
347 | if ($isselected) { |
348 | $inputattributes['checked'] = 'checked'; | |
dcedbb0e | 349 | $fraction = $ans->fraction; |
ab50232b TH |
350 | } else { |
351 | unset($inputattributes['checked']); | |
352 | } | |
ab50232b | 353 | |
ab50232b | 354 | $class = 'r' . ($value % 2); |
dcedbb0e TH |
355 | if ($options->correctness && $isselected) { |
356 | $feedbackimg = $this->feedback_image($ans->fraction); | |
357 | $class .= ' ' . $this->feedback_class($ans->fraction); | |
358 | } else { | |
359 | $feedbackimg = ''; | |
ab50232b | 360 | } |
12c6e008 | 361 | |
dcedbb0e TH |
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; | |
368 | ||
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')); | |
ab50232b | 374 | } |
12c6e008 | 375 | |
dcedbb0e | 376 | $result .= $this->choice_wrapper_end(); |
12c6e008 TH |
377 | } |
378 | ||
dcedbb0e | 379 | $result .= $this->all_choices_wrapper_end(); |
12c6e008 | 380 | |
b2a79cc1 | 381 | $feedback = array(); |
dcedbb0e TH |
382 | if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX && |
383 | $subq->maxmark > 0) { | |
384 | $a = new stdClass(); | |
385 | $a->mark = format_float($fraction * $subq->maxmark, $options->markdp); | |
386 | $a->max = format_float($subq->maxmark, $options->markdp); | |
ab50232b | 387 | |
b2a79cc1 TH |
388 | $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a)); |
389 | } | |
390 | ||
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, | |
a3f92b2e | 397 | $qa, 'question', 'answer', $ansid)); |
b2a79cc1 TH |
398 | break; |
399 | } | |
400 | } | |
12c6e008 | 401 | } |
ab50232b | 402 | |
b2a79cc1 TH |
403 | $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome')); |
404 | ||
dcedbb0e | 405 | return $result; |
12c6e008 TH |
406 | } |
407 | ||
dcedbb0e TH |
408 | /** |
409 | * @param string $class class attribute value. | |
410 | * @return string HTML to go before each choice. | |
411 | */ | |
412 | protected function choice_wrapper_start($class) { | |
413 | return html_writer::start_tag('div', array('class' => $class)); | |
12c6e008 TH |
414 | } |
415 | ||
dcedbb0e TH |
416 | /** |
417 | * @return string HTML to go after each choice. | |
418 | */ | |
419 | protected function choice_wrapper_end() { | |
420 | return html_writer::end_tag('div'); | |
12c6e008 TH |
421 | } |
422 | ||
dcedbb0e TH |
423 | /** |
424 | * @return string HTML to go before all the choices. | |
425 | */ | |
426 | protected function all_choices_wrapper_start() { | |
427 | return html_writer::start_tag('div', array('class' => 'answer')); | |
12c6e008 TH |
428 | } |
429 | ||
dcedbb0e TH |
430 | /** |
431 | * @return string HTML to go after all the choices. | |
432 | */ | |
433 | protected function all_choices_wrapper_end() { | |
434 | return html_writer::end_tag('div'); | |
12c6e008 | 435 | } |
dcedbb0e | 436 | } |
12c6e008 | 437 | |
ab50232b | 438 | |
dcedbb0e TH |
439 | /** |
440 | * Render an embedded multiple-choice question vertically, like for a normal | |
441 | * multiple-choice question. | |
442 | * | |
443 | * @copyright 2010 Pierre Pichet | |
444 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
445 | */ | |
446 | class qtype_multianswer_multichoice_horizontal_renderer | |
447 | extends qtype_multianswer_multichoice_vertical_renderer { | |
12c6e008 | 448 | |
dcedbb0e TH |
449 | protected function choice_wrapper_start($class) { |
450 | return html_writer::start_tag('td', array('class' => $class)); | |
ab50232b | 451 | } |
12c6e008 | 452 | |
dcedbb0e TH |
453 | protected function choice_wrapper_end() { |
454 | return html_writer::end_tag('td'); | |
455 | } | |
12c6e008 | 456 | |
dcedbb0e TH |
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'); | |
460 | } | |
ab50232b | 461 | |
dcedbb0e TH |
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'); | |
ab50232b | 465 | } |
ab50232b | 466 | } |