PageRenderTime 62ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/question/classes/bank/view.php

https://github.com/dongsheng/moodle
PHP | 940 lines | 615 code | 93 blank | 232 comment | 89 complexity | 14d84fc58bf4db34dc2052bb78b26541 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, GPL-3.0, Apache-2.0, LGPL-2.1
  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. namespace core_question\bank;
  17. /**
  18. * Functions used to show question editing interface
  19. *
  20. * @package moodlecore
  21. * @subpackage questionbank
  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. * This class prints a view of the question bank, including
  27. * + Some controls to allow users to to select what is displayed.
  28. * + A list of questions as a table.
  29. * + Further controls to do things with the questions.
  30. *
  31. * This class gives a basic view, and provides plenty of hooks where subclasses
  32. * can override parts of the display.
  33. *
  34. * The list of questions presented as a table is generated by creating a list of
  35. * core_question\bank\column objects, one for each 'column' to be displayed. These
  36. * manage
  37. * + outputting the contents of that column, given a $question object, but also
  38. * + generating the right fragments of SQL to ensure the necessary data is present,
  39. * and sorted in the right order.
  40. * + outputting table headers.
  41. *
  42. * @copyright 2009 Tim Hunt
  43. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44. */
  45. class view {
  46. const MAX_SORTS = 3;
  47. protected $baseurl;
  48. protected $editquestionurl;
  49. protected $quizorcourseid;
  50. protected $contexts;
  51. protected $cm;
  52. protected $course;
  53. protected $visiblecolumns;
  54. protected $extrarows;
  55. protected $requiredcolumns;
  56. protected $sort;
  57. protected $lastchangedid;
  58. protected $countsql;
  59. protected $loadsql;
  60. protected $sqlparams;
  61. /** @var array of \core_question\bank\search\condition objects. */
  62. protected $searchconditions = array();
  63. /**
  64. * Constructor
  65. * @param question_edit_contexts $contexts
  66. * @param moodle_url $pageurl
  67. * @param object $course course settings
  68. * @param object $cm (optional) activity settings.
  69. */
  70. public function __construct($contexts, $pageurl, $course, $cm = null) {
  71. global $CFG, $PAGE;
  72. $this->contexts = $contexts;
  73. $this->baseurl = $pageurl;
  74. $this->course = $course;
  75. $this->cm = $cm;
  76. if (!empty($cm) && $cm->modname == 'quiz') {
  77. $this->quizorcourseid = '&amp;quizid=' . $cm->instance;
  78. } else {
  79. $this->quizorcourseid = '&amp;courseid=' .$this->course->id;
  80. }
  81. // Create the url of the new question page to forward to.
  82. $returnurl = $pageurl->out_as_local_url(false);
  83. $this->editquestionurl = new \moodle_url('/question/question.php',
  84. array('returnurl' => $returnurl));
  85. if ($cm !== null) {
  86. $this->editquestionurl->param('cmid', $cm->id);
  87. } else {
  88. $this->editquestionurl->param('courseid', $this->course->id);
  89. }
  90. $this->lastchangedid = optional_param('lastchanged', 0, PARAM_INT);
  91. $this->init_columns($this->wanted_columns(), $this->heading_column());
  92. $this->init_sort();
  93. $this->init_search_conditions($this->contexts, $this->course, $this->cm);
  94. }
  95. /**
  96. * Initialize search conditions from plugins
  97. * local_*_get_question_bank_search_conditions() must return an array of
  98. * \core_question\bank\search\condition objects.
  99. */
  100. protected function init_search_conditions() {
  101. $searchplugins = get_plugin_list_with_function('local', 'get_question_bank_search_conditions');
  102. foreach ($searchplugins as $component => $function) {
  103. foreach ($function($this) as $searchobject) {
  104. $this->add_searchcondition($searchobject);
  105. }
  106. }
  107. }
  108. protected function wanted_columns() {
  109. global $CFG;
  110. if (empty($CFG->questionbankcolumns)) {
  111. $questionbankcolumns = array('checkbox_column', 'question_type_column',
  112. 'question_name_column', 'edit_action_column', 'copy_action_column',
  113. 'preview_action_column', 'delete_action_column',
  114. 'creator_name_column',
  115. 'modifier_name_column');
  116. } else {
  117. $questionbankcolumns = explode(',', $CFG->questionbankcolumns);
  118. }
  119. if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL, new \moodle_url(''))) {
  120. $questionbankcolumns[] = 'question_text_row';
  121. }
  122. foreach ($questionbankcolumns as $fullname) {
  123. if (! class_exists($fullname)) {
  124. if (class_exists('core_question\\bank\\' . $fullname)) {
  125. $fullname = 'core_question\\bank\\' . $fullname;
  126. } else {
  127. throw new \coding_exception("No such class exists: $fullname");
  128. }
  129. }
  130. $this->requiredcolumns[$fullname] = new $fullname($this);
  131. }
  132. return $this->requiredcolumns;
  133. }
  134. /**
  135. * Get a column object from its name.
  136. *
  137. * @param string $columnname.
  138. * @return \core_question\bank\column_base.
  139. */
  140. protected function get_column_type($columnname) {
  141. if (! class_exists($columnname)) {
  142. if (class_exists('core_question\\bank\\' . $columnname)) {
  143. $columnname = 'core_question\\bank\\' . $columnname;
  144. } else {
  145. throw new \coding_exception("No such class exists: $columnname");
  146. }
  147. }
  148. if (empty($this->requiredcolumns[$columnname])) {
  149. $this->requiredcolumns[$columnname] = new $columnname($this);
  150. }
  151. return $this->requiredcolumns[$columnname];
  152. }
  153. /**
  154. * Specify the column heading
  155. *
  156. * @return string Column name for the heading
  157. */
  158. protected function heading_column() {
  159. return 'question_bank_question_name_column';
  160. }
  161. /**
  162. * Initializing table columns
  163. *
  164. * @param array $wanted Collection of column names
  165. * @param string $heading The name of column that is set as heading
  166. */
  167. protected function init_columns($wanted, $heading = '') {
  168. $this->visiblecolumns = array();
  169. $this->extrarows = array();
  170. foreach ($wanted as $column) {
  171. if ($column->is_extra_row()) {
  172. $this->extrarows[get_class($column)] = $column;
  173. } else {
  174. $this->visiblecolumns[get_class($column)] = $column;
  175. }
  176. }
  177. if (array_key_exists($heading, $this->requiredcolumns)) {
  178. $this->requiredcolumns[$heading]->set_as_heading();
  179. }
  180. }
  181. /**
  182. * @param string $colname a column internal name.
  183. * @return bool is this column included in the output?
  184. */
  185. public function has_column($colname) {
  186. return isset($this->visiblecolumns[$colname]);
  187. }
  188. /**
  189. * @return int The number of columns in the table.
  190. */
  191. public function get_column_count() {
  192. return count($this->visiblecolumns);
  193. }
  194. public function get_courseid() {
  195. return $this->course->id;
  196. }
  197. protected function init_sort() {
  198. $this->init_sort_from_params();
  199. if (empty($this->sort)) {
  200. $this->sort = $this->default_sort();
  201. }
  202. }
  203. /**
  204. * Deal with a sort name of the form columnname, or colname_subsort by
  205. * breaking it up, validating the bits that are presend, and returning them.
  206. * If there is no subsort, then $subsort is returned as ''.
  207. * @return array array($colname, $subsort).
  208. */
  209. protected function parse_subsort($sort) {
  210. // Do the parsing.
  211. if (strpos($sort, '-') !== false) {
  212. list($colname, $subsort) = explode('-', $sort, 2);
  213. } else {
  214. $colname = $sort;
  215. $subsort = '';
  216. }
  217. // Validate the column name.
  218. $column = $this->get_column_type($colname);
  219. if (!isset($column) || !$column->is_sortable()) {
  220. for ($i = 1; $i <= self::MAX_SORTS; $i++) {
  221. $this->baseurl->remove_params('qbs' . $i);
  222. }
  223. throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $colname);
  224. }
  225. // Validate the subsort, if present.
  226. if ($subsort) {
  227. $subsorts = $column->is_sortable();
  228. if (!is_array($subsorts) || !isset($subsorts[$subsort])) {
  229. throw new \moodle_exception('unknownsortcolumn', '', $link = $this->baseurl->out(), $sort);
  230. }
  231. }
  232. return array($colname, $subsort);
  233. }
  234. protected function init_sort_from_params() {
  235. $this->sort = array();
  236. for ($i = 1; $i <= self::MAX_SORTS; $i++) {
  237. if (!$sort = optional_param('qbs' . $i, '', PARAM_TEXT)) {
  238. break;
  239. }
  240. // Work out the appropriate order.
  241. $order = 1;
  242. if ($sort[0] == '-') {
  243. $order = -1;
  244. $sort = substr($sort, 1);
  245. if (!$sort) {
  246. break;
  247. }
  248. }
  249. // Deal with subsorts.
  250. list($colname, $subsort) = $this->parse_subsort($sort);
  251. $this->requiredcolumns[$colname] = $this->get_column_type($colname);
  252. $this->sort[$sort] = $order;
  253. }
  254. }
  255. protected function sort_to_params($sorts) {
  256. $params = array();
  257. $i = 0;
  258. foreach ($sorts as $sort => $order) {
  259. $i += 1;
  260. if ($order < 0) {
  261. $sort = '-' . $sort;
  262. }
  263. $params['qbs' . $i] = $sort;
  264. }
  265. return $params;
  266. }
  267. protected function default_sort() {
  268. return array('core_question\bank\question_type_column' => 1, 'core_question\bank\question_name_column' => 1);
  269. }
  270. /**
  271. * @param $sort a column or column_subsort name.
  272. * @return int the current sort order for this column -1, 0, 1
  273. */
  274. public function get_primary_sort_order($sort) {
  275. $order = reset($this->sort);
  276. $primarysort = key($this->sort);
  277. if ($sort == $primarysort) {
  278. return $order;
  279. } else {
  280. return 0;
  281. }
  282. }
  283. /**
  284. * Get a URL to redisplay the page with a new sort for the question bank.
  285. * @param string $sort the column, or column_subsort to sort on.
  286. * @param bool $newsortreverse whether to sort in reverse order.
  287. * @return string The new URL.
  288. */
  289. public function new_sort_url($sort, $newsortreverse) {
  290. if ($newsortreverse) {
  291. $order = -1;
  292. } else {
  293. $order = 1;
  294. }
  295. // Tricky code to add the new sort at the start, removing it from where it was before, if it was present.
  296. $newsort = array_reverse($this->sort);
  297. if (isset($newsort[$sort])) {
  298. unset($newsort[$sort]);
  299. }
  300. $newsort[$sort] = $order;
  301. $newsort = array_reverse($newsort);
  302. if (count($newsort) > self::MAX_SORTS) {
  303. $newsort = array_slice($newsort, 0, self::MAX_SORTS, true);
  304. }
  305. return $this->baseurl->out(true, $this->sort_to_params($newsort));
  306. }
  307. /**
  308. * Create the SQL query to retrieve the indicated questions
  309. * @param stdClass $category no longer used.
  310. * @param bool $recurse no longer used.
  311. * @param bool $showhidden no longer used.
  312. * @deprecated since Moodle 2.7 MDL-40313.
  313. * @see build_query()
  314. * @see \core_question\bank\search\condition
  315. * @todo MDL-41978 This will be deleted in Moodle 2.8
  316. */
  317. protected function build_query_sql($category, $recurse, $showhidden) {
  318. debugging('build_query_sql() is deprecated, please use \core_question\bank\view::build_query() and ' .
  319. '\core_question\bank\search\condition classes instead.', DEBUG_DEVELOPER);
  320. self::build_query();
  321. }
  322. /**
  323. * Create the SQL query to retrieve the indicated questions, based on
  324. * \core_question\bank\search\condition filters.
  325. */
  326. protected function build_query() {
  327. global $DB;
  328. // Get the required tables and fields.
  329. $joins = array();
  330. $fields = array('q.hidden', 'q.category');
  331. foreach ($this->requiredcolumns as $column) {
  332. $extrajoins = $column->get_extra_joins();
  333. foreach ($extrajoins as $prefix => $join) {
  334. if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
  335. throw new \coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
  336. }
  337. $joins[$prefix] = $join;
  338. }
  339. $fields = array_merge($fields, $column->get_required_fields());
  340. }
  341. $fields = array_unique($fields);
  342. // Build the order by clause.
  343. $sorts = array();
  344. foreach ($this->sort as $sort => $order) {
  345. list($colname, $subsort) = $this->parse_subsort($sort);
  346. $sorts[] = $this->requiredcolumns[$colname]->sort_expression($order < 0, $subsort);
  347. }
  348. // Build the where clause.
  349. $tests = array('q.parent = 0');
  350. $this->sqlparams = array();
  351. foreach ($this->searchconditions as $searchcondition) {
  352. if ($searchcondition->where()) {
  353. $tests[] = '((' . $searchcondition->where() .'))';
  354. }
  355. if ($searchcondition->params()) {
  356. $this->sqlparams = array_merge($this->sqlparams, $searchcondition->params());
  357. }
  358. }
  359. // Build the SQL.
  360. $sql = ' FROM {question} q ' . implode(' ', $joins);
  361. $sql .= ' WHERE ' . implode(' AND ', $tests);
  362. $this->countsql = 'SELECT count(1)' . $sql;
  363. $this->loadsql = 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
  364. }
  365. protected function get_question_count() {
  366. global $DB;
  367. return $DB->count_records_sql($this->countsql, $this->sqlparams);
  368. }
  369. protected function load_page_questions($page, $perpage) {
  370. global $DB;
  371. $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, $page * $perpage, $perpage);
  372. if (!$questions->valid()) {
  373. // No questions on this page. Reset to page 0.
  374. $questions = $DB->get_recordset_sql($this->loadsql, $this->sqlparams, 0, $perpage);
  375. }
  376. return $questions;
  377. }
  378. public function base_url() {
  379. return $this->baseurl;
  380. }
  381. public function edit_question_url($questionid) {
  382. return $this->editquestionurl->out(true, array('id' => $questionid));
  383. }
  384. /**
  385. * Get the URL for duplicating a given question.
  386. * @param int $questionid the question id.
  387. * @return moodle_url the URL.
  388. */
  389. public function copy_question_url($questionid) {
  390. return $this->editquestionurl->out(true, array('id' => $questionid, 'makecopy' => 1));
  391. }
  392. /**
  393. * Get the context we are displaying the question bank for.
  394. * @return context context object.
  395. */
  396. public function get_most_specific_context() {
  397. return $this->contexts->lowest();
  398. }
  399. /**
  400. * Get the URL to preview a question.
  401. * @param stdClass $questiondata the data defining the question.
  402. * @return moodle_url the URL.
  403. */
  404. public function preview_question_url($questiondata) {
  405. return question_preview_url($questiondata->id, null, null, null, null,
  406. $this->get_most_specific_context());
  407. }
  408. /**
  409. * Shows the question bank editing interface.
  410. *
  411. * The function also processes a number of actions:
  412. *
  413. * Actions affecting the question pool:
  414. * move Moves a question to a different category
  415. * deleteselected Deletes the selected questions from the category
  416. * Other actions:
  417. * category Chooses the category
  418. * displayoptions Sets display options
  419. */
  420. public function display($tabname, $page, $perpage, $cat,
  421. $recurse, $showhidden, $showquestiontext) {
  422. global $PAGE, $OUTPUT;
  423. if ($this->process_actions_needing_ui()) {
  424. return;
  425. }
  426. $editcontexts = $this->contexts->having_one_edit_tab_cap($tabname);
  427. // Category selection form.
  428. echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
  429. array_unshift($this->searchconditions, new \core_question\bank\search\hidden_condition(!$showhidden));
  430. array_unshift($this->searchconditions, new \core_question\bank\search\category_condition(
  431. $cat, $recurse, $editcontexts, $this->baseurl, $this->course));
  432. $this->display_options_form($showquestiontext);
  433. // Continues with list of questions.
  434. $this->display_question_list($this->contexts->having_one_edit_tab_cap($tabname),
  435. $this->baseurl, $cat, $this->cm,
  436. null, $page, $perpage, $showhidden, $showquestiontext,
  437. $this->contexts->having_cap('moodle/question:add'));
  438. }
  439. protected function print_choose_category_message($categoryandcontext) {
  440. echo "<p style=\"text-align:center;\"><b>";
  441. print_string('selectcategoryabove', 'question');
  442. echo "</b></p>";
  443. }
  444. protected function get_current_category($categoryandcontext) {
  445. global $DB, $OUTPUT;
  446. list($categoryid, $contextid) = explode(',', $categoryandcontext);
  447. if (!$categoryid) {
  448. $this->print_choose_category_message($categoryandcontext);
  449. return false;
  450. }
  451. if (!$category = $DB->get_record('question_categories',
  452. array('id' => $categoryid, 'contextid' => $contextid))) {
  453. echo $OUTPUT->box_start('generalbox questionbank');
  454. echo $OUTPUT->notification('Category not found!');
  455. echo $OUTPUT->box_end();
  456. return false;
  457. }
  458. return $category;
  459. }
  460. /**
  461. * prints category information
  462. * @param stdClass $category the category row from the database.
  463. * @deprecated since Moodle 2.7 MDL-40313.
  464. * @see \core_question\bank\search\condition
  465. * @todo MDL-41978 This will be deleted in Moodle 2.8
  466. */
  467. protected function print_category_info($category) {
  468. $formatoptions = new \stdClass();
  469. $formatoptions->noclean = true;
  470. $formatoptions->overflowdiv = true;
  471. echo '<div class="boxaligncenter">';
  472. echo format_text($category->info, $category->infoformat, $formatoptions, $this->course->id);
  473. echo "</div>\n";
  474. }
  475. /**
  476. * Prints a form to choose categories
  477. * @deprecated since Moodle 2.7 MDL-40313.
  478. * @see \core_question\bank\search\condition
  479. * @todo MDL-41978 This will be deleted in Moodle 2.8
  480. */
  481. protected function display_category_form($contexts, $pageurl, $current) {
  482. global $OUTPUT;
  483. debugging('display_category_form() is deprecated, please use ' .
  484. '\core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
  485. // Get all the existing categories now.
  486. echo '<div class="choosecategory">';
  487. $catmenu = question_category_options($contexts, false, 0, true);
  488. $select = new \single_select($this->baseurl, 'category', $catmenu, $current, null, 'catmenu');
  489. $select->set_label(get_string('selectacategory', 'question'));
  490. echo $OUTPUT->render($select);
  491. echo "</div>\n";
  492. }
  493. /**
  494. * Display the options form.
  495. * @param bool $recurse no longer used.
  496. * @param bool $showhidden no longer used.
  497. * @param bool $showquestiontext whether to show the question text.
  498. * @deprecated since Moodle 2.7 MDL-40313.
  499. * @see display_options_form
  500. * @todo MDL-41978 This will be deleted in Moodle 2.8
  501. * @see \core_question\bank\search\condition
  502. */
  503. protected function display_options($recurse, $showhidden, $showquestiontext) {
  504. debugging('display_options() is deprecated, please use display_options_form instead.', DEBUG_DEVELOPER);
  505. return $this->display_options_form($showquestiontext);
  506. }
  507. /**
  508. * Print a single option checkbox.
  509. * @deprecated since Moodle 2.7 MDL-40313.
  510. * @see \core_question\bank\search\condition
  511. * @see html_writer::checkbox
  512. * @todo MDL-41978 This will be deleted in Moodle 2.8
  513. */
  514. protected function display_category_form_checkbox($name, $value, $label) {
  515. debugging('display_category_form_checkbox() is deprecated, ' .
  516. 'please use \core_question\bank\search\condition instead.', DEBUG_DEVELOPER);
  517. echo '<div><input type="hidden" id="' . $name . '_off" name="' . $name . '" value="0" />';
  518. echo '<input type="checkbox" id="' . $name . '_on" name="' . $name . '" value="1"';
  519. if ($value) {
  520. echo ' checked="checked"';
  521. }
  522. echo ' onchange="getElementById(\'displayoptions\').submit(); return true;" />';
  523. echo '<label for="' . $name . '_on">' . $label . '</label>';
  524. echo "</div>\n";
  525. }
  526. /**
  527. * Display the form with options for which questions are displayed and how they are displayed.
  528. * @param bool $showquestiontext Display the text of the question within the list.
  529. * @param string $path path to the script displaying this page.
  530. * @param bool $showtextoption whether to include the 'Show question text' checkbox.
  531. */
  532. protected function display_options_form($showquestiontext, $scriptpath = '/question/edit.php',
  533. $showtextoption = true) {
  534. global $PAGE;
  535. echo \html_writer::start_tag('form', array('method' => 'get',
  536. 'action' => new \moodle_url($scriptpath), 'id' => 'displayoptions'));
  537. echo \html_writer::start_div();
  538. echo \html_writer::input_hidden_params($this->baseurl, array('recurse', 'showhidden', 'qbshowtext'));
  539. foreach ($this->searchconditions as $searchcondition) {
  540. echo $searchcondition->display_options($this);
  541. }
  542. if ($showtextoption) {
  543. $this->display_showtext_checkbox($showquestiontext);
  544. }
  545. $this->display_advanced_search_form();
  546. $go = \html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('go')));
  547. echo \html_writer::tag('noscript', \html_writer::div($go), array('class' => 'inline'));
  548. echo \html_writer::end_div();
  549. echo \html_writer::end_tag('form');
  550. $PAGE->requires->yui_module('moodle-question-searchform', 'M.question.searchform.init');
  551. }
  552. /**
  553. * Print the "advanced" UI elements for the form to select which questions. Hidden by default.
  554. */
  555. protected function display_advanced_search_form() {
  556. print_collapsible_region_start('', 'advancedsearch', get_string('advancedsearchoptions', 'question'),
  557. 'question_bank_advanced_search');
  558. foreach ($this->searchconditions as $searchcondition) {
  559. echo $searchcondition->display_options_adv($this);
  560. }
  561. print_collapsible_region_end();
  562. }
  563. /**
  564. * Display the checkbox UI for toggling the display of the question text in the list.
  565. * @param bool $showquestiontext the current or default value for whether to display the text.
  566. */
  567. protected function display_showtext_checkbox($showquestiontext) {
  568. echo '<div>';
  569. echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'qbshowtext',
  570. 'value' => 0, 'id' => 'qbshowtext_off'));
  571. echo \html_writer::checkbox('qbshowtext', '1', $showquestiontext, get_string('showquestiontext', 'question'),
  572. array('id' => 'qbshowtext_on', 'class' => 'searchoptions'));
  573. echo "</div>\n";
  574. }
  575. protected function create_new_question_form($category, $canadd) {
  576. global $CFG;
  577. echo '<div class="createnewquestion">';
  578. if ($canadd) {
  579. create_new_question_button($category->id, $this->editquestionurl->params(),
  580. get_string('createnewquestion', 'question'));
  581. } else {
  582. print_string('nopermissionadd', 'question');
  583. }
  584. echo '</div>';
  585. }
  586. /**
  587. * Prints the table of questions in a category with interactions
  588. *
  589. * @param array $contexts Not used!
  590. * @param moodle_url $pageurl The URL to reload this page.
  591. * @param string $categoryandcontext 'categoryID,contextID'.
  592. * @param stdClass $cm Not used!
  593. * @param bool $recurse Whether to include subcategories.
  594. * @param int $page The number of the page to be displayed
  595. * @param int $perpage Number of questions to show per page
  596. * @param bool $showhidden whether deleted questions should be displayed.
  597. * @param bool $showquestiontext whether the text of each question should be shown in the list. Deprecated.
  598. * @param array $addcontexts contexts where the user is allowed to add new questions.
  599. */
  600. protected function display_question_list($contexts, $pageurl, $categoryandcontext,
  601. $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
  602. $showquestiontext = false, $addcontexts = array()) {
  603. global $CFG, $DB, $OUTPUT;
  604. // This function can be moderately slow with large question counts and may time out.
  605. // We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
  606. // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc).
  607. \core_php_time_limit::raise(300);
  608. $category = $this->get_current_category($categoryandcontext);
  609. $strselectall = get_string('selectall');
  610. $strselectnone = get_string('deselectall');
  611. list($categoryid, $contextid) = explode(',', $categoryandcontext);
  612. $catcontext = \context::instance_by_id($contextid);
  613. $canadd = has_capability('moodle/question:add', $catcontext);
  614. $this->create_new_question_form($category, $canadd);
  615. $this->build_query();
  616. $totalnumber = $this->get_question_count();
  617. if ($totalnumber == 0) {
  618. return;
  619. }
  620. $questions = $this->load_page_questions($page, $perpage);
  621. echo '<div class="categorypagingbarcontainer">';
  622. $pageingurl = new \moodle_url('edit.php');
  623. $r = $pageingurl->params($pageurl->params());
  624. $pagingbar = new \paging_bar($totalnumber, $page, $perpage, $pageingurl);
  625. $pagingbar->pagevar = 'qpage';
  626. echo $OUTPUT->render($pagingbar);
  627. echo '</div>';
  628. echo '<form method="post" action="edit.php">';
  629. echo '<fieldset class="invisiblefieldset" style="display: block;">';
  630. echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
  631. echo \html_writer::input_hidden_params($this->baseurl);
  632. echo '<div class="categoryquestionscontainer">';
  633. $this->start_table();
  634. $rowcount = 0;
  635. foreach ($questions as $question) {
  636. $this->print_table_row($question, $rowcount);
  637. $rowcount += 1;
  638. }
  639. $this->end_table();
  640. echo "</div>\n";
  641. echo '<div class="categorypagingbarcontainer pagingbottom">';
  642. echo $OUTPUT->render($pagingbar);
  643. if ($totalnumber > DEFAULT_QUESTIONS_PER_PAGE) {
  644. if ($perpage == DEFAULT_QUESTIONS_PER_PAGE) {
  645. $url = new \moodle_url('edit.php', array_merge($pageurl->params(),
  646. array('qperpage' => MAXIMUM_QUESTIONS_PER_PAGE)));
  647. if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE) {
  648. $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', MAXIMUM_QUESTIONS_PER_PAGE).'</a>';
  649. } else {
  650. $showall = '<a href="'.$url.'">'.get_string('showall', 'moodle', $totalnumber).'</a>';
  651. }
  652. } else {
  653. $url = new \moodle_url('edit.php', array_merge($pageurl->params(),
  654. array('qperpage' => DEFAULT_QUESTIONS_PER_PAGE)));
  655. $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE).'</a>';
  656. }
  657. echo "<div class='paging'>{$showall}</div>";
  658. }
  659. echo '</div>';
  660. $this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts);
  661. echo '</fieldset>';
  662. echo "</form>\n";
  663. }
  664. /**
  665. * Display the controls at the bottom of the list of questions.
  666. * @param int $totalnumber Total number of questions that might be shown (if it was not for paging).
  667. * @param bool $recurse Whether to include subcategories.
  668. * @param stdClass $category The question_category row from the database.
  669. * @param context $catcontext The context of the category being displayed.
  670. * @param array $addcontexts contexts where the user is allowed to add new questions.
  671. */
  672. protected function display_bottom_controls($totalnumber, $recurse, $category, \context $catcontext, array $addcontexts) {
  673. $caneditall = has_capability('moodle/question:editall', $catcontext);
  674. $canuseall = has_capability('moodle/question:useall', $catcontext);
  675. $canmoveall = has_capability('moodle/question:moveall', $catcontext);
  676. echo '<div class="modulespecificbuttonscontainer">';
  677. if ($caneditall || $canmoveall || $canuseall) {
  678. echo '<strong>&nbsp;'.get_string('withselected', 'question').':</strong><br />';
  679. // Print delete and move selected question.
  680. if ($caneditall) {
  681. echo '<input type="submit" name="deleteselected" value="' . get_string('delete') . "\" />\n";
  682. }
  683. if ($canmoveall && count($addcontexts)) {
  684. echo '<input type="submit" name="move" value="' . get_string('moveto', 'question') . "\" />\n";
  685. question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
  686. }
  687. }
  688. echo "</div>\n";
  689. }
  690. protected function start_table() {
  691. echo '<table id="categoryquestions">' . "\n";
  692. echo "<thead>\n";
  693. $this->print_table_headers();
  694. echo "</thead>\n";
  695. echo "<tbody>\n";
  696. }
  697. protected function end_table() {
  698. echo "</tbody>\n";
  699. echo "</table>\n";
  700. }
  701. protected function print_table_headers() {
  702. echo "<tr>\n";
  703. foreach ($this->visiblecolumns as $column) {
  704. $column->display_header();
  705. }
  706. echo "</tr>\n";
  707. }
  708. protected function get_row_classes($question, $rowcount) {
  709. $classes = array();
  710. if ($question->hidden) {
  711. $classes[] = 'dimmed_text';
  712. }
  713. if ($question->id == $this->lastchangedid) {
  714. $classes[] = 'highlight';
  715. }
  716. $classes[] = 'r' . ($rowcount % 2);
  717. return $classes;
  718. }
  719. protected function print_table_row($question, $rowcount) {
  720. $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
  721. if ($rowclasses) {
  722. echo '<tr class="' . $rowclasses . '">' . "\n";
  723. } else {
  724. echo "<tr>\n";
  725. }
  726. foreach ($this->visiblecolumns as $column) {
  727. $column->display($question, $rowclasses);
  728. }
  729. echo "</tr>\n";
  730. foreach ($this->extrarows as $row) {
  731. $row->display($question, $rowclasses);
  732. }
  733. }
  734. public function process_actions() {
  735. global $CFG, $DB;
  736. // Now, check for commands on this page and modify variables as necessary.
  737. if (optional_param('move', false, PARAM_BOOL) and confirm_sesskey()) {
  738. // Move selected questions to new category.
  739. $category = required_param('category', PARAM_SEQUENCE);
  740. list($tocategoryid, $contextid) = explode(',', $category);
  741. if (! $tocategory = $DB->get_record('question_categories', array('id' => $tocategoryid, 'contextid' => $contextid))) {
  742. print_error('cannotfindcate', 'question');
  743. }
  744. $tocontext = \context::instance_by_id($contextid);
  745. require_capability('moodle/question:add', $tocontext);
  746. $rawdata = (array) data_submitted();
  747. $questionids = array();
  748. foreach ($rawdata as $key => $value) { // Parse input for question ids.
  749. if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
  750. $key = $matches[1];
  751. $questionids[] = $key;
  752. }
  753. }
  754. if ($questionids) {
  755. list($usql, $params) = $DB->get_in_or_equal($questionids);
  756. $sql = "";
  757. $questions = $DB->get_records_sql("
  758. SELECT q.*, c.contextid
  759. FROM {question} q
  760. JOIN {question_categories} c ON c.id = q.category
  761. WHERE q.id {$usql}", $params);
  762. foreach ($questions as $question) {
  763. question_require_capability_on($question, 'move');
  764. }
  765. question_move_questions_to_category($questionids, $tocategory->id);
  766. redirect($this->baseurl->out(false,
  767. array('category' => "{$tocategoryid},{$contextid}")));
  768. }
  769. }
  770. if (optional_param('deleteselected', false, PARAM_BOOL)) { // Delete selected questions from the category.
  771. // If teacher has already confirmed the action.
  772. if (($confirm = optional_param('confirm', '', PARAM_ALPHANUM)) and confirm_sesskey()) {
  773. $deleteselected = required_param('deleteselected', PARAM_RAW);
  774. if ($confirm == md5($deleteselected)) {
  775. if ($questionlist = explode(',', $deleteselected)) {
  776. // For each question either hide it if it is in use or delete it.
  777. foreach ($questionlist as $questionid) {
  778. $questionid = (int)$questionid;
  779. question_require_capability_on($questionid, 'edit');
  780. if (questions_in_use(array($questionid))) {
  781. $DB->set_field('question', 'hidden', 1, array('id' => $questionid));
  782. } else {
  783. question_delete_question($questionid);
  784. }
  785. }
  786. }
  787. redirect($this->baseurl);
  788. } else {
  789. print_error('invalidconfirm', 'question');
  790. }
  791. }
  792. }
  793. // Unhide a question.
  794. if (($unhide = optional_param('unhide', '', PARAM_INT)) and confirm_sesskey()) {
  795. question_require_capability_on($unhide, 'edit');
  796. $DB->set_field('question', 'hidden', 0, array('id' => $unhide));
  797. // Purge these questions from the cache.
  798. \question_bank::notify_question_edited($unhide);
  799. redirect($this->baseurl);
  800. }
  801. }
  802. public function process_actions_needing_ui() {
  803. global $DB, $OUTPUT;
  804. if (optional_param('deleteselected', false, PARAM_BOOL)) {
  805. // Make a list of all the questions that are selected.
  806. $rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted.
  807. $questionlist = ''; // comma separated list of ids of questions to be deleted
  808. $questionnames = ''; // string with names of questions separated by <br /> with
  809. // an asterix in front of those that are in use
  810. $inuse = false; // set to true if at least one of the questions is in use
  811. foreach ($rawquestions as $key => $value) { // Parse input for question ids.
  812. if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
  813. $key = $matches[1];
  814. $questionlist .= $key.',';
  815. question_require_capability_on($key, 'edit');
  816. if (questions_in_use(array($key))) {
  817. $questionnames .= '* ';
  818. $inuse = true;
  819. }
  820. $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
  821. }
  822. }
  823. if (!$questionlist) { // No questions were selected.
  824. redirect($this->baseurl);
  825. }
  826. $questionlist = rtrim($questionlist, ',');
  827. // Add an explanation about questions in use.
  828. if ($inuse) {
  829. $questionnames .= '<br />'.get_string('questionsinuse', 'question');
  830. }
  831. $baseurl = new \moodle_url('edit.php', $this->baseurl->params());
  832. $deleteurl = new \moodle_url($baseurl, array('deleteselected' => $questionlist, 'confirm' => md5($questionlist),
  833. 'sesskey' => sesskey()));
  834. $continue = new \single_button($deleteurl, get_string('delete'), 'post');
  835. echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $continue, $baseurl);
  836. return true;
  837. }
  838. }
  839. /**
  840. * Add another search control to this view.
  841. * @param \core_question\bank\search\condition $searchcondition the condition to add.
  842. */
  843. public function add_searchcondition($searchcondition) {
  844. $this->searchconditions[] = $searchcondition;
  845. }
  846. }