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