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

FormulasQuestion / moodle-qtype_formulas / 14420692267

12 Apr 2025 02:52PM UTC coverage: 95.807%. Remained the same
14420692267

Pull #180

github

web-flow
Merge 406cc967a into 5b2df2dfa
Pull Request #180: Remove workaround for BS4/5 compatibility

0 of 1 new or added line in 1 file covered. (0.0%)

14 existing lines in 2 files now uncovered.

3770 of 3935 relevant lines covered (95.81%)

1228.34 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

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')) {
×
NEW
102
            $this->page->requires->js_call_amd('theme_boost/bs4-compat', 'init');
×
103
        }
104

UNCOV
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;
357✔
120
        // If using adaptivemultipart behaviour, adjust feedback display options for this part.
121
        if ($qa->get_behaviour_name() === 'adaptivemultipart') {
357✔
122
            $qa->get_behaviour()->adjust_display_options_for_part($part->partindex, $partoptions);
68✔
123
        }
124
        $sub = $this->get_part_image_and_class($qa, $partoptions, $part);
357✔
125

126
        $output = $this->get_part_formulation(
357✔
127
            $qa,
357✔
128
            $partoptions,
357✔
129
            $part->partindex,
357✔
130
            $sub
357✔
131
        );
357✔
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) {
357✔
135
            $output = str_replace('{_m}', $sub->feedbackimage, $output);
×
136
        } else {
137
            $output .= $sub->feedbackimage;
357✔
138
        }
139

140
        $feedback = $this->part_combined_feedback($qa, $partoptions, $part, $sub->fraction);
357✔
141
        $feedback .= $this->part_general_feedback($qa, $partoptions, $part);
357✔
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) {
357✔
146
            $feedback .= $this->part_correct_response($part->partindex, $qa);
272✔
147
        }
148
        $output .= html_writer::nonempty_tag(
357✔
149
            'div',
357✔
150
            $feedback,
357✔
151
            ['class' => 'formulaspartoutcome']
357✔
152
        );
357✔
153
        return html_writer::tag('div', $output , ['class' => 'formulaspart']);
357✔
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();
357✔
166

167
        $sub = new StdClass;
357✔
168

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

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

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

179
        // Get the class and image for the feedback.
180
        if ($options->correctness) {
357✔
181
            $sub->feedbackimage = $this->feedback_image($sub->fraction);
340✔
182
            $sub->feedbackclass = $this->feedback_class($sub->fraction);
340✔
183
            if ($part->unitpenalty >= 1) { // All boxes must be correct at the same time, so they are of the same color.
340✔
184
                $sub->unitfeedbackclass = $sub->feedbackclass;
102✔
185
                $sub->boxfeedbackclass = $sub->feedbackclass;
102✔
186
            } else {  // Show individual color, all four color combinations are possible.
187
                $sub->unitfeedbackclass = $this->feedback_class($unitcorrect);
238✔
188
                $sub->boxfeedbackclass = $this->feedback_class($answergrade);
292✔
189
            }
190
        } else {  // There should be no feedback if options->correctness is not set for this part.
191
            $sub->feedbackimage = '';
323✔
192
            $sub->feedbackclass = '';
323✔
193
            $sub->unitfeedbackclass = '';
323✔
194
            $sub->boxfeedbackclass = '';
323✔
195
        }
196
        return $sub;
357✔
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':
17✔
209
                $number = chr(ord('a') + $num);
17✔
210
                break;
17✔
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 . '. ';
17✔
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();
357✔
244
        $part = &$question->parts[$i];
357✔
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;
357✔
249
        $evaluator->remove_special_vars();
357✔
250
        $text = $evaluator->substitute_variables_in_text($part->subqtext);
357✔
251

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

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

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

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

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

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

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

493
        foreach ($inputs as $placeholder => $replacement) {
357✔
494
            $subqreplaced = preg_replace('/'.$boxes[$placeholder]['placeholder'].'/', $replacement, $subqreplaced, 1);
357✔
495
        }
496
        return $subqreplaced;
357✔
497
    }
498

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

509
    /**
510
     * Generate HTML code to be included after each choice in multiple choice questions.
511
     *
512
     * @return string
513
     */
514
    protected function choice_wrapper_end() {
515
        return html_writer::end_tag('div');
17✔
516
    }
517

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

527
    /**
528
     * Generate HTML code to be included after all choices in multiple choice questions.
529
     *
530
     * @return string
531
     */
532
    protected function all_choices_wrapper_end() {
533
        return html_writer::end_tag('div');
17✔
534
    }
535

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

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

560
        if ($question->parts[$i]->answernotunique) {
272✔
561
            $string = 'correctansweris';
272✔
562
        } else {
563
            $string = 'uniquecorrectansweris';
17✔
564
        }
565
        return html_writer::nonempty_tag('div', get_string($string, 'qtype_formulas', $answertext),
272✔
566
                    ['class' => 'formulaspartcorrectanswer']);
272✔
567
    }
568

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

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

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

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

610
        $state = $qa->get_state();
357✔
611

612
        if (!$state->is_finished()) {
357✔
613
            $response = $qa->get_last_qt_data();
102✔
614
            if (!$qa->get_question()->is_gradable_response($response)) {
102✔
615
                return '';
68✔
616
            }
617
            list($notused, $state) = $qa->get_question()->grade_response($response);
102✔
618
        }
619

620
        $feedback = '';
357✔
621
        $field = $state->get_feedback_class() . 'feedback';
357✔
622
        $format = $state->get_feedback_class() . 'feedbackformat';
357✔
623
        if ($question->$field) {
357✔
624
            $feedback .= $question->format_text($question->$field, $question->$format,
357✔
625
                    $qa, 'question', $field, $question->id, false);
357✔
626
        }
627

628
        return $feedback;
357✔
629
    }
630

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

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

656
        $feedback = '';
204✔
657
        $gradingdetails = '';
204✔
658
        $question = $qa->get_question();
204✔
659
        $state = $qa->get_state();
204✔
660

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

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

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

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

© 2025 Coveralls, Inc