MDL-49041 qtype_multianswer: don't reveal marks on partial responses
[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
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
108abstract 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 */
171class 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 */
252class 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 321class 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 */
446class 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}