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