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
 * More object oriented wrappers around parts of the Moodle question bank.
19
 *
20
 * In due course, I expect that the question bank will be converted to a
21
 * fully object oriented structure, at which point this file can be a
22
 * starting point.
23
 *
24
 * @package    moodlecore
25
 * @subpackage questionbank
26
 * @copyright  2009 The Open University
27
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28
 */
29
 
30
use core_question\local\bank\question_version_status;
31
use core_question\output\question_version_info;
32
 
33
defined('MOODLE_INTERNAL') || die();
34
 
35
require_once(__DIR__ . '/../type/questiontypebase.php');
36
 
37
 
38
/**
39
 * This static class provides access to the other question bank.
40
 *
41
 * It provides functions for managing question types and question definitions.
42
 *
43
 * @copyright  2009 The Open University
44
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45
 */
46
abstract class question_bank {
47
    // TODO: This limit can be deleted if someday we move all TEXTS to BIG ones. MDL-19603
48
    const MAX_SUMMARY_LENGTH = 32000;
49
 
50
    /** @var array question type name => question_type subclass. */
51
    private static $questiontypes = array();
52
 
53
    /** @var array question type name => 1. Records which question definitions have been loaded. */
54
    private static $loadedqdefs = array();
55
 
56
    /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
57
    private static $testmode = false;
58
    private static $testdata = array();
59
 
60
    private static $questionconfig = null;
61
 
62
    /**
63
     * @var array string => string The standard set of grade options (fractions)
64
     * to use when editing questions, in the range 0 to 1 inclusive. Array keys
65
     * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
66
     * have float array keys in PHP.
67
     * Initialised by {@link ensure_grade_options_initialised()}.
68
     */
69
    private static $fractionoptions = null;
70
    /** @var array string => string The full standard set of (fractions) -1 to 1 inclusive. */
71
    private static $fractionoptionsfull = null;
72
 
73
    /**
74
     * @param string $qtypename a question type name, e.g. 'multichoice'.
75
     * @return bool whether that question type is installed in this Moodle.
76
     */
77
    public static function is_qtype_installed($qtypename) {
78
        $plugindir = core_component::get_plugin_directory('qtype', $qtypename);
79
        return $plugindir && is_readable($plugindir . '/questiontype.php');
80
    }
81
 
82
    /**
83
     * Get the question type class for a particular question type.
84
     * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
85
     * @param bool $mustexist if false, the missing question type is returned when
86
     *      the requested question type is not installed.
87
     * @return question_type the corresponding question type class.
88
     */
89
    public static function get_qtype($qtypename, $mustexist = true) {
90
        global $CFG;
91
        if (isset(self::$questiontypes[$qtypename])) {
92
            return self::$questiontypes[$qtypename];
93
        }
94
        $file = core_component::get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
95
        if (!is_readable($file)) {
96
            if ($mustexist || $qtypename == 'missingtype') {
97
                throw new coding_exception('Unknown question type ' . $qtypename);
98
            } else {
99
                return self::get_qtype('missingtype');
100
            }
101
        }
102
        include_once($file);
103
        $class = 'qtype_' . $qtypename;
104
        if (!class_exists($class)) {
105
            throw new coding_exception("Class {$class} must be defined in {$file}.");
106
        }
107
        self::$questiontypes[$qtypename] = new $class();
108
        return self::$questiontypes[$qtypename];
109
    }
110
 
111
    /**
112
     * Load the question configuration data from config_plugins.
113
     * @return object get_config('question') with caching.
114
     */
115
    public static function get_config() {
116
        if (is_null(self::$questionconfig)) {
117
            self::$questionconfig = get_config('question');
118
        }
119
        return self::$questionconfig;
120
    }
121
 
122
    /**
123
     * @param string $qtypename the internal name of a question type. For example multichoice.
124
     * @return bool whether users are allowed to create questions of this type.
125
     */
126
    public static function qtype_enabled($qtypename) {
127
        $config = self::get_config();
128
        $enabledvar = $qtypename . '_disabled';
129
        return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
130
                self::get_qtype($qtypename)->menu_name() != '';
131
    }
132
 
133
    /**
134
     * @param string $qtypename the internal name of a question type. For example multichoice.
135
     * @return bool whether this question type exists.
136
     */
137
    public static function qtype_exists($qtypename) {
138
        return array_key_exists($qtypename, core_component::get_plugin_list('qtype'));
139
    }
140
 
141
    /**
142
     * @param $qtypename the internal name of a question type, for example multichoice.
143
     * @return string the human_readable name of this question type, from the language pack.
144
     */
145
    public static function get_qtype_name($qtypename) {
146
        return self::get_qtype($qtypename)->local_name();
147
    }
148
 
149
    /**
150
     * @return array all the installed question types.
151
     */
152
    public static function get_all_qtypes() {
153
        $qtypes = array();
154
        foreach (core_component::get_plugin_list('qtype') as $plugin => $notused) {
155
            try {
156
                $qtypes[$plugin] = self::get_qtype($plugin);
157
            } catch (coding_exception $e) {
158
                // Catching coding_exceptions here means that incompatible
159
                // question types do not cause the rest of Moodle to break.
160
            }
161
        }
162
        return $qtypes;
163
    }
164
 
165
    /**
166
     * Sort an array of question types according to the order the admin set up,
167
     * and then alphabetically for the rest.
168
     * @param array qtype->name() => qtype->local_name().
169
     * @return array sorted array.
170
     */
171
    public static function sort_qtype_array($qtypes, $config = null) {
172
        if (is_null($config)) {
173
            $config = self::get_config();
174
        }
175
 
176
        $sortorder = array();
177
        $otherqtypes = array();
178
        foreach ($qtypes as $name => $localname) {
179
            $sortvar = $name . '_sortorder';
180
            if (isset($config->$sortvar)) {
181
                $sortorder[$config->$sortvar] = $name;
182
            } else {
183
                $otherqtypes[$name] = $localname;
184
            }
185
        }
186
 
187
        ksort($sortorder);
188
        core_collator::asort($otherqtypes);
189
 
190
        $sortedqtypes = array();
191
        foreach ($sortorder as $name) {
192
            $sortedqtypes[$name] = $qtypes[$name];
193
        }
194
        foreach ($otherqtypes as $name => $notused) {
195
            $sortedqtypes[$name] = $qtypes[$name];
196
        }
197
        return $sortedqtypes;
198
    }
199
 
200
    /**
201
     * @return array all the question types that users are allowed to create,
202
     *      sorted into the preferred order set on the admin screen.
203
     */
204
    public static function get_creatable_qtypes() {
205
        $config = self::get_config();
206
        $allqtypes = self::get_all_qtypes();
207
 
208
        $qtypenames = array();
209
        foreach ($allqtypes as $name => $qtype) {
210
            if (self::qtype_enabled($name)) {
211
                $qtypenames[$name] = $qtype->local_name();
212
            }
213
        }
214
 
215
        $qtypenames = self::sort_qtype_array($qtypenames);
216
 
217
        $creatableqtypes = array();
218
        foreach ($qtypenames as $name => $notused) {
219
            $creatableqtypes[$name] = $allqtypes[$name];
220
        }
221
        return $creatableqtypes;
222
    }
223
 
224
    /**
225
     * Load the question definition class(es) belonging to a question type. That is,
226
     * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
227
     * of checking.
228
     * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
229
     */
230
    public static function load_question_definition_classes($qtypename) {
231
        global $CFG;
232
        if (isset(self::$loadedqdefs[$qtypename])) {
233
            return;
234
        }
235
        $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
236
        if (!is_readable($file)) {
237
            throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
238
        }
239
        include_once($file);
240
        self::$loadedqdefs[$qtypename] = 1;
241
    }
242
 
243
    /**
244
     * This method needs to be called whenever a question is edited.
245
     */
246
    public static function notify_question_edited($questionid) {
247
        question_finder::get_instance()->uncache_question($questionid);
248
    }
249
 
250
    /**
251
     * Load a question definition data from the database. The data will be
252
     * returned as a plain stdClass object.
253
     * @param int $questionid the id of the question to load.
254
     * @return object question definition loaded from the database.
255
     */
256
    public static function load_question_data($questionid) {
257
        return question_finder::get_instance()->load_question_data($questionid);
258
    }
259
 
260
    /**
261
     * Load a question definition from the database. The object returned
262
     * will actually be of an appropriate {@link question_definition} subclass.
263
     * @param int $questionid the id of the question to load.
264
     * @param bool $allowshuffle if false, then any shuffle option on the selected
265
     *      quetsion is disabled.
266
     * @return question_definition loaded from the database.
267
     */
268
    public static function load_question($questionid, $allowshuffle = true) {
269
 
270
        if (self::$testmode) {
271
            // Evil, test code in production, but no way round it.
272
            return self::return_test_question_data($questionid);
273
        }
274
 
275
        $questiondata = self::load_question_data($questionid);
276
 
277
        if (!$allowshuffle) {
278
            $questiondata->options->shuffleanswers = false;
279
        }
280
        return self::make_question($questiondata);
281
    }
282
 
283
    /**
284
     * Convert the question information loaded with {@link get_question_options()}
285
     * to a question_definintion object.
286
     * @param object $questiondata raw data loaded from the database.
287
     * @return question_definition loaded from the database.
288
     */
289
    public static function make_question($questiondata) {
290
        $definition = self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
291
        question_version_info::$pendingdefinitions[$definition->id] = $definition;
292
        return $definition;
293
    }
294
 
295
    /**
296
     * Get all the versions of a particular question.
297
     *
298
     * @param int $questionid id of the question
299
     * @return array The array keys are version number, and the values are objects with three int fields
300
     * version (same as array key), versionid and questionid.
301
     */
302
    public static function get_all_versions_of_question(int $questionid): array {
303
        global $DB;
304
        $sql = "SELECT qv.id AS versionid, qv.version, qv.questionid
305
                  FROM {question_versions} qv
306
                 WHERE qv.questionbankentryid = (SELECT DISTINCT qbe.id
307
                                                   FROM {question_bank_entries} qbe
308
                                                   JOIN {question_versions} qv ON qbe.id = qv.questionbankentryid
309
                                                   JOIN {question} q ON qv.questionid = q.id
310
                                                  WHERE q.id = ?)
311
              ORDER BY qv.version DESC";
312
 
313
        return $DB->get_records_sql($sql, [$questionid]);
314
    }
315
 
316
    /**
317
     * Get all the versions of questions.
318
     *
319
     * @param array $questionids Array of question ids.
320
     * @return array two dimensional array question_bank_entries.id => version number => question.id.
321
     *      Versions in descending order.
322
     */
323
    public static function get_all_versions_of_questions(array $questionids): array {
324
        global $DB;
325
 
326
        [$listquestionid, $params] = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED);
327
        $sql = "SELECT qv.questionid, qv.version, qv.questionbankentryid
328
                  FROM {question_versions} qv
329
                  JOIN {question_versions} qv2 ON qv.questionbankentryid = qv2.questionbankentryid
330
                 WHERE qv2.questionid $listquestionid
331
              ORDER BY qv.questionbankentryid, qv.version DESC";
332
        $result = [];
333
        $rows = $DB->get_recordset_sql($sql, $params);
334
        foreach ($rows as $row) {
335
            $result[$row->questionbankentryid][$row->version] = $row->questionid;
336
        }
337
 
338
        return $result;
339
    }
340
 
341
    /**
342
     * @return question_finder a question finder.
343
     */
344
    public static function get_finder() {
345
        return question_finder::get_instance();
346
    }
347
 
348
    /**
349
     * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
350
     */
351
    public static function start_unit_test() {
352
        self::$testmode = true;
353
    }
354
 
355
    /**
356
     * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
357
     */
358
    public static function end_unit_test() {
359
        self::$testmode = false;
360
        self::$testdata = array();
361
    }
362
 
363
    private static function return_test_question_data($questionid) {
364
        if (!isset(self::$testdata[$questionid])) {
365
            throw new coding_exception('question_bank::return_test_data(' . $questionid .
366
                    ') called, but no matching question has been loaded by load_test_data.');
367
        }
368
        return self::$testdata[$questionid];
369
    }
370
 
371
    /**
372
     * To be used for unit testing only. Will throw an exception if
373
     * {@link start_unit_test()} has not been called first.
374
     * @param object $questiondata a question data object to put in the test data store.
375
     */
376
    public static function load_test_question_data(question_definition $question) {
377
        if (!self::$testmode) {
378
            throw new coding_exception('question_bank::load_test_data called when ' .
379
                    'not in test mode.');
380
        }
381
        self::$testdata[$question->id] = $question;
382
    }
383
 
384
    protected static function ensure_fraction_options_initialised() {
385
        if (!is_null(self::$fractionoptions)) {
386
            return;
387
        }
388
 
389
        // define basic array of grades. This list comprises all fractions of the form:
390
        // a. p/q for q <= 6, 0 <= p <= q
391
        // b. p/10 for 0 <= p <= 10
392
        // c. 1/q for 1 <= q <= 10
393
        // d. 1/20
394
        $rawfractions = array(
395
            0.9000000,
396
            0.8333333,
397
            0.8000000,
398
            0.7500000,
399
            0.7000000,
400
            0.6666667,
401
            0.6000000,
402
            0.5000000,
403
            0.4000000,
404
            0.3333333,
405
            0.3000000,
406
            0.2500000,
407
            0.2000000,
408
            0.1666667,
409
            0.1428571,
410
            0.1250000,
411
            0.1111111,
412
            0.1000000,
413
            0.0500000,
414
        );
415
 
416
        // Put the None option at the top.
417
        self::$fractionoptions = array(
418
            '0.0' => get_string('none'),
419
            '1.0' => '100%',
420
        );
421
        self::$fractionoptionsfull = array(
422
            '0.0' => get_string('none'),
423
            '1.0' => '100%',
424
        );
425
 
426
        // The the positive grades in descending order.
427
        foreach ($rawfractions as $fraction) {
428
            $percentage = format_float(100 * $fraction, 5, true, true) . '%';
429
            self::$fractionoptions["{$fraction}"] = $percentage;
430
            self::$fractionoptionsfull["{$fraction}"] = $percentage;
431
        }
432
 
433
        // The the negative grades in descending order.
434
        foreach (array_reverse($rawfractions) as $fraction) {
435
            self::$fractionoptionsfull['' . (-$fraction)] =
436
                    format_float(-100 * $fraction, 5, true, true) . '%';
437
        }
438
 
439
        self::$fractionoptionsfull['-1.0'] = '-100%';
440
    }
441
 
442
    /**
443
     * @return array string => string The standard set of grade options (fractions)
444
     * to use when editing questions, in the range 0 to 1 inclusive. Array keys
445
     * are string becuase: a) we want grades to exactly 7 d.p., and b. you can't
446
     * have float array keys in PHP.
447
     * Initialised by {@link ensure_grade_options_initialised()}.
448
     */
449
    public static function fraction_options() {
450
        self::ensure_fraction_options_initialised();
451
        return self::$fractionoptions;
452
    }
453
 
454
    /** @return array string => string The full standard set of (fractions) -1 to 1 inclusive. */
455
    public static function fraction_options_full() {
456
        self::ensure_fraction_options_initialised();
457
        return self::$fractionoptionsfull;
458
    }
459
 
460
    /**
461
     * Return a list of the different question types present in the given categories.
462
     *
463
     * @param  array $categories a list of category ids
464
     * @return array the list of question types in the categories
465
     * @since  Moodle 3.1
466
     */
467
    public static function get_all_question_types_in_categories($categories) {
468
        global $DB;
469
 
470
        list($categorysql, $params) = $DB->get_in_or_equal($categories);
471
        $sql = "SELECT DISTINCT q.qtype
472
                           FROM {question} q
473
                           JOIN {question_versions} qv ON qv.questionid = q.id
474
                           JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
475
                          WHERE qbe.questioncategoryid $categorysql";
476
 
477
        $qtypes = $DB->get_fieldset_sql($sql, $params);
478
        return $qtypes;
479
    }
480
}
481
 
482
 
483
/**
484
 * Class for loading questions according to various criteria.
485
 *
486
 * @copyright  2009 The Open University
487
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
488
 */
489
class question_finder implements cache_data_source {
490
    /** @var question_finder the singleton instance of this class. */
491
    protected static $questionfinder = null;
492
 
493
    /**
494
     * @return question_finder a question finder.
495
     */
496
    public static function get_instance() {
497
        if (is_null(self::$questionfinder)) {
498
            self::$questionfinder = new question_finder();
499
        }
500
        return self::$questionfinder;
501
    }
502
 
503
    /* See cache_data_source::get_instance_for_cache. */
504
    public static function get_instance_for_cache(cache_definition $definition) {
505
        return self::get_instance();
506
    }
507
 
508
    /**
509
     * @return cache_application the question definition cache we are using.
510
     */
511
    protected function get_data_cache() {
512
        // Do not double cache here because it may break cache resetting.
513
        return cache::make('core', 'questiondata');
514
    }
515
 
516
    /**
517
     * This method needs to be called whenever a question is edited.
518
     */
519
    public function uncache_question($questionid) {
520
        $this->get_data_cache()->delete($questionid);
521
    }
522
 
523
    /**
524
     * Load a question definition data from the database. The data will be
525
     * returned as a plain stdClass object.
526
     * @param int $questionid the id of the question to load.
527
     * @return object question definition loaded from the database.
528
     */
529
    public function load_question_data($questionid) {
530
        return $this->get_data_cache()->get($questionid);
531
    }
532
 
533
    /**
534
     * Get the ids of all the questions in a list of categories.
535
     * @param array $categoryids either a category id, or a comma-separated list
536
     *      of category ids, or an array of them.
537
     * @param string $extraconditions extra conditions to AND with the rest of
538
     *      the where clause. Must use named parameters.
539
     * @param array $extraparams any parameters used by $extraconditions.
540
     * @return array questionid => questionid.
541
     */
542
    public function get_questions_from_categories($categoryids, $extraconditions,
543
            $extraparams = array()) {
544
        global $DB;
545
 
546
        list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
547
 
548
        if ($extraconditions) {
549
            $extraconditions = ' AND (' . $extraconditions . ')';
550
        }
551
        $qcparams['readystatus'] = question_version_status::QUESTION_STATUS_READY;
552
        $qcparams['readystatusqv'] = question_version_status::QUESTION_STATUS_READY;
553
        $sql = "SELECT q.id, q.id AS id2
554
                  FROM {question} q
555
                  JOIN {question_versions} qv ON qv.questionid = q.id
556
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
557
                 WHERE qbe.questioncategoryid {$qcsql}
558
                       AND q.parent = 0
559
                       AND qv.status = :readystatus
560
                       AND qv.version = (SELECT MAX(v.version)
561
                                          FROM {question_versions} v
562
                                          JOIN {question_bank_entries} be
563
                                            ON be.id = v.questionbankentryid
564
                                         WHERE be.id = qbe.id
565
                                           AND v.status = :readystatusqv)
566
                       {$extraconditions}";
567
 
568
        return $DB->get_records_sql_menu($sql, $qcparams + $extraparams);
569
    }
570
 
571
    /**
572
     * Get the ids of all the questions in a list of categories, with the number
573
     * of times they have already been used in a given set of usages.
574
     *
575
     * The result array is returned in order of increasing (count previous uses).
576
     *
577
     * @param array $categoryids an array question_category ids.
578
     * @param qubaid_condition $qubaids which question_usages to count previous uses from.
579
     * @param string $extraconditions extra conditions to AND with the rest of
580
     *      the where clause. Must use named parameters.
581
     * @param array $extraparams any parameters used by $extraconditions.
582
     * @return array questionid => count of number of previous uses.
583
     *
584
     * @deprecated since Moodle 4.3
585
     * @todo Final deprecation on Moodle 4.7 MDL-78091
586
     */
587
    public function get_questions_from_categories_with_usage_counts($categoryids,
588
            qubaid_condition $qubaids, $extraconditions = '', $extraparams = array()) {
589
        debugging(
590
            'Function get_questions_from_categories_with_usage_counts() is deprecated, please do not use the function.',
591
            DEBUG_DEVELOPER
592
        );
593
        return $this->get_questions_from_categories_and_tags_with_usage_counts(
594
                $categoryids, $qubaids, $extraconditions, $extraparams);
595
    }
596
 
597
    /**
598
     * Get the ids of all the questions in a list of categories that have ALL the provided tags,
599
     * with the number of times they have already been used in a given set of usages.
600
     *
601
     * The result array is returned in order of increasing (count previous uses).
602
     *
603
     * @param array $categoryids an array of question_category ids.
604
     * @param qubaid_condition $qubaids which question_usages to count previous uses from.
605
     * @param string $extraconditions extra conditions to AND with the rest of
606
     *      the where clause. Must use named parameters.
607
     * @param array $extraparams any parameters used by $extraconditions.
608
     * @param array $tagids an array of tag ids
609
     * @return array questionid => count of number of previous uses.
610
     * @deprecated since Moodle 4.3
611
     * @todo Final deprecation on Moodle 4.7 MDL-78091
612
     */
613
    public function get_questions_from_categories_and_tags_with_usage_counts($categoryids,
614
            qubaid_condition $qubaids, $extraconditions = '', $extraparams = array(), $tagids = array()) {
615
        debugging(
616
            'Function get_questions_from_categories_and_tags_with_usage_counts() is deprecated, please do not use the function.',
617
            DEBUG_DEVELOPER
618
        );
619
        global $DB;
620
 
621
        list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
622
 
623
        $readystatus = question_version_status::QUESTION_STATUS_READY;
624
        $select = "q.id, (SELECT COUNT(1)
625
                            FROM " . $qubaids->from_question_attempts('qa') . "
626
                           WHERE qa.questionid = q.id AND " . $qubaids->where() . "
627
                         ) AS previous_attempts";
628
        $from   = "{question} q";
629
        $join   = "JOIN {question_versions} qv ON qv.questionid = q.id
630
                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid";
631
        $from = $from . " " . $join;
632
        $where  = "qbe.questioncategoryid {$qcsql}
633
               AND q.parent = 0
634
               AND qv.status = '$readystatus'
635
               AND qv.version = (SELECT MAX(v.version)
636
                                  FROM {question_versions} v
637
                                  JOIN {question_bank_entries} be
638
                                    ON be.id = v.questionbankentryid
639
                                 WHERE be.id = qbe.id)";
640
        $params = $qcparams;
641
 
642
        if (!empty($tagids)) {
643
            // We treat each additional tag as an AND condition rather than
644
            // an OR condition.
645
            //
646
            // For example, if the user filters by the tags "foo" and "bar" then
647
            // we reduce the question list to questions that are tagged with both
648
            // "foo" AND "bar". Any question that does not have ALL of the specified
649
            // tags will be omitted.
650
            list($tagsql, $tagparams) = $DB->get_in_or_equal($tagids, SQL_PARAMS_NAMED, 'ti');
651
            $tagparams['tagcount'] = count($tagids);
652
            $tagparams['questionitemtype'] = 'question';
653
            $tagparams['questioncomponent'] = 'core_question';
654
            $where .= " AND q.id IN (SELECT ti.itemid
655
                                       FROM {tag_instance} ti
656
                                      WHERE ti.itemtype = :questionitemtype
657
                                            AND ti.component = :questioncomponent
658
                                            AND ti.tagid {$tagsql}
659
                                   GROUP BY ti.itemid
660
                                     HAVING COUNT(itemid) = :tagcount)";
661
            $params += $tagparams;
662
        }
663
 
664
        if ($extraconditions) {
665
            $extraconditions = ' AND (' . $extraconditions . ')';
666
        }
667
 
668
        return $DB->get_records_sql_menu("SELECT $select
669
                                                FROM $from
670
                                               WHERE $where $extraconditions
671
                                            ORDER BY previous_attempts",
672
                $qubaids->from_where_params() + $params + $extraparams);
673
    }
674
 
675
    /* See cache_data_source::load_for_cache. */
676
    public function load_for_cache($questionid) {
677
        global $DB;
678
 
679
        $sql = 'SELECT q.id, qc.id as category, q.parent, q.name, q.questiontext, q.questiontextformat,
680
                       q.generalfeedback, q.generalfeedbackformat, q.defaultmark, q.penalty, q.qtype,
681
                       q.length, q.stamp, q.timecreated, q.timemodified,
682
                       q.createdby, q.modifiedby, qbe.idnumber,
683
                       qc.contextid,
684
                       qv.status,
685
                       qv.id as versionid,
686
                       qv.version,
687
                       qv.questionbankentryid
688
                  FROM {question} q
689
                  JOIN {question_versions} qv ON qv.questionid = q.id
690
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
691
                  JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
692
                 WHERE q.id = :id';
693
 
694
        $questiondata = $DB->get_record_sql($sql, ['id' => $questionid], MUST_EXIST);
695
        get_question_options($questiondata);
696
        return $questiondata;
697
    }
698
 
699
    /* See cache_data_source::load_many_for_cache. */
700
    public function load_many_for_cache(array $questionids) {
701
        global $DB;
702
 
703
        list($idcondition, $params) = $DB->get_in_or_equal($questionids);
704
        $sql = 'SELECT q.id, qc.id as category, q.parent, q.name, q.questiontext, q.questiontextformat,
705
                       q.generalfeedback, q.generalfeedbackformat, q.defaultmark, q.penalty, q.qtype,
706
                       q.length, q.stamp, q.timecreated, q.timemodified,
707
                       q.createdby, q.modifiedby, qbe.idnumber,
708
                       qc.contextid,
709
                       qv.status,
710
                       qv.id as versionid,
711
                       qv.version,
712
                       qv.questionbankentryid
713
                  FROM {question} q
714
                  JOIN {question_versions} qv ON qv.questionid = q.id
715
                  JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
716
                  JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
717
                 WHERE q.id ';
718
 
719
        $questiondata = $DB->get_records_sql($sql . $idcondition, $params);
720
 
721
        foreach ($questionids as $id) {
722
            if (!array_key_exists($id, $questiondata)) {
723
                throw new dml_missing_record_exception('question', '', ['id' => $id]);
724
            }
725
            get_question_options($questiondata[$id]);
726
        }
727
        return $questiondata;
728
    }
729
}