Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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/>.
16
 
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
 */
26
 
27
 
28
require_once($CFG->dirroot . '/question/type/shortanswer/renderer.php');
29
 
30
 
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 {
42
 
43
    public function formulation_and_controls(question_attempt $qa,
44
            question_display_options $options) {
45
        $question = $qa->get_question();
46
 
47
        $output = '';
48
        $subquestions = array();
49
 
50
        $missingsubquestions = false;
51
        foreach ($question->textfragments as $i => $fragment) {
52
            if ($i > 0) {
53
                $index = $question->places[$i];
54
                $questionisvalid = !empty($question->subquestions[$index]) &&
55
                                 $question->subquestions[$index]->qtype->name() !== 'subquestion_replacement';
56
 
57
                if (!$questionisvalid) {
58
                    $missingsubquestions = true;
59
                    $questionreplacement = qtype_multianswer::deleted_subquestion_replacement();
60
 
61
                    // It is possible that the subquestion index does not exist. When corrupted quizzes (see MDL-54724) are
62
                    // restored, the sequence column of mdl_quiz_multianswer can be empty, in this case
63
                    // qtype_multianswer::get_question_options cannot fill in deleted questions, so we need to do it here.
64
                    $question->subquestions[$index] = $question->subquestions[$index] ?? $questionreplacement;
65
                }
66
 
67
                $token = 'qtypemultianswer' . $i . 'marker';
68
                $token = '<span class="nolink">' . $token . '</span>';
69
                $output .= $token;
70
                $subquestions[$token] = $this->subquestion($qa, $options, $index,
71
                        $question->subquestions[$index]);
72
            }
73
 
74
            $output .= $fragment;
75
        }
76
 
77
        if ($missingsubquestions) {
78
            $output = $this->notification(get_string('corruptedquestion', 'qtype_multianswer'), 'error') . $output;
79
        }
80
 
81
        $output = $question->format_text($output, $question->questiontextformat,
82
                $qa, 'question', 'questiontext', $question->id);
83
        $output = str_replace(array_keys($subquestions), array_values($subquestions), $output);
84
 
85
        if ($qa->get_state() == question_state::$invalid) {
86
            $output .= html_writer::nonempty_tag('div',
87
                    $question->get_validation_error($qa->get_last_qt_data()),
88
                    array('class' => 'validationerror'));
89
        }
90
 
91
        return $output;
92
    }
93
 
94
    public function subquestion(question_attempt $qa,
95
            question_display_options $options, $index, question_automatically_gradable $subq) {
96
 
97
        $subtype = $subq->qtype->name();
98
        if ($subtype == 'numerical' || $subtype == 'shortanswer') {
99
            $subrenderer = 'textfield';
100
        } else if ($subtype == 'multichoice') {
101
            if ($subq instanceof qtype_multichoice_multi_question) {
102
                if ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL) {
103
                    $subrenderer = 'multiresponse_vertical';
104
                } else {
105
                    $subrenderer = 'multiresponse_horizontal';
106
                }
107
            } else {
108
                if ($subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
109
                    $subrenderer = 'multichoice_inline';
110
                } else if ($subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL) {
111
                    $subrenderer = 'multichoice_horizontal';
112
                } else {
113
                    $subrenderer = 'multichoice_vertical';
114
                }
115
            }
116
        } else if ($subtype == 'subquestion_replacement') {
117
            return html_writer::div(
118
                get_string('missingsubquestion', 'qtype_multianswer'),
119
                'notifyproblem'
120
            );
121
        } else {
122
            throw new coding_exception('Unexpected subquestion type.', $subq);
123
        }
124
        /** @var qtype_multianswer_subq_renderer_base $renderer */
125
        $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer);
126
        return $renderer->subquestion($qa, $options, $index, $subq);
127
    }
128
 
129
    public function correct_response(question_attempt $qa) {
130
        return '';
131
    }
132
}
133
 
134
 
135
/**
136
 * Subclass for generating the bits of output specific to shortanswer
137
 * subquestions.
138
 *
139
 * @copyright 2011 The Open University
140
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
141
 */
