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

FormulasQuestion / moodle-qtype_formulas / 13200038469

07 Feb 2025 12:40PM UTC coverage: 76.583% (+1.5%) from 75.045%
13200038469

Pull #62

github

web-flow
Merge 27bf7cac9 into acd272945
Pull Request #62: Rewrite the parser

2517 of 3116 new or added lines in 22 files covered. (80.78%)

146 existing lines in 6 files now uncovered.

2976 of 3886 relevant lines covered (76.58%)

431.97 hits per line

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

91.37
/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();
170✔
48

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

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

77
        $result = html_writer::tag('div', $questiontext, ['class' => 'qtext']);
170✔
78
        if ($qa->get_state() == question_state::$invalid) {
170✔
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;
170✔
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
        $this->page->requires->js('/question/type/formulas/script/formatcheck.js');
×
NEW
97
        return '';
×
98
    }
99

100
    /**
101
     * Return the part text, controls, grading details and feedbacks.
102
     *
103
     * @param question_attempt $qa question attempt that will be displayed on the page
104
     * @param question_display_options $options
105
     * @param qtype_formulas_part $part
106
     * @return void
107
     */
108
    public function part_formulation_and_controls(question_attempt $qa, question_display_options $options,
109
            qtype_formulas_part $part) {
110

111
        $partoptions = clone $options;
170✔
112
        // If using adaptivemultipart behaviour, adjust feedback display options for this part.
113
        if ($qa->get_behaviour_name() === 'adaptivemultipart') {
170✔
114
            $qa->get_behaviour()->adjust_display_options_for_part($part->partindex, $partoptions);
17✔
115
        }
116
        $sub = $this->get_part_image_and_class($qa, $partoptions, $part);
170✔
117

118
        $output = $this->get_part_formulation(
170✔
119
            $qa,
170✔
120
            $partoptions,
170✔
121
            $part->partindex,
170✔
122
            $sub
170✔
123
        );
170✔
124
        // Place for the right/wrong feeback image or appended at part's end.
125
        // TODO: this is not documented anywhere.
126
        if (strpos($output, '{_m}') !== false) {
170✔
127
            $output = str_replace('{_m}', $sub->feedbackimage, $output);
×
128
        } else {
129
            $output .= $sub->feedbackimage;
170✔
130
        }
131

132
        $feedback = $this->part_combined_feedback($qa, $partoptions, $part, $sub->fraction);
170✔
133
        $feedback .= $this->part_general_feedback($qa, $partoptions, $part);
170✔
134
        // If one of the part's coordinates is a MC or select question, the correct answer
135
        // stored in the database is not the right answer, but the index of the right answer,
136
        // so in that case, we need to calculate the right answer.
137
        if ($partoptions->rightanswer) {
170✔
138
            $feedback .= $this->part_correct_response($part->partindex, $qa);
136✔
139
        }
140
        $output .= html_writer::nonempty_tag(
170✔
141
            'div',
170✔
142
            $feedback,
170✔
143
            ['class' => 'formulaspartoutcome']
170✔
144
        );
170✔
145
        return html_writer::tag('div', $output , ['class' => 'formulaspart']);
170✔
146
    }
147

148
    /**
149
     * Return class and image for the part feedback.
150
     *
151
     * @param question_attempt $qa
152
     * @param question_display_options $options
153
     * @param qtype_formulas_part $part
154
     * @return object
155
     */
