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 /** 19 * Class to print a view of the question bank. 20 * 21 * @package core_question 22 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace core_question\bank; 27 defined('MOODLE_INTERNAL') || die(); 28 29 use core_question\bank\search\condition; 30 31 32 /** 33 * This class prints a view of the question bank, including 34 * + Some controls to allow users to to select what is displayed. 35 * + A list of questions as a table. 36 * + Further controls to do things with the questions. 37 * 38 * This class gives a basic view, and provides plenty of hooks where subclasses 39 * can override parts of the display. 40 * 41 * The list of questions presented as a table is generated by creating a list of 42 * core_question\bank\column objects, one for each 'column' to be displayed. These 43 * manage 44 * + outputting the contents of that column, given a $question object, but also 45 * + generating the right fragments of SQL to ensure the necessary data is present, 46 * and sorted in the right order. 47 * + outputting table headers. 48 * 49 * @copyright 2009 Tim Hunt 50 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 51 */ 52 class view { 53 const MAX_SORTS = 3; 54 55 /** 56 * @var \moodle_url base URL for the current page. Used as the 57 * basis for making URLs for actions that reload the page. 58 */ 59 protected $baseurl; 60 61 /** 62 * @var \moodle_url used as a basis for URLs that edit a question. 63 */ 64 protected $editquestionurl; 65 66 /** 67 * @var \question_edit_contexts 68 */ 69 protected $contexts; 70 71 /** 72 * @var object|\cm_info|null if we are in a module context, the cm. 73 */ 74 protected $cm; 75 76 /** 77 * @var object the course we are within. 78 */ 79 protected $course; 80 81 /** 82 * @var \question_bank_column_base[] these are all the 'columns' that are 83 * part of the display. Array keys are the class name. 84 */ 85 protected $requiredcolumns; 86 87 /** 88 * @var \question_bank_column_base[] these are the 'columns' that are 89 * actually displayed as a column, in order. Array keys are the class name. 90 */ 91 protected $visiblecolumns; 92 93 /** 94 * @var \question_bank_column_base[] these are the 'columns' that are 95 * actually displayed as an additional row (e.g. question text), in order. 96 * Array keys are the class name. 97 */ 98 protected $extrarows; 99 100 /** 101 * @var array list of column class names for which columns to sort on. 102 */ 103 protected $sort; 104 105 /** 106 * @var int|null id of the a question to highlight in the list (if present). 107 */ 108 protected $lastchangedid; 109 110 /** 111 * @var string SQL to count the number of questions matching the current 112 * search conditions. 113 */ 114 protected $countsql; 115 116 /** 117 * @var string SQL to actually load the question data to display. 118 */ 119 protected $loadsql; 120 121 /** 122 * @var array params used by $countsql and $loadsql (which currently must be the same). 123 */ 124 protected $sqlparams; 125 126 /** 127 * @var condition[] search conditions. 128 */ 129 protected $searchconditions = array(); 130 131 /** 132 * Constructor 133 * @param \question_edit_contexts $contexts 134 * @param \moodle_url $pageurl 135 * @param object $course course settings 136 * @param object $cm (optional) activity settings. 137 */ 138 public function __construct($contexts, $pageurl, $course, $cm = null) { 139 $this->contexts = $contexts; 140 $this->baseurl = $pageurl; 141 $this->course = $course; 142 $this->cm = $cm; 143 144 // Create the url of the new question page to forward to. 145 $returnurl = $pageurl->out_as_local_url(false); 146 $this->editquestionurl = new \moodle_url('/question/question.php', 147 array('returnurl' => $returnurl)); 148 if ($cm !== null) { 149 $this->editquestionurl->param('cmid', $cm->id); 150 } else { 151 $this->editquestionurl->param('courseid', $this->course->id); 152 } 153 154 $this->lastchangedid = optional_param('lastchanged', 0, PARAM_INT); 155 156 $this->init_columns($this->wanted_columns(), $this->heading_column()); 157 $this->init_sort(); 158 $this->init_search_conditions(); 159 } 160 161 /** 162 * Initialize search conditions from plugins 163 * local_*_get_question_bank_search_conditions() must return an array of 164 * \core_question\bank\search\condition objects. 165 */ 166 protected function init_search_conditions() { 167 $searchplugins = get_plugin_list_with_function('local', 'get_question_bank_search_conditions'); 168 foreach ($searchplugins as $component => $function) { 169 foreach ($function($this) as $searchobject) { 170 $this->add_searchcondition($searchobject); 171 } 172 } 173 } 174 175 protected function wanted_columns() { 176 global $CFG; 177 178 if (empty($CFG->questionbankcolumns)) { 179 $questionbankcolumns = array('checkbox_column', 'question_type_column', 180 'question_name_idnumber_tags_column', 'edit_menu_column', 181 'edit_action_column', 'copy_action_column', 'tags_action_column', 182 'preview_action_column', 'delete_action_column', 'export_xml_action_column', 183 'creator_name_column', 'modifier_name_column'); 184 } else { 185 $questionbankcolumns = explode(',', $CFG->questionbankcolumns); 186 } 187 if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new \moodle_url(''))) { 188 $questionbankcolumns[] = 'question_text_row'; 189 } 190 191 foreach ($questionbankcolumns as $fullname) { 192 if (! class_exists($fullname)) { 193 if (class_exists('core_question\\bank\\' . $fullname)) { 194 $fullname = 'core_question\\bank\\' . $fullname; 195 } else { 196 throw new \coding_exception("No such class exists: $fullname"); 197 } 198 } 199 $this->requiredcolumns[$fullname] = new $fullname($this); 200 } 201 return $this->requiredcolumns; 202 } 203 204 205 /** 206 * Get a column object from its name. 207 * 208 * @param string $columnname. 209 * @return \core_question\bank\column_base. 210 */ 211 protected function get_column_type($columnname) { 212 if (! class_exists($columnname)) { 213 if (class_exists('core_question\\bank\\' . $columnname)) { 214 $columnname = 'core_question\\bank\\' . $columnname; 215 } else { 216 throw new \coding_exception("No such class exists: $columnname"); 217 } 218 } 219 if (empty($this->requiredcolumns[$columnname])) { 220 $this->requiredcolumns[$columnname] = new $columnname($this); 221 } 222 return $this->requiredcolumns[$columnname]; 223 } 224 225 /** 226 * Specify the column heading 227 * 228 * @return string Column name for the heading 229 */ 230 protected function heading_column() { 231 return 'question_bank_question_name_column'; 232 } 233 234 /** 235 * Initializing table columns 236 * 237 * @param array $wanted Collection of column names 238 * @param string $heading The name of column that is set as heading 239 */ 240 protected function init_columns($wanted, $heading = '') { 241 // If we are using the edit menu column, allow it to absorb all the actions. 242 foreach ($wanted as $column) { 243 if ($column instanceof edit_menu_column) { 244 $wanted = $column->claim_menuable_columns($wanted); 245 break; 246 } 247 } 248 249 // Now split columns into real columns and rows. 250 $this->visiblecolumns = array(); 251 $this->extrarows = array(); 252 foreach ($wanted as $column) { 253 if ($column->is_extra_row()) { 254 $this->extrarows[get_class($column)] = $column; 255 } else { 256 $this->visiblecolumns[get_class($column)] = $column; 257 } 258 } 259 if (array_key_exists($heading, $this->requiredcolumns)) { 260 $this->requiredcolumns[$heading]->set_as_heading(); 261 } 262 } 263 264 /** 265 * @param string $colname a column internal name. 266 * @return bool is this column included in the output? 267 */ 268 public function has_column($colname) { 269 return isset($this->visiblecolumns[$colname]); 270 } 271 272 /** 273 * @return int The number of columns in the table. 274 */ 275 public function get_column_count() { 276 return count($this->visiblecolumns); 277 } 278 279 public function get_courseid() { 280 return $this->course->id; 281 } 282 283 protected function init_sort() { 284 $this->init_sort_from_params(); 285 if (empty($this->sort)) { 286 $this->sort = $this->default_sort(); 287 } 288 } 289 290 /** 291 * Deal with a sort name of the form columnname, or colname_subsort by 292 * breaking it up, validating the bits that are present, and returning them. 293 * If there is no subsort, then $subsort is returned as ''. 294 * 295 * @param string $sort the sort parameter to process. 296 * @return array array($colname, $subsort). 297 */ 298 protected function parse_subsort($sort) { 299 // Do the parsing. 300 if (strpos($sort, '-') !== false) { 301 list($colname, $subsort) = explode('-', $sort, 2); 302 } else { 303 $colname = $sort; 304 $subsort = ''; 305 } 306 // Validate the column name. 307 $column = $this->get_column_type($colname); 308 if (!isset($column) || !$column->is_sortable()) { 309 for ($i = 1; $i <= self::MAX_SORTS; $i++) { 310 $this->baseurl->remove_params('qbs' . $i); 311 } 312 throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $colname); 313 } 314 // Validate the subsort, if present. 315 if ($subsort) { 316 $subsorts = $column->is_sortable(); 317 if (!is_array($subsorts) || !isset($subsorts[$subsort])) { 318 throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $sort); 319 } 320 } 321 return array($colname, $subsort); 322 } 323 324 protected function init_sort_from_params() { 325 $this->sort = array(); 326 for ($i = 1; $i <= self::MAX_SORTS; $i++) { 327 if (!$sort = optional_param('qbs' . $i, '', PARAM_TEXT)) { 328 break; 329 } 330 // Work out the appropriate order. 331 $order = 1; 332 if ($sort[0] == '-') { 333 $order = -1; 334 $sort = substr($sort, 1); 335 if (!$sort) { 336 break; 337 } 338 } 339 // Deal with subsorts. 340 list($colname) = $this->parse_subsort($sort); 341 $this->requiredcolumns[$colname] = $this->get_column_type($colname); 342 $this->sort[$sort] = $order; 343 } 344 } 345 346 protected function sort_to_params($sorts) { 347 $params = array(); 348 $i = 0; 349 foreach ($sorts as $sort => $order) { 350 $i += 1; 351 if ($order < 0) { 352 $sort = '-' . $sort; 353 } 354 $params['qbs' . $i] = $sort; 355 } 356 return $params; 357 } 358 359 protected function default_sort() { 360 return array( 361 'core_question\bank\question_type_column' => 1, 362 'core_question\bank\question_name_idnumber_tags_column-name' => 1 363 ); 364 } 365 366 /** 367 * @param string $sort a column or column_subsort name. 368 * @return int the current sort order for this column -1, 0, 1 369 */ 370 public function get_primary_sort_order($sort) { 371 $order = reset($this->sort); 372 $primarysort = key($this->sort); 373 if ($sort == $primarysort) { 374 return $order; 375 } else { 376 return 0; 377 } 378 } 379 380 /** 381 * Get a URL to redisplay the page with a new sort for the question bank. 382 * 383 * @param string $sort the column, or column_subsort to sort on. 384 * @param bool $newsortreverse whether to sort in reverse order. 385 * @return string The new URL. 386 */ 387 public function new_sort_url($sort, $newsortreverse) { 388 if ($newsortreverse) { 389 $order = -1; 390 } else { 391 $order = 1; 392 } 393 // Tricky code to add the new sort at the start, removing it from where it was before, if it was present. 394 $newsort = array_reverse($this->sort); 395 if (isset($newsort[$sort])) { 396 unset($newsort[$sort]); 397 } 398 $newsort[$sort] = $order; 399 $newsort = array_reverse($newsort); 400 if (count($newsort) > self::MAX_SORTS) { 401 $newsort = array_slice($newsort, 0, self::MAX_SORTS, true); 402 } 403 return $this->baseurl->out(true, $this->sort_to_params($newsort)); 404 } 405 406 /** 407 * Create the SQL query to retrieve the indicated questions 408 * 409 * @param \stdClass $category no longer used. 410 * @param bool $recurse no longer used. 411 * @param bool $showhidden no longer used. 412 * @deprecated since Moodle 2.7 MDL-40313. 413 * @see build_query() 414 * @see \core_question\bank\search\condition 415 * @todo MDL-41978 This will be deleted in Moodle 2.8 416 */ 417 protected function build_query_sql($category, $recurse, $showhidden) { 418 debugging('build_query_sql() is deprecated, please use \core_question\bank\view::build_query() and ' . 419 '\core_question\bank\search\condition classes instead.', DEBUG_DEVELOPER); 420 self::build_query(); 421 } 422 423 /** 424 * Create the SQL query to retrieve the indicated questions, based on 425 * \core_question\bank\search\condition filters. 426 */ 427 protected function build_query() { 428 // Get the required tables and fields. 429 $joins = array(); 430 $fields = array('q.hidden', 'q.category'); 431 foreach ($this->requiredcolumns as $column) { 432 $extrajoins = $column->get_extra_joins(); 433 foreach ($extrajoins as $prefix => $join) { 434 if (isset($joins[$prefix]) && $joins[$prefix] != $join) { 435 throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]); 436 } 437 $joins[$prefix] = $join; 438 } 439 $fields = array_merge($fields, $column->get_required_fields()); 440 } 441 $fields = array_unique($fields); 442 443 // Build the order by clause. 444 $sorts = array(); 445 foreach ($this->sort as $sort => $order) { 446 list($colname, $subsort) = $this->parse_subsort($sort); 447 $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort); 448 } 449 450 // Build the where clause. 451 $tests = array('q.parent = 0'); 452 $this->sqlparams = array(); 453 foreach ($this->searchconditions as $searchcondition) { 454 if ($searchcondition->where()) { 455 $tests[] = '((' . $searchcondition->where() .'))'; 456 } 457 if ($searchcondition->params()) { 458 $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params()); 459 } 460 } 461 // Build the SQL. 462 $sql = ' FROM {question} q ' . implode(' ', $joins); 463 $sql .= ' WHERE ' . implode(' AND ', $tests); 464 $this->countsql = 'SELECT count(1)' . $sql; 465 $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts); 466 } 467 468 protected function get_question_count() { 469 global $DB; 470 return $DB->count_records_sql($this->countsql, $this->sqlparams); 471 } 472 473 /** 474 * Load the questions we need to display. 475 * 476 * @param int $page page to display. 477 * @param int $perpage number of questions per page. 478 * @return \moodle_recordset questionid => data about each question. 479 */ 480 protected function load_page_questions($page, $perpage) { 481 global $DB; 482 $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage); 483 if (empty($questions)) { 484 $questions->close(); 485 // No questions on this page. Reset to page 0. 486 $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage); 487 } 488 return $questions; 489 } 490 491 public function base_url() { 492 return $this->baseurl; 493 } 494 495 /** 496 * Get the URL for editing a question as a {@link \moodle_url}. 497 * 498 * @param int $questionid the question id. 499 * @return \moodle_url the URL, HTML-escaped. 500 */ 501 public function edit_question_moodle_url($questionid) { 502 return new \moodle_url($this->editquestionurl, ['id' => $questionid]); 503 } 504 505 /** 506 * Get the URL for editing a question as a HTML-escaped string. 507 * 508 * @param int $questionid the question id. 509 * @return string the URL, HTML-escaped. 510 */ 511 public function edit_question_url($questionid) { 512 return $this->edit_question_moodle_url($questionid)->out(); 513 } 514 515 /** 516 * Get the URL for duplicating a question as a {@link \moodle_url}. 517 * 518 * @param int $questionid the question id. 519 * @return \moodle_url the URL. 520 */ 521 public function copy_question_moodle_url($questionid) { 522 return new \moodle_url($this->editquestionurl, ['id' => $questionid, 'makecopy' => 1]); 523 } 524 525 /** 526 * Get the URL for duplicating a given question. 527 * @param int $questionid the question id. 528 * @return string the URL, HTML-escaped. 529 */ 530 public function copy_question_url($questionid) { 531 return $this->copy_question_moodle_url($questionid)->out(); 532 } 533 534 /** 535 * Get the context we are displaying the question bank for. 536 * @return \context context object. 537 */ 538 public function get_most_specific_context() { 539 return $this->contexts->lowest(); 540 } 541 542 /** 543 * Get the URL to preview a question. 544 * @param \stdClass $questiondata the data defining the question. 545 * @return \moodle_url the URL. 546 */ 547 public function preview_question_url($questiondata) { 548 return question_preview_url($questiondata->id, null, null, null, null, 549 $this->get_most_specific_context()); 550 } 551 552 /** 553 * Shows the question bank editing interface. 554 * 555 * The function also processes a number of actions: 556 * 557 * Actions affecting the question pool: 558 * move Moves a question to a different category 559 * deleteselected Deletes the selected questions from the category 560 * Other actions: 561 * category Chooses the category 562 * 563 * @param string $tabname question bank edit tab name, for permission checking. 564 * @param int $page the page number to show. 565 * @param int $perpage the number of questions per page to show. 566 * @param string $cat 'categoryid,contextid'. 567 * @param int $recurse Whether to include subcategories. 568 * @param bool $showhidden whether deleted questions should be displayed. 569 * @param bool $showquestiontext whether the text of each question should be shown in the list. Deprecated. 570 * @param array $tagids current list of selected tags. 571 */ 572 public function display($tabname, $page, $perpage, $cat, 573 $recurse, $showhidden, $showquestiontext, $tagids = []) { 574 global $PAGE, $CFG; 575 576 if ($this->process_actions_needing_ui()) { 577 return; 578 } 579 $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname); 580 list(, $contextid) = explode(',', $cat); 581 $catcontext = \context::instance_by_id($contextid); 582 $thiscontext = $this->get_most_specific_context(); 583 // Category selection form. 584 $this->display_question_bank_header(); 585 586 // Display tag filter if usetags setting is enabled. 587 if ($CFG->usetags) { 588 array_unshift($this->searchconditions, 589 new \core_question\bank\search\tag_condition([$catcontext, $thiscontext], $tagids)); 590 $PAGE->requires->js_call_amd('core_question/edit_tags', 'init', ['#questionscontainer']); 591 } 592 593 array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden)); 594 array_unshift($this->searchconditions, new \core_question\bank\search\category_condition( 595 $cat, $recurse, $editcontexts, $this->baseurl, $this->course)); 596 $this->display_options_form($showquestiontext); 597 598 // Continues with list of questions. 599 $this->display_question_list($editcontexts, 600 $this->baseurl, $cat, $this->cm, 601 null, $page, $perpage, $showhidden, $showquestiontext, 602 $this->contexts->having_cap('moodle/question:add')); 603 604 } 605 606 protected function print_choose_category_message($categoryandcontext) { 607 echo "<p style=\"text-align:center;\"><b>"; 608 print_string('selectcategoryabove', 'question'); 609 echo "</b></p>"; 610 } 611 612 protected function get_current_category($categoryandcontext) { 613 global $DB, $OUTPUT; 614 list($categoryid, $contextid) = explode(',', $categoryandcontext); 615 if (!$categoryid) { 616 $this->print_choose_category_message($categoryandcontext); 617 return false; 618 } 619 620 if (!$category = $DB->get_record('question_categories', 621 array('id' => $categoryid, 'contextid' => $contextid))) { 622 echo $OUTPUT->box_start('generalbox questionbank'); 623 echo $OUTPUT->notification('Category not found!'); 624 echo $OUTPUT->box_end(); 625 return false; 626 } 627 628 return $category; 629 } 630 631 /** 632 * prints category information 633 * @param \stdClass $category the category row from the database. 634 * @deprecated since Moodle 2.7 MDL-40313. 635 * @see \core_question\bank\search\condition 636 * @todo MDL-41978 This will be deleted in Moodle 2.8 637 */ 638 protected function print_category_info($category) { 639 $formatoptions = new \stdClass(); 640 $formatoptions->noclean = true; 641 $formatoptions->overflowdiv = true; 642 echo '<div class="boxaligncenter">'; 643 echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id); 644 echo "</div>\n"; 645 } 646 647 /** 648 * Prints a form to choose categories 649 * @deprecated since Moodle 2.7 MDL-40313. 650 * @see \core_question\bank\search\condition 651 * @todo MDL-41978 This will be deleted in Moodle 2.8 652 */ 653 protected function display_category_form($contexts, $pageurl, $current) { 654 global $OUTPUT; 655 656 debugging('display_category_form() is deprecated, please use ' . 657 '\core_question\bank\search\condition instead.', DEBUG_DEVELOPER); 658 // Get all the existing categories now. 659 echo '<div class="choosecategory">'; 660 $catmenu = question_category_options($contexts, false, 0, true); 661 662 $select = new \single_select($this->baseurl, 'category', $catmenu, $current, null, 'catmenu'); 663 $select->set_label(get_string('selectacategory', 'question')); 664 echo $OUTPUT->render($select); 665 echo "</div>\n"; 666 } 667 668 /** 669 * Display the options form. 670 * @param bool $recurse no longer used. 671 * @param bool $showhidden no longer used. 672 * @param bool $showquestiontext whether to show the question text. 673 * @deprecated since Moodle 2.7 MDL-40313. 674 * @see display_options_form 675 * @todo MDL-41978 This will be deleted in Moodle 2.8 676 * @see \core_question\bank\search\condition 677 */ 678 protected function display_options($recurse, $showhidden, $showquestiontext) { 679 debugging('display_options() is deprecated, please use display_options_form instead.', DEBUG_DEVELOPER); 680 $this->display_options_form($showquestiontext); 681 } 682 683 /** 684 * Print a single option checkbox. 685 * @deprecated since Moodle 2.7 MDL-40313. 686 * @see \core_question\bank\search\condition 687 * @see html_writer::checkbox 688 * @todo MDL-41978 This will be deleted in Moodle 2.8 689 */ 690 protected function display_category_form_checkbox($name, $value, $label) { 691 debugging('display_category_form_checkbox() is deprecated, ' . 692 'please use \core_question\bank\search\condition instead.', DEBUG_DEVELOPER); 693 echo '<div><input type="hidden" id="' . $name . '_off" name="' . $name . '" value="0" />'; 694 echo '<input type="checkbox" id="' . $name . '_on" name="' . $name . '" value="1"'; 695 if ($value) { 696 echo ' checked="checked"'; 697 } 698 echo ' onchange="getElementById(\'displayoptions\').submit(); return true;" />'; 699 echo '<label for="' . $name . '_on">' . $label . '</label>'; 700 echo "</div>\n"; 701 } 702 703 /** 704 * Display the form with options for which questions are displayed and how they are displayed. 705 * @param bool $showquestiontext Display the text of the question within the list. 706 * @param string $scriptpath path to the script displaying this page. 707 * @param bool $showtextoption whether to include the 'Show question text' checkbox. 708 */ 709 protected function display_options_form($showquestiontext, $scriptpath = '/question/edit.php', 710 $showtextoption = true) { 711 global $PAGE; 712 713 echo \html_writer::start_tag('form', array('method' => 'get', 714 'action' => new \moodle_url($scriptpath), 'id' => 'displayoptions')); 715 echo \html_writer::start_div(); 716 717 $excludes = array('recurse', 'showhidden', 'qbshowtext'); 718 // If the URL contains any tags then we need to prevent them 719 // being added to the form as hidden elements because the tags 720 // are managed separately. 721 if ($this->baseurl->param('qtagids[0]')) { 722 $index = 0; 723 while ($this->baseurl->param("qtagids[{$index}]")) { 724 $excludes[] = "qtagids[{$index}]"; 725 $index++; 726 } 727 } 728 echo \html_writer::input_hidden_params($this->baseurl, $excludes); 729 730 foreach ($this->searchconditions as $searchcondition) { 731 echo $searchcondition->display_options(); 732 } 733 if ($showtextoption) { 734 $this->display_showtext_checkbox($showquestiontext); 735 } 736 $this->display_advanced_search_form(); 737 $go = \html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('go'))); 738 echo \html_writer::tag('noscript', \html_writer::div($go), array('class' => 'inline')); 739 echo \html_writer::end_div(); 740 echo \html_writer::end_tag('form'); 741 $PAGE->requires->yui_module('moodle-question-searchform', 'M.question.searchform.init'); 742 } 743 744 /** 745 * Print the "advanced" UI elements for the form to select which questions. Hidden by default. 746 */ 747 protected function display_advanced_search_form() { 748 print_collapsible_region_start('', 'advancedsearch', get_string('advancedsearchoptions', 'question'), 749 'question_bank_advanced_search'); 750 foreach ($this->searchconditions as $searchcondition) { 751 echo $searchcondition->display_options_adv(); 752 } 753 print_collapsible_region_end(); 754 } 755 756 /** 757 * Display the checkbox UI for toggling the display of the question text in the list. 758 * @param bool $showquestiontext the current or default value for whether to display the text. 759 */ 760 protected function display_showtext_checkbox($showquestiontext) { 761 echo '<div>'; 762 echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'qbshowtext', 763 'value' => 0, 'id' => 'qbshowtext_off')); 764 echo \html_writer::checkbox('qbshowtext', '1', $showquestiontext, ' ' . get_string('showquestiontext', 'question'), 765 array('id' => 'qbshowtext_on', 'class' => 'searchoptions mr-1')); 766 echo "</div>\n"; 767 } 768 769 /** 770 * Display the header element for the question bank. 771 */ 772 protected function display_question_bank_header() { 773 global $OUTPUT; 774 echo $OUTPUT->heading(get_string('questionbank', 'question'), 2); 775 } 776 777 protected function create_new_question_form($category, $canadd) { 778 echo '<div class="createnewquestion">'; 779 if ($canadd) { 780 create_new_question_button($category->id, $this->editquestionurl->params(), 781 get_string('createnewquestion', 'question')); 782 } else { 783 print_string('nopermissionadd', 'question'); 784 } 785 echo '</div>'; 786 } 787 788 /** 789 * Prints the table of questions in a category with interactions 790 * 791 * @param array $contexts Not used! 792 * @param \moodle_url $pageurl The URL to reload this page. 793 * @param string $categoryandcontext 'categoryID,contextID'. 794 * @param \stdClass $cm Not used! 795 * @param int $recurse Whether to include subcategories. 796 * @param int $page The number of the page to be displayed 797 * @param int $perpage Number of questions to show per page 798 * @param bool $showhidden Not used! This is now controlled in a different way. 799 * @param bool $showquestiontext Not used! This is now controlled in a different way. 800 * @param array $addcontexts contexts where the user is allowed to add new questions. 801 */ 802 protected function display_question_list($contexts, $pageurl, $categoryandcontext, 803 $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false, 804 $showquestiontext = false, $addcontexts = array()) { 805 global $OUTPUT; 806 807 // This function can be moderately slow with large question counts and may time out. 808 // We probably do not want to raise it to unlimited, so randomly picking 5 minutes. 809 // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc). 810 \core_php_time_limit::raise(300); 811 812 $category = $this->get_current_category($categoryandcontext); 813 814 list($categoryid, $contextid) = explode(',', $categoryandcontext); 815 $catcontext = \context::instance_by_id($contextid); 816 817 $canadd = has_capability('moodle/question:add', $catcontext); 818 819 $this->create_new_question_form($category, $canadd); 820 821 $this->build_query(); 822 $totalnumber = $this->get_question_count(); 823 if ($totalnumber == 0) { 824 return; 825 } 826 $questionsrs = $this->load_page_questions($page, $perpage); 827 $questions = []; 828 foreach ($questionsrs as $question) { 829 $questions[$question->id] = $question; 830 } 831 $questionsrs->close(); 832 foreach ($this->requiredcolumns as $name => $column) { 833 $column->load_additional_data($questions); 834 } 835 836 echo '<div class="categorypagingbarcontainer">'; 837 $pageingurl = new \moodle_url('edit.php', $pageurl->params()); 838 $pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl); 839 $pagingbar->pagevar = 'qpage'; 840 echo $OUTPUT->render($pagingbar); 841 echo '</div>'; 842 843 echo '<form method="post" action="edit.php">'; 844 echo '<fieldset class="invisiblefieldset" style="display: block;">'; 845 echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />'; 846 echo \html_writer::input_hidden_params($this->baseurl); 847 848 echo '<div class="categoryquestionscontainer" id="questionscontainer">'; 849 $this->start_table(); 850 $rowcount = 0; 851 foreach ($questions as $question) { 852 $this->print_table_row($question, $rowcount); 853 $rowcount += 1; 854 } 855 $this->end_table(); 856 echo "</div>\n"; 857 858 echo '<div class="categorypagingbarcontainer pagingbottom">'; 859 echo $OUTPUT->render($pagingbar); 860 if ($totalnumber > DEFAULT_QUESTIONS_PER_PAGE) { 861 if ($perpage == DEFAULT_QUESTIONS_PER_PAGE) { 862 $url = new \moodle_url('edit.php', array_merge($pageurl->params(), 863 array('qpage' => 0, 'qperpage' => MAXIMUM_QUESTIONS_PER_PAGE))); 864 if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE) { 865 $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', MAXIMUM_QUESTIONS_PER_PAGE).'</a>'; 866 } else { 867 $showall = '<a href="'.$url.'">'.get_string('showall', 'moodle', $totalnumber).'</a>'; 868 } 869 } else { 870 $url = new \moodle_url('edit.php', array_merge($pageurl->params(), 871 array('qperpage' => DEFAULT_QUESTIONS_PER_PAGE))); 872 $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE).'</a>'; 873 } 874 echo "<div class='paging'>{$showall}</div>"; 875 } 876 echo '</div>'; 877 878 $this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts); 879 880 echo '</fieldset>'; 881 echo "</form>\n"; 882 } 883 884 /** 885 * Display the controls at the bottom of the list of questions. 886 * @param int $totalnumber Total number of questions that might be shown (if it was not for paging). 887 * @param bool $recurse Whether to include subcategories. 888 * @param \stdClass $category The question_category row from the database. 889 * @param \context $catcontext The context of the category being displayed. 890 * @param array $addcontexts contexts where the user is allowed to add new questions. 891 */ 892 protected function display_bottom_controls($totalnumber, $recurse, $category, \context $catcontext, array $addcontexts) { 893 $caneditall = has_capability('moodle/question:editall', $catcontext); 894 $canuseall = has_capability('moodle/question:useall', $catcontext); 895 $canmoveall = has_capability('moodle/question:moveall', $catcontext); 896 897 echo '<div class="modulespecificbuttonscontainer">'; 898 if ($caneditall || $canmoveall || $canuseall) { 899 echo '<strong> '.get_string('withselected', 'question').':</strong><br />'; 900 901 // Print delete and move selected question. 902 if ($caneditall) { 903 echo \html_writer::empty_tag('input', [ 904 'type' => 'submit', 905 'class' => 'btn btn-secondary mr-1', 906 'name' => 'deleteselected', 907 'value' => get_string('delete'), 908 'data-action' => 'toggle', 909 'data-togglegroup' => 'qbank', 910 'data-toggle' => 'action', 911 'disabled' => true, 912 ]); 913 } 914 915 if ($canmoveall && count($addcontexts)) { 916 echo \html_writer::empty_tag('input', [ 917 'type' => 'submit', 918 'class' => 'btn btn-secondary mr-1', 919 'name' => 'move', 920 'value' => get_string('moveto', 'question'), 921 'data-action' => 'toggle', 922 'data-togglegroup' => 'qbank', 923 'data-toggle' => 'action', 924 'disabled' => true, 925 ]); 926 question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}"); 927 } 928 } 929 echo "</div>\n"; 930 } 931 932 protected function start_table() { 933 echo '<table id="categoryquestions">' . "\n"; 934 echo "<thead>\n"; 935 $this->print_table_headers(); 936 echo "</thead>\n"; 937 echo "<tbody>\n"; 938 } 939 940 protected function end_table() { 941 echo "</tbody>\n"; 942 echo "</table>\n"; 943 } 944 945 protected function print_table_headers() { 946 echo "<tr>\n"; 947 foreach ($this->visiblecolumns as $column) { 948 $column->display_header(); 949 } 950 echo "</tr>\n"; 951 } 952 953 protected function get_row_classes($question, $rowcount) { 954 $classes = array(); 955 if ($question->hidden) { 956 $classes[] = 'dimmed_text'; 957 } 958 if ($question->id == $this->lastchangedid) { 959 $classes[] = 'highlight text-dark'; 960 } 961 $classes[] = 'r' . ($rowcount % 2); 962 return $classes; 963 } 964 965 protected function print_table_row($question, $rowcount) { 966 $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount)); 967 if ($rowclasses) { 968 echo '<tr class="' . $rowclasses . '">' . "\n"; 969 } else { 970 echo "<tr>\n"; 971 } 972 foreach ($this->visiblecolumns as $column) { 973 $column->display($question, $rowclasses); 974 } 975 echo "</tr>\n"; 976 foreach ($this->extrarows as $row) { 977 $row->display($question, $rowclasses); 978 } 979 } 980 981 public function process_actions() { 982 global $DB; 983 // Now, check for commands on this page and modify variables as necessary. 984 if (optional_param('move', false, PARAM_BOOL) and confirm_sesskey()) { 985 // Move selected questions to new category. 986 $category = required_param('category', PARAM_SEQUENCE); 987 list($tocategoryid, $contextid) = explode(',', $category); 988 if (! $tocategory = $DB->get_record('question_categories', array('id' => $tocategoryid, 'contextid' => $contextid))) { 989 print_error('cannotfindcate', 'question'); 990 } 991 $tocontext = \context::instance_by_id($contextid); 992 require_capability('moodle/question:add', $tocontext); 993 $rawdata = (array) data_submitted(); 994 $questionids = array(); 995 foreach ($rawdata as $key => $value) { // Parse input for question ids. 996 if (preg_match('!^q([0-9]+)$!', $key, $matches)) { 997 $key = $matches[1]; 998 $questionids[] = $key; 999 } 1000 } 1001 if ($questionids) { 1002 list($usql, $params) = $DB->get_in_or_equal($questionids); 1003 $questions = $DB->get_records_sql(" 1004 SELECT q.*, c.contextid 1005 FROM {question} q 1006 JOIN {question_categories} c ON c.id = q.category 1007 WHERE q.id {$usql}", $params); 1008 foreach ($questions as $question) { 1009 question_require_capability_on($question, 'move'); 1010 } 1011 question_move_questions_to_category($questionids, $tocategory->id); 1012 redirect($this->baseurl->out(false, 1013 array('category' => "{$tocategoryid},{$contextid}"))); 1014 } 1015 } 1016 1017 if (optional_param('deleteselected', false, PARAM_BOOL)) { // Delete selected questions from the category. 1018 // If teacher has already confirmed the action. 1019 if (($confirm = optional_param('confirm', '', PARAM_ALPHANUM)) and confirm_sesskey()) { 1020 $deleteselected = required_param('deleteselected', PARAM_RAW); 1021 if ($confirm == md5($deleteselected)) { 1022 if ($questionlist = explode(',', $deleteselected)) { 1023 // For each question either hide it if it is in use or delete it. 1024 foreach ($questionlist as $questionid) { 1025 $questionid = (int)$questionid; 1026 question_require_capability_on($questionid, 'edit'); 1027 if (questions_in_use(array($questionid))) { 1028 $DB->set_field('question', 'hidden', 1, array('id' => $questionid)); 1029 } else { 1030 question_delete_question($questionid); 1031 } 1032 } 1033 } 1034 redirect($this->baseurl); 1035 } else { 1036 print_error('invalidconfirm', 'question'); 1037 } 1038 } 1039 } 1040 1041 // Unhide a question. 1042 if (($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) { 1043 question_require_capability_on($unhide, 'edit'); 1044 $DB->set_field('question', 'hidden', 0, array('id' => $unhide)); 1045 1046 // Purge these questions from the cache. 1047 \question_bank::notify_question_edited($unhide); 1048 1049 redirect($this->baseurl); 1050 } 1051 } 1052 1053 public function process_actions_needing_ui() { 1054 global $DB, $OUTPUT; 1055 if (optional_param('deleteselected', false, PARAM_BOOL)) { 1056 // Make a list of all the questions that are selected. 1057 $rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted. 1058 $questionlist = ''; // comma separated list of ids of questions to be deleted 1059 $questionnames = ''; // string with names of questions separated by <br /> with 1060 // an asterix in front of those that are in use 1061 $inuse = false; // set to true if at least one of the questions is in use 1062 foreach ($rawquestions as $key => $value) { // Parse input for question ids. 1063 if (preg_match('!^q([0-9]+)$!', $key, $matches)) { 1064 $key = $matches[1]; 1065 $questionlist .= $key.','; 1066 question_require_capability_on((int)$key, 'edit'); 1067 if (questions_in_use(array($key))) { 1068 $questionnames .= '* '; 1069 $inuse = true; 1070 } 1071 $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />'; 1072 } 1073 } 1074 if (!$questionlist) { // No questions were selected. 1075 redirect($this->baseurl); 1076 } 1077 $questionlist = rtrim($questionlist, ','); 1078 1079 // Add an explanation about questions in use. 1080 if ($inuse) { 1081 $questionnames .= '<br />'.get_string('questionsinuse', 'question'); 1082 } 1083 $baseurl = new \moodle_url('edit.php', $this->baseurl->params()); 1084 $deleteurl = new \moodle_url($baseurl, array('deleteselected' => $questionlist, 'confirm' => md5($questionlist), 1085 'sesskey' => sesskey())); 1086 1087 $continue = new \single_button($deleteurl, get_string('delete'), 'post'); 1088 echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $continue, $baseurl); 1089 1090 return true; 1091 } 1092 1093 return false; 1094 } 1095 1096 /** 1097 * Add another search control to this view. 1098 * @param condition $searchcondition the condition to add. 1099 */ 1100 public function add_searchcondition($searchcondition) { 1101 $this->searchconditions[] = $searchcondition; 1102 } 1103 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body