Proyectos de Subversion Moodle

Rev

Rev 11 | | 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
 * Class to print a view of the question bank.
19
 *
20
 * @package   core_question
21
 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_question\local\bank;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
require_once($CFG->dirroot . '/question/editlib.php');
30
 
31
use coding_exception;
32
use core\output\datafilter;
33
use core_question\local\bank\condition;
34
use core_question\local\statistics\statistics_bulk_loader;
35
use core_question\output\question_bank_filter_ui;
36
use core_question\local\bank\column_manager_base;
37
use qbank_managecategories\category_condition;
38
 
39
/**
40
 * This class prints a view of the question bank.
41
 *
42
 * including
43
 *  + Some controls to allow users to to select what is displayed.
44
 *  + A list of questions as a table.
45
 *  + Further controls to do things with the questions.
46
 *
47
 * This class gives a basic view, and provides plenty of hooks where subclasses
48
 * can override parts of the display.
49
 *
50
 * The list of questions presented as a table is generated by creating a list of
51
 * core_question\bank\column objects, one for each 'column' to be displayed. These
52
 * manage
53
 *  + outputting the contents of that column, given a $question object, but also
54
 *  + generating the right fragments of SQL to ensure the necessary data is present,
55
 *    and sorted in the right order.
56
 *  + outputting table headers.
57
 *
58
 * @copyright 2009 Tim Hunt
59
 * @author    2021 Safat Shahin <safatshahin@catalyst-au.net>
60
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
61
 */
62
class view {
63
 
64
    /**
65
     * Maximum number of sorts allowed.
66
     */
67
    const MAX_SORTS = 3;
68
 
69
    /**
70
     * @var \moodle_url base URL for the current page. Used as the
71
     * basis for making URLs for actions that reload the page.
72
     */
73
    protected $baseurl;
74
 
75
    /**
76
     * @var \moodle_url used as a basis for URLs that edit a question.
77
     */
78
    protected $editquestionurl;
79
 
80
    /**
81
     * @var \core_question\local\bank\question_edit_contexts
82
     */
83
    public $contexts;
84
 
85
    /**
86
     * @var object|\cm_info|null if we are in a module context, the cm.
87
     */
88
    public $cm;
89
 
90
    /**
91
     * @var object the course we are within.
92
     */
93
    public $course;
94
 
95
    /**
96
     * @var column_base[] these are all the 'columns' that are
97
     * part of the display. Array keys are the class name.
98
     */
99
    protected $requiredcolumns;
100
 
101
    /**
102
     * @var question_action_base[] these are all the actions that can be displayed in a question's action menu.
103
     *
104
     * Array keys are the class name.
105
     */
106
    protected $questionactions;
107
 
108
    /**
109
     * @var column_base[] these are the 'columns' that are
110
     * actually displayed as a column, in order. Array keys are the class name.
111
     */
112
    protected $visiblecolumns;
113
 
114
    /**
115
     * @var column_base[] these are the 'columns' that are
116
     * common to the question bank.
117
     */
118
    protected $corequestionbankcolumns;
119
 
120
    /**
121
     * @var column_base[] these are the 'columns' that are
122
     * actually displayed as an additional row (e.g. question text), in order.
123
     * Array keys are the class name.
124
     */
125
    protected $extrarows;
126
 
127
    /**
128
     * @var array list of column class names for which columns to sort on.
129
     */
130
    protected $sort;
131
 
132
    /**
133
     * @var int page size to use (when we are not showing all questions).
134
     */
135
    protected $pagesize = DEFAULT_QUESTIONS_PER_PAGE;
136
 
137
    /**
138
     * @var int|null id of the a question to highlight in the list (if present).
139
     */
140
    protected $lastchangedid;
141
 
142
    /**
143
     * @var string SQL to count the number of questions matching the current
144
     * search conditions.
145
     */
146
    protected $countsql;
147
 
148
    /**
149
     * @var string SQL to actually load the question data to display.
150
     */
151
    protected $loadsql;
152
 
153
    /**
154
     * @var array params used by $countsql and $loadsql (which currently must be the same).
155
     */
156
    protected $sqlparams;
157
 
158
    /**
159
     * @var ?array Stores all the average statistics that this question bank view needs.
160
     *
161
     * This field gets initialised in {@see display_question_list()}. It is a two dimensional
162
     * $this->loadedstatistics[$questionid][$fieldname] = $average value of that statistics for that question.
163
     * Column classes in qbank plugins can access these values using {@see get_aggregate_statistic()}.
164
     */
165
    protected $loadedstatistics = null;
166
 
167
    /**
168
     * @var condition[] search conditions.
169
     */
170
    protected $searchconditions = [];
171
 
172
    /**
173
     * @var string url of the new question page.
174
     */
175
    public $returnurl;
176
 
177
    /**
1441 ariadna 178
     * @var bulk_action_base[] $bulkactions bulk actions for the api.
1 efrain 179
     */
180
    public $bulkactions = [];
181
 
182
    /**
183
     * @var int|null Number of questions.
184
     */
185
    protected $totalcount = null;
186
 
187
    /**
188
     * @var array Parameters for the page URL.
189
     */
190
    protected $pagevars = [];
191
 
192
    /**
193
     * @var plugin_features_base[] $plugins Plugin feature objects for all enabled qbank plugins.
194
     */
195
    protected $plugins = [];
196
 
197
    /**
198
     * @var string $component the component the api is used from.
199
     */
200
    public $component = 'core_question';
201
 
202
    /**
203
     * @var string $callback name of the callback for the api call via filter js.
204
     */
205
    public $callback = 'question_data';
206
 
207
    /**
208
     * @var array $extraparams extra parameters for the extended apis.
209
     */
210
    public $extraparams = [];
211
 
212
    /**
213
     * @var column_manager_base $columnmanager The column manager, can be overridden by plugins.
214
     */
215
    protected $columnmanager;
216
 
217
    /**
218
     * Constructor for view.
219
     *
220
     * @param \core_question\local\bank\question_edit_contexts $contexts
221
     * @param \moodle_url $pageurl
222
     * @param object $course course settings
223
     * @param null $cm (optional) activity settings.
224
     * @param array $params the parameters required to initialize the api.
225
     * @param array $extraparams any extra parameters required by a particular view class.
226
     */
227
    public function __construct($contexts, $pageurl, $course, $cm = null, $params = [], $extraparams = []) {
228
        $this->contexts = $contexts;
229
        $this->baseurl = $pageurl;
230
        $this->course = $course;
231
        $this->cm = $cm;
232
        $this->extraparams = $extraparams;
233
 
1441 ariadna 234
        // Add the default qperpage to extra params array so we can switch back and forth between it and "all".
235
        $this->extraparams['defaultqperpage'] = $this->pagesize;
236
        $this->extraparams['maxqperpage'] = MAXIMUM_QUESTIONS_PER_PAGE;
237
 
238
        if ($cm === null) {
239
            debugging('Passing $cm to the view constructor is now required.', DEBUG_DEVELOPER);
240
        }
241
 
1 efrain 242
        // Default filter condition.
243
        if (!isset($params['filter']) && isset($params['cat'])) {
1441 ariadna 244
            $params['filter']  = filter_condition_manager::get_default_filter($params['cat']);
1 efrain 245
            $params['jointype'] = datafilter::JOINTYPE_ALL;
246
        }
247
        if (!empty($params['filter'])) {
248
            $params['filter'] = filter_condition_manager::unpack_filteroptions_param($params['filter']);
249
        }
250
        if (isset($params['filter']['jointype'])) {
251
            $params['jointype'] = $params['filter']['jointype'];
252
            unset($params['filter']['jointype']);
253
        }
254
 
255
        // Create the url of the new question page to forward to.
256
        $this->returnurl = $pageurl->out_as_local_url(false);
257
        $this->editquestionurl = new \moodle_url('/question/bank/editquestion/question.php', ['returnurl' => $this->returnurl]);
1441 ariadna 258
        $this->editquestionurl->param('cmid', $this->cm->id);
1 efrain 259
 
260
        $this->lastchangedid = clean_param($pageurl->param('lastchanged'), PARAM_INT);
261
 
262
        $this->init_plugins();
263
        $this->init_column_manager();
264
        // Possibly the heading part can be removed.
265
        $this->set_pagevars($params);
266
        $this->init_columns($this->wanted_columns(), $this->heading_column());
267
        $this->init_question_actions();
268
        $this->init_sort();
269
        $this->init_bulk_actions();
1441 ariadna 270
 
271
        // Record that this question bank has been used.
272
        question_bank_helper::add_bank_context_to_recently_viewed($contexts->lowest());
1 efrain 273
    }
274
 
275
    /**
276
     * Get an array of plugin features objects for all enabled qbank plugins.
277
     *
278
     * @return void
279
     */
280
    protected function init_plugins(): void {
281
        $plugins = \core_component::get_plugin_list_with_class('qbank', 'plugin_feature', 'plugin_feature.php');
282
        foreach ($plugins as $componentname => $pluginclass) {
283
            if (!\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
284
                continue;
285
            }
286
            $this->plugins[$componentname] = new $pluginclass();
287
        }
288
        // Sort plugin list by component name.
289
        ksort($this->plugins);
290
    }
291
 
292
    /**
293
     * Allow qbank plugins to override the column manager.
294
     *
295
     * If multiple qbank plugins define a column manager, this will pick the first one sorted alphabetically.
296
     *
297
     * @return void
298
     */
299
    protected function init_column_manager(): void {
300
        $this->columnmanager = new column_manager_base();
301
        foreach ($this->plugins as $plugin) {
302
            if ($columnmanager = $plugin->get_column_manager()) {
303
                $this->columnmanager = $columnmanager;
304
                break;
305
            }
306
        }
307
    }
308
 
309
    /**
310
     * Initialize bulk actions.
311
     */
312
    protected function init_bulk_actions(): void {
313
        foreach ($this->plugins as $componentname => $plugin) {
1441 ariadna 314
            $bulkactions = $plugin->get_bulk_actions($this);
1 efrain 315
            if (!is_array($bulkactions)) {
316
                debugging("The method {$componentname}::get_bulk_actions() must return an " .
317
                    "array of bulk actions instead of a single bulk action. " .
318
                    "Please update your implementation of get_bulk_actions() to return an array. " .
319
                    "Check out the qbank_bulkmove plugin for a working example.", DEBUG_DEVELOPER);
320
                $bulkactions = [$bulkactions];
321
            }
322
 
323
            foreach ($bulkactions as $bulkactionobject) {
1441 ariadna 324
                $this->bulkactions[$bulkactionobject->get_key()] = $bulkactionobject;
1 efrain 325
            }
326
        }
327
    }
328
 
329
    /**
330
     * @deprecated Since Moodle 4.3
331
     */
1441 ariadna 332
    #[\core\attribute\deprecated(
333
        'create a qbank plugin and implement a filter object',
334
        since: '4.3',
335
        mdl: 'MDL-72321',
336
        final: true
337
    )]