156
    public function get_part_image_and_class($qa, $options, $part) {
157
        $question = $qa->get_question();
170✔
158

159
        $sub = new StdClass;
170✔
160

161
        $response = $qa->get_last_qt_data();
170✔
162
        $response = $question->normalize_response($response);
170✔
163

164
        list('answer' => $answergrade, 'unit' => $unitcorrect) = $part->grade($response);
170✔
165

166
        $sub->fraction = $answergrade;
170✔
167
        if ($unitcorrect === false) {
170✔
168
            $sub->fraction *= (1 - $part->unitpenalty);
102✔
169
        }
170

171
        // Get the class and image for the feedback.
172
        if ($options->correctness) {
170✔
173
            $sub->feedbackimage = $this->feedback_image($sub->fraction);
153✔
174
            $sub->feedbackclass = $this->feedback_class($sub->fraction);
153✔
175
            if ($part->unitpenalty >= 1) { // All boxes must be correct at the same time, so they are of the same color.
153✔
176
                $sub->unitfeedbackclass = $sub->feedbackclass;
102✔
177
                $sub->boxfeedbackclass = $sub->feedbackclass;
102✔
178
            } else {  // Show individual color, all four color combinations are possible.
179
                $sub->unitfeedbackclass = $this->feedback_class($unitcorrect);
51✔
180
                $sub->boxfeedbackclass = $this->feedback_class($answergrade);
105✔
181
            }
182
        } else {  // There should be no feedback if options->correctness is not set for this part.
183
            $sub->feedbackimage = '';
136✔
184
            $sub->feedbackclass = '';
136✔
185
            $sub->unitfeedbackclass = '';
136✔
186
            $sub->boxfeedbackclass = '';
136✔
187
        }
188
        return $sub;
170✔
189
    }
190

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

224
    /**
225
     * Return the part's text with variables replaced by their values.
226
     *
227
     * @param question_attempt $qa
228
     * @param question_display_options $options
229
     * @param int $i part index
230
     * @param object $sub class and image for the part feedback
231
     * @return string
232
     */