142
abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
143
 
144
    /** @var int[] Stores the counts of answer instances for questions. */
145
    protected static $answercount = [];
146
 
147
    /** @var question_display_options Question display options instance for any necessary information for rendering the question. */
148
    protected $displayoptions;
149
 
150
    abstract public function subquestion(question_attempt $qa,
151
            question_display_options $options, $index,
152
            question_graded_automatically $subq);
153
 
154
    /**
155
     * Render the feedback pop-up contents.
156
     *
157
     * @param question_graded_automatically $subq the subquestion.
158
     * @param float $fraction the mark the student got. null if this subq was not answered.
159
     * @param string $feedbacktext the feedback text, already processed with format_text etc.
160
     * @param string $rightanswer the right answer, already processed with format_text etc.
161
     * @param question_display_options $options the display options.
162
     * @return string the HTML for the feedback popup.
163
     */
164
    protected function feedback_popup(question_graded_automatically $subq,
165
            $fraction, $feedbacktext, $rightanswer, question_display_options $options) {
166
 
167
        $feedback = array();
168
        if ($options->correctness) {
169
            if (is_null($fraction)) {
170
                $state = question_state::$gaveup;
171
            } else {
172
                $state = question_state::graded_state_for_fraction($fraction);
173
            }
174
            $feedback[] = $state->default_string(true);
175
        }
176
 
177
        if ($options->feedback && $feedbacktext) {
178
            $feedback[] = $feedbacktext;
179
        }
180
 
181
        if ($options->rightanswer) {
182
            $feedback[] = get_string('correctansweris', 'qtype_shortanswer', $rightanswer);
183
        }
184
 
185
        $subfraction = '';
186
        if ($options->marks >= question_display_options::MARK_AND_MAX && $subq->defaultmark > 0
187
                && (!is_null($fraction) || $feedback)) {
188
            $a = new stdClass();
189
            $a->mark = format_float($fraction * $subq->defaultmark, $options->markdp);
190
            $a->max = format_float($subq->defaultmark, $options->markdp);
191
            $feedback[] = get_string('markoutofmax', 'question', $a);
192
        }
193
 
194
        if (!$feedback) {
195
            return '';
196
        }
197
 
198
        return html_writer::tag('span', implode('<br />', $feedback), [
199
            'class' => 'feedbackspan',
200
        ]);
201
    }
202
 
203
    /**
204
     * Render the feedback icon for a sub-question which is also the trigger for the feedback popover.
205
     *
206
     * @param string $icon The feedback icon
207
     * @param string $feedbackcontents The feedback contents to be shown on the popover.
208
     * @return string
209
     */
210
    protected function get_feedback_image(string $icon, string $feedbackcontents): string {
211
        global $PAGE;
212
        if ($icon === '') {
213
            return '';
214
        }
215
 
216
        $PAGE->requires->js_call_amd('qtype_multianswer/feedback', 'initPopovers');
217
 
218
        return html_writer::link('#', $icon, [
219
            'role' => 'button',
220
            'tabindex' => 0,
221
            'class' => 'feedbacktrigger btn btn-link p-0',
1441 ariadna 222
            'data-bs-toggle' => 'popover',
223
            'data-bs-container' => 'body',
224
            'data-bs-content' => $feedbackcontents,
225
            'data-bs-placement' => 'right',
226
            'data-bs-trigger' => 'hover focus',
227
            'data-bs-html' => 'true',
1 efrain 228
        ]);
229
    }
230
 
231
    /**
232
     * Generates a label for an answer field.
233
     *
234
     * If the question number is set ({@see qtype_renderer::$questionnumber}), the label will
235
     * include the question number in order to indicate which question the answer field belongs to.
236
     *
237
     * @param string $langkey The lang string key for the lang string that does not include the question number.
238
     * @param string $component The Frankenstyle component name.
239
     * @return string
240
     * @throws coding_exception
241
     */