1 efrain 338
    protected function init_search_conditions(): void {
1441 ariadna 339
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 340
    }
341
 
342
    /**
343
     * Initialise list of menu actions for enabled question bank plugins.
344
     *
345
     * Menu action objects are stored in $this->menuactions, keyed by class name.
346
     *
347
     * @return void
348
     */
349
    protected function init_question_actions(): void {
350
        $this->questionactions = [];
351
        foreach ($this->plugins as $plugin) {
352
            $menuactions = $plugin->get_question_actions($this);
353
            foreach ($menuactions as $menuaction) {
354
                $this->questionactions[$menuaction::class] = $menuaction;
355
                if ($menuaction->get_menu_position() === question_action_base::MENU_POSITION_NOT_SET) {
356
                    debugging('Question bank actions must define the get_menu_position method. ' .
357
                        $menuaction::class . ' does not.', DEBUG_DEVELOPER);
358
                }
359
            }
360
        }
361
 
362
        // Sort according to each action's desired position.
363
        // Note, we are relying on the sort to be stable for
364
        // equal values of get_menu_position.
365
        uasort(
366
            $this->questionactions,
367
            function (question_action_base $a, question_action_base $b) {
368
                return $a->get_menu_position() <=> $b->get_menu_position();
369
            },
370
        );
371
    }
372
 
373
    /**
374
     * Get class for each question bank columns.
375
     *
376
     * @return array
377
     */
378
    protected function get_question_bank_plugins(): array {
379
        $questionbankclasscolumns = [];
380
        $newpluginclasscolumns = [];
381
        $corequestionbankcolumns = [
382
            'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column',
383
            'core_question\local\bank\edit_menu_column' . column_base::ID_SEPARATOR . 'edit_menu_column',
384
        ];
385
 
386
        foreach ($corequestionbankcolumns as $columnid) {
387
            [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $columnid, 2);
388
            if (class_exists($columnclass)) {
389
                $questionbankclasscolumns[$columnid] = $columnclass::from_column_name($this, $columnname);
390
            }
391
        }
392
 
393
        foreach ($this->plugins as $plugin) {
394
            $plugincolumnobjects = $plugin->get_question_columns($this);
395
            foreach ($plugincolumnobjects as $columnobject) {
396
                $columnid = $columnobject->get_column_id();
397
                foreach ($corequestionbankcolumns as $corequestionbankcolumn) {
398
                    // Check if it has custom preference selector to view/hide.
399
                    if ($columnobject->has_preference()) {
400
                        if (!$columnobject->get_preference()) {
401
                            continue;
402
                        }
403
                    }
404
                    if ($corequestionbankcolumn === $columnid) {
405
                        $questionbankclasscolumns[$columnid] = $columnobject;
406
                    } else {
407
                        // Any community plugin for column/action.
408
                        $newpluginclasscolumns[$columnid] = $columnobject;
409
                    }
410
                }
411
            }
412
        }
413
 
414
        // New plugins added at the end of the array, will change in sorting feature.
415
        foreach ($newpluginclasscolumns as $key => $newpluginclasscolumn) {
416
            $questionbankclasscolumns[$key] = $newpluginclasscolumn;
417
        }
418
 
419
        $questionbankclasscolumns = $this->columnmanager->get_sorted_columns($questionbankclasscolumns);
420
        $questionbankclasscolumns = $this->columnmanager->set_columns_visibility($questionbankclasscolumns);
421
 
422
        // Mitigate the error in case of any regression.
423
        foreach ($questionbankclasscolumns as $shortname => $questionbankclasscolumn) {
424
            if (!is_object($questionbankclasscolumn) || !$questionbankclasscolumn->isvisible) {
425
                unset($questionbankclasscolumns[$shortname]);
426
            }
427
        }
428
 
429
        return $questionbankclasscolumns;
430
    }
