Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://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 <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * This file defines the question attempt step class, and a few related classes.
19
 *
20
 * @package    moodlecore
21
 * @subpackage questionengine
22
 * @copyright  2009 The Open University
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
 
30
/**
31
 * Stores one step in a {@see question_attempt}.
32
 *
33
 * The most important attributes of a step are the state, which is one of the
34
 * {@see question_state} constants, the fraction, which may be null, or a
35
 * number bewteen the attempt's minfraction and maxfraction, and the array of submitted
36
 * data, about which more later.
37
 *
38
 * A step also tracks the time it was created, and the user responsible for
39
 * creating it.
40
 *
41
 * The submitted data is basically just an array of name => value pairs, with
42
 * certain conventions about the to divide the variables into five = 2 x 2 + 1 categories.
43
 *
44
 * Variables may either belong to the behaviour, in which case the
45
 * name starts with a '-', or they may belong to the question type in which case
46
 * they name does not start with a '-'.
47
 *
48
 * Second, variables may either be ones that came form the original request, in
49
 * which case the name does not start with an _, or they are cached values that
50
 * were created during processing, in which case the name does start with an _.
51
 *
52
 * In addition, we can store 'metadata', typically only in the first step of a
53
 * question attempt. These are stored with the initial characters ':_'.
54
 *
55
 * That is, each name will start with one of '', '_', '-', '-_' or ':_'. The remainder
56
 * of the name was supposed to match the regex [a-z][a-z0-9]* - but this has never
57
 * been enforced. Question types exist which break this rule. E.g. qtype_combined.
58
 * Perhpas now, an accurate regex would be [a-z][a-z0-9_:]*.
59
 *
60
 * These variables can be accessed with {@see get_behaviour_var()} and {@see get_qt_var()},
61
 * - to be clear, ->get_behaviour_var('x') gets the variable with name '-x' -
62
 * and values whose names start with '_' can be set using {@see set_behaviour_var()}
63
 * and {@see set_qt_var()}. There are some other methods like {@see has_behaviour_var()}
64
 * to check wether a varaible with a particular name is set, and {@see get_behaviour_data()}
65
 * to get all the behaviour data as an associative array. There are also
66
 * {@see get_metadata_var()}, {@see set_metadata_var()} and {@see has_metadata_var()},
67
 *
68
 * @copyright  2009 The Open University
69
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
70
 */
