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

FormulasQuestion / moodle-qtype_formulas / 13217446514

08 Feb 2025 04:37PM UTC coverage: 76.899% (+1.9%) from 75.045%
13217446514

Pull #62

github

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

2547 of 3139 new or added lines in 22 files covered. (81.14%)

146 existing lines in 6 files now uncovered.

3006 of 3909 relevant lines covered (76.9%)

438.31 hits per line

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

99.2
/classes/external/instantiation.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
 * qtype_formulas external file
19
 *
20
 * @package    qtype_formulas
21
 * @category   external
22
 * @copyright  2022 Philipp Imhof
23
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25

26
namespace qtype_formulas\external;
27

28
use Exception;
29
use qtype_formulas\local\evaluator;
30
use qtype_formulas\local\parser;
31
use qtype_formulas\local\random_parser;
32
use qtype_formulas\local\token;
33
use qtype_formulas\local\variable;
34

UNCOV
35
defined('MOODLE_INTERNAL') || die();
×
36

37
// TODO: in the future, this must be changed to $CFG->dirrot . '/lib/externallib.php'.
UNCOV
38
require_once($CFG->libdir . "/externallib.php");
×
39

40
/**
41
 * Class containing various methods for validation of variable definitions and
42
 * instantiation of questions inside the edit form.
43
 */