233
    public function get_part_formulation(question_attempt $qa, question_display_options $options, $i, $sub) {
234
        /** @var qype_formulas_question $question */
235
        $question = $qa->get_question();
170✔
236
        $part = &$question->parts[$i];
170✔
237

238
        // Clone the part's evaluator and remove special variables like _0 etc., because they must
239
        // not be substituted here; otherwise, we would lose input boxes.
240
        $evaluator = clone $part->evaluator;
170✔
241
        $evaluator->remove_special_vars();
170✔
242
        $text = $evaluator->substitute_variables_in_text($part->subqtext);
170✔
243

244
        $subqreplaced = $question->format_text($text,
170✔
245
                $part->subqtextformat, $qa, 'qtype_formulas', 'answersubqtext', $part->id, false);
170✔
246
        $types = [0 => 'number', 10 => 'numeric', 100 => 'numerical_formula', 1000 => 'algebraic_formula'];
170✔
247
        $gradingtype = ($part->answertype != 10 && $part->answertype != 100 && $part->answertype != 1000) ? 0 : $part->answertype;
170✔
248
        $gtype = $types[$gradingtype];
170✔
249

250
        // Get the set of defined placeholders and their options.
251
        $boxes = $part->scan_for_answer_boxes($subqreplaced);
170✔
252
        // Append missing placholders at the end of part.
253
        foreach (range(0, $part->numbox) as $j) {
170✔
254
            $placeholder = ($j == $part->numbox) ? "_u" : "_$j";
170✔
255
            if (!array_key_exists($placeholder, $boxes)) {
170✔
256
                $boxes[$placeholder] = ['placeholder' => "{".$placeholder."}", 'options' => '', 'dropdown' => false];
85✔
257
                $subqreplaced .= "{".$placeholder."}";  // Appended at the end.
85✔
258
            }
259
        }
260

261
        // If part has combined unit answer input.
262
        if ($part->has_combined_unit_field()) {
170✔
263
            $variablename = "{$i}_";
68✔
264
            $currentanswer = $qa->get_last_qt_var($variablename);
68✔
265
            $inputname = $qa->get_qt_field_name($variablename);
68✔
266
            $inputattributes = [
68✔
267
                'type' => 'text',
68✔
268
                'name' => $inputname,
68✔
269
                'title' => get_string($gtype . ($part->postunit == '' ? '' : '_unit'), 'qtype_formulas'),
68✔
270
                'value' => $currentanswer,
68✔
271
                'id' => $inputname,
68✔
272
                'class' => 'formulas_' . $gtype . '_unit ' . $sub->feedbackclass,
68✔
273
                'maxlength' => 128,
68✔
274
                'aria-labelledby' => 'lbl_' . str_replace(':', '__', $inputname),
68✔
275
            ];
68✔
276

277
            if ($options->readonly) {
68✔
278
                $inputattributes['readonly'] = 'readonly';
68✔
279
            }
280
            // Create a meaningful label for accessibility.
281
            $a = new stdClass();
68✔
282
            $a->part = $i + 1;
68✔
283
            $a->numanswer = '';
68✔
284
            if ($question->numparts == 1) {
68✔
285
                $label = get_string('answercombinedunitsingle', 'qtype_formulas', $a);
34✔
286
            } else {
287
                $label = get_string('answercombinedunitmulti', 'qtype_formulas', $a);
34✔
288
            }
289
            $input = html_writer::tag(
68✔
290
                'label',
68✔
291
                $label,
68✔
292
                [
68✔
293
                    'class' => 'subq accesshide',
68✔
294
                    'for' => $inputattributes['id'],
68✔
295
                    'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
68✔
296
                ]
68✔
297
            );
68✔
298
            $input .= html_writer::empty_tag('input', $inputattributes);
68✔
299
            $subqreplaced = str_replace("{_0}{_u}", $input, $subqreplaced);
68✔
300
        }
301

302
        // Get the set of string for each candidate input box {_0}, {_1}, ..., {_u}.
303
        $inputs = [];
170✔
304
        foreach (range(0, $part->numbox) as $j) {    // Replace the input box for each placeholder {_0}, {_1} ...
170✔
305
            $placeholder = ($j == $part->numbox) ? "_u" : "_$j";    // The last one is unit.
170✔
306
            $variablename = "{$i}_$j";
170✔
307
            $currentanswer = $qa->get_last_qt_var($variablename);
170✔
308
            $inputname = $qa->get_qt_field_name($variablename);
170✔
309
            $inputattributes = [
170✔
310
                'name' => $inputname,
170✔
311
                'value' => $currentanswer,
170✔
312
                'id' => $inputname,
170✔
313
                'maxlength' => 128,
170✔
314
                'aria-labelledby' => 'lbl_' . str_replace(':', '__', $inputname),
170✔
315
            ];
170✔
316
            if ($options->readonly) {
170✔
317
                $inputattributes['readonly'] = 'readonly';
153✔
318
            }
319

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

407
            // Coordinate as shortanswer question.
408
            $inputs[$placeholder] = '';
170✔
409
            $inputattributes['type'] = 'text';
170✔
410
            if ($options->readonly) {
170✔
411
                $inputattributes['readonly'] = 'readonly';
153✔
412
            }
413
            if ($j == $part->numbox) {
170✔
414
                // Check if it's an input for unit.
415
                if (strlen($part->postunit) > 0) {
170✔
416
                    $inputattributes['title'] = get_string('unit', 'qtype_formulas');
102✔
417
                    $inputattributes['class'] = 'formulas_unit '.$sub->unitfeedbackclass;
102✔
418
                    $a = new stdClass();
102✔
419
                    $a->part = $i + 1;
102✔
420
                    $a->numanswer = $j + 1;
102✔
421
                    if ($question->numparts == 1) {
102✔
422
                        $label = get_string('answerunitsingle', 'qtype_formulas', $a);
68✔
423
                    } else {
424
                        $label = get_string('answerunitmulti', 'qtype_formulas', $a);
34✔
425
                    }
426
                    $inputs[$placeholder] = html_writer::tag(
102✔
427
                        'label',
102✔
428
                        $label,
102✔
429
                        [
102✔
430
                            'class' => 'subq accesshide',
102✔
431
                            'for' => $inputattributes['id'],
102✔
432
                            'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
102✔
433
                        ]
102✔
434
                    );
102✔
435
                    $inputs[$placeholder] .= html_writer::empty_tag('input', $inputattributes);
138✔
436
                }
437
            } else {
438
                $inputattributes['title'] = get_string($gtype, 'qtype_formulas');
136✔
439
                $inputattributes['class'] = 'formulas_'.$gtype.' '.$sub->boxfeedbackclass;
136✔
440
                $inputattributes['aria-labelledby'] = 'lbl_' . str_replace(':', '__', $inputattributes['id']);
136✔
441
                $a = new stdClass();
136✔
442
                $a->part = $i + 1;
136✔
443
                $a->numanswer = $j + 1;
136✔
444
                if ($part->numbox == 1) {
136✔
445
                    if ($question->numparts == 1) {
136✔
446
                        $label = get_string('answersingle', 'qtype_formulas', $a);
102✔
447
                    } else {
448
                        $label = get_string('answermulti', 'qtype_formulas', $a);
88✔
449
                    }
450
                } else {
NEW
451
                    if ($question->numparts == 1) {
×
452
                        $label = get_string('answercoordinatesingle', 'qtype_formulas', $a);
×
453
                    } else {
454
                        $label = get_string('answercoordinatemulti', 'qtype_formulas', $a);
×
455
                    }
456
                }
457
                $inputs[$placeholder] = html_writer::tag(
136✔
458
                    'label',
136✔
459
                    $label,
136✔
460
                    [
136✔
461
                        'class' => 'subq accesshide',
136✔
462
                        'for' => $inputattributes['id'],
136✔
463
                        'id' => 'lbl_' . str_replace(':', '__', $inputattributes['id']),
136✔
464
                    ]
136✔
465
                );
136✔
466
                $inputs[$placeholder] .= html_writer::empty_tag('input', $inputattributes);
136✔
467
            }
468
        }
469

470
        foreach ($inputs as $placeholder => $replacement) {
170✔
471
            $subqreplaced = preg_replace('/'.$boxes[$placeholder]['placeholder'].'/', $replacement, $subqreplaced, 1);
170✔
472
        }
473
        return $subqreplaced;
170✔
474
    }