242
    protected function get_answer_label(
243
        string $langkey = 'answerx',
244
        string $component = 'question'
245
    ): string {
246
        // There may be multiple answer fields for a question, so we need to increment the answer fields in order to distinguish
247
        // them from one another.
248
        $questionnumber = $this->displayoptions->questionidentifier ?? '';
249
        $questionnumberindex = $questionnumber !== '' ? $questionnumber : 0;
250
        if (isset(self::$answercount[$questionnumberindex][$langkey])) {
251
            self::$answercount[$questionnumberindex][$langkey]++;
252
        } else {
253
            self::$answercount[$questionnumberindex][$langkey] = 1;
254
        }
255
 
256
        $params = self::$answercount[$questionnumberindex][$langkey];
257
 
258
        return $this->displayoptions->add_question_identifier_to_label(get_string($langkey, $component, $params));
259
    }
260
}
261
 
262
 
263
/**
264
 * Subclass for generating the bits of output specific to shortanswer
265
 * subquestions.
266
 *
267
 * @copyright 2011 The Open University
268
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
269
 */
270
class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_renderer_base {
271
 
272
    public function subquestion(question_attempt $qa, question_display_options $options,
273
            $index, question_graded_automatically $subq) {
274
 
275
        $this->displayoptions = $options;
276
 
277
        $fieldprefix = 'sub' . $index . '_';
278
        $fieldname = $fieldprefix . 'answer';
279
 
280
        $response = $qa->get_last_qt_var($fieldname);
281
        if ($subq->qtype->name() == 'shortanswer') {
282
            $matchinganswer = $subq->get_matching_answer(array('answer' => $response));
283
        } else if ($subq->qtype->name() == 'numerical') {
284
            list($value, $unit, $multiplier) = $subq->ap->apply_units($response, '');
285
            $matchinganswer = $subq->get_matching_answer($value, 1);
286
        } else {
287
            $matchinganswer = $subq->get_matching_answer($response);
288
        }
289
 
290
        if (!$matchinganswer) {
291
            if (is_null($response) || $response === '') {
292
                $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
293
            } else {
294
                $matchinganswer = new question_answer(0, '', 0.0, '', FORMAT_HTML);
295
            }
296
        }
297
 
298
        // Work out a good input field size.
299
        $size = max(1, core_text::strlen(trim($response ?? '')) + 1);
300
        foreach ($subq->answers as $ans) {
301
            $size = max($size, core_text::strlen(trim($ans->answer)));
302
        }
303
        $size = min(60, round($size + rand(0, (int)($size * 0.15))));
304
        // The rand bit is to make guessing harder.
305
 
306
        $inputattributes = array(
307
            'type' => 'text',
308
            'name' => $qa->get_qt_field_name($fieldname),
309
            'value' => $response,
310
            'id' => $qa->get_qt_field_name($fieldname),
311
            'size' => $size,
312
            'class' => 'form-control d-inline mb-1',
313
        );
314
        if ($options->readonly) {
315
            $inputattributes['readonly'] = 'readonly';
316
        }
317
 
318
        $feedbackimg = '';
319
        if ($options->correctness) {
320
            $inputattributes['class'] .= ' ' . $this->feedback_class($matchinganswer->fraction);
321
            $feedbackimg = $this->feedback_image($matchinganswer->fraction);
322
        }
323
 
324
        if ($subq->qtype->name() == 'shortanswer') {
325
            $correctanswer = $subq->get_matching_answer($subq->get_correct_response());
326
        } else {
327
            $correctanswer = $subq->get_correct_answer();
328
        }
329
 
330
        $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
331
                $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
332
                        $qa, 'question', 'answerfeedback', $matchinganswer->id),
333
                s($correctanswer->answer), $options);
334
 
335
        $output = html_writer::start_tag('span', ['class' => 'subquestion']);
336
 
337
        $output .= html_writer::tag('label', $this->get_answer_label(),
338
                array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
339
        $output .= html_writer::empty_tag('input', $inputattributes);
340
        $output .= $this->get_feedback_image($feedbackimg, $feedbackpopup);
341
        $output .= html_writer::end_tag('span');
342
 
343
        return $output;
344
    }