431
 
432
    /**
433
     * Loads all the available columns.
434
     *
435
     * @return array
436
     */
437
    protected function wanted_columns(): array {
438
        $this->requiredcolumns = [];
439
        $questionbankcolumns = $this->get_question_bank_plugins();
440
        foreach ($questionbankcolumns as $classobject) {
441
            if (empty($classobject) || !($classobject instanceof \core_question\local\bank\column_base)) {
442
                continue;
443
            }
444
            $this->requiredcolumns[$classobject->get_column_name()] = $classobject;
445
        }
446
 
447
        return $this->requiredcolumns;
448
    }
449
 
450
 
451
    /**
452
     * Check a column object from its name and get the object for sort.
453
     *
454
     * @param string $columnname
455
     */
456
    protected function get_column_type($columnname) {
457
        if (empty($this->requiredcolumns[$columnname])) {
458
            $this->requiredcolumns[$columnname] = new $columnname($this);
459
        }
460
    }
461
 
462
    /**
463
     * Specify the column heading
464
     *
465
     * @return string Column name for the heading
466
     */
467
    protected function heading_column(): string {
468
        return 'qbank_viewquestionname\viewquestionname_column_helper';
469
    }
470
 
471
    /**
472
     * Initializing table columns
473
     *
474
     * @param array $wanted Collection of column names
475
     * @param string $heading The name of column that is set as heading
476
     */
477
    protected function init_columns($wanted, $heading = ''): void {
478
        // Now split columns into real columns and rows.
479
        $this->visiblecolumns = [];
480
        $this->extrarows = [];
481
        foreach ($wanted as $column) {
482
            if ($column->is_extra_row()) {
483
                $this->extrarows[$column->get_column_name()] = $column;
484
            } else {
485
                // Only add columns which are visible.
486
                if ($column->isvisible) {
487
                    $this->visiblecolumns[$column->get_column_name()] = $column;
488
                }
489
            }
490
        }
491
 
492
        if (array_key_exists($heading, $this->requiredcolumns)) {
493
            $this->requiredcolumns[$heading]->set_as_heading();
494
        }
495
    }
496
 
497
    /**
498
     * Checks if the column included in the output.
499
     *
500
     * @param string $colname a column internal name.
501
     * @return bool is this column included in the output?
502
     */
503
    public function has_column($colname): bool {
504
        return isset($this->visiblecolumns[$colname]);
505
    }
506
 
507
    /**
508
     * Get the count of the columns.
509
     *
510
     * @return int The number of columns in the table.
511
     */
512
    public function get_column_count(): int {
513
        return count($this->visiblecolumns);
514
    }
515
 
516
    /**
517
     * Get course id.
518
     * @return mixed
519
     */
520
    public function get_courseid() {
521
        return $this->course->id;
522
    }
523
 
524
    /**
525
     * Initialise sorting.
526
     */
527
    protected function init_sort(): void {
528
        $this->sort = [];
529
        $sorts = optional_param_array('sortdata', [], PARAM_INT);
530
        if (empty($sorts)) {
531
            $sorts = $this->get_pagevars('sortdata');
532
        }
533
        if (empty($sorts)) {
534
            $sorts = $this->default_sort();
535
        }
536
        $sorts = array_slice($sorts, 0, self::MAX_SORTS);
537
        foreach ($sorts as $sortname => $sortorder) {
538
            // Deal with subsorts.
539
            [$colname] = $this->parse_subsort($sortname);
540
            $this->get_column_type($colname);
541
        }
542
        $this->sort = $sorts;
543
    }
544
 
545
    /**
546
     * Deal with a sort name of the form columnname, or colname_subsort by
547
     * breaking it up, validating the bits that are present, and returning them.
548
     * If there is no subsort, then $subsort is returned as ''.
549
     *
550
     * @param string $sort the sort parameter to process.
551
     * @return array [$colname, $subsort].
552
     */
553
    protected function parse_subsort($sort): array {
554
        // Do the parsing.
555
        if (strpos($sort, '-') !== false) {
1441 ariadna 556
            [$colname, $subsort] = explode('-', $sort, 2);
1 efrain 557
        } else {
558
            $colname = $sort;
559
            $subsort = '';
560
        }
561
        $colname = str_replace('__', '\\', $colname);
562
        // Validate the column name.
563
        $this->get_column_type($colname);
564
        $column = $this->requiredcolumns[$colname];
565
        if (!isset($column) || !$column->is_sortable()) {
566
            $this->baseurl->remove_params('sortdata');
567
            throw new \moodle_exception('unknownsortcolumn', '', $this->baseurl->out(), $colname);
568
        }
569
        // Validate the subsort, if present.
570
        if ($subsort) {
571
            $subsorts = $column->is_sortable();
572
            if (!is_array($subsorts) || !isset($subsorts[$subsort])) {
573
                throw new \moodle_exception('unknownsortcolumn', '', $this->baseurl->out(), $sort);
574
            }
575
        }
576
        return [$colname, $subsort];
577
    }
578
 
579
    /**
580
     * Sort to parameters.
581
     *
582
     * @param array $sorts
583
     * @return array
584
     */
585
    protected function sort_to_params($sorts): array {
586
        $params = [];
587
        foreach ($sorts as $sortname => $sortorder) {
588
            $params['sortdata[' . $sortname . ']'] = $sortorder;
589
        }
590
        return $params;
591
    }
592
 
593
    /**
1441 ariadna 594
     * Get the default sort columns for this view of the question bank.
595
     *
596
     * The array keys in the return value need be the standard keys used
597
     * to represent sorts. That is [plugin_frankenstyle]__[class_name] and then
598
     * and optional bit like '-name' if the column supports muliple ways of sorting.
599
     *
600
     * The array values should be one of the constants SORT_ASC or SORT_DESC for the default order.
601
     *
602
     * Therefore, the default implementation below is a good example.
603
     *
604
     * @return int[] sort key => SORT_ASC or SORT_DESC.
1 efrain 605
     */
606
    protected function default_sort(): array {
607
        $defaultsort = [];
608
        if (class_exists('\\qbank_viewquestiontype\\question_type_column')) {
609
            $defaultsort['qbank_viewquestiontype__question_type_column'] = SORT_ASC;
610
        }
611
        if (class_exists('\\qbank_viewquestionname\\question_name_idnumber_tags_column')) {
612
            $defaultsort['qbank_viewquestionname__question_name_idnumber_tags_column-name'] = SORT_ASC;
613
        }
614
 
615
        return $defaultsort;
616
    }
617
 
