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

FormulasQuestion / moodle-qtype_formulas / 14663698887

25 Apr 2025 11:37AM UTC coverage: 95.959% (+0.1%) from 95.81%
14663698887

push

github

web-flow
rewrite on-the-fly validation for answers and units (#163)


This also adds new MathJax rendering of student answers and units in the teacher's edit form.

245 of 250 new or added lines in 7 files covered. (98.0%)

4013 of 4182 relevant lines covered (95.96%)

1506.0 hits per line

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

92.62
/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();
441✔
48

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

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

77
        $result = html_writer::tag('div', $questiontext, ['class' => 'qtext']);
441✔
78
        if ($qa->get_state() == question_state::$invalid) {
441✔
79
            $result .= html_writer::nonempty_tag(
42✔
80
                'div',
42✔
81
                $question->get_validation_error($qa->get_last_qt_data()),
42✔
82
                ['class' => 'validationerror']
42✔
83
            );
42✔
84
        }
85
        return $result;
441✔
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;
NEW
97
        $this->page->requires->js_call_amd('qtype_formulas/answervalidation', 'init');
×
98

99
        // Include backwards-compatibility layer for Bootstrap 4 data attributes, if available.
100
        // We may safely assume that if the uncompiled version is there, the minified one exists as well.
101
        if (file_exists($CFG->dirroot . '/theme/boost/amd/src/bs4-compat.js')) {
×
102
            $this->page->requires->js_call_amd('theme_boost/bs4-compat', 'init');
×
103
        }
104

105
        return '';
×
106
    }
107

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

119
        $partoptions = clone $options;
441✔
120
        // If using adaptivemultipart behaviour, adjust feedback display options for this part.
121
        if ($qa->get_behaviour_name() === 'adaptivemultipart') {
441✔
122
            $qa->get_behaviour()->adjust_display_options_for_part($part->partindex, $partoptions);
84✔
123
        }
124
        $sub = $this->get_part_image_and_class($qa, $partoptions, $part);
441✔
125

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

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

156
    /**
157
     * Return class and image for the part feedback.
158
     *
159
     * @param question_attempt $qa
160
     * @param question_display_options $options
161
     * @param qtype_formulas_part $part
162
     * @return object
163
     */
164
    public function get_part_image_and_class($qa, $options, $part) {
165
        $question = $qa->get_question();
441✔
166

167
        $sub = new StdClass;
441✔
168

169
        $response = $qa->get_last_qt_data();
441✔
170
        $response = $question->normalize_response($response);
441✔
171

172
        list('answer' => $answergrade, 'unit' => $unitcorrect) = $part->grade($response);
441✔
173

174
        $sub->fraction = $answergrade;
441✔
175
        if ($unitcorrect === false) {
441✔
176
            $sub->fraction *= (1 - $part->unitpenalty);
357✔
177
        }
178

179
        // Get the class and image for the feedback.
180
        if ($options->correctness) {
441✔
181
            $sub->feedbackimage = $this->feedback_image($sub->fraction);
420✔
182
            $sub->feedbackclass = $this->feedback_class($sub->fraction);
420✔
183
            if ($part->unitpenalty >= 1) { // All boxes must be correct at the same time, so they are of the same color.
420✔
184
                $sub->unitfeedbackclass = $sub->feedbackclass;
126✔
185
                $sub->boxfeedbackclass = $sub->feedbackclass;
126✔
186
            } else {  // Show individual color, all four color combinations are possible.
187
                $sub->unitfeedbackclass = $this->feedback_class($unitcorrect);
294✔
188
                $sub->boxfeedbackclass = $this->feedback_class($answergrade);
348✔
189
            }
190
        } else {  // There should be no feedback if options->correctness is not set for this part.
191
            $sub->feedbackimage = '';
399✔
192
            $sub->feedbackclass = '';
399✔
193
            $sub->unitfeedbackclass = '';
399✔
194
            $sub->boxfeedbackclass = '';
399✔
195
        }
196
        return $sub;
441✔
197
    }
198

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

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

246
        // Clone the part's evaluator and remove special variables like _0 etc., because they must
247
        // not be substituted here; otherwise, we would lose input boxes.
248
        $evaluator = clone $part->evaluator;
441✔
249
        $evaluator->remove_special_vars();
441✔
250
        $text = $evaluator->substitute_variables_in_text($part->subqtext);
441✔
251

252
        $subqreplaced = $question->format_text($text,
441✔
253
                $part->subqtextformat, $qa, 'qtype_formulas', 'answersubqtext', $part->id, false);
441✔
254
        $types = [0 => 'number', 10 => 'numeric', 100 => 'numerical_formula', 1000 => 'algebraic_formula'];
