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