345
}
346
 
347
 
348
/**
349
 * Render an embedded multiple-choice question that is displayed as a select menu.
350
 *
351
 * @copyright  2011 The Open University
352
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
353
 */
354
class qtype_multianswer_multichoice_inline_renderer
355
        extends qtype_multianswer_subq_renderer_base {
356
 
357
    public function subquestion(question_attempt $qa, question_display_options $options,
358
            $index, question_graded_automatically $subq) {
359
 
360
        $this->displayoptions = $options;
361
 
362
        $fieldprefix = 'sub' . $index . '_';
363
        $fieldname = $fieldprefix . 'answer';
364
 
365
        $response = $qa->get_last_qt_var($fieldname);
366
        $choices = array();
367
        $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
368
        $rightanswer = null;
369
        foreach ($subq->get_order($qa) as $value => $ansid) {
370
            $ans = $subq->answers[$ansid];
371
            $choices[$value] = $subq->format_text($ans->answer, $ans->answerformat,
372
                    $qa, 'question', 'answer', $ansid);
373
            if ($subq->is_choice_selected($response, $value)) {
374
                $matchinganswer = $ans;
375
            }
376
        }
377
 
378
        $inputattributes = array(
379
            'id' => $qa->get_qt_field_name($fieldname),
1441 ariadna 380
            'class' => 'form-select d-inline-block mb-1',
1 efrain 381
        );
382
        if ($options->readonly) {
383
            $inputattributes['disabled'] = 'disabled';
384
        }
385
 
386
        $feedbackimg = '';
387
        if ($options->correctness) {
388
            $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
389
            $feedbackimg = $this->feedback_image($matchinganswer->fraction);
390
        }
391
        $select = html_writer::select($choices, $qa->get_qt_field_name($fieldname),
392
                $response, array('' => '&nbsp;'), $inputattributes);
393
 
394
        $order = $subq->get_order($qa);
395
        $correctresponses = $subq->get_correct_response();
396
        $rightanswer = $subq->answers[$order[reset($correctresponses)]];
397
        if (!$matchinganswer) {
398
            $matchinganswer = new question_answer(0, '', null, '', FORMAT_HTML);
399
        }
400
        $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
401
                $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
402
                        $qa, 'question', 'answerfeedback', $matchinganswer->id),
403
                $subq->format_text($rightanswer->answer, $rightanswer->answerformat,
404
                        $qa, 'question', 'answer', $rightanswer->id), $options);
405
 
406
        $output = html_writer::start_tag('span', array('class' => 'subquestion'));
407
        $output .= html_writer::tag('label', $this->get_answer_label(),
408
                array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
409
        $output .= $select;
410
        $output .= $this->get_feedback_image($feedbackimg, $feedbackpopup);
411
        $output .= html_writer::end_tag('span');
412
 
413
        return $output;
414
    }
415
}
416
 
417
 
418
/**
419
 * Render an embedded multiple-choice question vertically, like for a normal
420
 * multiple-choice question.
421
 *
422
 * @copyright  2010 Pierre Pichet
423
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
424
 */