618
    /**
619
     * Gets the primary sort order according to the default sort.
620
     *
621
     * @param string $sortname a column or column_subsort name.
622
     * @return int the current sort order for this column -1, 0, 1
623
     */
624
    public function get_primary_sort_order($sortname): int {
625
        $order = reset($this->sort);
626
        $primarysort = key($this->sort);
627
        if ($sortname == $primarysort) {
628
            return $order;
629
        }
630
 
631
        return 0;
632
    }
633
 
634
    /**
635
     * Get a URL to redisplay the page with a new sort for the question bank.
636
     *
637
     * @param string $sortname the column, or column_subsort to sort on.
638
     * @param bool $newsortreverse whether to sort in reverse order.
639
     * @return string The new URL.
640
     */
641
    public function new_sort_url($sortname, $newsortreverse): string {
642
        // Tricky code to add the new sort at the start, removing it from where it was before, if it was present.
643
        $newsort = array_reverse($this->sort);
644
        if (isset($newsort[$sortname])) {
645
            unset($newsort[$sortname]);
646
        }
647
        $newsort[$sortname] = $newsortreverse ? SORT_DESC : SORT_ASC;
648
        $newsort = array_reverse($newsort);
649
        if (count($newsort) > self::MAX_SORTS) {
650
            $newsort = array_slice($newsort, 0, self::MAX_SORTS, true);
651
        }
652
        return $this->baseurl->out(true, $this->sort_to_params($newsort));
653
    }
654
 
655
    /**
656
     * Return an array 'table_alias' => 'JOIN clause' to bring in any data that
657
     * the core view requires.
658
     *
659
     * @return string[] 'table_alias' => 'JOIN clause'
660
     */
661
    protected function get_required_joins(): array {
662
        return [
663
            'qv' => 'JOIN {question_versions} qv ON qv.questionid = q.id',
664
            'qbe' => 'JOIN {question_bank_entries} qbe on qbe.id = qv.questionbankentryid',
665
            'qc' => 'JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid',
666
        ];
667
    }
668
 
669
    /**
670
     * Return an array of fields for any data that the core view requires.
671
     *
672
     * Use table alias 'q' for the question table, or one of the ones from get_required_joins.
673
     * Every field requested must specify a table prefix.
674
     *
675
     * @return string[] fields required.
676
     */
677
    protected function get_required_fields(): array {
678
        return [
679
            'q.id',
680
            'q.qtype',
681
            'q.createdby',
682
            'qc.id as categoryid',
683
            'qc.contextid',
684
            'qv.status',
685
            'qv.version',
686
            'qv.id as versionid',
687
            'qbe.id as questionbankentryid',
688
        ];
689
    }
690
 
691
    /**
692
     * Gather query requirements from view component objects.
693
     *
694
     * This will take the required fields and joins for this view, and combine them with those for all active view components.
695
     * Fields will be de-duplicated in multiple components require the same field.
696
     * Joins will be de-duplicated if the alias and join clause match exactly.
697
     *
698
     * @throws \coding_exception If two components attempt to use the same alias for different joins.
699
     * @param view_component[] $viewcomponents List of component objects included in the current view
700
     * @return array [$fields, $joins] SQL fields and joins to add to the query.
701
     */
702
    protected function get_component_requirements(array $viewcomponents): array {
703
        $fields = $this->get_required_fields();
704
        $joins = $this->get_required_joins();
705
        if (!empty($viewcomponents)) {
706
            foreach ($viewcomponents as $viewcomponent) {
707
                $extrajoins = $viewcomponent->get_extra_joins();
708
                foreach ($extrajoins as $prefix => $join) {
709
                    if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
710
                        throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
711
                    }
712
                    $joins[$prefix] = $join;
713
                }
714
                $fields = array_merge($fields, $viewcomponent->get_required_fields());
715
            }
716
        }
717
        return [array_unique($fields), $joins];
718
    }
719
 
720
    /**
721
     * Create the SQL query to retrieve the indicated questions, based on
722
     * \core_question\bank\search\condition filters.
723
     */
724
    protected function build_query(): void {
725
        // Get the required tables and fields.
726
        [$fields, $joins] = $this->get_component_requirements(array_merge($this->requiredcolumns, $this->questionactions));
727
 
728
        // Build the order by clause.
729
        $sorts = [];
730
        foreach ($this->sort as $sortname => $sortorder) {
731
            [$colname, $subsort] = $this->parse_subsort($sortname);
732
            $sorts[] = $this->requiredcolumns[$colname]->sort_expression($sortorder == SORT_DESC, $subsort);
733
        }
734
        $this->sqlparams = [];
735
        $conditions = [];
1441 ariadna 736
        $showhiddenquestion = true;
737
        // Build the where clause.
1 efrain 738
        foreach ($this->searchconditions as $searchcondition) {
1441 ariadna 739
            // This is nasty hack to check if the filter is using "Show hidden question" option.
740
            if (array_key_exists('hidden_condition', $searchcondition->params())) {
741
                $showhiddenquestion = false;
742
            }
1 efrain 743
            if ($searchcondition->where()) {
744
                $conditions[] = '((' . $searchcondition->where() .'))';
745
            }
746
            if ($searchcondition->params()) {
747
                $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
748
            }
749
        }
1441 ariadna 750
        $extracondition = '';
751
        if (!$showhiddenquestion) {
752
            // If Show hidden question option is off, then we need get the latest version that is not hidden.
753
            $extracondition = ' AND v.status <> :hiddenstatus';
754
            $this->sqlparams = array_merge($this->sqlparams, ['hiddenstatus' => question_version_status::QUESTION_STATUS_HIDDEN]);
755
        }
756
        $latestversion = "qv.version = (SELECT MAX(v.version)
757
                                          FROM {question_versions} v
758
                                          JOIN {question_bank_entries} be
759
                                            ON be.id = v.questionbankentryid
760
                                         WHERE be.id = qbe.id $extracondition)";
761
 
1 efrain 762
        // Get higher level filter condition.
763
        $jointype = isset($this->pagevars['jointype']) ? (int)$this->pagevars['jointype'] : condition::JOINTYPE_DEFAULT;
764
        $nonecondition = ($jointype === datafilter::JOINTYPE_NONE) ? ' NOT ' : '';
765
        $separator = ($jointype === datafilter::JOINTYPE_ALL) ? ' AND ' : ' OR ';
766
        // Build the SQL.
767
        $sql = ' FROM {question} q ' . implode(' ', $joins);
768
        $sql .= ' WHERE q.parent = 0 AND ' . $latestversion;
769
        if (!empty($conditions)) {
770
            $sql .= ' AND ' . $nonecondition . ' ( ';
771
            $sql .= implode($separator, $conditions);
772
            $sql .= ' ) ';
773
        }
774
        $this->countsql = 'SELECT count(1)' . $sql;
775
        $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
776
    }
777
 
778
    /**
779
     * Get the number of questions.
780
     *
781
     * @return int
782
     */
783
    public function get_question_count(): int {
784
        global $DB;
785
        if (is_null($this->totalcount)) {
786
            $this->totalcount = $DB->count_records_sql($this->countsql, $this->sqlparams);
787
        }
788
        return $this->totalcount;
789
    }