475

476
    /**
477
     * Generate HTML code to be included before each choice in multiple choice questions.
478
     *
479
     * @param string $class class attribute value
480
     * @return string
481
     */
482
    protected function choice_wrapper_start($class) {
483
        return html_writer::start_tag('div', ['class' => $class]);
17✔
484
    }
485

486
    /**
487
     * Generate HTML code to be included after each choice in multiple choice questions.
488
     *
489
     * @return string
490
     */
491
    protected function choice_wrapper_end() {
492
        return html_writer::end_tag('div');
17✔
493
    }
494

495
    /**
496
     * Generate HTML code to be included before all choices in multiple choice questions.
497
     *
498
     * @return string
499
     */
500
    protected function all_choices_wrapper_start() {
501
        return html_writer::start_tag('div', ['class' => 'multichoice_answer']);
17✔
502
    }
503

504
    /**
505
     * Generate HTML code to be included after all choices in multiple choice questions.
506
     *
507
     * @return string
508
     */
509
    protected function all_choices_wrapper_end() {
510
        return html_writer::end_tag('div');
17✔
511
    }
512

513
    /**
514
     * Correct response for the question. This is not needed for the Formulas question, because
515
     * answers are relative to parts.
516
     *
517
     * @param question_attempt $qa the question attempt to display
518
     * @return string empty string
519
     */
520
    public function correct_response(question_attempt $qa) {
521
        return '';
136✔
522
    }
523

524
    /**
525
     * Generate an automatic description of the correct response for a given part.
526
     *
527
     * @param int $i part index
528
     * @param question_attempt $qa question attempt to display
529
     * @return string HTML fragment
530
     */