425
class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_subq_renderer_base {
426
 
427
    public function subquestion(question_attempt $qa, question_display_options $options,
428
            $index, question_graded_automatically $subq) {
429
 
430
        $this->displayoptions = $options;
431
 
432
        $fieldprefix = 'sub' . $index . '_';
433
        $fieldname = $fieldprefix . 'answer';
434
        $response = $qa->get_last_qt_var($fieldname);
435
 
436
        $inputattributes = array(
437
            'type' => 'radio',
438
            'name' => $qa->get_qt_field_name($fieldname),
439
            'class' => 'form-check-input',
440
        );
441
        if ($options->readonly) {
442
            $inputattributes['disabled'] = 'disabled';
443
        }
444
 
445
        $result = $this->all_choices_wrapper_start();
446
        $fraction = null;
447
        foreach ($subq->get_order($qa) as $value => $ansid) {
448
            $ans = $subq->answers[$ansid];
449
 
450
            $inputattributes['value'] = $value;
451
            $inputattributes['id'] = $inputattributes['name'] . $value;
452
 
453
            $isselected = $subq->is_choice_selected($response, $value);
454
            if ($isselected) {
455
                $inputattributes['checked'] = 'checked';
456
                $fraction = $ans->fraction;
457
            } else {
458
                unset($inputattributes['checked']);
459
            }
460
 
461
            $class = 'form-check text-wrap text-break';
462
            if ($options->correctness && $isselected) {
463
                $feedbackimg = $this->feedback_image($ans->fraction);
464
                $class .= ' ' . $this->feedback_class($ans->fraction);
465
            } else {
466
                $feedbackimg = '';
467
            }
468
 
469
            $result .= $this->choice_wrapper_start($class);
470
            $result .= html_writer::empty_tag('input', $inputattributes);
471
            $result .= html_writer::tag('label', $subq->format_text($ans->answer,
472
                    $ans->answerformat, $qa, 'question', 'answer', $ansid),
473
                    array('for' => $inputattributes['id'], 'class' => 'form-check-label text-body'));
474
            $result .= $feedbackimg;
475
 
476
            if ($options->feedback && $isselected && trim($ans->feedback)) {
477
                $result .= html_writer::tag('div',
478
                        $subq->format_text($ans->feedback, $ans->feedbackformat,
479
                                $qa, 'question', 'answerfeedback', $ansid),
480
                        array('class' => 'specificfeedback'));
481
            }
482
 
483
            $result .= $this->choice_wrapper_end();
484
        }
485
 
486
        $result .= $this->all_choices_wrapper_end();
487
 
488
        $feedback = array();
489
        if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
490
                $subq->defaultmark > 0) {
491
            $a = new stdClass();
492
            $a->mark = format_float($fraction * $subq->defaultmark, $options->markdp);
493
            $a->max = format_float($subq->defaultmark, $options->markdp);
494
 
495
            $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
496
        }
497
 
498
        if ($options->rightanswer) {
499
            foreach ($subq->answers as $ans) {
500
                if (question_state::graded_state_for_fraction($ans->fraction) ==
501
                        question_state::$gradedright) {
502
                    $feedback[] = get_string('correctansweris', 'qtype_multichoice',
503
                            $subq->format_text($ans->answer, $ans->answerformat,
504
                                    $qa, 'question', 'answer', $ansid));
505
                    break;
506
                }
507
            }
508
        }
509
 
510
        $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
511
 
512
        return $result;
513
    }
514
 
515
    /**
516
     * @param string $class class attribute value.
517
     * @return string HTML to go before each choice.
518
     */
519
    protected function choice_wrapper_start($class) {
520
        return html_writer::start_tag('div', array('class' => $class));
521
    }
522
 
523
    /**
524
     * @return string HTML to go after each choice.
525
     */
526
    protected function choice_wrapper_end() {
527
        return html_writer::end_tag('div');
528
    }
529
 
530
    /**
531
     * @return string HTML to go before all the choices.
532
     */
533
    protected function all_choices_wrapper_start() {
534
        $wrapperstart = html_writer::start_tag('fieldset', array('class' => 'answer'));
535
        $legendtext = $this->get_answer_label('multichoicex', 'qtype_multianswer');
1441 ariadna 536
        $wrapperstart .= html_writer::tag('legend', $legendtext, ['class' => 'visually-hidden']);
1 efrain 537
        return $wrapperstart;
538
    }
539
 
540
    /**
541
     * @return string HTML to go after all the choices.
542
     */
543
    protected function all_choices_wrapper_end() {
544
        return html_writer::end_tag('fieldset');
545
    }
546
}
547
 
548
 
549
/**
550
 * Render an embedded multiple-choice question vertically, like for a normal
551
 * multiple-choice question.
552
 *
553
 * @copyright  2010 Pierre Pichet
554
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
555
 */