790
 
791
    /**
792
     * Load the questions we need to display.
793
     *
794
     * @return \moodle_recordset questionid => data about each question.
795
     */
796
    protected function load_page_questions(): \moodle_recordset {
797
        global $DB;
1441 ariadna 798
 
799
        // Load the questions based on the page we are on.
1 efrain 800
        $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams,
1441 ariadna 801
            (int)$this->pagevars['qpage'] * (int)$this->pagevars['qperpage'], (int)$this->pagevars['qperpage']);
802
 
803
        if (!$questions->valid()) {
1 efrain 804
            $questions->close();
1441 ariadna 805
            // No questions on this page. Reset to the nearest page that contains questions.
806
            $this->pagevars['qpage'] = max(0,
807
                ceil($this->totalcount / (int)$this->pagevars['qperpage']) - 1);
808
            $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams,
809
                (int)$this->pagevars['qpage'] * (int)$this->pagevars['qperpage'], (int)$this->pagevars['qperpage']);
1 efrain 810
        }
811
        return $questions;
812
    }
813
 
814
    /**
815
     * Returns the base url.
816
     *
817
     * @return \moodle_url
818
     */
819
    public function base_url(): \moodle_url {
820
        return $this->baseurl;
821
    }
822
 
823
    /**
824
     * Get the URL for editing a question as a moodle url.
825
     *
826
     * @param int $questionid the question id.
827
     * @return \moodle_url the URL, HTML-escaped.
828
     */
829
    public function edit_question_moodle_url($questionid) {
830
        return new \moodle_url($this->editquestionurl, ['id' => $questionid]);
831
    }
832
 
833
    /**
834
     * Get the URL for editing a question as a HTML-escaped string.
835
     *
836
     * @param int $questionid the question id.
837
     * @return string the URL, HTML-escaped.
838
     */
839
    public function edit_question_url($questionid) {
840
        return $this->edit_question_moodle_url($questionid)->out();
841
    }
842
 
843
    /**
844
     * Get the URL for duplicating a question as a moodle url.
845
     *
846
     * @param int $questionid the question id.
847
     * @return \moodle_url the URL.
848
     */
849
    public function copy_question_moodle_url($questionid) {
850
        return new \moodle_url($this->editquestionurl, ['id' => $questionid, 'makecopy' => 1]);
851
    }
852
 
853
    /**
854
     * Get the URL for duplicating a given question.
855
     * @param int $questionid the question id.
856
     * @return string the URL, HTML-escaped.
857
     */
858
    public function copy_question_url($questionid) {
859
        return $this->copy_question_moodle_url($questionid)->out();
860
    }
861
 
862
    /**
863
     * Get the context we are displaying the question bank for.
864
     * @return \context context object.
865
     */
866
    public function get_most_specific_context(): \context {
867
        return $this->contexts->lowest();
868
    }
869
 
870
    /**
871
     * Get fields from the pagevars array.
872
     *
873
     * If a field is specified, that particlar pagevars field will be returned. Otherwise the entire array will be returned.
874
     *
875
     * If a field is specified but it does not exist, null will be returned.
876
     *
877
     * @param ?string $field
878
     * @return mixed
879
     */
880
    public function get_pagevars(?string $field = null): mixed {
881
        if (is_null($field)) {
882
            return $this->pagevars;
883
        } else {
884
            return $this->pagevars[$field] ?? null;
885
        }
886
    }
887
 
888
    /**
889
     * Set the pagevars property with the provided array.
890
     *
891
     * @param array $pagevars
892
     */
893
    public function set_pagevars(array $pagevars): void {
894
        $this->pagevars = $pagevars;
895
    }
896
 
897
    /**
898
     * Shows the question bank interface.
899
     */
900
    public function display(): void {
901
        $editcontexts = $this->contexts->having_one_edit_tab_cap('questions');
902
 
903
        echo \html_writer::start_div('questionbankwindow boxwidthwide boxaligncenter', [
904
            'data-component' => 'core_question',
905
            'data-callback' => 'display_question_bank',
906
            'data-contextid' => $editcontexts[array_key_last($editcontexts)]->id,
907
        ]);
908
 
909
        // Show the filters and search options.
910
        $this->wanted_filters();
911
        // Continues with list of questions.
912
        $this->display_question_list();
913
        echo \html_writer::end_div();
914
 
915
    }
916
 
917
    /**
918
     * The filters for the question bank.
919
     */
920
    public function wanted_filters(): void {
921
        global $OUTPUT;
922
        [, $contextid] = explode(',', $this->pagevars['cat']);
923
        $catcontext = \context::instance_by_id($contextid);
924
        // Category selection form.
925
        $this->display_question_bank_header();
926
        // Add search conditions.
927
        $this->add_standard_search_conditions();
928
        // Render the question bank filters.
929
        $additionalparams = [
930
            'perpage' => $this->pagevars['qperpage'],
931
        ];
932
        $filter = new question_bank_filter_ui($catcontext, $this->searchconditions, $additionalparams, $this->component,
933
                $this->callback, static::class, 'qbank-table', $this->cm?->id, $this->pagevars,
934
                $this->extraparams);
935
        echo $OUTPUT->render($filter);
936
    }
937
 
938
    /**
939
     * @deprecated since Moodle 4.3 MDL-72321
940
     */
1441 ariadna 941
    #[\core\attribute\deprecated(
942
        'question/bank/managecategories/classes/category_confition.php',
943
        since: '4.3',
944
        mdl: 'MDL-72321',
945
        final: true
946
    )]
1 efrain 947
    protected function print_choose_category_message(): void {
1441 ariadna 948
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 949
    }
950
 
951
    /**
952
     * @deprecated since Moodle 4.3 MDL-72321
953
     */
1441 ariadna 954
    #[\core\attribute\deprecated(
955
        'question/bank/managecategories/classes/category_confition.php',
956
        since: '4.3',
957
        mdl: 'MDL-72321',
958
        final: true
959
    )]
1 efrain 960
    protected function get_current_category($categoryandcontext) {
1441 ariadna 961
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 962
    }
963
 
964
    /**
965
     * @deprecated since Moodle 4.3 MDL-72321
966
     */
1441 ariadna 967
    #[\core\attribute\deprecated('filtering objects', since: '4.3', mdl: 'MDL-72321', final: true)]
1 efrain 968
    protected function display_options_form($showquestiontext): void {
1441 ariadna 969
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 970
    }
971
 
972
    /**
973
     * @deprecated since Moodle 4.3 MDL-72321
974
     */
1441 ariadna 975
    #[\core\attribute\deprecated('filtering objects', since: '4.3', mdl: 'MDL-72321', final: true)]
1 efrain 976
    protected function display_advanced_search_form($advancedsearch): void {
1441 ariadna 977
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 978
    }
979
 
980
    /**
981
     * @deprecated since Moodle 4.3 MDL-72321
982
     */
1441 ariadna 983
    #[\core\attribute\deprecated('filtering objects', since: '4.3', mdl: 'MDL-72321', final: true)]