71
class question_attempt_step {
72
    /**
1441 ariadna 73
     * @var int Indicates that timecreated will be set the first time the attempt is rendered
74
     */
75
    const TIMECREATED_ON_FIRST_RENDER = -1;
76
 
77
    /**
1 efrain 78
     * @var integer if this attempts is stored in the question_attempts table,
79
     * the id of that row.
80
     */
81
    private $id = null;
82
 
83
    /**
84
     * @var question_state one of the {@see question_state} constants.
85
     * The state after this step.
86
     */
87
    private $state;
88
 
89
    /**
90
     * @var null|number the fraction (grade on a scale of
91
     * minfraction .. maxfraction, normally 0..1) or null.
92
     */
93
    private $fraction = null;
94
 
95
    /** @var int the timestamp when this step was created. */
96
    private $timecreated;
97
 
98
    /** @var int the id of the user responsible for creating this step. */
99
    private $userid;
100
 
101
    /** @var array name => value pairs. The submitted data. */
102
    private $data;
103
 
104
    /** @var array name => array of {@see stored_file}s. Caches the contents of file areas. */
105
    private $files = array();
106
 
107
    /** @var stdClass|null User information. */
108
    private $user = null;
109
 
110
    /**
111
     * You should not need to call this constructor in your own code. Steps are
112
     * normally created by {@see question_attempt} methods like
113
     * {@see question_attempt::process_action()}.
114
     * @param array $data the submitted data that defines this step.
1441 ariadna 115
     * @param int|null $timecreated the time to record for the action. If null, use now. If
116
     *     {@see self::TIMECREATED_ON_FIRST_RENDER}, the time will be set the first time the attempt is rendered.
1 efrain 117
     * @param int|null $userid the user to attribute the aciton to. (If not given, use the current user.)
118
     * @param int|null $existingstepid if this step is going to replace an existing step
119
     *      (for example, during a regrade) this is the id of the previous step we are replacing.
120
     */
121
    public function __construct($data = [], $timecreated = null, $userid = null,
122
            $existingstepid = null) {
123
        global $USER;
124
 
125
        if (!is_array($data)) {
126
            throw new coding_exception('$data must be an array when constructing a question_attempt_step.');
127
        }
128
        $this->state = question_state::$unprocessed;
129
        $this->data = $data;
130
        if (is_null($timecreated)) {
131
            $this->timecreated = time();
132
        } else {
133
            $this->timecreated = $timecreated;
134
        }
135
        if (is_null($userid)) {
136
            $this->userid = $USER->id;
137
        } else {
138
            $this->userid = $userid;
139
        }
140
 
141
        if (!is_null($existingstepid)) {
142
            $this->id = $existingstepid;
143
        }
144
    }
145
 
146
    /**
147
     * @return int|null The id of this step in the database. null if this step
148
     * is not stored in the database.
149
     */
150
    public function get_id() {
151
        return $this->id;
152
    }
153
 
154
    /** @return question_state The state after this step. */
155
    public function get_state() {
156
        return $this->state;
157
    }
158
 
159
    /**
160
     * Set the state. Normally only called by behaviours.
161
     * @param question_state $state one of the {@see question_state} constants.
162
     */
163
    public function set_state($state) {
164
        $this->state = $state;
165
    }
166
 
167
    /**
168
     * @return null|number the fraction (grade on a scale of
169
     * minfraction .. maxfraction, normally 0..1),
170
     * or null if this step has not been marked.
171
     */
172
    public function get_fraction() {
173
        return $this->fraction;
174
    }
175
 
176
    /**
177
     * Set the fraction. Normally only called by behaviours.
178
     * @param null|number $fraction the fraction to set.
179
     */
180
    public function set_fraction($fraction) {
181
        $this->fraction = $fraction;
182
    }
183
 
184
    /** @return int the id of the user resonsible for creating this step. */
185
    public function get_user_id() {
186
        return $this->userid;
187
    }
188
 
189
    /**
190
     * Update full user information for step.
191
     *
192
     * @param stdClass $user Full user object.
193
     * @throws coding_exception
194
     */
195
    public function add_full_user_object(stdClass $user): void {
196
        if ($user->id != $this->userid) {
197
            throw new coding_exception('Wrong user passed to add_full_user_object');
198
        }
199
        $this->user = $user;
200
    }
201
 
202
    /**
203
     * Return the full user object.
204
     *
205
     * @return null|stdClass Get full user object.
206
     */
207
    public function get_user(): ?stdClass {
208
        if ($this->user === null) {
209
            debugging('Attempt to access the step user before it was initialised. ' .
210
                'Did you forget to call question_usage_by_activity::preload_all_step_users() or similar?', DEBUG_DEVELOPER);
211
        }
212
        return $this->user;
213
    }
214
 
215
    /**
216
     * Get full name of user who did action.
217
     *
218
     * @return string full name of user.
219
     */
220
    public function get_user_fullname(): string {
221
        return fullname($this->get_user());
222
    }
223
 
224
    /** @return int the timestamp when this step was created. */
225
    public function get_timecreated() {
226
        return $this->timecreated;
227
    }
228
 
229
    /**
1441 ariadna 230
     * Setter for $this->timecreated.
231
     *
232
     * @param int $timecreated
233
     * @return void
234
     */
235
    public function set_timecreated(int $timecreated): void {
236
        $this->timecreated = $timecreated;
237
    }
238
 
239
    /**
1 efrain 240
     * @param string $name the name of a question type variable to look for in the submitted data.
241
     * @return bool whether a variable with this name exists in the question type data.
242
     */
243
    public function has_qt_var($name) {
244
        return array_key_exists($name, $this->data);
245
    }
246
 
247
    /**
248
     * @param string $name the name of a question type variable to look for in the submitted data.
249
     * @return string the requested variable, or null if the variable is not set.
250
     */
251
    public function get_qt_var($name) {
252
        if (!$this->has_qt_var($name)) {
253
            return null;
254
        }
255
        return $this->data[$name];
256
    }
257
 
258
    /**
259
     * Set a cached question type variable.
260
     * @param string $name the name of the variable to set. Must match _[a-z][a-z0-9]*.
261
     * @param string $value the value to set.
262
     */
263
    public function set_qt_var($name, $value) {
264
        if ($name[0] != '_') {
265
            throw new coding_exception('Cannot set question type data ' . $name .
266
                    ' on an attempt step. You can only set variables with names begining with _.');
267
        }
268
        $this->data[$name] = $value;
269
    }
270
 
271
    /**
272
     * Get the latest set of files for a particular question type variable of
273
     * type question_attempt::PARAM_FILES.
274
     *
275
     * @param string $name the name of the associated variable.
276
     * @param int $contextid contextid of the question attempt
277
     * @return array of {@see stored_files}.
278
     */
279
    public function get_qt_files($name, $contextid) {
280
        if (array_key_exists($name, $this->files)) {
281
            return $this->files[$name];
282
        }
283
 
284
        if (!$this->has_qt_var($name)) {
285
            $this->files[$name] = array();
286
            return array();
287
        }
288
 
289
        $fs = get_file_storage();
290
        $filearea = question_file_saver::clean_file_area_name('response_' . $name);
291
        $this->files[$name] = $fs->get_area_files($contextid, 'question',
292
                $filearea, $this->id, 'sortorder', false);
293
 
294
        return $this->files[$name];
295
    }
296
 
297
    /**
298
     * Prepare a draft file are for the files belonging the a response variable
299
     * of this step.
300
     *
301
     * @param string $name the variable name the files belong to.
302
     * @param int $contextid the id of the context the quba belongs to.
303
     * @return int the draft itemid.
304
     */
305
    public function prepare_response_files_draft_itemid($name, $contextid) {
306
        list($draftid, $notused) = $this->prepare_response_files_draft_itemid_with_text(
307
                $name, $contextid, null);
308
        return $draftid;
309
    }
310
 
311
    /**
312
     * Prepare a draft file are for the files belonging the a response variable
313
     * of this step, while rewriting the URLs in some text.
314
     *
315
     * @param string $name the variable name the files belong to.
316
     * @param int $contextid the id of the context the quba belongs to.
317
     * @param string|null $text the text to update the URLs in.
318
     * @return array(int, string) the draft itemid and the text with URLs rewritten.
319
     */
320
    public function prepare_response_files_draft_itemid_with_text($name, $contextid, $text) {
321
        $filearea = question_file_saver::clean_file_area_name('response_' . $name);
322
        $draftid = 0; // Will be filled in by file_prepare_draft_area.
323
        $newtext = file_prepare_draft_area($draftid, $contextid, 'question',
324
                $filearea, $this->id, null, $text);
325
        return array($draftid, $newtext);
326
    }
327
 
328
    /**
329
     * Rewrite the @@PLUGINFILE@@ tokens in a response variable from this step
330
     * that contains links to file. Normally you should probably call
331
     * {@see question_attempt::rewrite_response_pluginfile_urls()} instead of
332
     * calling this method directly.
333
     *
334
     * @param string $text the text to update the URLs in.
335
     * @param int $contextid the id of the context the quba belongs to.
336
     * @param string $name the variable name the files belong to.
337
     * @param array $extras extra file path components.
338
     * @return string the rewritten text.
339
     */
340
    public function rewrite_response_pluginfile_urls($text, $contextid, $name, $extras) {
341
        $filearea = question_file_saver::clean_file_area_name('response_' . $name);
342
        return question_rewrite_question_urls($text, 'pluginfile.php', $contextid,
343
                'question', $filearea, $extras, $this->id);
344
    }
345
 
346
    /**
347
     * Get all the question type variables.
348
     * @param array name => value pairs.
349
     */
350
    public function get_qt_data() {
351
        $result = array();
352
        foreach ($this->data as $name => $value) {
353
            if ($name[0] != '-' && $name[0] != ':') {
354
                $result[$name] = $value;
355
            }
356
        }
357
        return $result;
358
    }
359
 
360
    /**
361
     * @param string $name the name of a behaviour variable to look for in the submitted data.
362
     * @return bool whether a variable with this name exists in the question type data.
363
     */
364
    public function has_behaviour_var($name) {
365
        return array_key_exists('-' . $name, $this->data);
366
    }
367
 
368
    /**
369
     * @param string $name the name of a behaviour variable to look for in the submitted data.
370
     * @return string the requested variable, or null if the variable is not set.
371
     */
372
    public function get_behaviour_var($name) {
373
        if (!$this->has_behaviour_var($name)) {
374
            return null;
375
        }
376
        return $this->data['-' . $name];
377
    }
378
 
379
    /**
380
     * Set a cached behaviour variable.
381
     * @param string $name the name of the variable to set. Must match _[a-z][a-z0-9]*.
382
     * @param string $value the value to set.
383
     */
384
    public function set_behaviour_var($name, $value) {
385
        if ($name[0] != '_') {
386
            throw new coding_exception('Cannot set question type data ' . $name .
387
                    ' on an attempt step. You can only set variables with names begining with _.');
388
        }
389
        return $this->data['-' . $name] = $value;
390
    }
391
 
392
    /**
393
     * Get all the behaviour variables.
394
     *
395
     * @return array name => value pairs. NOTE! the name has the leading - stripped off.
396
     *      (If you don't understand the note, read the comment at the top of this class :-))
397
     */
398
    public function get_behaviour_data() {
399
        $result = array();
400
        foreach ($this->data as $name => $value) {
401
            if ($name[0] == '-') {
402
                $result[substr($name, 1)] = $value;
403
            }
404
        }
405
        return $result;
406
    }
407
 
408
    /**
409
     * Get all the submitted data, but not the cached data. behaviour
410
     * variables have the - at the start of their name. This is only really
411
     * intended for use by {@see question_attempt::regrade()}, it should not
412
     * be considered part of the public API.
413
     * @param array name => value pairs.
414
     */
415
    public function get_submitted_data() {
416
        $result = array();
417
        foreach ($this->data as $name => $value) {
418
            if ($name[0] == '_' || ($name[0] == '-' && $name[1] == '_')) {
419
                continue;
420
            }
421
            $result[$name] = $value;
422
        }
423
        return $result;
424
    }
425
 
426
    /**
427
     * Get all the data. behaviour variables have the - at the start of
428
     * their name. This is only intended for internal use, for example by
429
     * {@see question_engine_data_mapper::insert_question_attempt_step()},
430
     * however, it can occasionally be useful in test code. It should not be
431
     * considered part of the public API of this class.
432
     * @param array name => value pairs.
433
     */
434
    public function get_all_data() {
435
        return $this->data;
436
    }
437
 
438
    /**
439
     * Set a metadata variable.
440
     *
441
     * Do not call this method directly from  your code. It is for internal
442
     * use only. You should call {@see question_usage::set_question_attempt_metadata()}.
443
     *
444
     * @param string $name the name of the variable to set. [a-z][a-z0-9]*.
445
     * @param string $value the value to set.
446
     */
447
    public function set_metadata_var($name, $value) {
448
        $this->data[':_' . $name] = $value;
449
    }
450
 
451
    /**
452
     * Whether this step has a metadata variable.
453
     *
454
     * Do not call this method directly from  your code. It is for internal
455
     * use only. You should call {@see question_usage::get_question_attempt_metadata()}.
456
     *
457
     * @param string $name the name of the variable to set. [a-z][a-z0-9]*.
458
     * @return bool the value to set previously, or null if this variable was never set.
459
     */
460
    public function has_metadata_var($name) {
461
        return isset($this->data[':_' . $name]);
462
    }
463
 
464
    /**
465
     * Get a metadata variable.
466
     *
467
     * Do not call this method directly from  your code. It is for internal
468
     * use only. You should call {@see question_usage::get_question_attempt_metadata()}.
469
     *
470
     * @param string $name the name of the variable to set. [a-z][a-z0-9]*.
471
     * @return string the value to set previously, or null if this variable was never set.
472
     */
473
    public function get_metadata_var($name) {
474
        if (!$this->has_metadata_var($name)) {
475
            return null;
476
        }
477
        return $this->data[':_' . $name];
478
    }
479
 
480
    /**
481
     * Create a question_attempt_step from records loaded from the database.
482
     * @param Iterator $records Raw records loaded from the database.
483
     * @param int $stepid The id of the records to extract.
484
     * @param string $qtype The question type of which this is an attempt.
485
     *      If not given, each record must include a qtype field.
486
     * @return question_attempt_step The newly constructed question_attempt_step.
487
     */
488
    public static function load_from_records($records, $attemptstepid, $qtype = null) {
489
        $currentrec = $records->current();
490
        while ($currentrec->attemptstepid != $attemptstepid) {
491
            $records->next();
492
            if (!$records->valid()) {
493
                throw new coding_exception('Question attempt step ' . $attemptstepid .
494
                        ' not found in the database.');
495
            }
496
            $currentrec = $records->current();
497
        }
498
 
499
        $record = $currentrec;
500
        $contextid = null;
501
        $data = array();
502
        while ($currentrec && $currentrec->attemptstepid == $attemptstepid) {
503
            if (!is_null($currentrec->name)) {
504
                $data[$currentrec->name] = $currentrec->value;
505
            }
506
            $records->next();
507
            if ($records->valid()) {
508
                $currentrec = $records->current();
509
            } else {
510
                $currentrec = false;
511
            }
512
        }
513
 
514
        $step = new question_attempt_step_read_only($data, $record->timecreated, $record->userid);
515
        $step->state = question_state::get($record->state);
516
        $step->id = $record->attemptstepid;
517
        if (!is_null($record->fraction)) {
518
            $step->fraction = $record->fraction + 0;
519
        }
520
 
521
        // This next chunk of code requires getting $contextid and $qtype here.
522
        // Somehow, we need to get that information to this point by modifying
523
        // all the paths by which this method can be called.
524
        // Can we only return files when it's possible? Should there be some kind of warning?
525
        if (is_null($qtype)) {
526
            $qtype = $record->qtype;
527
        }
528
        foreach (question_bank::get_qtype($qtype)->response_file_areas() as $area) {
529
            if (empty($step->data[$area])) {
530
                continue;
531
            }
532
 
533
            $step->data[$area] = new question_file_loader($step, $area, $step->data[$area], $record->contextid);
534
        }
535
 
536
        return $step;
537
    }
538
}
539
 