556
class qtype_multianswer_multichoice_horizontal_renderer
557
        extends qtype_multianswer_multichoice_vertical_renderer {
558
 
559
    protected function choice_wrapper_start($class) {
560
        return html_writer::start_tag('div', array('class' => $class . ' form-check-inline'));
561
    }
562
 
563
    protected function choice_wrapper_end() {
564
        return html_writer::end_tag('div');
565
    }
566
 
567
    protected function all_choices_wrapper_start() {
568
        $wrapperstart = html_writer::start_tag('fieldset', ['class' => 'answer']);
569
        $captiontext = $this->get_answer_label('multichoicex', 'qtype_multianswer');
1441 ariadna 570
        $wrapperstart .= html_writer::tag('legend', $captiontext, ['class' => 'visually-hidden']);
1 efrain 571
        return $wrapperstart;
572
    }
573
 
574
    protected function all_choices_wrapper_end() {
575
        return html_writer::end_tag('fieldset');
576
    }
577
}
578
 
579
/**
580
 * Class qtype_multianswer_multiresponse_renderer
581
 *
582
 * @copyright  2016 Davo Smith, Synergy Learning
583
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
584
 */
585
class qtype_multianswer_multiresponse_vertical_renderer extends qtype_multianswer_subq_renderer_base {
586
 
587
    /**
588
     * Output the content of the subquestion.
589
     *
590
     * @param question_attempt $qa
591
     * @param question_display_options $options
592
     * @param int $index
593
     * @param question_graded_automatically $subq
594
     * @return string
595
     */
596
    public function subquestion(question_attempt $qa, question_display_options $options,
597
                                $index, question_graded_automatically $subq) {
598
 
599
        if (!$subq instanceof qtype_multichoice_multi_question) {
600
            throw new coding_exception('Expecting subquestion of type qtype_multichoice_multi_question');
601
        }
602
 
603
        $fieldprefix = 'sub' . $index . '_';
604
        $fieldname = $fieldprefix . 'choice';
605
 
606
        // Extract the responses that related to this question + strip off the prefix.
607
        $fieldprefixlen = strlen($fieldprefix);
608
        $response = [];
609
        foreach ($qa->get_last_qt_data() as $name => $val) {
610
            if (substr($name, 0, $fieldprefixlen) == $fieldprefix) {
611
                $name = substr($name, $fieldprefixlen);
612
                $response[$name] = $val;
613
            }
614
        }
615
 
616
        $basename = $qa->get_qt_field_name($fieldname);
617
        $inputattributes = array(
618
            'type' => 'checkbox',
619
            'value' => 1,
11 efrain 620
            'class' => 'form-check-input',
1 efrain 621
        );
622
        if ($options->readonly) {
623
            $inputattributes['disabled'] = 'disabled';
624
        }
625
 
626
        $result = $this->all_choices_wrapper_start();
627
 
628
        // Calculate the total score (as we need to know if choices should be marked as 'correct' or 'partial').
629
        $fraction = 0;
630
        foreach ($subq->get_order($qa) as $value => $ansid) {
631
            $ans = $subq->answers[$ansid];
632
            if ($subq->is_choice_selected($response, $value)) {
633
                $fraction += $ans->fraction;
634
            }
635
        }
636
        // Display 'correct' answers as correct, if we are at 100%, otherwise mark them as 'partial'.
637
        $answerfraction = ($fraction > 0.999) ? 1.0 : 0.5;
638
 
639
        foreach ($subq->get_order($qa) as $value => $ansid) {
640
            $ans = $subq->answers[$ansid];
641
 
642
            $name = $basename.$value;
643
            $inputattributes['name'] = $name;
644
            $inputattributes['id'] = $name;
645
 
646
            $isselected = $subq->is_choice_selected($response, $value);
647
            if ($isselected) {
648
                $inputattributes['checked'] = 'checked';
649
            } else {
650
                unset($inputattributes['checked']);
651
            }
652
 
11 efrain 653
            $class = 'form-check text-wrap text-break';
1 efrain 654
            if ($options->correctness && $isselected) {
655
                $thisfrac = ($ans->fraction > 0) ? $answerfraction : 0;
656
                $feedbackimg = $this->feedback_image($thisfrac);
657
                $class .= ' ' . $this->feedback_class($thisfrac);
658
            } else {
659
                $feedbackimg = '';
660
            }
661
 
662
            $result .= $this->choice_wrapper_start($class);
663
            $result .= html_writer::empty_tag('input', $inputattributes);
664
            $result .= html_writer::tag('label', $subq->format_text($ans->answer,
665
                                                                    $ans->answerformat, $qa, 'question', 'answer', $ansid),
11 efrain 666
                                        ['for' => $inputattributes['id'], 'class' => 'form-check-label text-body']);
1 efrain 667
            $result .= $feedbackimg;
668
 
669
            if ($options->feedback && $isselected && trim($ans->feedback)) {
670
                $result .= html_writer::tag('div',
671
                                            $subq->format_text($ans->feedback, $ans->feedbackformat,
672
                                                               $qa, 'question', 'answerfeedback', $ansid),
673
                                            array('class' => 'specificfeedback'));
674
            }
675
 
676
            $result .= $this->choice_wrapper_end();
677
        }
678
 
679
        $result .= $this->all_choices_wrapper_end();
680
 
681
        $feedback = array();
682
        if ($options->feedback && $options->marks >= question_display_options::MARK_AND_MAX &&
683
            $subq->defaultmark > 0) {
684
            $a = new stdClass();
685
            $a->mark = format_float($fraction * $subq->defaultmark, $options->markdp);
686
            $a->max = format_float($subq->defaultmark, $options->markdp);
687
 
688
            $feedback[] = html_writer::tag('div', get_string('markoutofmax', 'question', $a));
689
        }
690
 
691
        if ($options->rightanswer) {
692
            $correct = [];
693
            foreach ($subq->answers as $ans) {
694
                if (question_state::graded_state_for_fraction($ans->fraction) != question_state::$gradedwrong) {
695
                    $correct[] = $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ans->id);
696
                }
697
            }
698
            $correct = '<ul><li>'.implode('</li><li>', $correct).'</li></ul>';
699
            $feedback[] = get_string('correctansweris', 'qtype_multichoice', $correct);
700
        }
701
 
702
        $result .= html_writer::nonempty_tag('div', implode('<br />', $feedback), array('class' => 'outcome'));
703
 
704
        return $result;
705
    }
706
 
707
    /**
708
     * @param string $class class attribute value.
709
     * @return string HTML to go before each choice.
710
     */
711
    protected function choice_wrapper_start($class) {
712
        return html_writer::start_tag('div', array('class' => $class));
713
    }
714
 
715
    /**
716
     * @return string HTML to go after each choice.
717
     */
718
    protected function choice_wrapper_end() {
719
        return html_writer::end_tag('div');
720
    }
721
 
722
    /**
723
     * @return string HTML to go before all the choices.
724
     */
725
    protected function all_choices_wrapper_start() {
726
        return html_writer::start_tag('div', array('class' => 'answer'));
727
    }
728
 
729
    /**
730
     * @return string HTML to go after all the choices.
731
     */
732
    protected function all_choices_wrapper_end() {
733
        return html_writer::end_tag('div');
734
    }
735
}
736
 
737
/**
738
 * Render an embedded multiple-response question horizontally.
739
 *
740
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
741
 */
742
class qtype_multianswer_multiresponse_horizontal_renderer
743
    extends qtype_multianswer_multiresponse_vertical_renderer {
744
 
745
    protected function choice_wrapper_start($class) {
11 efrain 746
        return html_writer::start_tag('td', ['class' => $class . ' form-check-inline']);
1 efrain 747
    }
748
 
749
    protected function choice_wrapper_end() {
750
        return html_writer::end_tag('td');
751
    }
752
 
753
    protected function all_choices_wrapper_start() {
754
        return html_writer::start_tag('table', array('class' => 'answer')) .
755
        html_writer::start_tag('tbody') . html_writer::start_tag('tr');
756
    }
757
 
758
    protected function all_choices_wrapper_end() {
759
        return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
760
        html_writer::end_tag('table');
761
    }
762
}