1 efrain 984
    protected function display_showtext_checkbox($showquestiontext): void {
1441 ariadna 985
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 986
    }
987
 
988
    /**
989
     * Display the header element for the question bank.
990
     */
991
    protected function display_question_bank_header(): void {
992
        global $OUTPUT;
993
        echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
994
    }
995
 
996
    /**
997
     * Does the current view allow adding new questions?
998
     *
999
     * @return bool True if the view supports adding new questions.
1000
     */
1001
    public function allow_add_questions(): bool {
1002
        return true;
1003
    }
1004
 
1005
    /**
1006
     * Output the question bank controls for each plugin.
1007
     *
1008
     * Controls will be output in the order defined by the array keys returned from
1009
     * {@see plugin_features_base::get_question_bank_controls}. If more than one plugin defines a control in the same position,
1010
     * they will placed after one another based on the alphabetical order of the plugins.
1011
     *
1012
     * @param \core\context $context The current context, for permissions checks.
1013
     * @param int $categoryid The current question category.
1014
     */
1015
    protected function get_plugin_controls(\core\context $context, int $categoryid): string {
1016
        global $OUTPUT;
1017
        $orderedcontrols = [];
1018
        foreach ($this->plugins as $plugin) {
1019
            $plugincontrols = $plugin->get_question_bank_controls($this, $context, $categoryid);
1020
            foreach ($plugincontrols as $position => $plugincontrol) {
1021
                if (!array_key_exists($position, $orderedcontrols)) {
1022
                    $orderedcontrols[$position] = [];
1023
                }
1024
                $orderedcontrols[$position][] = $plugincontrol;
1025
            }
1026
        }
1027
        ksort($orderedcontrols);
1028
        $output = '';
1029
        foreach ($orderedcontrols as $controls) {
1030
            foreach ($controls as $control) {
1031
                $output .= $OUTPUT->render($control);
1032
            }
1033
        }
1034
        return $OUTPUT->render_from_template('core_question/question_bank_controls', ['controls' => $output]);
1035
    }
1036
 
1037
    /**
1038
     * Prints the table of questions in a category with interactions
1039
     */
1040
    public function display_question_list(): void {
1041
        // This function can be moderately slow with large question counts and may time out.
1042
        // We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
1043
        // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc).
1044
        \core_php_time_limit::raise(300);
1441 ariadna 1045
        raise_memory_limit(MEMORY_EXTRA);
1 efrain 1046
 
1047
        [$categoryid, $contextid] = category_condition::validate_category_param($this->pagevars['cat']);
1048
        $catcontext = \context::instance_by_id($contextid);
1441 ariadna 1049
        // Update the question in the list with correct category context when we have selected category filter.
1050
        if (isset($this->pagevars['filter']['category']['values'])) {
1051
            $categoryid = $this->pagevars['filter']['category']['values'][0];
1052
            foreach ($this->contexts->all() as $context) {
1053
                if ((int) $context->instanceid === (int) $categoryid) {
1054
                    $catcontext = $context;
1055
                    break;
1056
                }
1057
            }
1058
        }
1 efrain 1059
 
1060
        echo \html_writer::start_tag(
1061
            'div',
1062
            [
1063
                'id' => 'questionscontainer',
1064
                'data-component' => $this->component,
1065
                'data-callback' => $this->callback,
1066
                'data-contextid' => $this->get_most_specific_context()->id,
1067
            ]
1068
        );
1069
        echo $this->get_plugin_controls($catcontext, $categoryid);
1070
 
1441 ariadna 1071
        $questions = $this->load_questions();
1 efrain 1072
 
1073
        // This html will be refactored in the bulk actions implementation.
1074
        echo \html_writer::start_tag('form', ['action' => $this->baseurl, 'method' => 'post', 'id' => 'questionsubmit']);
1075
        echo \html_writer::start_tag('fieldset', ['class' => 'invisiblefieldset', 'style' => "display: block;"]);
1076
        echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()]);
1441 ariadna 1077
        echo \html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'addonpage']);
1 efrain 1078
        echo \html_writer::input_hidden_params($this->baseurl);
1079
 
1080
        $filtercondition = json_encode($this->get_pagevars());
1081
        // Embeded filterconditon into the div.
1082
        echo \html_writer::start_tag('div',
1083
            ['class' => 'categoryquestionscontainer', 'data-filtercondition' => $filtercondition]);
1441 ariadna 1084
        if ($this->totalcount > 0) {
1 efrain 1085
            // Bulk load any required statistics.
1086
            $this->load_required_statistics($questions);
1087
 
1088
            // Bulk load any extra data that any column requires.
1089
            foreach ($this->requiredcolumns as $column) {
1090
                $column->load_additional_data($questions);
1091
            }
1092
            $this->display_questions($questions, $this->pagevars['qpage'], $this->pagevars['qperpage']);
1093
        }
1094
        echo \html_writer::end_tag('div');
1095
 
1096
        $this->display_bottom_controls($catcontext);
1097
 
1098
        echo \html_writer::end_tag('fieldset');
1099
        echo \html_writer::end_tag('form');
1100
        echo \html_writer::end_tag('div');
1101
    }
1102
 
1103
    /**
1104
     * Work out the list of all the required statistics fields for this question bank view.
1105
     *
1106
     * This gathers all the required fields from all columns, so they can all be loaded at once.
1107
     *
1108
     * @return string[] the names of all the required fields for this question bank view.
1109
     */
1110
    protected function determine_required_statistics(): array {
1111
        $requiredfields = [];
1112
        foreach ($this->requiredcolumns as $column) {
1113
            $requiredfields = array_merge($requiredfields, $column->get_required_statistics_fields());
1114
        }
1115
 
1116
        return array_unique($requiredfields);
1117
    }
1118
 
1119
    /**
1120
     * Load the aggregate statistics that all the columns require.
1121
     *
1122
     * @param \stdClass[] $questions the questions that will be displayed indexed by question id.
1123
     */
1124
    protected function load_required_statistics(array $questions): void {
1125
        $requiredstatistics = $this->determine_required_statistics();
1126
        $this->loadedstatistics = statistics_bulk_loader::load_aggregate_statistics(
1127
                array_keys($questions), $requiredstatistics);
1128
    }
1129
 
1130
    /**
1131
     * Get the aggregated value of a particular statistic for a particular question.
1132
     *
1133
     * You can only get values for the questions on the current page of the question bank view,
1134
     * and only if you declared the need for this statistic in the get_required_statistics_fields()
1135
     * method of your question bank column.
1136
     *
1137
     * @param int $questionid the id of a question
1138
     * @param string $fieldname the name of a statistics field, e.g. 'facility'.
1139
     * @return float|null the average (across all users) of this statistic for this question.
1140
     *      Null if the value is not available right now.
1141
     */
1142
    public function get_aggregate_statistic(int $questionid, string $fieldname): ?float {
1143
        if (!array_key_exists($questionid, $this->loadedstatistics)) {
1144
            throw new \coding_exception('Question ' . $questionid . ' is not on the current page of ' .
1145
                    'this question bank view, so its statistics are not available.');
1146
        }
1147
 
1148
        // Must be array_key_exists, not isset, because we care about null values.
1149
        if (!array_key_exists($fieldname, $this->loadedstatistics[$questionid])) {
1150
            throw new \coding_exception('Statistics field ' . $fieldname . ' was not requested by any ' .
1151
                    'question bank column in this view, so it is not available.');
1152
        }
1153
 
1154
        return $this->loadedstatistics[$questionid][$fieldname];
1155
    }