540
 
541
/**
542
 * A subclass of {@see question_attempt_step} used when processing a new submission.
543
 *
544
 * When we are processing some new submitted data, which may or may not lead to
545
 * a new step being added to the {@see question_usage_by_activity} we create an
546
 * instance of this class. which is then passed to the question behaviour and question
547
 * type for processing. At the end of processing we then may, or may not, keep it.
548
 *
549
 * @copyright  2010 The Open University
550
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
551
 */
552
class question_attempt_pending_step extends question_attempt_step {
553
    /** @var string the new response summary, if there is one. */
554
    protected $newresponsesummary = null;
555
 
556
    /** @var int the new variant number, if there is one. */
557
    protected $newvariant = null;
558
 
559
    /**
560
     * If as a result of processing this step, the response summary for the
561
     * question attempt should changed, you should call this method to set the
562
     * new summary.
563
     * @param string $responsesummary the new response summary.
564
     */
565
    public function set_new_response_summary($responsesummary) {
566
        $this->newresponsesummary = $responsesummary;
567
    }
568
 
569
    /**
570
     * Get the new response summary, if there is one.
571
     * @return string the new response summary, or null if it has not changed.
572
     */
573
    public function get_new_response_summary() {
574
        return $this->newresponsesummary;
575
    }
576
 
577
    /**
578
     * Whether this processing this step has changed the response summary.
579
     * @return bool true if there is a new response summary.
580
     */
581
    public function response_summary_changed() {
582
        return !is_null($this->newresponsesummary);
583
    }
584
 
585
    /**
586
     * If as a result of processing this step, you identify that this variant of the
587
     * question is actually identical to the another one, you may change the
588
     * variant number recorded, in order to give better statistics. For an example
589
     * see qbehaviour_opaque.
590
     * @param int $variant the new variant number.
591
     */
592
    public function set_new_variant_number($variant) {
593
        $this->newvariant = $variant;
594
    }
595
 
596
    /**
597
     * Get the new variant number, if there is one.
598
     * @return int the new variant number, or null if it has not changed.
599
     */
600
    public function get_new_variant_number() {
601
        return $this->newvariant;
602
    }
603
 
604
    /**
605
     * Whether this processing this step has changed the variant number.
606
     * @return bool true if there is a new variant number.
607
     */
608
    public function variant_number_changed() {
609
        return !is_null($this->newvariant);
610
    }
611
}
612
 