531
    public function part_correct_response($i, question_attempt $qa) {
532
        /** @var qtype_formulas_question $question */
533
        $question = $qa->get_question();
136✔
534
        $answers = $question->parts[$i]->get_correct_response(true);
136✔
535
        $answertext = implode(', ', $answers);
136✔
536

537
        if ($question->parts[$i]->answernotunique) {
136✔
538
            $string = 'correctansweris';
136✔
539
        } else {
540
            $string = 'uniquecorrectansweris';
17✔
541
        }
542
        return html_writer::nonempty_tag('div', get_string($string, 'qtype_formulas', $answertext),
136✔
543
                    ['class' => 'formulaspartcorrectanswer']);
136✔
544
    }
545

546
    /**
547
     * Generate a brief statement of how many sub-parts of this question the
548
     * student got right.
549
     * @param question_attempt $qa the question attempt to display.
550
     * @return string HTML fragment.
551
     */
552
    protected function num_parts_correct(question_attempt $qa) {
553
        $response = $qa->get_last_qt_data();
85✔
554
        if (!$qa->get_question()->is_gradable_response($response)) {
85✔
555
            return '';
17✔
556
        }
557
        $numright = $qa->get_question()->get_num_parts_right($response);
85✔
558
        if ($numright[0] === 1) {
85✔
559
            return get_string('yougotoneright', 'qtype_formulas');
17✔
560
        } else {
561
            return get_string('yougotnright', 'qtype_formulas', $numright[0]);
85✔
562
        }
563
    }
564

565
    /**
566
     * We need to owerwrite this method to replace global variables by their value
567
     * @param question_attempt $qa the question attempt to display.
568
     * @return string HTML fragment.
569
     */