44
class instantiation extends \external_api {
45

46
    /**
47
     * Convert array to string of form '[1, 2, 3, ... ]'.
48
     * If argument is a string or a number, return it unchanged.
49
     *
50
     * @param mixed $maybearray value
51
     * @return string converted value, if conversion is needed
52
     */
53
    protected static function stringify($maybearray) {
54
        if (gettype($maybearray) == 'array') {
162✔
55
            return '[' . implode(', ', $maybearray) . ']';
17✔
56
        }
57
        return $maybearray;
145✔
58
    }
59

60
    /**
61
     * Remove variables that have just been copied from one evaluator to the next, unless
62
     * they have been changed. In that case, they should be kept and their name can be
63
     * marked to emphasize that change.
64
     *
65
     * @param array $base array of qtype_formulas\variable used as the base
66
     * @param array $new array of qtype_formulas\variable with updated/added variables
67
     * @param string $mark optional mark for overridden variables
68
     * @return array array of qtype_formulas\variable containing filtered variables
69
     */
70
    protected static function remove_duplicated_variables(array $base, array $new, $mark = '*'): array {
71
        $filtered = [];
230✔
72
        foreach ($base as $name => $variable) {
230✔
73
            // If the key exists in the subset, we might be overriding it.
74
            // This can be made clear by appending an asterisk to the variable name.
75
            $suffix = '';
213✔
76
            if (array_key_exists($name, $new)) {
213✔
77
                $suffix = $mark;
196✔
78
            }
79
            // If the timestamp of both variables is the same, we do not include it in the
80
            // subset, because there was no update.
81
            if ($suffix === $mark && $new[$name]->timestamp == $variable->timestamp) {
213✔
82
                continue;
179✔
83
            }
84
            $filtered[$name . $suffix] = $variable;
213✔
85
        }
86

87
        return $filtered;
230✔
88
    }
89

90
    /**
91
     * Convert the serialized variable context of an evaluator class into an array that
92
     * suits our needs. In case of an error, return an empty array.
93
     *
94
     * @param array $data serialized variable context
95
     * @return array
96
     */
97
    protected static function variable_context_to_array(array $data): array {
98
        // The data comes directly from the evaluator's export_variable_context() function, so
99
        // we don't have to expect an error.
100
        $context = unserialize($data['variables'], ['allowed_classes' => [variable::class, token::class]]);
289✔
101

102
        $result = [];
289✔
103
        foreach ($context as $name => $var) {
289✔
104
            $result[$name] = $var;
272✔
105
        }
106
        return $result;
289✔
107
    }
108

109
    /**
110
     * Instiantiate one set of variables.
111
     *
112
     * @param array $context variable context containing the random variables
113
     * @param array $parsedglobalvars array of qtype_formulas\expression containing the parsed definition of global vars
114
     * @param array $parsedlocalvars array of qtype_formulas\expression containing the parsed definition of local vars
115
     * @param array $parsedanswers array of qtype_formulas\expression containing the parsed answers for each part
116
     * @return mixed associative array containing one data set or an error message, if instantiation failed
117
     */
118
    protected static function fetch_one_instance($context, $parsedglobalvars, $parsedlocalvars, $parsedanswers) {
119
        $noparts = count($parsedanswers);
289✔
120
        $evaluator = new evaluator($context);
289✔
121

122
        try {
123
            $evaluator->instantiate_random_variables(null);
289✔
124
            $randomvars = self::variable_context_to_array($evaluator->export_variable_context());
289✔
125

126
            $evaluator->evaluate($parsedglobalvars);
289✔
127
            $globalvars = self::variable_context_to_array($evaluator->export_variable_context());
281✔
128

129
            $localvars = [];
281✔
130
            $answers = [];
281✔
131
            for ($i = 0; $i < $noparts; $i++) {
281✔
132
                // Clone the global evaluator. We do not need to keep it beyond the evaluations for
133
                // the part.
134
                $partevaluator = clone $evaluator;
247✔
135
                // Only evaluate the local variable definitions if there are any.
136
                if (isset($parsedlocalvars[$i])) {
247✔
137
                    $partevaluator->evaluate($parsedlocalvars[$i]);
94✔
138
                }
139
                $localvars[$i] = self::variable_context_to_array($partevaluator->export_variable_context());
247✔
140
                // Finally, evaluate the answer(s).
141
                $answers[$i] = $partevaluator->evaluate($parsedanswers[$i])[0];
247✔
142
            }
143
        } catch (Exception $e) {
68✔
144
            return $e->getMessage();
68✔
145
        }
146

147
        $row = ['randomvars' => [], 'globalvars' => [], 'parts' => []];
230✔
148
        foreach ($randomvars as $name => $variable) {
230✔
149
            $row['randomvars'][] = ['name' => $name, 'value' => self::stringify($variable->value)];
26✔
150
        }
151
        // Global variables might overwrite random variables. We mark those with a symbol.
152
        $filteredglobalvars = self::remove_duplicated_variables($globalvars, $randomvars);
230✔
153
        foreach ($filteredglobalvars as $name => $variable) {
230✔
154
            // If the variable has type SET, it is an algebraic variable. We only output its name
155
            // in curly braces to make that clear. For other variables, we put the value.
156
            if ($variable->type === variable::ALGEBRAIC) {
213✔
157
                $printname = str_replace('*', '', $name);
51✔
158
                $row['globalvars'][] = ['name' => $name, 'value' => "{{$printname}}"];
51✔
159
            } else {
160
                $row['globalvars'][] = ['name' => $name, 'value' => self::stringify($variable->value)];
162✔
161
            }
162
        }
163
        for ($i = 0; $i < $noparts; $i++) {
230✔
164
            $filteredlocalvars = self::remove_duplicated_variables($localvars[$i], $globalvars);
196✔
165
            $row['parts'][$i] = [];
196✔
166
            foreach ($filteredlocalvars as $name => $variable) {
196✔
167
                // If the variable has type SET, it is an algebraic variable. We only output its name
168
                // in curly braces to make that clear. For other variables, we put the value.
169
                if ($variable->type === variable::ALGEBRAIC) {
43✔
170
                    $printname = str_replace('*', '', $name);
17✔
171
                    $row['parts'][$i][] = ['name' => $name, 'value' => "{{$printname}}"];
17✔
172
                } else {
173
                    $row['parts'][$i][] = ['name' => $name, 'value' => self::stringify($variable->value)];
26✔
174
                }
175
            }
176
            // If the value is a scalar, that means the part has only one answer and it is _0.
177
            if (is_scalar($answers[$i]->value)) {
196✔
178
                $row['parts'][$i][] = ['name' => '_0', 'value' => $answers[$i]->value];
179✔
179
                continue;
179✔
180
            }
181
            // Otherwise, there are multiple answers from _0 to _n.
182
            foreach ($answers[$i]->value as $index => $token) {
34✔
183
                $row['parts'][$i][] = ['name' => "_{$index}", 'value' => self::stringify($token->value)];
34✔
184
            }
185
        }
186

187
        return $row;
230✔
188
    }
189

190
    /**
191
     * Description of the parameters for the external function 'instantiate'
192
     * @return \external_function_parameters
193
     */
194
    public static function instantiate_parameters() {
195
        return new \external_function_parameters([
323✔
196
            'n' => new \external_value(PARAM_INT, 'number of data sets', VALUE_DEFAULT, 1),
323✔
197
            'randomvars' => new \external_value(PARAM_RAW, 'random variables', VALUE_REQUIRED),
323✔
198
            'globalvars' => new \external_value(PARAM_RAW, 'global variables', VALUE_REQUIRED),
323✔
199
            'localvars' => new \external_multiple_structure(
323✔
200
                new \external_value(PARAM_RAW, 'local variables, per part', VALUE_REQUIRED)
323✔
201
            ),
323✔
202
            'answers' => new \external_multiple_structure(
323✔
203
                new \external_value(PARAM_RAW, 'answers, per part', VALUE_REQUIRED)
323✔
204
            ),
323✔
205
        ]);
323✔
206
    }
207

208
    /**
209
     * AJAX function to instantiate a certain number of data sets.
210
     *
211
     * @param integer $n number of data sets to instantiate or -1 for "all"
212
     * @param string $randomvars string defining the random variables
213
     * @param string $globalvars string defining the global variables
214
     * @param array $localvars array of strings, each one defining the corresponding part's local variables
215
     * @param array $answers array of strings, each one defining the corresponding part's answers
216
     * @return mixed associative array containing the datasets, or an error message if instantiation failed
217
     */
218
    public static function instantiate($n, $randomvars, $globalvars, $localvars, $answers) {
219
        $params = self::validate_parameters(
323✔
220
            self::instantiate_parameters(),
323✔
221
            ['n' => $n, 'randomvars' => $randomvars, 'globalvars' => $globalvars, 'localvars' => $localvars, 'answers' => $answers]
323✔
222
        );
323✔
223

224
        // First, we check whether the variables can be parsed and we prepare an evaluator context
225
        // containing the random variables for (probably very very) slightly better performance.
226
        $noparts = count($params['answers']);
323✔
227
        try {
228
            $randomparser = new random_parser($params['randomvars']);
323✔
229
            $evaluator = new evaluator();
306✔
230
            $evaluator->evaluate($randomparser->get_statements());
306✔
231
            $randomcontext = $evaluator->export_variable_context();
306✔
232

233
            // When initializing the parser, use the known vars from the random parser.
234
            $globalparser = new parser($params['globalvars'], $randomparser->export_known_variables());
306✔
235
            $parsedglobalvars = $globalparser->get_statements();
306✔
236

237
            $parsedlocalvars = [];
306✔
238
            $parsedanswers = [];
306✔
239
            for ($i = 0; $i < $noparts; $i++) {
306✔
240
                // Get the known vars from the global parser and save them as a fallback.
241
                $knownvars = $globalparser->export_known_variables();
255✔
242
                if (!empty($params['localvars'][$i])) {
255✔
243
                    // For each part parser, use the known vars from the global parser.
244
                    $parser = new parser($params['localvars'][$i], $knownvars);
102✔
245
                    $parsedlocalvars[$i] = $parser->get_statements();
102✔
246

247
                    // If we are here, that means there are local variables. So we update the
248
                    // list of known vars.
249
                    $knownvars = $parser->export_known_variables();
102✔
250
                }
251

252
                // Initialize the answer parser using the known variables, either just the global ones
253
                // or global plus local vars.
254
                $parser = new parser($params['answers'][$i], $knownvars);
255✔
255
                $parsedanswers[$i] = $parser->get_statements();
255✔
256
            }
257
        } catch (Exception $e) {
17✔
258
            // If parsing failed, we leave now.
259
            return ['status' => 'error', 'message' => $e->getMessage()];
17✔
260
        }
261

262
        // If requested number is -1, we try to instantiate all possible combinations, but not more than 1000.
263
        $n = $params['n'];
306✔
264
        if ($n == -1) {
306✔
265
            $n = min(1000, $evaluator->get_number_of_variants());
34✔
266
        }
267

268
        // All clear, we can now start to generate instances.
269
        $data = [];
306✔
270
        for ($i = 0; $i < $n; $i++) {
306✔
271
            $result = self::fetch_one_instance($randomcontext, $parsedglobalvars, $parsedlocalvars, $parsedanswers);
289✔
272
            if (gettype($result) == 'string') {
289✔
273
                return ['status' => 'error', 'message' => $result];
68✔
274
            }
275
            $data[] = $result;
230✔
276
        }
277

278
        return ['status' => 'ok', 'data' => $data];
238✔
279
    }
280

281
    /**
282
     * Description of the return value for the external function 'instantiate'
283
     * @return external_description
284
     */
285
    public static function instantiate_returns() {
286
        return new \external_single_structure([
323✔
287
            'status' => new \external_value(PARAM_TEXT, 'status', VALUE_REQUIRED),
323✔
288
            'message' => new \external_value(PARAM_TEXT, 'error message, if failed', VALUE_OPTIONAL),
323✔
289
            'data' => new \external_multiple_structure(
323✔
290
                new \external_single_structure([
323✔
291
                    'randomvars' => new \external_multiple_structure(
323✔
292
                        new \external_single_structure(
323✔
293
                            [
323✔
294
                                'name' => new \external_value(PARAM_TEXT, 'variable name', VALUE_REQUIRED),
323✔
295
                                'value' => new \external_value(PARAM_TEXT, 'value', VALUE_REQUIRED),
323✔
296
                            ],
323✔
297
                            'description of each random variable',
323✔
298
                            VALUE_REQUIRED
323✔
299
                        ),
323✔
300
                        'list of random variables',
323✔
301
                        VALUE_REQUIRED
323✔
302
                    ),
323✔
303
                    'globalvars' => new \external_multiple_structure(
323✔
304
                        new \external_single_structure(
323✔
305
                            [
323✔
306
                                'name' => new \external_value(PARAM_TEXT, 'variable name', VALUE_REQUIRED),
323✔
307
                                'value' => new \external_value(PARAM_TEXT, 'value', VALUE_REQUIRED),
323✔
308
                            ],
323✔
309
                            'description of each global variable',
323✔
310
                            VALUE_REQUIRED
323✔
311
                        ),
323✔
312
                        'list of global variables',
323✔
313
                        VALUE_REQUIRED
323✔
314
                    ),
323✔
315
                    'parts' => new \external_multiple_structure(
323✔
316
                        new \external_multiple_structure(
323✔
317
                            new \external_single_structure(
323✔
318
                                [
323✔
319
                                    'name' => new \external_value(PARAM_TEXT, 'variable name', VALUE_REQUIRED),
323✔
320
                                    'value' => new \external_value(PARAM_TEXT, 'value', VALUE_REQUIRED),
323✔
321
                                ]
323✔
322
                            ),
323✔
323
                            'list of variables for the corresponding part',
323✔
324
                            VALUE_REQUIRED
323✔
325
                        ),
323✔
326
                        'list of parts',
323✔
327
                        VALUE_REQUIRED
323✔
328
                    ),
323✔
329
                ]
323✔
330
            ),
323✔
331
            'data, if successful',
323✔
332
            VALUE_OPTIONAL
323✔
333
        )]);
323✔
334
    }
335

336
    /**
337
     * Returns description of method parameters
338
     * @return \external_function_parameters
339
     */
340
    public static function check_random_global_vars_parameters() {
341
        return new \external_function_parameters([
187✔
342
            'randomvars' => new \external_value(PARAM_RAW, 'random variables', VALUE_DEFAULT, ''),
187✔
343
            'globalvars' => new \external_value(PARAM_RAW, 'global variables', VALUE_DEFAULT, ''),
187✔
344
        ]);
187✔
345
    }
346

347
    /**
348
     * Try to parse and instantiate the given random vars (if any). If global vars are given,
349
     * the result from this first step will be used as a base to evaluate assignments of the
350
     * global vars.
351
     *
352
     * @param string $randomvars definition of random variables or empty string, if none
353
     * @param string $globalvars definition of global variables or empty string, if none
354
     * @return string error message (if any) or empty string (if definitions are valid)
355
     */
356
    public static function check_random_global_vars($randomvars, $globalvars) {
357
        $params = self::validate_parameters(
187✔
358
            self::check_random_global_vars_parameters(),
187✔
359
            ['randomvars' => $randomvars, 'globalvars' => $globalvars]
187✔
360
        );
187✔
361

362
        // Evaluation of global variables can fail, because there is an error in the random
363
        // variables. In order to know that, we need to have two separate try-catch constructions.
364
        try {
365
            $randomparser = new random_parser($params['randomvars']);
187✔
366
            $evaluator = new evaluator();
187✔
367
            $evaluator->evaluate($randomparser->get_statements());
187✔
368
        } catch (Exception $e) {
17✔
369
            return ['source' => 'random', 'message' => $e->getMessage()];
17✔
370
        }
371
        try {
372
            // Initialize the parser, taking into account the vars that are known after evaluation of
373
            // random variables assignments.
374
            $globalparser = new parser($params['globalvars'], $evaluator->export_variable_list());
170✔
375
            $evaluator->instantiate_random_variables();
136✔
376
            $evaluator->evaluate($globalparser->get_statements());
136✔
377
        } catch (Exception $e) {
51✔
378
            return ['source' => 'global', 'message' => $e->getMessage()];
51✔
379
        }
380

381
        return ['source' => '', 'message' => ''];
119✔
382
    }
383

384
    /**
385
     * Returns description of method result value
386
     * @return external_description
387
     */
388
    public static function check_random_global_vars_returns() {
389
        return new \external_single_structure([
187✔
390
            'source' => new \external_value(PARAM_RAW, 'source of the error or empty string'),
187✔
391
            'message' => new \external_value(PARAM_RAW, 'empty string or error message'),
187✔
392
        ]);
187✔
393
    }
394

395
    /**
396
     * Returns description of method parameters
397
     * @return \external_function_parameters
398
     */
399
    public static function check_local_vars_parameters() {
400
        return new \external_function_parameters([
272✔
401
            'randomvars' => new \external_value(PARAM_RAW, 'random variables', VALUE_DEFAULT, ''),
272✔
402
            'globalvars' => new \external_value(PARAM_RAW, 'global variables', VALUE_DEFAULT, ''),
272✔
403
            'localvars' => new \external_value(PARAM_RAW, 'local variables', VALUE_DEFAULT, ''),
272✔
404
        ]);
272✔
405
    }
406

407
    /**
408
     * Try to parse and evaluate the given local variables. In order to do so, the random and
409
     * global variables have to be considered as well, as local variables can be linked to them.
410
     *
411
     * @param string $randomvars definition of random variables or empty string, if none
412
     * @param string $globalvars definition of global variables or empty string, if none
413
     * @param string $localvars definition of part's local variables or empty string, if none
414
     * @return string error message (if any) or empty string (if definitions are valid)
415
     */
416
    public static function check_local_vars($randomvars, $globalvars, $localvars) {
417
        $params = self::validate_parameters(
272✔
418
            self::check_local_vars_parameters(),
272✔
419
            ['randomvars' => $randomvars, 'globalvars' => $globalvars, 'localvars' => $localvars]
272✔
420
        );
272✔
421

422
        // Evaluation of global variables can fail, because there is an error in the random
423
        // variables. In order to know that, we need to have two separate try-catch constructions.
424
        try {
425
            $randomparser = new random_parser($params['randomvars']);
272✔
426
            $evaluator = new evaluator();
204✔
427
            $evaluator->evaluate($randomparser->get_statements());
204✔
428
        } catch (Exception $e) {
68✔
429
            return ['source' => 'random', 'message' => $e->getMessage()];
68✔
430
        }
431
        try {
432
            // Initialize the parser, taking into account the vars that are known after evaluation of
433
            // random variables assignments.
434
            $parser = new parser($params['globalvars'], $evaluator->export_variable_list());
204✔
435
            $evaluator->instantiate_random_variables();
170✔
436
            $evaluator->evaluate($parser->get_statements());
170✔
437
        } catch (Exception $e) {
34✔
438
            return ['source' => 'global', 'message' => $e->getMessage()];
34✔
439
        }
440
        try {
441
            // Initialize the local variable parser, taking into account all vars that have been created
442
            // by random or global vars assignments.
443
            $parser = new parser($params['localvars'], $evaluator->export_variable_list());
170✔
444
            $evaluator->evaluate($parser->get_statements());
136✔
445
        } catch (Exception $e) {
51✔
446
            return ['source' => 'local', 'message' => $e->getMessage()];
51✔
447
        }
448

449
        return ['source' => '', 'message' => ''];
119✔
450
    }
451

452
    /**
453
     * Returns description of method result value
454
     * @return external_description
455
     */
456
    public static function check_local_vars_returns() {
457
        return new \external_single_structure([
272✔
458
            'source' => new \external_value(PARAM_RAW, 'source of the error or empty string'),
272✔
459
            'message' => new \external_value(PARAM_RAW, 'empty string or error message'),
272✔
460
        ]);
272✔
461
    }
462

463
    /**
464
     * Returns description of method parameters
465
     * @return \external_function_parameters
466
     */
467
    public static function render_question_text_parameters() {
468
        return new \external_function_parameters([
153✔
469
            'questiontext' => new \external_value(PARAM_RAW, 'question text with placeholders', VALUE_REQUIRED),
153✔
470
            'parttexts' => new \external_multiple_structure(
153✔
471
                new \external_value(PARAM_RAW, 'text for each part', VALUE_REQUIRED)
153✔
472
            ),
153✔
473
            'globalvars' => new \external_value(
153✔
474
                PARAM_RAW,
153✔
475
                'definition for global (and instantiated random) variables',
153✔
476
                VALUE_DEFAULT,
153✔
477
                ''
153✔
478
            ),
153✔
479
            'partvars' => new \external_multiple_structure(
153✔
480
                new \external_value(PARAM_RAW, 'definition for part\'s local variables', VALUE_DEFAULT, '')
153✔
481
            ),
153✔
482
        ]);
153✔
483
    }
484

485
    /**
486
     * Replace variable placeholders in the question text and all parts' text, using global variables
487
     * and parts' local variables. This only makes sense after random variables have been instantiated,
488
     * so we expect the caller to include them as normal global variables. (That's what they are once they
489
     * have lost their randomness.)
490
     *
491
     * @param string $questiontext the question's text, possibly including place holders or calculations
492
     * @param array $parttexts array of parts' texts, possibly including place holders or calculations
493
     * @param string $globalvars string defining the global (and instantiated random) variables
494
     * @param array $partvars array of strings defining the parts' local variables
495
     * @return array associative array with the rendered question text and array of parts' texts
496
     */
497
    public static function render_question_text($questiontext, $parttexts, $globalvars, $partvars) {
498
        $params = self::validate_parameters(
153✔
499
            self::render_question_text_parameters(),
153✔
500
            ['questiontext' => $questiontext, 'parttexts' => $parttexts, 'globalvars' => $globalvars, 'partvars' => $partvars]
153✔
501
        );
153✔
502

503
        $evaluator = new evaluator();
153✔
504

505
        // First prepare the main question text.
506
        try {
507
            // In this case, we do not start by parsing and evaluating random vars. Instead, the random vars
508
            // are already instantiated and are treated like normal global vars. Therefore, we do not need
509
            // to use known vars upon initialisation of the parser.
510
            $parser = new parser($params['globalvars']);
153✔
511
            $evaluator->evaluate($parser->get_statements());
136✔
512
            $renderedquestiontext = $evaluator->substitute_variables_in_text($params['questiontext']);
136✔
513
        } catch (Exception $e) {
17✔
514
            return [
17✔
515
                'question' => get_string('previewerror', 'qtype_formulas') . ' ' . $e->getMessage(),
17✔
516
                'parts' => [],
17✔
517
            ];
17✔
518
        }
519

520
        $renderedparttexts = [];
136✔
521
        foreach ($params['partvars'] as $i => $partvar) {
136✔
522
            try {
523
                $partevaluator = clone $evaluator;
68✔
524
                // Initialize the parser for each part's local variables, taking into account
525
                // the (global and instantiated random) variables known so far.
526
                $parser = new parser($partvar, $partevaluator->export_variable_list());
68✔
527
                $partevaluator->evaluate($parser->get_statements());
68✔
528

529
                $renderedparttexts[$i] = $partevaluator->substitute_variables_in_text($params['parttexts'][$i]);
51✔
530
            } catch (Exception $e) {
17✔
531
                return [
17✔
532
                    'question' => get_string('previewerror', 'qtype_formulas') . ' ' . $e->getMessage(),
17✔
533
                    'parts' => [],
17✔
534
                ];
17✔
535
            }
536
        }
537

538
        return ['question' => $renderedquestiontext, 'parts' => $renderedparttexts];
119✔
539
    }
540

541
    /**
542
     * Returns description of method result value
543
     * @return external_description
544
     */
545
    public static function render_question_text_returns() {
546
        return new \external_single_structure([
153✔
547
            'question' => new \external_value(PARAM_RAW, 'rendered question text', VALUE_REQUIRED),
153✔
548
            'parts' => new \external_multiple_structure(
153✔
549
                new \external_value(PARAM_RAW, 'rendered part text', VALUE_REQUIRED),
153✔
550
                'array of rendered part texts',
153✔
551
                VALUE_REQUIRED
153✔
552
            ),
153✔
553
        ]);
153✔
554
    }
555
}
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