613
 
614
/**
615
 * A subclass of {@see question_attempt_step} that cannot be modified.
616
 *
617
 * @copyright  2009 The Open University
618
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
619
 */
620
class question_attempt_step_read_only extends question_attempt_step {
621
    public function set_state($state) {
622
        throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
623
    }
624
    public function set_fraction($fraction) {
625
        throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
626
    }
627
    public function set_qt_var($name, $value) {
628
        throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
629
    }
630
    public function set_behaviour_var($name, $value) {
631
        throw new coding_exception('Cannot modify a question_attempt_step_read_only.');
632
    }
633
}
634
 
635
 
636
/**
637
 * A null {@see question_attempt_step} returned from
638
 * {@see question_attempt::get_last_step()} etc. when a an attempt has just been
639
 * created and there is no actual step.
640
 *
641
 * @copyright  2009 The Open University
642
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
643
 */
644
class question_null_step {
645
    public function get_state() {
646
        return question_state::$notstarted;
647
    }
648
 
649
    public function set_state($state) {
650
        throw new coding_exception('This question has not been started.');
651
    }
652
 
653
    public function get_fraction() {
654
        return null;
655
    }
656
}
657
 
658
 
659
/**
660
 * This is an adapter class that wraps a {@see question_attempt_step} and
661
 * modifies the get/set_*_data methods so that they operate only on the parts
662
 * that belong to a particular subquestion, as indicated by an extra prefix.
663
 *
664
 * @copyright  2010 The Open University
665
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
666
 */
667
class question_attempt_step_subquestion_adapter extends question_attempt_step {
668
    /** @var question_attempt_step the step we are wrapping. */
669
    protected $realstep;
670
    /** @var string the exta prefix on fields we work with. */
671
    protected $extraprefix;
672
 
673
    /**
674
     * Constructor.
675
     * @param question_attempt_step $realstep the step to wrap. (Can be null if you
676
     *      just want to call add/remove.prefix.)
677
     * @param string $extraprefix the extra prefix that is used for date fields.
678
     */
679
    public function __construct($realstep, $extraprefix) {
680
        $this->realstep = $realstep;
681
        $this->extraprefix = $extraprefix;
682
    }
683
 
684
    /**
685
     * Add the extra prefix to a field name.
686
     * @param string $field the plain field name.
687
     * @return string the field name with the extra bit of prefix added.
688
     */
689
    public function add_prefix($field) {
690
        if (substr($field, 0, 2) === '-_') {
691
            return '-_' . $this->extraprefix . substr($field, 2);
692
        } else if (substr($field, 0, 1) === '-') {
693
            return '-' . $this->extraprefix . substr($field, 1);
694
        } else if (substr($field, 0, 1) === '_') {
695
            return '_' . $this->extraprefix . substr($field, 1);
696
        } else {
697
            return $this->extraprefix . $field;
698
        }
699
    }
700
 
701
    /**
702
     * Remove the extra prefix from a field name if it is present.
703
     * @param string $field the extended field name.
704
     * @return string the field name with the extra bit of prefix removed, or
705
     * null if the extre prefix was not present.
706
     */
707
    public function remove_prefix($field) {
708
        if (preg_match('~^(-?_?)' . preg_quote($this->extraprefix, '~') . '(.*)$~', $field, $matches)) {
709
            return $matches[1] . $matches[2];
710
        } else {
711
            return null;
712
        }
713
    }
714
 
715
    /**
716
     * Filter some data to keep only those entries where the key contains
717
     * extraprefix, and remove the extra prefix from the reutrned arrary.
718
     * @param array $data some of the data stored in this step.
719
     * @return array the data with the keys ajusted using {@see remove_prefix()}.
720
     */
721
    public function filter_array($data) {
722
        $result = array();
723
        foreach ($data as $fullname => $value) {
724
            if ($name = $this->remove_prefix($fullname)) {
725
                $result[$name] = $value;
726
            }
727
        }
728
        return $result;
729
    }
730
 
731
    public function get_state() {
732
        return $this->realstep->get_state();
733
    }
734
 
735
    public function set_state($state) {
736
        throw new coding_exception('Cannot modify a question_attempt_step_subquestion_adapter.');
737
    }
738
 
739
    public function get_fraction() {
740
        return $this->realstep->get_fraction();
741
    }
742
 
743
    public function set_fraction($fraction) {
744
        throw new coding_exception('Cannot modify a question_attempt_step_subquestion_adapter.');
745
    }
746
 
747
    public function get_user_id() {
748
        return $this->realstep->get_user_id();
749
    }
750
 
751
    public function get_timecreated() {
752
        return $this->realstep->get_timecreated();
753
    }
754
 
755
    public function has_qt_var($name) {
756
        return $this->realstep->has_qt_var($this->add_prefix($name));
757
    }
758
 
759
    public function get_qt_var($name) {
760
        return $this->realstep->get_qt_var($this->add_prefix($name));
761
    }
762
 
763
    public function set_qt_var($name, $value) {
764
        $this->realstep->set_qt_var($this->add_prefix($name), $value);
765
    }
766
 
767
    public function get_qt_data() {
768
        return $this->filter_array($this->realstep->get_qt_data());
769
    }
770
 
771
    public function has_behaviour_var($name) {
772
        return $this->realstep->has_behaviour_var($this->add_prefix($name));
773
    }
774
 
775
    public function get_behaviour_var($name) {
776
        return $this->realstep->get_behaviour_var($this->add_prefix($name));
777
    }
778
 
779
    public function set_behaviour_var($name, $value) {
780
        return $this->realstep->set_behaviour_var($this->add_prefix($name), $value);
781
    }
782
 
783
    public function get_behaviour_data() {
784
        return $this->filter_array($this->realstep->get_behaviour_data());
785
    }
786
 
787
    public function get_submitted_data() {
788
        return $this->filter_array($this->realstep->get_submitted_data());
789
    }
790
 
791
    public function get_all_data() {
792
        return $this->filter_array($this->realstep->get_all_data());
793
    }
794
 
795
    public function get_qt_files($name, $contextid) {
796
        throw new coding_exception('No attempt has yet been made to implement files support in ' .
797
                'question_attempt_step_subquestion_adapter.');
798
    }
799
 
800
    public function prepare_response_files_draft_itemid($name, $contextid) {
801
        throw new coding_exception('No attempt has yet been made to implement files support in ' .
802
                'question_attempt_step_subquestion_adapter.');
803
    }
804
 
805
    public function prepare_response_files_draft_itemid_with_text($name, $contextid, $text) {
806
        throw new coding_exception('No attempt has yet been made to implement files support in ' .
807
                'question_attempt_step_subquestion_adapter.');
808
    }
809
 
810
    public function rewrite_response_pluginfile_urls($text, $contextid, $name, $extras) {
811
        throw new coding_exception('No attempt has yet been made to implement files support in ' .
812
                'question_attempt_step_subquestion_adapter.');
813
    }
814
}