• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FormulasQuestion / moodle-qtype_formulas / 14217531563

02 Apr 2025 10:34AM UTC coverage: 95.807% (-0.03%) from 95.84%
14217531563

push

github

web-flow
use Bootstrap tooltips instead of own implementation (#176)


A compatibility layer is used to make tooltips work with Moodle 5.0 and 4.x. Also, we make our answer fields look more like those of other questions.

19 of 23 new or added lines in 1 file covered. (82.61%)

1 existing line in 1 file now uncovered.

3770 of 3935 relevant lines covered (95.81%)

1226.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.53
/renderer.php
1
<?php
2
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16

17
/**
18
 * Formulas question renderer class.
19
 *
20
 * @package    qtype_formulas
21
 * @copyright  2009 The Open University
22
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24

25

26
/**
27
 * Base class for generating the bits of output for formulas questions.
28
 *
29
 * @copyright  2009 The Open University
30
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31
 */
32
class qtype_formulas_renderer extends qtype_with_combined_feedback_renderer {
33

34
    /**
35
     * Generate the display of the formulation part of the question. This is the
36
     * area that contains the question text, and the controls for students to
37
     * input their answers. Once the question is answered, it will contain the green tick
38
     * or the red cross and the part's general / combined feedback.
39
     *
40
     * @param question_attempt $qa the question attempt to display.
41
     * @param question_display_options $options controls what should and should not be displayed.
42
     * @return ?string HTML fragment.
43
     */
44
    public function formulation_and_controls(question_attempt $qa, question_display_options $options): ?string {
45
        // First, fetch the instantiated question from the attempt.
46
        /** @var qtype_formulas_question $question */
47
        $question = $qa->get_question();
357✔
48

49
        if (count($question->textfragments) !== $question->numparts + 1) {
357✔
50
            $this->output->notification(get_string('error_question_damaged', 'qtype_formulas'), 'error');
×
51
            return null;
×
52
        }
53

54
        $questiontext = '';
357✔
55
        foreach ($question->parts as $part) {
357✔
56
            $questiontext .= $question->format_text(
357✔
57
                $question->textfragments[$part->partindex],
357✔
58
                $question->questiontextformat,
357✔
59
                $qa,
357✔
60
                'question',
357✔
61
                'questiontext',
357✔
62
                $question->id,
357✔
63
                false
357✔
64
            );
357✔
65
            $questiontext .= $this->part_formulation_and_controls($qa, $options, $part);
357✔
66
        }
67
        $questiontext .= $question->format_text(
357✔
68
            $question->textfragments[$question->numparts],
357✔
69
            $question->questiontextformat,
357✔
70
            $qa,
357✔
71
            'question',
357✔
72
            'questiontext',
357✔
73
            $question->id,
357✔
74
            false
357✔
75
        );
357✔
76

77
        $result = html_writer::tag('div', $questiontext, ['class' => 'qtext']);
357✔
78
        if ($qa->get_state() == question_state::$invalid) {
357✔
79
            $result .= html_writer::nonempty_tag(
34✔
80
                'div',
34✔
81
                $question->get_validation_error($qa->get_last_qt_data()),
34✔
82
                ['class' => 'validationerror']
34✔
83
            );
34✔
84
        }
85
        return $result;
357✔
86
    }
87

88
    /**
89
     * Return HTML that needs to be included in the page's <head> when this
90
     * question is used.
91
     *
92
     * @param question_attempt $qa question attempt that will be displayed on the page
93
     * @return string HTML fragment
94
     */
95
    public function head_code(question_attempt $qa) {
96
        global $CFG;
97
        $this->page->requires->js('/question/type/formulas/script/formatcheck.js');
×
98
        // Include backwards-compatibility layer for Bootstrap 4 data attributes, if available.
99
        // We may safely assume that if the uncompbiled version is there, the minified one exists as well.
NEW
100
        if (file_exists($CFG->dirroot . '/theme/boost/amd/src/bs4-compat.js')) {
×
101
            // We generate code similar to what js_call_amd() does, but we cannot use that function directly,
102
            // because (written in the style below) it would try to call BS4Compat.() rather than BS4Compat().
NEW
103
            $code = <<<EOF
104
                M.util.js_pending('theme_boost/bs4-compat');
105
                require(['theme_boost/bs4-compat'], function(BS4Compat) { BS4Compat(); });
106
                M.util.js_complete('theme_boost/bs4-compat');
NEW
107
            EOF;
NEW
108
            $this->page->requires->js_amd_inline($code);
×
109
        }
UNCOV
110
        return '';
×
111
    }
112

113
    /**
114
     * Return the part text, controls, grading details and feedbacks.
115
     *
116
     * @param question_attempt $qa question attempt that will be displayed on the page
117
     * @param question_display_options $options
118
     * @param qtype_formulas_part $part
119
     * @return void
120
     */
121
    public function part_formulation_and_controls(question_attempt $qa, question_display_options $options,
122
            qtype_formulas_part $part) {
123

124
        $partoptions = clone $options;
357✔
125
        // If using adaptivemultipart behaviour, adjust feedback display options for this part.
126
        if ($qa->get_behaviour_name() === 'adaptivemultipart') {
357✔
127
            $qa->get_behaviour()->adjust_display_options_for_part($part->partindex, $partoptions);
68✔
128
        }
129
        $sub = $this->get_part_image_and_class($qa, $partoptions, $part);
357✔
130

131
        $output = $this->get_part_formulation(
357✔
132
            $qa,
357✔
133
            $partoptions,
357✔
134
            $part->partindex,
357✔
135
            $sub
357✔
136
        );
357✔
137
        // Place for the right/wrong feeback image or appended at part's end.
138
        // TODO: this is not documented anywhere.
139
        if (strpos($output, '{_m}') !== false) {
357✔
140
            $output = str_replace('{_m}', $sub->feedbackimage, $output);
×
141
        } else {
142
            $output .= $sub->feedbackimage;
357✔
143
        }
144

145
        $feedback = $this->part_combined_feedback($qa, $partoptions, $part, $sub->fraction);
357✔
146
        $feedback .= $this->part_general_feedback($qa, $partoptions, $part);
357✔
147
        // If one of the part's coordinates is a MC or select question, the correct answer
148
        // stored in the database is not the right answer, but the index of the right answer,
149
        // so in that case, we need to calculate the right answer.
150
        if ($partoptions->rightanswer) {
357✔
151
            $feedback .= $this->part_correct_response($part->partindex, $qa);
272✔
152
        }
153
        $output .= html_writer::nonempty_tag(
357✔
154
            'div',
357✔
155
            $feedback,
357✔
156
            ['class' => 'formulaspartoutcome']
357✔
157
        );
357✔
158
        return html_writer::tag('div', $output , ['class' => 'formulaspart']);
357✔
159
    }
160

161
    /**
162
     * Return class and image for the part feedback.
163
     *
164
     * @param question_attempt $qa
165
     * @param question_display_options $options
166
     * @param qtype_formulas_part $part
167
     * @return object
168
     */
169
    public function get_part_image_and_class($qa, $options, $part) {
170
        $question = $qa->get_question();
357✔
171

172
        $sub = new StdClass;
357✔
173

174
        $response = $qa->get_last_qt_data();
357✔
175
        $response = $question->normalize_response($response);
357✔
176

177
        list('answer' => $answergrade, 'unit' => $unitcorrect) = $part->grade($response);
357✔
178

179
        $sub->fraction = $answergrade;
357✔
180
        if ($unitcorrect === false) {
357✔
181
            $sub->fraction *= (1 - $part->unitpenalty);
289✔
182
        }
183

184
        // Get the class and image for the feedback.
185
        if ($options->correctness) {
357✔
186
            $sub->feedbackimage = $this->feedback_image($sub->fraction);
340✔
187
            $sub->feedbackclass = $this->feedback_class($sub->fraction);
340✔
188
            if ($part->unitpenalty >= 1) { // All boxes must be correct at the same time, so they are of the same color.
340✔
189
                $sub->unitfeedbackclass = $sub->feedbackclass;
102✔
190
                $sub->boxfeedbackclass = $sub->feedbackclass;
102✔
191
            } else {  // Show individual color, all four color combinations are possible.
192
                $sub->unitfeedbackclass = $this->feedback_class($unitcorrect);
238✔
193
                $sub->boxfeedbackclass = $this->feedback_class($answergrade);
292✔
194
            }
195
        } else {  // There should be no feedback if options->correctness is not set for this part.
196
            $sub->feedbackimage = '';
323✔
197
            $sub->feedbackclass = '';
323✔
198
            $sub->unitfeedbackclass = '';
323✔
199
            $sub->boxfeedbackclass = '';
323✔
200
        }
201
        return $sub;
357✔
202
    }
203

204
    /**
205
     * Format given number according to numbering style, e. g. abc or 123.
206
     *
207
     * @param int $num number
208
     * @param string $style style to render the number in, acccording to {@link qtype_multichoice::get_numbering_styles()}
209
     * @return string number $num in the requested style
210
     */
211
    protected function number_in_style($num, $style) {
212
        switch ($style) {
213
            case 'abc':
17✔
214
                $number = chr(ord('a') + $num);
17✔
215
                break;
17✔
216
            case 'ABCD':
×
217
                $number = chr(ord('A') + $num);
×
218
                break;
×
219
            case '123':
×
220
                $number = $num + 1;
×
221
                break;
×
222
            case 'iii':
×
223
                $number = question_utils::int_to_roman($num + 1);
×
224
                break;
×
225
            case 'IIII':
×
226
                $number = strtoupper(question_utils::int_to_roman($num + 1));
×
227
                break;
×
228
            case 'none':
×
229
                return '';
×
230
            default:
231
                // Default similar to none for compatibility with old questions.
232
                return '';
×
233
        }
234
        return $number . '. ';
17✔
235
    }
236

237
    /**
238
     * Return the part's text with variables replaced by their values.
239
     *
240
     * @param question_attempt $qa
241
     * @param question_display_options $options
242
     * @param int $i part index
243
     * @param object $sub class and image for the part feedback
244
     * @return string
245
     */
246
    public function get_part_formulation(question_attempt $qa, question_display_options $options, $i, $sub) {
247
        /** @var qype_formulas_question $question */
248
        $question = $qa->get_question();
357✔
249
        $part = &$question->parts[$i];
357✔
250

251
        // Clone the part's evaluator and remove special variables like _0 etc., because they must
252
        // not be substituted here; otherwise, we would lose input boxes.
253
        $evaluator = clone $part->evaluator;
357✔
254
        $evaluator->remove_special_vars();
357✔
255
        $text = $evaluator->substitute_variables_in_text($part->subqtext);
357✔
256

257
        $subqreplaced = $question->format_text($text,
357✔
258
                $part->subqtextformat, $qa, 'qtype_formulas', 'answersubqtext', $part->id, false);
357✔
259
        $types = [0 => 'number', 10 => 'numeric', 100 => 'numerical_formula', 1000 => 'algebraic_formula'];
357✔
260
        $gradingtype = ($part->answertype != 10 && $part->answertype != 100 && $part->answertype != 1000) ? 0 : $part->answertype;
357✔
261
        $gtype = $types[$gradingtype];
357✔
262

263
        // Get the set of defined placeholders and their options.
264
        $boxes = $part->scan_for_answer_boxes($subqreplaced);
357✔
265
        // Append missing placholders at the end of part.
266
        foreach (range(0, $part->numbox) as $j) {
357✔
267
            $placeholder = ($j == $part->numbox) ? "_u" : "_$j";
357✔
268
            if (!array_key_exists($placeholder, $boxes)) {
357✔
269
                $boxes[$placeholder] = ['placeholder' => "{".$placeholder."}", 'options' => '', 'dropdown' => false];
85✔
270
                $subqreplaced .= "{".$placeholder."}";  // Appended at the end.
85✔
271
            }
272
        }
273

274
        // If part has combined unit answer input.
275
        if ($part->has_combined_unit_field()) {
357✔
276
            $variablename = "{$i}_";
255✔
277
            $currentanswer = $qa->get_last_qt_var($variablename);
255✔
278
            $inputname = $qa->get_qt_field_name($variablename);
255✔
279
            $title = get_string($gtype . ($part->postunit == '' ? '' : '_unit'), 'qtype_formulas');
255✔
280
            $inputattributes = [
255✔
281
                'type' => 'text',
255✔
282
                'name' => $inputname,
255✔
283
                'data-toggle' => 'tooltip',
255✔
284
                'data-title' => $title,
255✔
285
                'title' => $title,
255✔
286
                'value' => $currentanswer,
255✔
287
                'id' => $inputname,
255✔
288
                'class' => 'form-control formulas_' . $gtype . '_unit ' . $sub->feedbackclass,
255✔
289
                'maxlength' => 128,
255✔
290
                'aria-labelledby' => 'lbl_' . str_replace(':', '__', $inputname),
255✔
291
            ];
255✔
292

293
            if ($options->readonly) {
255✔
294
                $inputattributes['readonly'] = 'readonly';
204✔
295
            }
296
            // Create a meaningful label for accessibility.
297
            $a = new stdClass();
255✔
298
            $a->part = $i + 1;
255✔
299
            $a->numanswer = '';
255✔
300
            if ($question->numparts == 1) {
255✔
301
                $label = get_string('answercombinedunitsingle', 'qtype_formulas', $a);
221✔
302
            } else {
303
                $label = get_string('answercombinedunitmulti', 'qtype_formulas', $a);
34✔
304
            }
305
            $input = html_writer::tag(
255✔
306
                'label',
255✔
307
                $label,
255✔
308
                [
255✔
309
                    'class' => 'subq accesshide',
255✔
310
                    'for' => $inputattributes['id'],
255✔
311
                    'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
255✔
312
                ]
255✔
313
            );
255✔
314
            $input .= html_writer::empty_tag('input', $inputattributes);
255✔
315
            $subqreplaced = str_replace("{_0}{_u}", $input, $subqreplaced);
255✔
316
        }
317

318
        // Get the set of string for each candidate input box {_0}, {_1}, ..., {_u}.
319
        $inputs = [];
357✔
320
        foreach (range(0, $part->numbox) as $j) {    // Replace the input box for each placeholder {_0}, {_1} ...
357✔
321
            $placeholder = ($j == $part->numbox) ? "_u" : "_$j";    // The last one is unit.
357✔
322
            $variablename = "{$i}_$j";
357✔
323
            $currentanswer = $qa->get_last_qt_var($variablename);
357✔
324
            $inputname = $qa->get_qt_field_name($variablename);
357✔
325
            $title = get_string($placeholder == '_u' ? 'unit' : $gtype, 'qtype_formulas');
357✔
326
            $inputattributes = [
357✔
327
                'name' => $inputname,
357✔
328
                'value' => $currentanswer,
357✔
329
                'id' => $inputname,
357✔
330
                'data-toggle' => 'tooltip',
357✔
331
                'data-title' => $title,
357✔
332
                'title' => $title,
357✔
333
                'maxlength' => 128,
357✔
334
                'aria-labelledby' => 'lbl_' . str_replace(':', '__', $inputname),
357✔
335
            ];
357✔
336
            if ($options->readonly) {
357✔
337
                $inputattributes['readonly'] = 'readonly';
289✔
338
            }
339

340
            $stexts = null;
357✔
341
            if (strlen($boxes[$placeholder]['options']) != 0) { // Then it's a multichoice answer..
357✔
342
                try {
343
                    $stexts = $part->evaluator->export_single_variable($boxes[$placeholder]['options']);
34✔
344
                } catch (Exception $e) {
×
345
                    // TODO: use non-capturing catch.
346
                    unset($e);
×
347
                }
348
            }
349
            // Coordinate as multichoice options.
350
            if ($stexts != null) {
357✔
351
                if ($boxes[$placeholder]['dropdown']) {
34✔
352
                    // Select menu.
353
                    if ($options->readonly) {
17✔
354
                        $inputattributes['disabled'] = 'disabled';
17✔
355
                    }
356
                    $choices = [];
17✔
357
                    foreach ($stexts->value as $x => $mctxt) {
17✔
358
                        $choices[$x] = $question->format_text($mctxt, $part->subqtextformat , $qa,
17✔
359
                                'qtype_formulas', 'answersubqtext', $part->id, false);
17✔
360
                    }
361
                    unset($inputattributes['data-toggle']);
17✔
362
                    unset($inputattributes['data-title']);
17✔
363
                    $select = html_writer::select($choices, $inputname,
17✔
364
                            $currentanswer, ['' => ''], $inputattributes);
17✔
365
                    $output = html_writer::start_tag('span', ['class' => 'formulas_menu']);
17✔
366
                    $a = new stdClass();
17✔
367
                    $a->numanswer = $j + 1;
17✔
368
                    $a->part = $i + 1;
17✔
369
                    if (count($question->parts) > 1) {
17✔
370
                        $labeltext = get_string('answercoordinatemulti', 'qtype_formulas', $a);
×
371
                    } else {
372
                        $labeltext = get_string('answercoordinatesingle', 'qtype_formulas', $a);
17✔
373
                    }
374
                    $output .= html_writer::tag(
17✔
375
                        'label',
17✔
376
                        $labeltext,
17✔
377
                        [
17✔
378
                            'class' => 'subq accesshide',
17✔
379
                            'for' => $inputattributes['id'],
17✔
380
                            'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
17✔
381
                        ]
17✔
382
                    );
17✔
383
                    $output .= $select;
17✔
384
                    $output .= html_writer::end_tag('span');
17✔
385
                    $inputs[$placeholder] = $output;
17✔
386
                } else {
387
                    // Multichoice single question.
388
                    $inputattributes['type'] = 'radio';
17✔
389
                    if ($options->readonly) {
17✔
390
                        $inputattributes['disabled'] = 'disabled';
×
391
                    }
392
                    $output = $this->all_choices_wrapper_start();
17✔
393
                    foreach ($stexts->value as $x => $mctxt) {
17✔
394
                        $mctxt = html_writer::span($this->number_in_style($x, $question->answernumbering), 'answernumber')
17✔
395
                                . $question->format_text($mctxt, $part->subqtextformat , $qa,
17✔
396
                                'qtype_formulas', 'answersubqtext', $part->id, false);
17✔
397
                        $inputattributes['id'] = $inputname.'_'.$x;
17✔
398
                        $inputattributes['value'] = $x;
17✔
399
                        $inputattributes['aria-labelledby'] = 'lbl_' . str_replace(':', '__', $inputattributes['id']);
17✔
400
                        $isselected = ($currentanswer != '' && $x == $currentanswer);
17✔
401
                        $class = 'r' . ($x % 2);
17✔
402
                        if ($isselected) {
17✔
403
                            $inputattributes['checked'] = 'checked';
17✔
404
                        } else {
405
                            unset($inputattributes['checked']);
17✔
406
                        }
407
                        if ($options->correctness && $isselected) {
17✔
408
                            $class .= ' ' . $sub->feedbackclass;
17✔
409
                        }
410
                        $output .= $this->choice_wrapper_start($class);
17✔
411
                        unset($inputattributes['data-toggle']);
17✔
412
                        unset($inputattributes['data-title']);
17✔
413
                        $output .= html_writer::empty_tag('input', $inputattributes);
17✔
414
                        $output .= html_writer::tag(
17✔
415
                            'label',
17✔
416
                            $mctxt,
17✔
417
                            [
17✔
418
                                'for' => $inputattributes['id'],
17✔
419
                                'class' => 'm-l-1',
17✔
420
                                'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
17✔
421
                            ]
17✔
422
                        );
17✔
423
                        $output .= $this->choice_wrapper_end();
17✔
424
                    }
425
                    $output .= $this->all_choices_wrapper_end();
17✔
426
                    $inputs[$placeholder] = $output;
17✔
427
                }
428
                continue;
34✔
429
            }
430

431
            // Coordinate as shortanswer question.
432
            $inputs[$placeholder] = '';
357✔
433
            $inputattributes['type'] = 'text';
357✔
434
            if ($options->readonly) {
357✔
435
                $inputattributes['readonly'] = 'readonly';
289✔
436
            }
437
            if ($j == $part->numbox) {
357✔
438
                // Check if it's an input for unit.
439
                if (strlen($part->postunit) > 0) {
357✔
440
                    $inputattributes['title'] = get_string('unit', 'qtype_formulas');
289✔
441
                    $inputattributes['class'] = 'form-control formulas_unit '.$sub->unitfeedbackclass;
289✔
442
                    $inputattributes['data-title'] = get_string('unit', 'qtype_formulas');
289✔
443
                    $inputattributes['data-toggle'] = 'tooltip';
289✔
444
                    $a = new stdClass();
289✔
445
                    $a->part = $i + 1;
289✔
446
                    $a->numanswer = $j + 1;
289✔
447
                    if ($question->numparts == 1) {
289✔
448
                        $label = get_string('answerunitsingle', 'qtype_formulas', $a);
255✔
449
                    } else {
450
                        $label = get_string('answerunitmulti', 'qtype_formulas', $a);
34✔
451
                    }
452
                    $inputs[$placeholder] = html_writer::tag(
289✔
453
                        'label',
289✔
454
                        $label,
289✔
455
                        [
289✔
456
                            'class' => 'subq accesshide',
289✔
457
                            'for' => $inputattributes['id'],
289✔
458
                            'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
289✔
459
                        ]
289✔
460
                    );
289✔
461
                    $inputs[$placeholder] .= html_writer::empty_tag('input', $inputattributes);
325✔
462
                }
463
            } else {
464
                $inputattributes['title'] = get_string($gtype, 'qtype_formulas');
323✔
465
                $inputattributes['class'] = 'form-control formulas_'.$gtype.' '.$sub->boxfeedbackclass;
323✔
466
                $inputattributes['data-toggle'] = 'tooltip';
323✔
467
                $inputattributes['data-title'] = get_string($gtype, 'qtype_formulas');
323✔
468
                $inputattributes['aria-labelledby'] = 'lbl_' . str_replace(':', '__', $inputattributes['id']);
323✔
469
                $a = new stdClass();
323✔
470
                $a->part = $i + 1;
323✔
471
                $a->numanswer = $j + 1;
323✔
472
                if ($part->numbox == 1) {
323✔
473
                    if ($question->numparts == 1) {
323✔
474
                        $label = get_string('answersingle', 'qtype_formulas', $a);
289✔
475
                    } else {
476
                        $label = get_string('answermulti', 'qtype_formulas', $a);
187✔
477
                    }
478
                } else {
479
                    if ($question->numparts == 1) {
×
480
                        $label = get_string('answercoordinatesingle', 'qtype_formulas', $a);
×
481
                    } else {
482
                        $label = get_string('answercoordinatemulti', 'qtype_formulas', $a);
×
483
                    }
484
                }
485
                $inputs[$placeholder] = html_writer::tag(
323✔
486
                    'label',
323✔
487
                    $label,
323✔
488
                    [
323✔
489
                        'class' => 'subq accesshide',
323✔
490
                        'for' => $inputattributes['id'],
323✔
491
                        'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
323✔
492
                    ]
323✔
493
                );
323✔
494
                $inputs[$placeholder] .= html_writer::empty_tag('input', $inputattributes);
323✔
495
            }
496
        }
497

498
        foreach ($inputs as $placeholder => $replacement) {
357✔
499
            $subqreplaced = preg_replace('/'.$boxes[$placeholder]['placeholder'].'/', $replacement, $subqreplaced, 1);
357✔
500
        }
501
        return $subqreplaced;
357✔
502
    }
503

504
    /**
505
     * Generate HTML code to be included before each choice in multiple choice questions.
506
     *
507
     * @param string $class class attribute value
508
     * @return string
509
     */
510
    protected function choice_wrapper_start($class) {
511
        return html_writer::start_tag('div', ['class' => $class]);
17✔
512
    }
513

514
    /**
515
     * Generate HTML code to be included after each choice in multiple choice questions.
516
     *
517
     * @return string
518
     */
519
    protected function choice_wrapper_end() {
520
        return html_writer::end_tag('div');
17✔
521
    }
522

523
    /**
524
     * Generate HTML code to be included before all choices in multiple choice questions.
525
     *
526
     * @return string
527
     */
528
    protected function all_choices_wrapper_start() {
529
        return html_writer::start_tag('div', ['class' => 'multichoice_answer']);
17✔
530
    }
531

532
    /**
533
     * Generate HTML code to be included after all choices in multiple choice questions.
534
     *
535
     * @return string
536
     */
537
    protected function all_choices_wrapper_end() {
538
        return html_writer::end_tag('div');
17✔
539
    }
540

541
    /**
542
     * Correct response for the question. This is not needed for the Formulas question, because
543
     * answers are relative to parts.
544
     *
545
     * @param question_attempt $qa the question attempt to display
546
     * @return string empty string
547
     */
548
    public function correct_response(question_attempt $qa) {
549
        return '';
272✔
550
    }
551

552
    /**
553
     * Generate an automatic description of the correct response for a given part.
554
     *
555
     * @param int $i part index
556
     * @param question_attempt $qa question attempt to display
557
     * @return string HTML fragment
558
     */
559
    public function part_correct_response($i, question_attempt $qa) {
560
        /** @var qtype_formulas_question $question */
561
        $question = $qa->get_question();
272✔
562
        $answers = $question->parts[$i]->get_correct_response(true);
272✔
563
        $answertext = implode(', ', $answers);
272✔
564

565
        if ($question->parts[$i]->answernotunique) {
272✔
566
            $string = 'correctansweris';
272✔
567
        } else {
568
            $string = 'uniquecorrectansweris';
17✔
569
        }
570
        return html_writer::nonempty_tag('div', get_string($string, 'qtype_formulas', $answertext),
272✔
571
                    ['class' => 'formulaspartcorrectanswer']);
272✔
572
    }
573

574
    /**
575
     * Generate a brief statement of how many sub-parts of this question the
576
     * student got right.
577
     * @param question_attempt $qa the question attempt to display.
578
     * @return string HTML fragment.
579
     */
580
    protected function num_parts_correct(question_attempt $qa) {
581
        $response = $qa->get_last_qt_data();
187✔
582
        if (!$qa->get_question()->is_gradable_response($response)) {
187✔
583
            return '';
68✔
584
        }
585
        $numright = $qa->get_question()->get_num_parts_right($response);
187✔
586
        if ($numright[0] === 1) {
187✔
587
            return get_string('yougotoneright', 'qtype_formulas');
34✔
588
        } else {
589
            return get_string('yougotnright', 'qtype_formulas', $numright[0]);
170✔
590
        }
591
    }
592

593
    /**
594
     * We need to owerwrite this method to replace global variables by their value
595
     * @param question_attempt $qa the question attempt to display.
596
     * @return string HTML fragment.
597
     */
598
    protected function hint(question_attempt $qa, question_hint $hint) {
599
        /** @var qtype_formulas_question $question */
600
        $question = $qa->get_question();
34✔
601
        $hint->hint = $question->evaluator->substitute_variables_in_text($hint->hint);
34✔
602

603
        return html_writer::nonempty_tag('div', $qa->get_question()->format_hint($hint, $qa), ['class' => 'hint']);
34✔
604
    }
605

606
    /**
607
     * Generate HTML fragment for the question's combined feedback.
608
     *
609
     * @param question_attempt $qa question attempt being displayed
610
     * @return string
611
     */
612
    protected function combined_feedback(question_attempt $qa) {
613
        $question = $qa->get_question();
357✔
614

615
        $state = $qa->get_state();
357✔
616

617
        if (!$state->is_finished()) {
357✔
618
            $response = $qa->get_last_qt_data();
102✔
619
            if (!$qa->get_question()->is_gradable_response($response)) {
102✔
620
                return '';
68✔
621
            }
622
            list($notused, $state) = $qa->get_question()->grade_response($response);
102✔
623
        }
624

625
        $feedback = '';
357✔
626
        $field = $state->get_feedback_class() . 'feedback';
357✔
627
        $format = $state->get_feedback_class() . 'feedbackformat';
357✔
628
        if ($question->$field) {
357✔
629
            $feedback .= $question->format_text($question->$field, $question->$format,
357✔
630
                    $qa, 'question', $field, $question->id, false);
357✔
631
        }
632

633
        return $feedback;
357✔
634
    }
635

636
    /**
637
     * Generate the specific feedback. This is feedback that varies according to
638
     * the response the student gave.
639
     *
640
     * @param question_attempt $qa question attempt being displayed
641
     * @return string
642
     */
643
    public function specific_feedback(question_attempt $qa) {
644
        return $this->combined_feedback($qa);
357✔
645
    }
646

647
    /**
648
     * Gereate the part's general feedback. This is feedback is shown to all students.
649
     *
650
     * @param int $i part index
651
     * @param question_attempt $qa question attempt being displayed
652
     * @param question_definition $question question being displayed
653
     * @param question_display_options $options controls what should and should not be displayed
654
     * @return string HTML fragment
655
     */
656
    protected function part_general_feedback(question_attempt $qa, question_display_options $options, $part) {
657
        if ($part->feedback == '') {
357✔
658
            return '';
153✔
659
        }
660

661
        $feedback = '';
204✔
662
        $gradingdetails = '';
204✔
663
        $question = $qa->get_question();
204✔
664
        $state = $qa->get_state();
204✔
665

666
        if ($qa->get_behaviour_name() == 'adaptivemultipart') {
204✔
667
            // This is rather a hack, but it will probably work.
668
            $renderer = $this->page->get_renderer('qbehaviour_adaptivemultipart');
51✔
669
            $details = $qa->get_behaviour()->get_part_mark_details($part->partindex);
51✔
670
            $gradingdetails = $renderer->render_adaptive_marks($details, $options);
51✔
671
            $state = $details->state;
51✔
672
        }
673
        $showfeedback = $options->feedback && $state->get_feedback_class() != '';
204✔
674
        if ($showfeedback) {
204✔
675
            // Clone the part's evaluator and substitute local / grading vars first.
676
            $evaluator = clone $part->evaluator;
204✔
677
            $feedbacktext = $evaluator->substitute_variables_in_text($part->feedback);
204✔
678

679
            $feedbacktext = $question->format_text(
204✔
680
              $feedbacktext,
204✔
681
              FORMAT_HTML,
204✔
682
              $qa,
204✔
683
              'qtype_formulas',
204✔
684
              'answerfeedback',
204✔
685
              $part->id,
204✔
686
              false
204✔
687
            );
204✔
688
            $feedback = html_writer::tag('div', $feedbacktext , ['class' => 'feedback formulaslocalfeedback']);
204✔
689
            return html_writer::nonempty_tag('div', $feedback . $gradingdetails,
204✔
690
                    ['class' => 'formulaspartfeedback formulaspartfeedback-' . $part->partindex]);
204✔
691
        }
692
        return '';
187✔
693
    }
694

695
    /**
696
     * Generate HTML fragment for the part's combined feedback.
697
     *
698
     * @param int $i part index
699
     * @param question_attempt $qa question attempt being displayed
700
     * @param question_definition $question question being displayed
701
     * @param question_display_options $options controls what should and should not be displayed
702
     * @return string HTML fragment
703
     */
704
    protected function part_combined_feedback(question_attempt $qa, question_display_options $options, $part, $fraction) {
705
        $feedback = '';
357✔
706
        $showfeedback = false;
357✔
707
        $gradingdetails = '';
357✔
708
        $question = $qa->get_question();
357✔
709
        $state = $qa->get_state();
357✔
710
        $feedbackclass = $state->get_feedback_class();
357✔
711

712
        if ($qa->get_behaviour_name() == 'adaptivemultipart') {
357✔
713
            // This is rather a hack, but it will probably work.
714
            $renderer = $this->page->get_renderer('qbehaviour_adaptivemultipart');
68✔
715
            $details = $qa->get_behaviour()->get_part_mark_details($part->partindex);
68✔
716
            $feedbackclass = $details->state->get_feedback_class();
68✔
717
        } else {
718
            $state = question_state::graded_state_for_fraction($fraction);
289✔
719
            $feedbackclass = $state->get_feedback_class();
289✔
720
        }
721
        if ($feedbackclass != '') {
357✔
722
            $showfeedback = $options->feedback;
357✔
723
            $field = 'part' . $feedbackclass . 'fb';
357✔
724
            $format = 'part' . $feedbackclass . 'fbformat';
357✔
725
            if ($part->$field) {
357✔
726
                // Clone the part's evaluator and substitute local / grading vars first.
727
                $evaluator = clone $part->evaluator;
357✔
728
                $part->$field = $evaluator->substitute_variables_in_text($part->$field);
357✔
729
                $feedback = $question->format_text($part->$field, $part->$format,
357✔
730
                        $qa, 'qtype_formulas', $field, $part->id, false);
357✔
731
            }
732
        }
733
        if ($showfeedback && $feedback) {
357✔
734
                $feedback = html_writer::tag('div', $feedback , ['class' => 'feedback formulaslocalfeedback']);
357✔
735
                return html_writer::nonempty_tag('div', $feedback,
357✔
736
                        ['class' => 'formulaspartfeedback formulaspartfeedback-' . $part->partindex]);
357✔
737
        }
738
        return '';
323✔
739
    }
740
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc