Proyectos de Subversion Moodle

Rev

| 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 defines the core classes of the Moodle question engine.
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
require_once($CFG->libdir . '/filelib.php');
30
require_once(__DIR__ . '/questionusage.php');
31
require_once(__DIR__ . '/questionattempt.php');
32
require_once(__DIR__ . '/questionattemptstep.php');
33
require_once(__DIR__ . '/states.php');
34
require_once(__DIR__ . '/datalib.php');
35
require_once(__DIR__ . '/renderer.php');
36
require_once(__DIR__ . '/bank.php');
37
require_once(__DIR__ . '/../type/questiontypebase.php');
38
require_once(__DIR__ . '/../type/questionbase.php');
39
require_once(__DIR__ . '/../type/rendererbase.php');
40
require_once(__DIR__ . '/../behaviour/behaviourtypebase.php');
41
require_once(__DIR__ . '/../behaviour/behaviourbase.php');
42
require_once(__DIR__ . '/../behaviour/rendererbase.php');
43
require_once($CFG->libdir . '/questionlib.php');
44
 
45
 
46
/**
47
 * This static class provides access to the other question engine classes.
48
 *
49
 * It provides functions for managing question behaviours), and for
50
 * creating, loading, saving and deleting {@link question_usage_by_activity}s,
51
 * which is the main class that is used by other code that wants to use questions.
52
 *
53
 * @copyright  2009 The Open University
54
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
55
 */
56
abstract class question_engine {
57
    /** @var array behaviour name => 1. Records which behaviours have been loaded. */
58
    private static $loadedbehaviours = array();
59
 
60
    /** @var array behaviour name => question_behaviour_type for this behaviour. */
61
    private static $behaviourtypes = array();
62
 
63
    /**
64
     * Create a new {@link question_usage_by_activity}. The usage is
65
     * created in memory. If you want it to persist, you will need to call
66
     * {@link save_questions_usage_by_activity()}.
67
     *
68
     * @param string $component the plugin creating this attempt. For example mod_quiz.
69
     * @param context $context the context this usage belongs to.
70
     * @return question_usage_by_activity the newly created object.
71
     */
72
    public static function make_questions_usage_by_activity($component, $context) {
73
        return new question_usage_by_activity($component, $context);
74
    }
75
 
76
    /**
77
     * Load a {@link question_usage_by_activity} from the database, based on its id.
78
     * @param int $qubaid the id of the usage to load.
79
     * @param moodle_database $db a database connectoin. Defaults to global $DB.
80
     * @return question_usage_by_activity loaded from the database.
81
     */
82
    public static function load_questions_usage_by_activity($qubaid, moodle_database $db = null) {
83
        $dm = new question_engine_data_mapper($db);
84
        return $dm->load_questions_usage_by_activity($qubaid);
85
    }
86
 
87
    /**
88
     * Save a {@link question_usage_by_activity} to the database. This works either
89
     * if the usage was newly created by {@link make_questions_usage_by_activity()}
90
     * or loaded from the database using {@link load_questions_usage_by_activity()}
91
     * @param question_usage_by_activity the usage to save.
92
     * @param moodle_database $db a database connectoin. Defaults to global $DB.
93
     */
94
    public static function save_questions_usage_by_activity(question_usage_by_activity $quba, moodle_database $db = null) {
95
        $dm = new question_engine_data_mapper($db);
96
        $observer = $quba->get_observer();
97
        if ($observer instanceof question_engine_unit_of_work) {
98
            $observer->save($dm);
99
        } else {
100
            $dm->insert_questions_usage_by_activity($quba);
101
        }
102
    }
103
 
104
    /**
105
     * Delete a {@link question_usage_by_activity} from the database, based on its id.
106
     * @param int $qubaid the id of the usage to delete.
107
     */
108
    public static function delete_questions_usage_by_activity($qubaid) {
109
        self::delete_questions_usage_by_activities(new qubaid_list(array($qubaid)));
110
    }
111
 
112
    /**
113
     * Delete {@link question_usage_by_activity}s from the database.
114
     * @param qubaid_condition $qubaids identifies which questions usages to delete.
115
     */
116
    public static function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
117
        $dm = new question_engine_data_mapper();
118
        $dm->delete_questions_usage_by_activities($qubaids);
119
    }
120
 
121
    /**
122
     * Change the maxmark for the question_attempt with number in usage $slot
123
     * for all the specified question_attempts.
124
     * @param qubaid_condition $qubaids Selects which usages are updated.
125
     * @param int $slot the number is usage to affect.
126
     * @param number $newmaxmark the new max mark to set.
127
     */
128
    public static function set_max_mark_in_attempts(qubaid_condition $qubaids,
129
            $slot, $newmaxmark) {
130
        $dm = new question_engine_data_mapper();
131
        $dm->set_max_mark_in_attempts($qubaids, $slot, $newmaxmark);
132
    }
133
 
134
    /**
135
     * Validate that the manual grade submitted for a particular question is in range.
136
     * @param int $qubaid the question_usage id.
137
     * @param int $slot the slot number within the usage.
138
     * @return bool whether the submitted data is in range.
139
     */
140
    public static function is_manual_grade_in_range($qubaid, $slot) {
141
        $prefix = 'q' . $qubaid . ':' . $slot . '_';
142
        $mark = question_utils::optional_param_mark($prefix . '-mark');
143
        $maxmark = optional_param($prefix . '-maxmark', null, PARAM_FLOAT);
144
        $minfraction = optional_param($prefix . ':minfraction', null, PARAM_FLOAT);
145
        $maxfraction = optional_param($prefix . ':maxfraction', null, PARAM_FLOAT);
146
        return $mark === '' ||
147
                ($mark !== null && $mark >= $minfraction * $maxmark && $mark <= $maxfraction * $maxmark) ||
148
                ($mark === null && $maxmark === null);
149
    }
150
 
151
    /**
152
     * @param array $questionids of question ids.
153
     * @param qubaid_condition $qubaids ids of the usages to consider.
154
     * @return boolean whether any of these questions are being used by any of
155
     *      those usages.
156
     */
157
    public static function questions_in_use(array $questionids, qubaid_condition $qubaids = null) {
158
        if (is_null($qubaids)) {
159
            return false;
160
        }
161
        $dm = new question_engine_data_mapper();
162
        return $dm->questions_in_use($questionids, $qubaids);
163
    }
164
 
165
    /**
166
     * Get the number of times each variant has been used for each question in a list
167
     * in a set of usages.
168
     * @param array $questionids of question ids.
169
     * @param qubaid_condition $qubaids ids of the usages to consider.
170
     * @return array questionid => variant number => num uses.
171
     */
172
    public static function load_used_variants(array $questionids, qubaid_condition $qubaids) {
173
        $dm = new question_engine_data_mapper();
174
        return $dm->load_used_variants($questionids, $qubaids);
175
    }
176
 
177
    /**
178
     * Create an archetypal behaviour for a particular question attempt.
179
     * Used by {@link question_definition::make_behaviour()}.
180
     *
181
     * @param string $preferredbehaviour the type of model required.
182
     * @param question_attempt $qa the question attempt the model will process.
183
     * @return question_behaviour an instance of appropriate behaviour class.
184
     */
185
    public static function make_archetypal_behaviour($preferredbehaviour, question_attempt $qa) {
186
        if (!self::is_behaviour_archetypal($preferredbehaviour)) {
187
            throw new coding_exception('The requested behaviour is not actually ' .
188
                    'an archetypal one.');
189
        }
190
 
191
        self::load_behaviour_class($preferredbehaviour);
192
        $class = 'qbehaviour_' . $preferredbehaviour;
193
        return new $class($qa, $preferredbehaviour);
194
    }
195
 
196
    /**
197
     * @param string $behaviour the name of a behaviour.
198
     * @return array of {@link question_display_options} field names, that are
199
     * not relevant to this behaviour before a 'finish' action.
200
     */
201
    public static function get_behaviour_unused_display_options($behaviour) {
202
        return self::get_behaviour_type($behaviour)->get_unused_display_options();
203
    }
204
 
205
    /**
206
     * With this behaviour, is it possible that a question might finish as the student
207
     * interacts with it, without a call to the {@link question_attempt::finish()} method?
208
     * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
209
     * @return bool whether with this behaviour, questions may finish naturally.
210
     */
211
    public static function can_questions_finish_during_the_attempt($behaviour) {
212
        return self::get_behaviour_type($behaviour)->can_questions_finish_during_the_attempt();
213
    }
214
 
215
    /**
216
     * Create a behaviour for a particular type. If that type cannot be
217
     * found, return an instance of qbehaviour_missing.
218
     *
219
     * Normally you should use {@link make_archetypal_behaviour()}, or
220
     * call the constructor of a particular model class directly. This method
221
     * is only intended for use by {@link question_attempt::load_from_records()}.
222
     *
223
     * @param string $behaviour the type of model to create.
224
     * @param question_attempt $qa the question attempt the model will process.
225
     * @param string $preferredbehaviour the preferred behaviour for the containing usage.
226
     * @return question_behaviour an instance of appropriate behaviour class.
227
     */
228
    public static function make_behaviour($behaviour, question_attempt $qa, $preferredbehaviour) {
229
        try {
230
            self::load_behaviour_class($behaviour);
231
        } catch (Exception $e) {
232
            self::load_behaviour_class('missing');
233
            return new qbehaviour_missing($qa, $preferredbehaviour);
234
        }
235
        $class = 'qbehaviour_' . $behaviour;
236
        return new $class($qa, $preferredbehaviour);
237
    }
238
 
239
    /**
240
     * Load the behaviour class(es) belonging to a particular model. That is,
241
     * include_once('/question/behaviour/' . $behaviour . '/behaviour.php'), with a bit
242
     * of checking.
243
     * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
244
     */
245
    public static function load_behaviour_class($behaviour) {
246
        global $CFG;
247
        if (isset(self::$loadedbehaviours[$behaviour])) {
248
            return;
249
        }
250
        $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviour.php';
251
        if (!is_readable($file)) {
252
            throw new coding_exception('Unknown question behaviour ' . $behaviour);
253
        }
254
        include_once($file);
255
 
256
        $class = 'qbehaviour_' . $behaviour;
257
        if (!class_exists($class)) {
258
            throw new coding_exception('Question behaviour ' . $behaviour .
259
                    ' does not define the required class ' . $class . '.');
260
        }
261
 
262
        self::$loadedbehaviours[$behaviour] = 1;
263
    }
264
 
265
    /**
266
     * Create a behaviour for a particular type. If that type cannot be
267
     * found, return an instance of qbehaviour_missing.
268
     *
269
     * Normally you should use {@link make_archetypal_behaviour()}, or
270
     * call the constructor of a particular model class directly. This method
271
     * is only intended for use by {@link question_attempt::load_from_records()}.
272
     *
273
     * @param string $behaviour the type of model to create.
274
     * @param question_attempt $qa the question attempt the model will process.
275
     * @param string $preferredbehaviour the preferred behaviour for the containing usage.
276
     * @return question_behaviour_type an instance of appropriate behaviour class.
277
     */
278
    public static function get_behaviour_type($behaviour) {
279
 
280
        if (array_key_exists($behaviour, self::$behaviourtypes)) {
281
            return self::$behaviourtypes[$behaviour];
282
        }
283
 
284
        self::load_behaviour_type_class($behaviour);
285
 
286
        $class = 'qbehaviour_' . $behaviour . '_type';
287
        if (class_exists($class)) {
288
            self::$behaviourtypes[$behaviour] = new $class();
289
        } else {
290
            debugging('Question behaviour ' . $behaviour .
291
                    ' does not define the required class ' . $class . '.', DEBUG_DEVELOPER);
292
            self::$behaviourtypes[$behaviour] = new question_behaviour_type_fallback($behaviour);
293
        }
294
 
295
        return self::$behaviourtypes[$behaviour];
296
    }
297
 
298
    /**
299
     * Load the behaviour type class for a particular behaviour. That is,
300
     * include_once('/question/behaviour/' . $behaviour . '/behaviourtype.php').
301
     * @param string $behaviour the behaviour name. For example 'interactive' or 'deferredfeedback'.
302
     */
303
    protected static function load_behaviour_type_class($behaviour) {
304
        global $CFG;
305
        if (isset(self::$behaviourtypes[$behaviour])) {
306
            return;
307
        }
308
        $file = $CFG->dirroot . '/question/behaviour/' . $behaviour . '/behaviourtype.php';
309
        if (!is_readable($file)) {
310
            debugging('Question behaviour ' . $behaviour .
311
                    ' is missing the behaviourtype.php file.', DEBUG_DEVELOPER);
312
        }
313
        include_once($file);
314
    }
315
 
316
    /**
317
     * Return an array where the keys are the internal names of the archetypal
318
     * behaviours, and the values are a human-readable name. An
319
     * archetypal behaviour is one that is suitable to pass the name of to
320
     * {@link question_usage_by_activity::set_preferred_behaviour()}.
321
     *
322
     * @return array model name => lang string for this behaviour name.
323
     */
324
    public static function get_archetypal_behaviours() {
325
        $archetypes = array();
326
        $behaviours = core_component::get_plugin_list('qbehaviour');
327
        foreach ($behaviours as $behaviour => $notused) {
328
            if (self::is_behaviour_archetypal($behaviour)) {
329
                $archetypes[$behaviour] = self::get_behaviour_name($behaviour);
330
            }
331
        }
332
        asort($archetypes, SORT_LOCALE_STRING);
333
        return $archetypes;
334
    }
335
 
336
    /**
337
     * @param string $behaviour the name of a behaviour. E.g. 'deferredfeedback'.
338
     * @return bool whether this is an archetypal behaviour.
339
     */
340
    public static function is_behaviour_archetypal($behaviour) {
341
        return self::get_behaviour_type($behaviour)->is_archetypal();
342
    }
343
 
344
    /**
345
     * Return an array where the keys are the internal names of the behaviours
346
     * in preferred order and the values are a human-readable name.
347
     *
348
     * @param array $archetypes, array of behaviours
349
     * @param string $orderlist, a comma separated list of behaviour names
350
     * @param string $disabledlist, a comma separated list of behaviour names
351
     * @param string $current, current behaviour name
352
     * @return array model name => lang string for this behaviour name.
353
     */
354
    public static function sort_behaviours($archetypes, $orderlist, $disabledlist, $current=null) {
355
 
356
        // Get disabled behaviours
357
        if ($disabledlist) {
358
            $disabled = explode(',', $disabledlist);
359
        } else {
360
            $disabled = array();
361
        }
362
 
363
        if ($orderlist) {
364
            $order = explode(',', $orderlist);
365
        } else {
366
            $order = array();
367
        }
368
 
369
        foreach ($disabled as $behaviour) {
370
            if (array_key_exists($behaviour, $archetypes) && $behaviour != $current) {
371
                unset($archetypes[$behaviour]);
372
            }
373
        }
374
 
375
        // Get behaviours in preferred order
376
        $behaviourorder = array();
377
        foreach ($order as $behaviour) {
378
            if (array_key_exists($behaviour, $archetypes)) {
379
                $behaviourorder[$behaviour] = $archetypes[$behaviour];
380
            }
381
        }
382
        // Get the rest of behaviours and sort them alphabetically
383
        $leftover = array_diff_key($archetypes, $behaviourorder);
384
        asort($leftover, SORT_LOCALE_STRING);
385
 
386
        // Set up the final order to be displayed
387
        return $behaviourorder + $leftover;
388
    }
389
 
390
    /**
391
     * Return an array where the keys are the internal names of the behaviours
392
     * in preferred order and the values are a human-readable name.
393
     *
394
     * @param string $currentbehaviour
395
     * @return array model name => lang string for this behaviour name.
396
     */
397
    public static function get_behaviour_options($currentbehaviour) {
398
        $config = question_bank::get_config();
399
        $archetypes = self::get_archetypal_behaviours();
400
 
401
        // If no admin setting return all behavious
402
        if (empty($config->disabledbehaviours) && empty($config->behavioursortorder)) {
403
            return $archetypes;
404
        }
405
 
406
        if (empty($config->behavioursortorder)) {
407
            $order = '';
408
        } else {
409
            $order = $config->behavioursortorder;
410
        }
411
        if (empty($config->disabledbehaviours)) {
412
            $disabled = '';
413
        } else {
414
            $disabled = $config->disabledbehaviours;
415
        }
416
 
417
        return self::sort_behaviours($archetypes, $order, $disabled, $currentbehaviour);
418
    }
419
 
420
    /**
421
     * Get the translated name of a behaviour, for display in the UI.
422
     * @param string $behaviour the internal name of the model.
423
     * @return string name from the current language pack.
424
     */
425
    public static function get_behaviour_name($behaviour) {
426
        return get_string('pluginname', 'qbehaviour_' . $behaviour);
427
    }
428
 
429
    /**
430
     * @return array all the file area names that may contain response files.
431
     */
432
    public static function get_all_response_file_areas() {
433
        $variables = array();
434
        foreach (question_bank::get_all_qtypes() as $qtype) {
435
            $variables = array_merge($variables, $qtype->response_file_areas());
436
        }
437
 
438
        $areas = array();
439
        foreach (array_unique($variables) as $variable) {
440
            $areas[] = 'response_' . $variable;
441
        }
442
        return $areas;
443
    }
444
 
445
    /**
446
     * Returns the valid choices for the number of decimal places for showing
447
     * question marks. For use in the user interface.
448
     * @return array suitable for passing to {@link html_writer::select()} or similar.
449
     */
450
    public static function get_dp_options() {
451
        return question_display_options::get_dp_options();
452
    }
453
 
454
    /**
455
     * Initialise the JavaScript required on pages where questions will be displayed.
456
     *
457
     * @return string
458
     */
459
    public static function initialise_js() {
460
        return question_flags::initialise_js();
461
    }
462
}
463
 
464
 
465
/**
466
 * This class contains all the options that controls how a question is displayed.
467
 *
468
 * Normally, what will happen is that the calling code will set up some display
469
 * options to indicate what sort of question display it wants, and then before the
470
 * question is rendered, the behaviour will be given a chance to modify the
471
 * display options, so that, for example, A question that is finished will only
472
 * be shown read-only, and a question that has not been submitted will not have
473
 * any sort of feedback displayed.
474
 *
475
 * @copyright  2009 The Open University
476
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
477
 */
478
class question_display_options {
479
    /**#@+
480
     * @var int named constants for the values that most of the options take.
481
     */
482
    const SHOW_ALL = -1;
483
    const HIDDEN = 0;
484
    const VISIBLE = 1;
485
    const EDITABLE = 2;
486
    /**#@-*/
487
 
488
    /**#@+ @var int named constants for the {@see $marks} option. */
489
    const MAX_ONLY = 1;
490
    const MARK_AND_MAX = 2;
491
    /**#@-*/
492
 
493
    /**
494
     * @var int maximum value for the {@see $markpd} option. This is
495
     * effectively set by the database structure, which uses NUMBER(12,7) columns
496
     * for question marks/fractions.
497
     */
498
    const MAX_DP = 7;
499
 
500
    /**
501
     * @var boolean whether the question should be displayed as a read-only review,
502
     * or in an active state where you can change the answer.
503
     */
504
    public $readonly = false;
505
 
506
    /**
507
     * @var boolean whether the question type should output hidden form fields
508
     * to reset any incorrect parts of the resonse to blank.
509
     */
510
    public $clearwrong = false;
511
 
512
    /**
513
     * Should the student have what they got right and wrong clearly indicated.
514
     * This includes the green/red hilighting of the bits of their response,
515
     * whether the one-line summary of the current state of the question says
516
     * correct/incorrect or just answered.
517
     * @var int {@see question_display_options::HIDDEN} or
518
     * {@see question_display_options::VISIBLE}
519
     */
520
    public $correctness = self::VISIBLE;
521
 
522
    /**
523
     * The the mark and/or the maximum available mark for this question be visible?
524
     * @var int {@see question_display_options::HIDDEN},
525
     * {@see question_display_options::MAX_ONLY} or {@see question_display_options::MARK_AND_MAX}
526
     */
527
    public $marks = self::MARK_AND_MAX;
528
 
529
    /** @var int of decimal places to use when formatting marks for output. */
530
    public $markdp = 2;
531
 
532
    /**
533
     * Should the flag this question UI element be visible, and if so, should the
534
     * flag state be changeable?
535
     *
536
     * @var int {@see question_display_options::HIDDEN},
537
     * {@see question_display_options::VISIBLE} or {@see question_display_options::EDITABLE}
538
     */
539
    public $flags = self::VISIBLE;
540
 
541
    /**
542
     * Should the specific feedback be visible.
543
     *
544
     * Specific feedback is typically the part of the feedback that changes based on the
545
     * answer that the student gave. For example the feedback shown if a particular choice
546
     * has been chosen in a multi-choice question. It also includes the combined feedback
547
     * that a lost of question types have (e.g. feedback for any correct/incorrect response.)
548
     *
549
     * @var int {@see question_display_options::HIDDEN} or
550
     * {@see question_display_options::VISIBLE}
551
     */
552
    public $feedback = self::VISIBLE;
553
 
554
    /**
555
     * For questions with a number of sub-parts (like matching, or
556
     * multiple-choice, multiple-reponse) display the number of sub-parts that
557
     * were correct.
558
     * @var int {@see question_display_options::HIDDEN} or
559
     * {@see question_display_options::VISIBLE}
560
     */
561
    public $numpartscorrect = self::VISIBLE;
562
 
563
    /**
564
     * Should the general feedback be visible?
565
     *
566
     * This is typically feedback shown to all students after the question
567
     * is finished, irrespective of which answer they gave.
568
     *
569
     * @var int {@see question_display_options::HIDDEN} or
570
     * {@see question_display_options::VISIBLE}
571
     */
572
    public $generalfeedback = self::VISIBLE;
573
 
574
    /**
575
     * Should the automatically generated display of what the correct answer be visible?
576
     *
577
     * @var int {@see question_display_options::HIDDEN} or
578
     * {@see question_display_options::VISIBLE}
579
     */
580
    public $rightanswer = self::VISIBLE;
581
 
582
    /**
583
     * Should the manually added marker's comment be visible. Should the link for
584
     * adding/editing the comment be there.
585
     * @var int {@see question_display_options::HIDDEN},
586
     * {@see question_display_options::VISIBLE}, or {@see question_display_options::EDITABLE}.
587
     * Editable means that form fields are displayed inline.
588
     */
589
    public $manualcomment = self::VISIBLE;
590
 
591
    /**
592
     * Should we show a 'Make comment or override grade' link?
593
     * @var string base URL for the edit comment script, which will be shown if
594
     * $manualcomment = self::VISIBLE.
595
     */
596
    public $manualcommentlink = null;
597
 
598
    /**
599
     * Used in places like the question history table, to show a link to review
600
     * this question in a certain state. If blank, a link is not shown.
601
     * @var moodle_url base URL for a review question script.
602
     */
603
    public $questionreviewlink = null;
604
 
605
    /**
606
     * Should the history of previous question states table be visible?
607
     * @var int {@see question_display_options::HIDDEN} or
608
     * {@see question_display_options::VISIBLE}
609
     */
610
    public $history = self::HIDDEN;
611
 
612
    /**
613
     * @since 2.9
614
     * @var string extra HTML to include at the end of the outcome (feedback) box
615
     * of the question display.
616
     *
617
     * This field is now badly named. The place it included is was changed
618
     * (for the better) but the name was left unchanged for backwards compatibility.
619
     */
620
    public $extrainfocontent = '';
621
 
622
    /**
623
     * @since 2.9
624
     * @var string extra HTML to include in the history box of the question display,
625
     * if it is shown.
626
     */
627
    public $extrahistorycontent = '';
628
 
629
    /**
630
     * If not empty, then a link to edit the question will be included in
631
     * the info box for the question.
632
     *
633
     * If used, this array must contain an element courseid or cmid.
634
     *
635
     * It shoudl also contain a parameter returnurl => moodle_url giving a
636
     * sensible URL to go back to when the editing form is submitted or cancelled.
637
     *
638
     * @var array url parameter for the edit link. id => questiosnid will be
639
     * added automatically.
640
     */
641
    public $editquestionparams = array();
642
 
643
    /**
644
     * @var context the context the attempt being output belongs to.
645
     */
646
    public $context;
647
 
648
    /**
649
     * @var int The option to show the action author in the response history.
650
     */
651
    public $userinfoinhistory = self::HIDDEN;
652
 
653
    /**
654
     * This identifier should be added to the labels of all input fields in the question.
655
     *
656
     * This is so people using assistive technology can easily tell which input belong to
657
     * which question. The helper {@see self::add_question_identifier_to_label() makes this easier.
658
     *
659
     * If not set before the question is rendered, then it defaults to 'Question N'.
660
     * (lang string)
661
     *
662
     * @var string The identifier that the question being rendered is associated with.
663
     *              E.g. The question number when it is rendered on a quiz.
664
     */
665
    public $questionidentifier = null;
666
 
667
    /**
668
     * @var ?bool $versioninfo Should we display the version in the question info?
669
     */
670
    public ?bool $versioninfo = null;
671
 
672
    /**
673
     * Set all the feedback-related fields, feedback, numpartscorrect, generalfeedback,
674
     * rightanswer, manualcomment} and correctness to {@see question_display_options::HIDDEN}.
675
     */
676
    public function hide_all_feedback() {
677
        $this->feedback = self::HIDDEN;
678
        $this->numpartscorrect = self::HIDDEN;
679
        $this->generalfeedback = self::HIDDEN;
680
        $this->rightanswer = self::HIDDEN;
681
        $this->manualcomment = self::HIDDEN;
682
        $this->correctness = self::HIDDEN;
683
    }
684
 
685
    /**
686
     * Returns the valid choices for the number of decimal places for showing
687
     * question marks. For use in the user interface.
688
     *
689
     * Calling code should probably use {@see question_engine::get_dp_options()}
690
     * rather than calling this method directly.
691
     *
692
     * @return array suitable for passing to {@see html_writer::select()} or similar.
693
     */
694
    public static function get_dp_options() {
695
        $options = array();
696
        for ($i = 0; $i <= self::MAX_DP; $i += 1) {
697
            $options[$i] = $i;
698
        }
699
        return $options;
700
    }
701
 
702
    /**
703
     * Helper to add the question identify (if there is one) to the label of an input field in a question.
704
     *
705
     * @param string $label The plain field label. E.g. 'Answer 1'
706
     * @param bool $sridentifier If true, the question identifier, if added, will be wrapped in a sr-only span. Default false.
707
     * @param bool $addbefore If true, the question identifier will be added before the label.
708
     * @return string The amended label. For example 'Answer 1, Question 1'.
709
     */
710
    public function add_question_identifier_to_label(string $label, bool $sridentifier = false, bool $addbefore = false): string {
711
        if (!$this->has_question_identifier()) {
712
            return $label;
713
        }
714
        $identifier = $this->questionidentifier;
715
        if ($sridentifier) {
716
            $identifier = html_writer::span($identifier, 'sr-only');
717
        }
718
        $fieldlang = 'fieldinquestion';
719
        if ($addbefore) {
720
            $fieldlang = 'fieldinquestionpre';
721
        }
722
        return get_string($fieldlang, 'question', (object)['fieldname' => $label, 'questionindentifier' => $identifier]);
723
    }
724
 
725
    /**
726
     * Whether a question number has been provided for the question that is being displayed.
727
     *
728
     * @return bool
729
     */
730
    public function has_question_identifier(): bool {
731
        return $this->questionidentifier !== null && trim($this->questionidentifier) !== '';
732
    }
733
}
734
 
735
 
736
/**
737
 * Contains the logic for handling question flags.
738
 *
739
 * @copyright  2010 The Open University
740
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
741
 */
742
abstract class question_flags {
743
    /**
744
     * Get the checksum that validates that a toggle request is valid.
745
     * @param int $qubaid the question usage id.
746
     * @param int $questionid the question id.
747
     * @param int $sessionid the question_attempt id.
748
     * @param object $user the user. If null, defaults to $USER.
749
     * @return string that needs to be sent to question/toggleflag.php for it to work.
750
     */
751
    protected static function get_toggle_checksum($qubaid, $questionid,
752
            $qaid, $slot, $user = null) {
753
        if (is_null($user)) {
754
            global $USER;
755
            $user = $USER;
756
        }
757
        return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
758
    }
759
 
760
    /**
761
     * Get the postdata that needs to be sent to question/toggleflag.php to change the flag state.
762
     * You need to append &newstate=0/1 to this.
763
     * @return the post data to send.
764
     */
765
    public static function get_postdata(question_attempt $qa) {
766
        $qaid = $qa->get_database_id();
767
        $qubaid = $qa->get_usage_id();
768
        $qid = $qa->get_question_id();
769
        $slot = $qa->get_slot();
770
        $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
771
        return "qaid={$qaid}&qubaid={$qubaid}&qid={$qid}&slot={$slot}&checksum={$checksum}&sesskey=" .
772
                sesskey() . '&newstate=';
773
    }
774
 
775
    /**
776
     * If the request seems valid, update the flag state of a question attempt.
777
     * Throws exceptions if this is not a valid update request.
778
     * @param int $qubaid the question usage id.
779
     * @param int $questionid the question id.
780
     * @param int $sessionid the question_attempt id.
781
     * @param string $checksum checksum, as computed by {@link get_toggle_checksum()}
782
     *      corresponding to the last three arguments.
783
     * @param bool $newstate the new state of the flag. true = flagged.
784
     */
785
    public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
786
        // Check the checksum - it is very hard to know who a question session belongs
787
        // to, so we require that checksum parameter is matches an md5 hash of the
788
        // three ids and the users username. Since we are only updating a flag, that
789
        // probably makes it sufficiently difficult for malicious users to toggle
790
        // other users flags.
791
        if ($checksum != self::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
792
            throw new moodle_exception('errorsavingflags', 'question');
793
        }
794
 
795
        $dm = new question_engine_data_mapper();
796
        $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
797
    }
798
 
799
    public static function initialise_js() {
800
        global $CFG, $PAGE, $OUTPUT;
801
        static $done = false;
802
        if ($done) {
803
            return;
804
        }
805
        $module = array(
806
            'name' => 'core_question_flags',
807
            'fullpath' => '/question/flags.js',
808
            'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
809
        );
810
        $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
811
        $flagattributes = array(
812
 
813
                'src' => $OUTPUT->image_url('i/unflagged') . '',
814
                'title' => get_string('clicktoflag', 'question'),
815
                'alt' => get_string('flagged', 'question'), // Label on toggle should not change.
816
                'text' => get_string('clickflag', 'question'),
817
            ),
818
            1 => array(
819
                'src' => $OUTPUT->image_url('i/flagged') . '',
820
                'title' => get_string('clicktounflag', 'question'),
821
                'alt' => get_string('flagged', 'question'),
822
                'text' => get_string('clickunflag', 'question'),
823
            ),
824
        );
825
        $PAGE->requires->js_init_call('M.core_question_flags.init',
826
                array($actionurl, $flagattributes), false, $module);
827
        $done = true;
828
    }
829
}
830
 
831
 
832
/**
833
 * Exception thrown when the system detects that a student has done something
834
 * out-of-order to a question. This can happen, for example, if they click
835
 * the browser's back button in a quiz, then try to submit a different response.
836
 *
837
 * @copyright  2010 The Open University
838
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
839
 */
840
class question_out_of_sequence_exception extends moodle_exception {
841
    public function __construct($qubaid, $slot, $postdata) {
842
        if ($postdata == null) {
843
            $postdata = data_submitted();
844
        }
845
        parent::__construct('submissionoutofsequence', 'question', '', null,
846
                "QUBAid: {$qubaid}, slot: {$slot}, post data: " . print_r($postdata, true));
847
    }
848
}
849
 
850
 
851
/**
852
 * Useful functions for writing question types and behaviours.
853
 *
854
 * @copyright 2010 The Open University
855
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
856
 */
857
abstract class question_utils {
858
    /**
859
     * @var float tolerance to use when comparing question mark/fraction values.
860
     *
861
     * When comparing floating point numbers in a computer, the representation is not
862
     * necessarily exact. Therefore, we need to allow a tolerance.
863
     * Question marks are stored in the database as decimal numbers with 7 decimal places.
864
     * Therefore, this is the appropriate tolerance to use.
865
     */
866
    const MARK_TOLERANCE = 0.00000005;
867
 
868
    /**
869
     * Tests to see whether two arrays have the same keys, with the same values
870
     * (as compared by ===) for each key. However, the order of the arrays does
871
     * not have to be the same.
872
     * @param array $array1 the first array.
873
     * @param array $array2 the second array.
874
     * @return bool whether the two arrays have the same keys with the same
875
     *      corresponding values.
876
     */
877
    public static function arrays_have_same_keys_and_values(array $array1, array $array2) {
878
        if (count($array1) != count($array2)) {
879
            return false;
880
        }
881
        foreach ($array1 as $key => $value1) {
882
            if (!array_key_exists($key, $array2)) {
883
                return false;
884
            }
885
            if (((string) $value1) !== ((string) $array2[$key])) {
886
                return false;
887
            }
888
        }
889
        return true;
890
    }
891
 
892
    /**
893
     * Tests to see whether two arrays have the same value at a particular key.
894
     * This method will return true if:
895
     * 1. Neither array contains the key; or
896
     * 2. Both arrays contain the key, and the corresponding values compare
897
     *      identical when cast to strings and compared with ===.
898
     * @param array $array1 the first array.
899
     * @param array $array2 the second array.
900
     * @param string $key an array key.
901
     * @return bool whether the two arrays have the same value (or lack of
902
     *      one) for a given key.
903
     */
904
    public static function arrays_same_at_key(array $array1, array $array2, $key) {
905
        if (array_key_exists($key, $array1) && array_key_exists($key, $array2)) {
906
            return ((string) $array1[$key]) === ((string) $array2[$key]);
907
        }
908
        if (!array_key_exists($key, $array1) && !array_key_exists($key, $array2)) {
909
            return true;
910
        }
911
        return false;
912
    }
913
 
914
    /**
915
     * Tests to see whether two arrays have the same value at a particular key.
916
     * Missing values are replaced by '', and then the values are cast to
917
     * strings and compared with ===.
918
     * @param array $array1 the first array.
919
     * @param array $array2 the second array.
920
     * @param string $key an array key.
921
     * @return bool whether the two arrays have the same value (or lack of
922
     *      one) for a given key.
923
     */
924
    public static function arrays_same_at_key_missing_is_blank(
925
            array $array1, array $array2, $key) {
926
        if (array_key_exists($key, $array1)) {
927
            $value1 = $array1[$key];
928
        } else {
929
            $value1 = '';
930
        }
931
        if (array_key_exists($key, $array2)) {
932
            $value2 = $array2[$key];
933
        } else {
934
            $value2 = '';
935
        }
936
        return ((string) $value1) === ((string) $value2);
937
    }
938
 
939
    /**
940
     * Tests to see whether two arrays have the same value at a particular key.
941
     * Missing values are replaced by 0, and then the values are cast to
942
     * integers and compared with ===.
943
     * @param array $array1 the first array.
944
     * @param array $array2 the second array.
945
     * @param string $key an array key.
946
     * @return bool whether the two arrays have the same value (or lack of
947
     *      one) for a given key.
948
     */
949
    public static function arrays_same_at_key_integer(
950
            array $array1, array $array2, $key) {
951
        if (array_key_exists($key, $array1)) {
952
            $value1 = (int) $array1[$key];
953
        } else {
954
            $value1 = 0;
955
        }
956
        if (array_key_exists($key, $array2)) {
957
            $value2 = (int) $array2[$key];
958
        } else {
959
            $value2 = 0;
960
        }
961
        return $value1 === $value2;
962
    }
963
 
964
    private static $units     = array('', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix');
965
    private static $tens      = array('', 'x', 'xx', 'xxx', 'xl', 'l', 'lx', 'lxx', 'lxxx', 'xc');
966
    private static $hundreds  = array('', 'c', 'cc', 'ccc', 'cd', 'd', 'dc', 'dcc', 'dccc', 'cm');
967
    private static $thousands = array('', 'm', 'mm', 'mmm');
968
 
969
    /**
970
     * Convert an integer to roman numerals.
971
     * @param int $number an integer between 1 and 3999 inclusive. Anything else
972
     *      will throw an exception.
973
     * @return string the number converted to lower case roman numerals.
974
     */
975
    public static function int_to_roman($number) {
976
        if (!is_integer($number) || $number < 1 || $number > 3999) {
977
            throw new coding_exception('Only integers between 0 and 3999 can be ' .
978
                    'converted to roman numerals.', $number);
979
        }
980
 
981
        return self::$thousands[floor($number / 1000) % 10] . self::$hundreds[floor($number / 100) % 10] .
982
                self::$tens[floor($number / 10) % 10] . self::$units[$number % 10];
983
    }
984
 
985
    /**
986
     * Convert an integer to a letter of alphabet.
987
     * @param int $number an integer between 1 and 26 inclusive.
988
     * Anything else will throw an exception.
989
     * @return string the number converted to upper case letter of alphabet.
990
     */
991
    public static function int_to_letter($number) {
992
        $alphabet = [
993
                '1' => 'A',
994
                '2' => 'B',
995
                '3' => 'C',
996
                '4' => 'D',
997
                '5' => 'E',
998
                '6' => 'F',
999
                '7' => 'G',
1000
                '8' => 'H',
1001
                '9' => 'I',
1002
                '10' => 'J',
1003
                '11' => 'K',
1004
                '12' => 'L',
1005
                '13' => 'M',
1006
                '14' => 'N',
1007
                '15' => 'O',
1008
                '16' => 'P',
1009
                '17' => 'Q',
1010
                '18' => 'R',
1011
                '19' => 'S',
1012
                '20' => 'T',
1013
                '21' => 'U',
1014
                '22' => 'V',
1015
                '23' => 'W',
1016
                '24' => 'X',
1017
                '25' => 'Y',
1018
                '26' => 'Z'
1019
        ];
1020
        if (!is_integer($number) || $number < 1 || $number > count($alphabet)) {
1021
            throw new coding_exception('Only integers between 1 and 26 can be converted to letters.', $number);
1022
        }
1023
        return $alphabet[$number];
1024
    }
1025
 
1026
    /**
1027
     * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).
1028
     * This method copes with:
1029
     *  - keeping null or '' input unchanged - important to let teaches set a question back to requries grading.
1030
     *  - numbers that were typed as either 1.00 or 1,00 form.
1031
     *  - invalid things, which get turned into null.
1032
     *
1033
     * @param string|null $mark raw use input of a mark.
1034
     * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
1035
     */
1036
    public static function clean_param_mark($mark) {
1037
        if ($mark === '' || is_null($mark)) {
1038
            return $mark;
1039
        }
1040
 
1041
        $mark = str_replace(',', '.', $mark);
1042
        // This regexp should match the one in validate_param.
1043
        if (!preg_match('/^[\+-]?[0-9]*\.?[0-9]*(e[-+]?[0-9]+)?$/i', $mark)) {
1044
            return null;
1045
        }
1046
 
1047
        return clean_param($mark, PARAM_FLOAT);
1048
    }
1049
 
1050
    /**
1051
     * Get a sumitted variable (from the GET or POST data) that is a mark.
1052
     * @param string $parname the submitted variable name.
1053
     * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
1054
     */
1055
    public static function optional_param_mark($parname) {
1056
        return self::clean_param_mark(
1057
                optional_param($parname, null, PARAM_RAW_TRIMMED));
1058
    }
1059
 
1060
    /**
1061
     * Convert part of some question content to plain text.
1062
     * @param string $text the text.
1063
     * @param int $format the text format.
1064
     * @param array $options formatting options. Passed to {@link format_text}.
1065
     * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
1066
     */
1067
    public static function to_plain_text($text, $format, $options = array('noclean' => 'true')) {
1068
        // The following call to html_to_text uses the option that strips out
1069
        // all URLs, but format_text complains if it finds @@PLUGINFILE@@ tokens.
1070
        // So, we need to replace @@PLUGINFILE@@ with a real URL, but it doesn't
1071
        // matter what. We use http://example.com/.
1072
        $text = str_replace('@@PLUGINFILE@@/', 'http://example.com/', $text);
1073
        return html_to_text(format_text($text, $format, $options), 0, false);
1074
    }
1075
 
1076
    /**
1077
     * Get the options required to configure the filepicker for one of the editor
1078
     * toolbar buttons.
1079
     *
1080
     * @param mixed $acceptedtypes array of types of '*'.
1081
     * @param int $draftitemid the draft area item id.
1082
     * @param context $context the context.
1083
     * @return object the required options.
1084
     */
1085
    protected static function specific_filepicker_options($acceptedtypes, $draftitemid, $context) {
1086
        $filepickeroptions = new stdClass();
1087
        $filepickeroptions->accepted_types = $acceptedtypes;
1088
        $filepickeroptions->return_types = FILE_INTERNAL | FILE_EXTERNAL;
1089
        $filepickeroptions->context = $context;
1090
        $filepickeroptions->env = 'filepicker';
1091
 
1092
        $options = initialise_filepicker($filepickeroptions);
1093
        $options->context = $context;
1094
        $options->client_id = uniqid();
1095
        $options->env = 'editor';
1096
        $options->itemid = $draftitemid;
1097
 
1098
        return $options;
1099
    }
1100
 
1101
    /**
1102
     * Get filepicker options for question related text areas.
1103
     *
1104
     * @param context $context the context.
1105
     * @param int $draftitemid the draft area item id.
1106
     * @return array An array of options
1107
     */
1108
    public static function get_filepicker_options($context, $draftitemid) {
1109
        return [
1110
                'image' => self::specific_filepicker_options(['image'], $draftitemid, $context),
1111
                'media' => self::specific_filepicker_options(['video', 'audio'], $draftitemid, $context),
1112
                'link'  => self::specific_filepicker_options('*', $draftitemid, $context),
1113
            ];
1114
    }
1115
 
1116
    /**
1117
     * Get editor options for question related text areas.
1118
     *
1119
     * @param context $context the context.
1120
     * @return array An array of options
1121
     */
1122
    public static function get_editor_options($context) {
1123
        global $CFG;
1124
 
1125
        $editoroptions = [
1126
                'subdirs'  => 0,
1127
                'context'  => $context,
1128
                'maxfiles' => EDITOR_UNLIMITED_FILES,
1129
                'maxbytes' => $CFG->maxbytes,
1130
                'noclean' => 0,
1131
                'trusttext' => 0,
1132
                'autosave' => false
1133
        ];
1134
 
1135
        return $editoroptions;
1136
    }
1137
}
1138
 
1139
 
1140
/**
1141
 * The interface for strategies for controlling which variant of each question is used.
1142
 *
1143
 * @copyright  2011 The Open University
1144
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1145
 */
1146
interface question_variant_selection_strategy {
1147
    /**
1148
     * @param int $maxvariants the num
1149
     * @param string $seed data that can be used to controls how the variant is selected
1150
     *      in a semi-random way.
1151
     * @return int the variant to use, a number betweeb 1 and $maxvariants inclusive.
1152
     */
1153
    public function choose_variant($maxvariants, $seed);
1154
}
1155
 
1156
 
1157
/**
1158
 * A {@link question_variant_selection_strategy} that is completely random.
1159
 *
1160
 * @copyright  2011 The Open University
1161
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1162
 */
1163
class question_variant_random_strategy implements question_variant_selection_strategy {
1164
    public function choose_variant($maxvariants, $seed) {
1165
        return rand(1, $maxvariants);
1166
    }
1167
}
1168
 
1169
 
1170
/**
1171
 * A {@link question_variant_selection_strategy} that is effectively random
1172
 * for the first attempt, and then after that cycles through the available
1173
 * variants so that the students will not get a repeated variant until they have
1174
 * seen them all.
1175
 *
1176
 * @copyright  2011 The Open University
1177
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1178
 */
1179
class question_variant_pseudorandom_no_repeats_strategy
1180
        implements question_variant_selection_strategy {
1181
 
1182
    /** @var int the number of attempts this users has had, including the curent one. */
1183
    protected $attemptno;
1184
 
1185
    /** @var int the user id the attempt belongs to. */
1186
    protected $userid;
1187
 
1188
    /** @var string extra input fed into the pseudo-random code. */
1189
    protected $extrarandomness = '';
1190
 
1191
    /**
1192
     * Constructor.
1193
     * @param int $attemptno The attempt number.
1194
     * @param int $userid the user the attempt is for (defaults to $USER->id).
1195
     */
1196
    public function __construct($attemptno, $userid = null, $extrarandomness = '') {
1197
        $this->attemptno = $attemptno;
1198
        if (is_null($userid)) {
1199
            global $USER;
1200
            $this->userid = $USER->id;
1201
        } else {
1202
            $this->userid = $userid;
1203
        }
1204
 
1205
        if ($extrarandomness) {
1206
            $this->extrarandomness = '|' . $extrarandomness;
1207
        }
1208
    }
1209
 
1210
    public function choose_variant($maxvariants, $seed) {
1211
        if ($maxvariants == 1) {
1212
            return 1;
1213
        }
1214
 
1215
        $hash = sha1($seed . '|user' . $this->userid . $this->extrarandomness);
1216
        $randint = hexdec(substr($hash, 17, 7));
1217
 
1218
        return ($randint + $this->attemptno) % $maxvariants + 1;
1219
    }
1220
}
1221
 
1222
/**
1223
 * A {@link question_variant_selection_strategy} designed ONLY for testing.
1224
 * For selected questions it wil return a specific variants. For the other
1225
 * slots it will use a fallback strategy.
1226
 *
1227
 * @copyright  2013 The Open University
1228
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1229
 */
1230
class question_variant_forced_choices_selection_strategy
1231
    implements question_variant_selection_strategy {
1232
 
1233
    /** @var array seed => variant to select. */
1234
    protected $forcedchoices;
1235
 
1236
    /** @var question_variant_selection_strategy strategy used to make the non-forced choices. */
1237
    protected $basestrategy;
1238
 
1239
    /**
1240
     * Constructor.
1241
     * @param array $forcedchoices array seed => variant to select.
1242
     * @param question_variant_selection_strategy $basestrategy strategy used
1243
     *      to make the non-forced choices.
1244
     */
1245
    public function __construct(array $forcedchoices, question_variant_selection_strategy $basestrategy) {
1246
        $this->forcedchoices = $forcedchoices;
1247
        $this->basestrategy  = $basestrategy;
1248
    }
1249
 
1250
    public function choose_variant($maxvariants, $seed) {
1251
        if (array_key_exists($seed, $this->forcedchoices)) {
1252
            if ($this->forcedchoices[$seed] > $maxvariants) {
1253
                throw new coding_exception('Forced variant out of range.');
1254
            }
1255
            return $this->forcedchoices[$seed];
1256
        } else {
1257
            return $this->basestrategy->choose_variant($maxvariants, $seed);
1258
        }
1259
    }
1260
 
1261
    /**
1262
     * Helper method for preparing the $forcedchoices array.
1263
     * @param array                      $variantsbyslot slot number => variant to select.
1264
     * @param question_usage_by_activity $quba           the question usage we need a strategy for.
1265
     * @throws coding_exception when variant cannot be forced as doesn't work.
1266
     * @return array that can be passed to the constructor as $forcedchoices.
1267
     */
1268
    public static function prepare_forced_choices_array(array $variantsbyslot,
1269
                                                        question_usage_by_activity $quba) {
1270
 
1271
        $forcedchoices = array();
1272
 
1273
        foreach ($variantsbyslot as $slot => $varianttochoose) {
1274
            $question = $quba->get_question($slot);
1275
            $seed = $question->get_variants_selection_seed();
1276
            if (array_key_exists($seed, $forcedchoices) && $forcedchoices[$seed] != $varianttochoose) {
1277
                throw new coding_exception('Inconsistent forced variant detected at slot ' . $slot);
1278
            }
1279
            if ($varianttochoose > $question->get_num_variants()) {
1280
                throw new coding_exception('Forced variant out of range at slot ' . $slot);
1281
            }
1282
            $forcedchoices[$seed] = $varianttochoose;
1283
        }
1284
        return $forcedchoices;
1285
    }
1286
}