441✔
255
        $gradingtype = ($part->answertype != 10 && $part->answertype != 100 && $part->answertype != 1000) ? 0 : $part->answertype;
441✔
256
        $gtype = $types[$gradingtype];
441✔
257

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

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

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

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

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

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

498
        foreach ($inputs as $placeholder => $replacement) {
441✔
499
            $subqreplaced = preg_replace('/'.$boxes[$placeholder]['placeholder'].'/', $replacement, $subqreplaced, 1);
441✔
500
        }
501
        return $subqreplaced;
441✔
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]);
21✔
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');
21✔
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']);
21✔
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');
21✔
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 '';
336✔
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();
336✔
562
        $answers = $question->parts[$i]->get_correct_response(true);
336✔
563
        $answertext = implode(', ', $answers);
336✔
564

565
        if ($question->parts[$i]->answernotunique) {
336✔
566
            $string = 'correctansweris';
336✔
567
        } else {
568
            $string = 'uniquecorrectansweris';
21✔
569
        }
570
        return html_writer::nonempty_tag('div', get_string($string, 'qtype_formulas', $answertext),
336✔
571
                    ['class' => 'formulaspartcorrectanswer']);
336✔
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();
231✔
582
        if (!$qa->get_question()->is_gradable_response($response)) {
231✔
583
            return '';
84✔
584
        }
585
        $numright = $qa->get_question()->get_num_parts_right($response);
231✔
586
        if ($numright[0] === 1) {
231✔
587
            return get_string('yougotoneright', 'qtype_formulas');
42✔
588
        } else {
589
            return get_string('yougotnright', 'qtype_formulas', $numright[0]);
210✔
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();
42✔
601
        $hint->hint = $question->evaluator->substitute_variables_in_text($hint->hint);
42✔
602

603
        return html_writer::nonempty_tag('div', $qa->get_question()->format_hint($hint, $qa), ['class' => 'hint']);
42✔
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();
441✔
614

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

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

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

633
        return $feedback;
441✔
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);
441✔
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 == '') {
441✔
658
            return '';
189✔
659
        }
660

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

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

679
            $feedbacktext = $question->format_text(
252✔
680
              $feedbacktext,
252✔
681
              FORMAT_HTML,
252✔
682
              $qa,
252✔
683
              'qtype_formulas',
252✔
684
              'answerfeedback',
252✔
685
              $part->id,
252✔
686
              false
252✔
687
            );
252✔
688
            $feedback = html_writer::tag('div', $feedbacktext , ['class' => 'feedback formulaslocalfeedback']);
252✔
689
            return html_writer::nonempty_tag('div', $feedback . $gradingdetails,
252✔
690
                    ['class' => 'formulaspartfeedback formulaspartfeedback-' . $part->partindex]);
252✔
691
        }
692
        return '';
231✔
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 = '';
441✔
706
        $showfeedback = false;
441✔
707
        $gradingdetails = '';
441✔
708
        $question = $qa->get_question();
441✔
709
        $state = $qa->get_state();
441✔
710
        $feedbackclass = $state->get_feedback_class();
441✔
711

712
        if ($qa->get_behaviour_name() == 'adaptivemultipart') {
441✔
713
            // This is rather a hack, but it will probably work.
714
            $renderer = $this->page->get_renderer('qbehaviour_adaptivemultipart');
84✔
715
            $details = $qa->get_behaviour()->get_part_mark_details($part->partindex);
84✔
716
            $feedbackclass = $details->state->get_feedback_class();
84✔
717
        } else {
718
            $state = question_state::graded_state_for_fraction($fraction);
357✔
719
            $feedbackclass = $state->get_feedback_class();
357✔
720
        }
721
        if ($feedbackclass != '') {
441✔
722
            $showfeedback = $options->feedback;
441✔
723
            $field = 'part' . $feedbackclass . 'fb';
441✔
724
            $format = 'part' . $feedbackclass . 'fbformat';
441✔
725
            if ($part->$field) {
441✔
726
                // Clone the part's evaluator and substitute local / grading vars first.
727
                $evaluator = clone $part->evaluator;
441✔
728
                $part->$field = $evaluator->substitute_variables_in_text($part->$field);
441✔
729
                $feedback = $question->format_text($part->$field, $part->$format,
441✔
730
                        $qa, 'qtype_formulas', $field, $part->id, false);
441✔
731
            }
732
        }
733
        if ($showfeedback && $feedback) {
441✔
734
                $feedback = html_writer::tag('div', $feedback , ['class' => 'feedback formulaslocalfeedback']);
441✔
735
                return html_writer::nonempty_tag('div', $feedback,
441✔
736
                        ['class' => 'formulaspartfeedback formulaspartfeedback-' . $part->partindex]);
441✔
737
        }
738
        return '';
399✔
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