1156
 
1157
    /**
1158
     * Display the top pagination bar.
1159
     *
1160
     * @param object $pagination
1161
     * @deprecated since Moodle 4.3
1162
     * @todo Final deprecation on Moodle 4.7 MDL-78091
1163
     */
1164
    public function display_top_pagnation($pagination): void {
1165
        debugging(
1166
            'Function display_top_pagnation() is deprecated, please use display_questions() for ajax based pagination.',
1167
            DEBUG_DEVELOPER
1168
        );
1169
        global $PAGE;
1170
        $displaydata = [
1171
            'pagination' => $pagination
1172
        ];
1173
        echo $PAGE->get_renderer('core_question', 'bank')->render_question_pagination($displaydata);
1174
    }
1175
 
1176
    /**
1177
     * Display bottom pagination bar.
1178
     *
1179
     * @param string $pagination
1180
     * @param int $totalnumber
1181
     * @param int $perpage
1182
     * @param \moodle_url $pageurl
1183
     * @deprecated since Moodle 4.3
1184
     * @todo Final deprecation on Moodle 4.7 MDL-78091
1185
     */
1186
    public function display_bottom_pagination($pagination, $totalnumber, $perpage, $pageurl): void {
1187
        debugging(
1188
            'Function display_bottom_pagination() is deprecated, please use display_questions() for ajax based pagination.',
1189
            DEBUG_DEVELOPER
1190
        );
1191
        global $PAGE;
1192
        $displaydata = array (
1193
            'extraclasses' => 'pagingbottom',
1194
            'pagination' => $pagination,
1195
            'biggertotal' => true,
1196
        );
1197
        if ($totalnumber > $this->pagesize) {
1198
            $displaydata['showall'] = true;
1199
            if ($perpage == $this->pagesize) {
1200
                $url = new \moodle_url($pageurl, array_merge($pageurl->params(),
1201
                    ['qpage' => 0, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE]));
1202
                if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE) {
1203
                    $displaydata['totalnumber'] = MAXIMUM_QUESTIONS_PER_PAGE;
1204
                } else {
1205
                    $displaydata['biggertotal'] = false;
1206
                    $displaydata['totalnumber'] = $totalnumber;
1207
                }
1208
            } else {
1209
                $url = new \moodle_url($pageurl, array_merge($pageurl->params(),
1210
                    ['qperpage' => $this->pagesize]));
1211
                $displaydata['totalnumber'] = $this->pagesize;
1212
            }
1213
            $displaydata['showallurl'] = $url;
1214
        }
1215
        echo $PAGE->get_renderer('core_question', 'bank')->render_question_pagination($displaydata);
1216
    }
1217
 
1218
    /**
1219
     * Display the controls at the bottom of the list of questions.
1220
     *
1221
     * @param \context $catcontext The context of the category being displayed.
1222
     */
1223
    protected function display_bottom_controls(\context $catcontext): void {
1224
        $caneditall = has_capability('moodle/question:editall', $catcontext);
1225
        $canuseall = has_capability('moodle/question:useall', $catcontext);
1226
        $canmoveall = has_capability('moodle/question:moveall', $catcontext);
1227
        if ($caneditall || $canmoveall || $canuseall) {
1228
            global $PAGE;
1229
            $bulkactiondatas = [];
1230
            $params = $this->base_url()->params();
1231
            $returnurl = new \moodle_url($this->base_url(), ['filter' => json_encode($this->pagevars['filter'])]);
1232
            $params['returnurl'] = $returnurl;
1233
            foreach ($this->bulkactions as $key => $action) {
1234
                // Check capabilities.
1235
                $capcount = 0;
1441 ariadna 1236
                foreach ($action->get_bulk_action_capabilities() as $capability) {
1 efrain 1237
                    if (has_capability($capability, $catcontext)) {
1238
                        $capcount ++;
1239
                    }
1240
                }
1241
                // At least one cap need to be there.
1242
                if ($capcount === 0) {
1243
                    unset($this->bulkactions[$key]);
1244
                    continue;
1245
                }
1246
                $actiondata = new \stdClass();
1441 ariadna 1247
                $actiondata->actionname = $action->get_bulk_action_title();
1 efrain 1248
                $actiondata->actionkey = $key;
1441 ariadna 1249
                $actiondata->actionurl = new \moodle_url($action->get_bulk_action_url(), $params);
1 efrain 1250
                $bulkactiondata[] = $actiondata;
1251
 
1252
                $bulkactiondatas ['bulkactionitems'] = $bulkactiondata;
1253
            }
1254
            // We dont need to show this section if none of the plugins are enabled.
1255
            if (!empty($bulkactiondatas)) {
1256
                echo $PAGE->get_renderer('core_question', 'bank')->render_bulk_actions_ui($bulkactiondatas);
1257
            }
1258
        }
1259
    }
1260
 
1261
    /**
1441 ariadna 1262
     * Give each bulk action a chance to load its own javascript module.
1263
     * @return void
1264
     */
1265
    public function init_bulk_actions_js(): void {
1266
        foreach ($this->bulkactions as $action) {
1267
            $action->initialise_javascript();
1268
        }
1269
    }
1270
 
1271
    /**
1 efrain 1272
     * Display the questions.
1273
     *
1274
     * @param array $questions
1275
     */
1276
    public function display_questions($questions, $page = 0, $perpage = DEFAULT_QUESTIONS_PER_PAGE): void {
1277
        global $OUTPUT;
1278
        if (!isset($this->pagevars['filter']['category'])) {
1279
            // We must have a category filter selected.
1280
            echo $OUTPUT->render_from_template('qbank_managecategories/choose_category', []);
1281
            return;
1282
        }
1283
        // Pagination.
1284
        $pageingurl = new \moodle_url($this->base_url());
11 efrain 1285
        // TODO MDL-82312: it really should not be necessary to set filter here, and not like this.
1286
        // This should be handled in baseurl, but it isn't so we do this so Moodle basically works for now.
1287
        $pageingurl->param('filter', json_encode($this->pagevars['filter']));
1 efrain 1288
        $pagingbar = new \paging_bar($this->totalcount, $page, $perpage, $pageingurl);
1289
        $pagingbar->pagevar = 'qpage';
1290
        echo $OUTPUT->render($pagingbar);
1291
 
1292
        // Table of questions.
1293
        echo \html_writer::start_tag('div',
1294
            ['class' => 'question_table', 'id' => 'question_table']);
1295
        $this->print_table($questions);
1296
        echo \html_writer::end_tag('div');
1297
        echo $OUTPUT->render($pagingbar);
1298
    }
1299
 
1300
    /**
1301
     * Load the questions according to the search conditions.
1302
     *
1303
     * @return array
1304
     */