570
    protected function hint(question_attempt $qa, question_hint $hint) {
571
        /** @var qtype_formulas_question $question */
572
        $question = $qa->get_question();
34✔
573
        $hint->hint = $question->evaluator->substitute_variables_in_text($hint->hint);
34✔
574

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

578
    /**
579
     * Generate HTML fragment for the question's combined feedback.
580
     *
581
     * @param question_attempt $qa question attempt being displayed
582
     * @return string
583
     */
584
    protected function combined_feedback(question_attempt $qa) {
585
        $question = $qa->get_question();
170✔
586

587
        $state = $qa->get_state();
170✔
588

589
        if (!$state->is_finished()) {
170✔
590
            $response = $qa->get_last_qt_data();
51✔
591
            if (!$qa->get_question()->is_gradable_response($response)) {
51✔
592
                return '';
17✔
593
            }
594
            list($notused, $state) = $qa->get_question()->grade_response($response);
51✔
595
        }
596

597
        $feedback = '';
170✔
598
        $field = $state->get_feedback_class() . 'feedback';
170✔
599
        $format = $state->get_feedback_class() . 'feedbackformat';
170✔
600
        if ($question->$field) {
170✔
601
            $feedback .= $question->format_text($question->$field, $question->$format,
170✔
602
                    $qa, 'question', $field, $question->id, false);
170✔
603
        }
604

605
        return $feedback;
170✔
606
    }
607

608
    /**
609
     * Generate the specific feedback. This is feedback that varies according to
610
     * the response the student gave.
611
     *
612
     * @param question_attempt $qa question attempt being displayed
613
     * @return string
614
     */
615
    public function specific_feedback(question_attempt $qa) {
616
        return $this->combined_feedback($qa);
170✔
617
    }
618

619
    /**
620
     * Gereate the part's general feedback. This is feedback is shown to all students.
621
     *
622
     * @param int $i part index
623
     * @param question_attempt $qa question attempt being displayed
624
     * @param question_definition $question question being displayed
625
     * @param question_display_options $options controls what should and should not be displayed
626
     * @return string HTML fragment
627
     */
628
    protected function part_general_feedback(question_attempt $qa, question_display_options $options, $part) {
629
        if ($part->feedback == '') {
170✔
630
            return '';
153✔
631
        }
632

633
        $feedback = '';
17✔
634
        $gradingdetails = '';
17✔
635
        $question = $qa->get_question();
17✔
636
        $state = $qa->get_state();
17✔
637

638
        if ($qa->get_behaviour_name() == 'adaptivemultipart') {
17✔
639
            // This is rather a hack, but it will probably work.
640
            $renderer = $this->page->get_renderer('qbehaviour_adaptivemultipart');
×
641
            $details = $qa->get_behaviour()->get_part_mark_details($part->partindex);
×
642
            $gradingdetails = $renderer->render_adaptive_marks($details, $options);
×
643
            $state = $details->state;
×
644
        }
645
        $showfeedback = $options->feedback && $state->get_feedback_class() != '';
17✔
646
        if ($showfeedback) {
17✔
647
            // Clone the part's evaluator and substitute local / grading vars first.
648
            $evaluator = clone $part->evaluator;
17✔
649
            $feedbacktext = $evaluator->substitute_variables_in_text($part->feedback);
17✔
650

651
            $feedbacktext = $question->format_text(
17✔
652
              $feedbacktext,
17✔
653
              FORMAT_HTML,
17✔
654
              $qa,
17✔
655
              'qtype_formulas',
17✔
656
              'answerfeedback',
17✔
657
              $part->id,
17✔
658
              false
17✔
659
            );
17✔
660
            $feedback = html_writer::tag('div', $feedbacktext , ['class' => 'feedback formulaslocalfeedback']);
17✔
661
            return html_writer::nonempty_tag('div', $feedback . $gradingdetails,
17✔
662
                    ['class' => 'formulaspartfeedback formulaspartfeedback-' . $part->partindex]);
17✔
663
        }
664
        return '';
×
665
    }
666

667
    /**
668
     * Generate HTML fragment for the part's combined feedback.
669
     *
670
     * @param int $i part index
671
     * @param question_attempt $qa question attempt being displayed
672
     * @param question_definition $question question being displayed
673
     * @param question_display_options $options controls what should and should not be displayed
674
     * @return string HTML fragment
675
     */
676
    protected function part_combined_feedback(question_attempt $qa, question_display_options $options, $part, $fraction) {
677
        $feedback = '';
170✔
678
        $showfeedback = false;
170✔
679
        $gradingdetails = '';
170✔
680
        $question = $qa->get_question();
170✔
681
        $state = $qa->get_state();
170✔
682
        $feedbackclass = $state->get_feedback_class();
170✔
683

684
        if ($qa->get_behaviour_name() == 'adaptivemultipart') {
170✔
685
            // This is rather a hack, but it will probably work.
686
            $renderer = $this->page->get_renderer('qbehaviour_adaptivemultipart');
17✔
687
            $details = $qa->get_behaviour()->get_part_mark_details($part->partindex);
17✔
688
            $feedbackclass = $details->state->get_feedback_class();
17✔
689
        } else {
690
            $state = question_state::graded_state_for_fraction($fraction);
153✔
691
            $feedbackclass = $state->get_feedback_class();
153✔
692
        }
693
        if ($feedbackclass != '') {
170✔
694
            $showfeedback = $options->feedback;
170✔
695
            $field = 'part' . $feedbackclass . 'fb';
170✔
696
            $format = 'part' . $feedbackclass . 'fbformat';
170✔
697
            if ($part->$field) {
170✔
698
                // Clone the part's evaluator and substitute local / grading vars first.
699
                $evaluator = clone $part->evaluator;
170✔
700
                $part->$field = $evaluator->substitute_variables_in_text($part->$field);
170✔
701
                $feedback = $question->format_text($part->$field, $part->$format,
170✔
702
                        $qa, 'qtype_formulas', $field, $part->id, false);
170✔
703
            }
704
        }
705
        if ($showfeedback && $feedback) {
170✔
706
                $feedback = html_writer::tag('div', $feedback , ['class' => 'feedback formulaslocalfeedback']);
170✔
707
                return html_writer::nonempty_tag('div', $feedback,
170✔
708
                        ['class' => 'formulaspartfeedback formulaspartfeedback-' . $part->partindex]);
170✔
709
        }
710
        return '';
136✔
711
    }
712
}
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