1305
    public function load_questions() {
1306
        $this->build_query();
1441 ariadna 1307
        $this->get_question_count();
1 efrain 1308
        $questionsrs = $this->load_page_questions();
1309
        $questions = [];
1310
        foreach ($questionsrs as $question) {
1311
            if (!empty($question->id)) {
1312
                $questions[$question->id] = $question;
1313
            }
1314
        }
1315
        $questionsrs->close();
1316
        foreach ($this->requiredcolumns as $name => $column) {
1317
            $column->load_additional_data($questions);
1318
        }
1319
        return $questions;
1320
    }
1321
 
1322
    /**
1323
     * Prints the actual table with question.
1324
     *
1325
     * @param array $questions
1326
     */
1327
    protected function print_table($questions): void {
1328
        // Start of the table.
1329
        echo \html_writer::start_tag('table', [
1330
            'id' => 'categoryquestions',
1441 ariadna 1331
            'class' => 'question-bank-table table generaltable',
1 efrain 1332
            'data-defaultsort' => json_encode($this->sort),
1333
        ]);
1334
 
1335
        // Prints the table header.
1336
        echo \html_writer::start_tag('thead');
1337
        echo \html_writer::start_tag('tr', ['class' => 'qbank-column-list']);
1338
        $this->print_table_headers();
1339
        echo \html_writer::end_tag('tr');
1340
        echo \html_writer::end_tag('thead');
1341
 
1342
        // Prints the table row or content.
1343
        echo \html_writer::start_tag('tbody');
1344
        $rowcount = 0;
1345
        foreach ($questions as $question) {
1346
            $this->print_table_row($question, $rowcount);
1347
            $rowcount += 1;
1348
        }
1349
        echo \html_writer::end_tag('tbody');
1350
 
1351
        // End of the table.
1352
        echo \html_writer::end_tag('table');
1353
    }
1354
 
1355
    /**
1356
     * @deprecated since Moodle 4.3 MDL-72321
1357
     */
1441 ariadna 1358
    #[\core\attribute\deprecated('print_table()', since: '4.3', mdl: 'MDL-72321', final: true)]
1 efrain 1359
    protected function start_table() {
1441 ariadna 1360
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 1361
    }
1362
 
1363
    /**
1364
     * @deprecated since Moodle 4.3 MDL-72321
1365
     */
1441 ariadna 1366
    #[\core\attribute\deprecated('print_table()', since: '4.3', mdl: 'MDL-72321', final: true)]
1 efrain 1367
    protected function end_table() {
1441 ariadna 1368
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 1369
    }
1370
 
1371
    /**
1372
     * Print table headers from child classes.
1373
     */
1374
    protected function print_table_headers(): void {
1375
        $columnactions = $this->columnmanager->get_column_actions($this);
1376
        foreach ($this->visiblecolumns as $column) {
1377
            $width = $this->columnmanager->get_column_width($column);
1378
            $column->display_header($columnactions, $width);
1379
        }
1380
    }
1381
 
1382
    /**
1383
     * Gets the classes for the row.
1384
     *
1385
     * @param \stdClass $question
1386
     * @param int $rowcount
1387
     * @return array
1388
     */
1389
    protected function get_row_classes($question, $rowcount): array {
1390
        $classes = [];
1391
        if ($question->status === question_version_status::QUESTION_STATUS_HIDDEN) {
1392
            $classes[] = 'dimmed_text';
1393
        }
1394
        if ($question->id == $this->lastchangedid) {
1395
            $classes[] = 'highlight text-dark';
1396
        }
1397
        $classes[] = 'r' . ($rowcount % 2);
1398
        return $classes;
1399
    }
1400
 
1401
    /**
1402
     * Prints the table row from child classes.
1403
     *
1404
     * @param \stdClass $question
1405
     * @param int $rowcount
1406
     */
1407
    public function print_table_row($question, $rowcount): void {
1408
        $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
1409
        $attributes = [];
1441 ariadna 1410
 
1411
        // If the question type is invalid we highlight it red.
1412
        if (!\question_bank::is_qtype_usable($question->qtype)) {
1413
            $rowclasses .= ' table-danger';
1414
        }
1 efrain 1415
        if ($rowclasses) {
1416
            $attributes['class'] = $rowclasses;
1417
        }
1418
        echo \html_writer::start_tag('tr', $attributes);
1419
        foreach ($this->visiblecolumns as $column) {
1420
            $column->display($question, $rowclasses);
1421
        }
1422
        echo \html_writer::end_tag('tr');
1423
        foreach ($this->extrarows as $row) {
1424
            $row->display($question, $rowclasses);
1425
        }
1426
    }
1427
 
1428
    /**
1429
     * Add another search control to this view.
1430
     * @param condition $searchcondition the condition to add.
1431
     * @param string|null $fieldname
1432
     */
1433
    public function add_searchcondition(condition $searchcondition, ?string $fieldname = null): void {
1434
        if (is_null($fieldname)) {
1435
            $this->searchconditions[] = $searchcondition;
1436
        } else {
1437
            $this->searchconditions[$fieldname] = $searchcondition;
1438
        }
1439
    }
1440
 
1441
    /**
1442
     * Add standard search conditions.
1443
     * Params must be set into this object before calling this function.
1444
     */
1445
    public function add_standard_search_conditions(): void {
1446
        foreach ($this->plugins as $componentname => $plugin) {
1447
            if (\core\plugininfo\qbank::is_plugin_enabled($componentname)) {
1448
                $pluginentrypointobject = new $plugin();
1449
                $pluginobjects = $pluginentrypointobject->get_question_filters($this);
1450
                foreach ($pluginobjects as $pluginobject) {
1451
                    $this->add_searchcondition($pluginobject, $pluginobject->get_condition_key());
1452
                }
1453
            }
1454
        }
1455
    }
1456
 
1457
    /**
1458
     * Gets visible columns.
1459
     * @return array Visible columns.
1460
     */
1461
    public function get_visiblecolumns(): array {
1462
        return $this->visiblecolumns;
1463
    }
1464
 
1465
    /**
1466
     * Is this view showing separate versions of a question?
1467
     *
1468
     * @return bool
1469
     */
1470
    public function is_listing_specific_versions(): bool {
1471
        return false;
1472
    }
1473
 
1474
    /**
1475
     * Return array of menu actions.
1476
     *
1477
     * @return question_action_base[]
1478
     */
1479
    public function get_question_actions(): array {
1480
        return $this->questionactions;
1481
    }
1482
 
1483
    /**
1484
     * Display the questions table for the fragment/ajax.
1485
     *
1486
     * @return string HTML for the question table
1487
     */
1488
    public function display_questions_table(): string {
1489
        $this->add_standard_search_conditions();
1490
        $questions = $this->load_questions();
1491
        $questionhtml = '';
1441 ariadna 1492
        if ($this->get_question_count() > 0) {
1 efrain 1493
            $this->load_required_statistics($questions);
1494
            ob_start();
1495
            $this->display_questions($questions, $this->pagevars['qpage'], $this->pagevars['qperpage']);
1496
            $questionhtml = ob_get_clean();
1497
        }
1498
        return $questionhtml;
1499
    }
1500
}