2 // This file is part of Moodle - http://moodle.org/
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.
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.
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
;
21 * Functions used to show question editing interface
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
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.
36 * This class gives a basic view, and provides plenty of hooks where subclasses
37 * can override parts of the display.
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
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.
47 * @copyright 2009 Tim Hunt
48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
54 protected $editquestionurl;
55 protected $quizorcourseid;
59 protected $visiblecolumns;
61 protected $requiredcolumns;
63 protected $lastchangedid;
67 /** @var array of \core_question\bank\search\condition objects. */
68 protected $searchconditions = array();
72 * @param question_edit_contexts $contexts
73 * @param moodle_url $pageurl
74 * @param object $course course settings
75 * @param object $cm (optional) activity settings.
77 public function __construct($contexts, $pageurl, $course, $cm = null) {
80 $this->contexts
= $contexts;
81 $this->baseurl
= $pageurl;
82 $this->course
= $course;
85 if (!empty($cm) && $cm->modname
== 'quiz') {
86 $this->quizorcourseid
= '&quizid=' . $cm->instance
;
88 $this->quizorcourseid
= '&courseid=' .$this->course
->id
;
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));
96 $this->editquestionurl
->param('cmid', $cm->id
);
98 $this->editquestionurl
->param('courseid', $this->course
->id
);
101 $this->lastchangedid
= optional_param('lastchanged', 0, PARAM_INT
);
103 $this->init_columns($this->wanted_columns(), $this->heading_column());
105 $this->init_search_conditions($this->contexts
, $this->course
, $this->cm
);
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.
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);
122 protected function wanted_columns() {
125 if (empty($CFG->questionbankcolumns
)) {
126 $questionbankcolumns = array('checkbox_column', 'question_type_column',
127 'question_name_column', 'edit_action_column', 'copy_action_column',
128 'preview_action_column', 'delete_action_column',
129 'creator_name_column',
130 'modifier_name_column');
132 $questionbankcolumns = explode(',', $CFG->questionbankcolumns
);
134 if (question_get_display_preference('qbshowtext', 0, PARAM_BOOL
, new \
moodle_url(''))) {
135 $questionbankcolumns[] = 'question_text_row';
138 foreach ($questionbankcolumns as $fullname) {
139 if (! class_exists($fullname)) {
140 if (class_exists('core_question\\bank\\' . $fullname)) {
141 $fullname = 'core_question\\bank\\' . $fullname;
143 throw new \
coding_exception("No such class exists: $fullname");
146 $this->requiredcolumns
[$fullname] = new $fullname($this);
148 return $this->requiredcolumns
;
153 * Get a column object from its name.
155 * @param string $columnname.
156 * @return \core_question\bank\column_base.
158 protected function get_column_type($columnname) {
159 if (! class_exists($columnname)) {
160 if (class_exists('core_question\\bank\\' . $columnname)) {
161 $columnname = 'core_question\\bank\\' . $columnname;
163 throw new \
coding_exception("No such class exists: $columnname");
166 if (empty($this->requiredcolumns
[$columnname])) {
167 $this->requiredcolumns
[$columnname] = new $columnname($this);
169 return $this->requiredcolumns
[$columnname];
173 * Specify the column heading
175 * @return string Column name for the heading
177 protected function heading_column() {
178 return 'question_bank_question_name_column';
182 * Initializing table columns
184 * @param array $wanted Collection of column names
185 * @param string $heading The name of column that is set as heading
187 protected function init_columns($wanted, $heading = '') {
188 $this->visiblecolumns
= array();
189 $this->extrarows
= array();
190 foreach ($wanted as $column) {
191 if ($column->is_extra_row()) {
192 $this->extrarows
[get_class($column)] = $column;
194 $this->visiblecolumns
[get_class($column)] = $column;
197 if (array_key_exists($heading, $this->requiredcolumns
)) {
198 $this->requiredcolumns
[$heading]->set_as_heading();
203 * @param string $colname a column internal name.
204 * @return bool is this column included in the output?
206 public function has_column($colname) {
207 return isset($this->visiblecolumns
[$colname]);
211 * @return int The number of columns in the table.
213 public function get_column_count() {
214 return count($this->visiblecolumns
);
217 public function get_courseid() {
218 return $this->course
->id
;
221 protected function init_sort() {
222 $this->init_sort_from_params();
223 if (empty($this->sort
)) {
224 $this->sort
= $this->default_sort();
229 * Deal with a sort name of the form columnname, or colname_subsort by
230 * breaking it up, validating the bits that are presend, and returning them.
231 * If there is no subsort, then $subsort is returned as ''.
232 * @return array array($colname, $subsort).
234 protected function parse_subsort($sort) {
236 if (strpos($sort, '-') !== false) {
237 list($colname, $subsort) = explode('-', $sort, 2);
242 // Validate the column name.
243 $column = $this->get_column_type($colname);
244 if (!isset($column) ||
!$column->is_sortable()) {
245 for ($i = 1; $i <= self
::MAX_SORTS
; $i++
) {
246 $this->baseurl
->remove_params('qbs' . $i);
248 throw new \
moodle_exception('unknownsortcolumn', '', $link = $this->baseurl
->out(), $colname);
250 // Validate the subsort, if present.
252 $subsorts = $column->is_sortable();
253 if (!is_array($subsorts) ||
!isset($subsorts[$subsort])) {
254 throw new \
moodle_exception('unknownsortcolumn', '', $link = $this->baseurl
->out(), $sort);
257 return array($colname, $subsort);
260 protected function init_sort_from_params() {
261 $this->sort
= array();
262 for ($i = 1; $i <= self
::MAX_SORTS
; $i++
) {
263 if (!$sort = optional_param('qbs' . $i, '', PARAM_TEXT
)) {
266 // Work out the appropriate order.
268 if ($sort[0] == '-') {
270 $sort = substr($sort, 1);
275 // Deal with subsorts.
276 list($colname, $subsort) = $this->parse_subsort($sort);
277 $this->requiredcolumns
[$colname] = $this->get_column_type($colname);
278 $this->sort
[$sort] = $order;
282 protected function sort_to_params($sorts) {
285 foreach ($sorts as $sort => $order) {
290 $params['qbs' . $i] = $sort;
295 protected function default_sort() {
296 return array('core_question\bank\question_type_column' => 1, 'core_question\bank\question_name_column' => 1);
300 * @param $sort a column or column_subsort name.
301 * @return int the current sort order for this column -1, 0, 1
303 public function get_primary_sort_order($sort) {
304 $order = reset($this->sort
);
305 $primarysort = key($this->sort
);
306 if ($sort == $primarysort) {
314 * Get a URL to redisplay the page with a new sort for the question bank.
315 * @param string $sort the column, or column_subsort to sort on.
316 * @param bool $newsortreverse whether to sort in reverse order.
317 * @return string The new URL.
319 public function new_sort_url($sort, $newsortreverse) {
320 if ($newsortreverse) {
325 // Tricky code to add the new sort at the start, removing it from where it was before, if it was present.
326 $newsort = array_reverse($this->sort
);
327 if (isset($newsort[$sort])) {
328 unset($newsort[$sort]);
330 $newsort[$sort] = $order;
331 $newsort = array_reverse($newsort);
332 if (count($newsort) > self
::MAX_SORTS
) {
333 $newsort = array_slice($newsort, 0, self
::MAX_SORTS
, true);
335 return $this->baseurl
->out(true, $this->sort_to_params($newsort));
339 * Create the SQL query to retrieve the indicated questions
340 * @param stdClass $category no longer used.
341 * @param bool $recurse no longer used.
342 * @param bool $showhidden no longer used.
343 * @deprecated since Moodle 2.7 MDL-40313.
345 * @see \core_question\bank\search\condition
346 * @todo MDL-41978 This will be deleted in Moodle 2.8
348 protected function build_query_sql($category, $recurse, $showhidden) {
349 debugging('build_query_sql() is deprecated, please use \core_question\bank\view::build_query() and ' .
350 '\core_question\bank\search\condition classes instead.', DEBUG_DEVELOPER
);
355 * Create the SQL query to retrieve the indicated questions, based on
356 * \core_question\bank\search\condition filters.
358 protected function build_query() {
361 // Get the required tables and fields.
363 $fields = array('q.hidden', 'q.category');
364 foreach ($this->requiredcolumns
as $column) {
365 $extrajoins = $column->get_extra_joins();
366 foreach ($extrajoins as $prefix => $join) {
367 if (isset($joins[$prefix]) && $joins[$prefix] != $join) {
368 throw new \
coding_exception('Join ' . $join . ' conflicts with previous join ' . $joins[$prefix]);
370 $joins[$prefix] = $join;
372 $fields = array_merge($fields, $column->get_required_fields());
374 $fields = array_unique($fields);
376 // Build the order by clause.
378 foreach ($this->sort
as $sort => $order) {
379 list($colname, $subsort) = $this->parse_subsort($sort);
380 $sorts[] = $this->requiredcolumns
[$colname]->sort_expression($order < 0, $subsort);
383 // Build the where clause.
384 $tests = array('q.parent = 0');
385 $this->sqlparams
= array();
386 foreach ($this->searchconditions
as $searchcondition) {
387 if ($searchcondition->where()) {
388 $tests[] = '((' . $searchcondition->where() .'))';
390 if ($searchcondition->params()) {
391 $this->sqlparams
= array_merge($this->sqlparams
, $searchcondition->params());
395 $sql = ' FROM {question} q ' . implode(' ', $joins);
396 $sql .= ' WHERE ' . implode(' AND ', $tests);
397 $this->countsql
= 'SELECT count(1)' . $sql;
398 $this->loadsql
= 'SELECT ' . implode(', ', $fields) . $sql . ' ORDER BY ' . implode(', ', $sorts);
401 protected function get_question_count() {
403 return $DB->count_records_sql($this->countsql
, $this->sqlparams
);
406 protected function load_page_questions($page, $perpage) {
408 $questions = $DB->get_recordset_sql($this->loadsql
, $this->sqlparams
, $page * $perpage, $perpage);
409 if (!$questions->valid()) {
410 // No questions on this page. Reset to page 0.
412 $questions = $DB->get_recordset_sql($this->loadsql
, $this->sqlparams
, 0, $perpage);
417 public function base_url() {
418 return $this->baseurl
;
421 public function edit_question_url($questionid) {
422 return $this->editquestionurl
->out(true, array('id' => $questionid));
426 * Get the URL for duplicating a given question.
427 * @param int $questionid the question id.
428 * @return moodle_url the URL.
430 public function copy_question_url($questionid) {
431 return $this->editquestionurl
->out(true, array('id' => $questionid, 'makecopy' => 1));
435 * Get the context we are displaying the question bank for.
436 * @return context context object.
438 public function get_most_specific_context() {
439 return $this->contexts
->lowest();
443 * Get the URL to preview a question.
444 * @param stdClass $questiondata the data defining the question.
445 * @return moodle_url the URL.
447 public function preview_question_url($questiondata) {
448 return question_preview_url($questiondata->id
, null, null, null, null,
449 $this->get_most_specific_context());
453 * Shows the question bank editing interface.
455 * The function also processes a number of actions:
457 * Actions affecting the question pool:
458 * move Moves a question to a different category
459 * deleteselected Deletes the selected questions from the category
461 * category Chooses the category
462 * displayoptions Sets display options
464 public function display($tabname, $page, $perpage, $cat,
465 $recurse, $showhidden, $showquestiontext) {
466 global $PAGE, $OUTPUT;
468 if ($this->process_actions_needing_ui()) {
471 $editcontexts = $this->contexts
->having_one_edit_tab_cap($tabname);
472 // Category selection form.
473 echo $OUTPUT->heading(get_string('questionbank', 'question'), 2);
474 array_unshift($this->searchconditions
, new \core_question\bank\search\
hidden_condition(!$showhidden));
475 array_unshift($this->searchconditions
, new \core_question\bank\search\
category_condition(
476 $cat, $recurse, $editcontexts, $this->baseurl
, $this->course
));
477 $this->display_options_form($showquestiontext);
479 // Continues with list of questions.
480 $this->display_question_list($this->contexts
->having_one_edit_tab_cap($tabname),
481 $this->baseurl
, $cat, $this->cm
,
482 null, $page, $perpage, $showhidden, $showquestiontext,
483 $this->contexts
->having_cap('moodle/question:add'));
486 protected function print_choose_category_message($categoryandcontext) {
487 echo "<p style=\"text-align:center;\"><b>";
488 print_string('selectcategoryabove', 'question');
492 protected function get_current_category($categoryandcontext) {
494 list($categoryid, $contextid) = explode(',', $categoryandcontext);
496 $this->print_choose_category_message($categoryandcontext);
500 if (!$category = $DB->get_record('question_categories',
501 array('id' => $categoryid, 'contextid' => $contextid))) {
502 echo $OUTPUT->box_start('generalbox questionbank');
503 echo $OUTPUT->notification('Category not found!');
504 echo $OUTPUT->box_end();
512 * prints category information
513 * @param stdClass $category the category row from the database.
514 * @deprecated since Moodle 2.7 MDL-40313.
515 * @see \core_question\bank\search\condition
516 * @todo MDL-41978 This will be deleted in Moodle 2.8
518 protected function print_category_info($category) {
519 $formatoptions = new \
stdClass();
520 $formatoptions->noclean
= true;
521 $formatoptions->overflowdiv
= true;
522 echo '<div class="boxaligncenter">';
523 echo format_text($category->info
, $category->infoformat
, $formatoptions, $this->course
->id
);
528 * Prints a form to choose categories
529 * @deprecated since Moodle 2.7 MDL-40313.
530 * @see \core_question\bank\search\condition
531 * @todo MDL-41978 This will be deleted in Moodle 2.8
533 protected function display_category_form($contexts, $pageurl, $current) {
536 debugging('display_category_form() is deprecated, please use ' .
537 '\core_question\bank\search\condition instead.', DEBUG_DEVELOPER
);
538 // Get all the existing categories now.
539 echo '<div class="choosecategory">';
540 $catmenu = question_category_options($contexts, false, 0, true);
542 $select = new \
single_select($this->baseurl
, 'category', $catmenu, $current, null, 'catmenu');
543 $select->set_label(get_string('selectacategory', 'question'));
544 echo $OUTPUT->render($select);
549 * Display the options form.
550 * @param bool $recurse no longer used.
551 * @param bool $showhidden no longer used.
552 * @param bool $showquestiontext whether to show the question text.
553 * @deprecated since Moodle 2.7 MDL-40313.
554 * @see display_options_form
555 * @todo MDL-41978 This will be deleted in Moodle 2.8
556 * @see \core_question\bank\search\condition
558 protected function display_options($recurse, $showhidden, $showquestiontext) {
559 debugging('display_options() is deprecated, please use display_options_form instead.', DEBUG_DEVELOPER
);
560 return $this->display_options_form($showquestiontext);
564 * Print a single option checkbox.
565 * @deprecated since Moodle 2.7 MDL-40313.
566 * @see \core_question\bank\search\condition
567 * @see html_writer::checkbox
568 * @todo MDL-41978 This will be deleted in Moodle 2.8
570 protected function display_category_form_checkbox($name, $value, $label) {
571 debugging('display_category_form_checkbox() is deprecated, ' .
572 'please use \core_question\bank\search\condition instead.', DEBUG_DEVELOPER
);
573 echo '<div><input type="hidden" id="' . $name . '_off" name="' . $name . '" value="0" />';
574 echo '<input type="checkbox" id="' . $name . '_on" name="' . $name . '" value="1"';
576 echo ' checked="checked"';
578 echo ' onchange="getElementById(\'displayoptions\').submit(); return true;" />';
579 echo '<label for="' . $name . '_on">' . $label . '</label>';
584 * Display the form with options for which questions are displayed and how they are displayed.
585 * @param bool $showquestiontext Display the text of the question within the list.
586 * @param string $path path to the script displaying this page.
587 * @param bool $showtextoption whether to include the 'Show question text' checkbox.
589 protected function display_options_form($showquestiontext, $scriptpath = '/question/edit.php',
590 $showtextoption = true) {
593 echo \html_writer
::start_tag('form', array('method' => 'get',
594 'action' => new \
moodle_url($scriptpath), 'id' => 'displayoptions'));
595 echo \html_writer
::start_div();
596 echo \html_writer
::input_hidden_params($this->baseurl
, array('recurse', 'showhidden', 'qbshowtext'));
598 foreach ($this->searchconditions
as $searchcondition) {
599 echo $searchcondition->display_options($this);
601 if ($showtextoption) {
602 $this->display_showtext_checkbox($showquestiontext);
604 $this->display_advanced_search_form();
605 $go = \html_writer
::empty_tag('input', array('type' => 'submit', 'value' => get_string('go')));
606 echo \html_writer
::tag('noscript', \html_writer
::div($go), array('class' => 'inline'));
607 echo \html_writer
::end_div();
608 echo \html_writer
::end_tag('form');
609 $PAGE->requires
->yui_module('moodle-question-searchform', 'M.question.searchform.init');
613 * Print the "advanced" UI elements for the form to select which questions. Hidden by default.
615 protected function display_advanced_search_form() {
616 print_collapsible_region_start('', 'advancedsearch', get_string('advancedsearchoptions', 'question'),
617 'question_bank_advanced_search');
618 foreach ($this->searchconditions
as $searchcondition) {
619 echo $searchcondition->display_options_adv($this);
621 print_collapsible_region_end();
625 * Display the checkbox UI for toggling the display of the question text in the list.
626 * @param bool $showquestiontext the current or default value for whether to display the text.
628 protected function display_showtext_checkbox($showquestiontext) {
630 echo \html_writer
::empty_tag('input', array('type' => 'hidden', 'name' => 'qbshowtext',
631 'value' => 0, 'id' => 'qbshowtext_off'));
632 echo \html_writer
::checkbox('qbshowtext', '1', $showquestiontext, get_string('showquestiontext', 'question'),
633 array('id' => 'qbshowtext_on', 'class' => 'searchoptions'));
637 protected function create_new_question_form($category, $canadd) {
639 echo '<div class="createnewquestion">';
641 create_new_question_button($category->id
, $this->editquestionurl
->params(),
642 get_string('createnewquestion', 'question'));
644 print_string('nopermissionadd', 'question');
650 * Prints the table of questions in a category with interactions
652 * @param array $contexts Not used!
653 * @param moodle_url $pageurl The URL to reload this page.
654 * @param string $categoryandcontext 'categoryID,contextID'.
655 * @param stdClass $cm Not used!
656 * @param bool $recurse Whether to include subcategories.
657 * @param int $page The number of the page to be displayed
658 * @param int $perpage Number of questions to show per page
659 * @param bool $showhidden whether deleted questions should be displayed.
660 * @param bool $showquestiontext whether the text of each question should be shown in the list. Deprecated.
661 * @param array $addcontexts contexts where the user is allowed to add new questions.
663 protected function display_question_list($contexts, $pageurl, $categoryandcontext,
664 $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
665 $showquestiontext = false, $addcontexts = array()) {
666 global $CFG, $DB, $OUTPUT;
668 // This function can be moderately slow with large question counts and may time out.
669 // We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
670 // Note: We do not call this in the loop because quiz ob_ captures this function (see raise() PHP doc).
671 \core_php_time_limit
::raise(300);
673 $category = $this->get_current_category($categoryandcontext);
675 $strselectall = get_string('selectall');
676 $strselectnone = get_string('deselectall');
678 list($categoryid, $contextid) = explode(',', $categoryandcontext);
679 $catcontext = \context
::instance_by_id($contextid);
681 $canadd = has_capability('moodle/question:add', $catcontext);
683 $this->create_new_question_form($category, $canadd);
685 $this->build_query();
686 $totalnumber = $this->get_question_count();
687 if ($totalnumber == 0) {
690 $questions = $this->load_page_questions($page, $perpage);
692 echo '<div class="categorypagingbarcontainer">';
693 $pageingurl = new \
moodle_url('edit.php');
694 $r = $pageingurl->params($pageurl->params());
695 $pagingbar = new \
paging_bar($totalnumber, $page, $perpage, $pageingurl);
696 $pagingbar->pagevar
= 'qpage';
697 echo $OUTPUT->render($pagingbar);
700 echo '<form method="post" action="edit.php">';
701 echo '<fieldset class="invisiblefieldset" style="display: block;">';
702 echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
703 echo \html_writer
::input_hidden_params($this->baseurl
);
705 echo '<div class="categoryquestionscontainer">';
706 $this->start_table();
708 foreach ($questions as $question) {
709 $this->print_table_row($question, $rowcount);
716 echo '<div class="categorypagingbarcontainer pagingbottom">';
717 echo $OUTPUT->render($pagingbar);
718 if ($totalnumber > DEFAULT_QUESTIONS_PER_PAGE
) {
719 if ($perpage == DEFAULT_QUESTIONS_PER_PAGE
) {
720 $url = new \
moodle_url('edit.php', array_merge($pageurl->params(),
721 array('qperpage' => MAXIMUM_QUESTIONS_PER_PAGE
)));
722 if ($totalnumber > MAXIMUM_QUESTIONS_PER_PAGE
) {
723 $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', MAXIMUM_QUESTIONS_PER_PAGE
).'</a>';
725 $showall = '<a href="'.$url.'">'.get_string('showall', 'moodle', $totalnumber).'</a>';
728 $url = new \
moodle_url('edit.php', array_merge($pageurl->params(),
729 array('qperpage' => DEFAULT_QUESTIONS_PER_PAGE
)));
730 $showall = '<a href="'.$url.'">'.get_string('showperpage', 'moodle', DEFAULT_QUESTIONS_PER_PAGE
).'</a>';
732 echo "<div class='paging'>{$showall}</div>";
736 $this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts);
743 * Display the controls at the bottom of the list of questions.
744 * @param int $totalnumber Total number of questions that might be shown (if it was not for paging).
745 * @param bool $recurse Whether to include subcategories.
746 * @param stdClass $category The question_category row from the database.
747 * @param context $catcontext The context of the category being displayed.
748 * @param array $addcontexts contexts where the user is allowed to add new questions.
750 protected function display_bottom_controls($totalnumber, $recurse, $category, \context
$catcontext, array $addcontexts) {
751 $caneditall = has_capability('moodle/question:editall', $catcontext);
752 $canuseall = has_capability('moodle/question:useall', $catcontext);
753 $canmoveall = has_capability('moodle/question:moveall', $catcontext);
755 echo '<div class="modulespecificbuttonscontainer">';
756 if ($caneditall ||
$canmoveall ||
$canuseall) {
757 echo '<strong> '.get_string('withselected', 'question').':</strong><br />';
759 // Print delete and move selected question.
761 echo '<input type="submit" class="btn btn-secondary" name="deleteselected" value="' . get_string('delete') . "\" />\n";
764 if ($canmoveall && count($addcontexts)) {
765 echo '<input type="submit" class="btn btn-secondary" name="move" value="' . get_string('moveto', 'question') . "\" />\n";
766 question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
772 protected function start_table() {
773 echo '<table id="categoryquestions">' . "\n";
775 $this->print_table_headers();
780 protected function end_table() {
785 protected function print_table_headers() {
787 foreach ($this->visiblecolumns
as $column) {
788 $column->display_header();
793 protected function get_row_classes($question, $rowcount) {
795 if ($question->hidden
) {
796 $classes[] = 'dimmed_text';
798 if ($question->id
== $this->lastchangedid
) {
799 $classes[] = 'highlight';
801 $classes[] = 'r' . ($rowcount %
2);
805 protected function print_table_row($question, $rowcount) {
806 $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount));
808 echo '<tr class="' . $rowclasses . '">' . "\n";
812 foreach ($this->visiblecolumns
as $column) {
813 $column->display($question, $rowclasses);
816 foreach ($this->extrarows
as $row) {
817 $row->display($question, $rowclasses);
821 public function process_actions() {
823 // Now, check for commands on this page and modify variables as necessary.
824 if (optional_param('move', false, PARAM_BOOL
) and confirm_sesskey()) {
825 // Move selected questions to new category.
826 $category = required_param('category', PARAM_SEQUENCE
);
827 list($tocategoryid, $contextid) = explode(',', $category);
828 if (! $tocategory = $DB->get_record('question_categories', array('id' => $tocategoryid, 'contextid' => $contextid))) {
829 print_error('cannotfindcate', 'question');
831 $tocontext = \context
::instance_by_id($contextid);
832 require_capability('moodle/question:add', $tocontext);
833 $rawdata = (array) data_submitted();
834 $questionids = array();
835 foreach ($rawdata as $key => $value) { // Parse input for question ids.
836 if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
838 $questionids[] = $key;
842 list($usql, $params) = $DB->get_in_or_equal($questionids);
844 $questions = $DB->get_records_sql("
845 SELECT q.*, c.contextid
847 JOIN {question_categories} c ON c.id = q.category
848 WHERE q.id {$usql}", $params);
849 foreach ($questions as $question) {
850 question_require_capability_on($question, 'move');
852 question_move_questions_to_category($questionids, $tocategory->id
);
853 redirect($this->baseurl
->out(false,
854 array('category' => "{$tocategoryid},{$contextid}")));
858 if (optional_param('deleteselected', false, PARAM_BOOL
)) { // Delete selected questions from the category.
859 // If teacher has already confirmed the action.
860 if (($confirm = optional_param('confirm', '', PARAM_ALPHANUM
)) and confirm_sesskey()) {
861 $deleteselected = required_param('deleteselected', PARAM_RAW
);
862 if ($confirm == md5($deleteselected)) {
863 if ($questionlist = explode(',', $deleteselected)) {
864 // For each question either hide it if it is in use or delete it.
865 foreach ($questionlist as $questionid) {
866 $questionid = (int)$questionid;
867 question_require_capability_on($questionid, 'edit');
868 if (questions_in_use(array($questionid))) {
869 $DB->set_field('question', 'hidden', 1, array('id' => $questionid));
871 question_delete_question($questionid);
875 redirect($this->baseurl
);
877 print_error('invalidconfirm', 'question');
882 // Unhide a question.
883 if (($unhide = optional_param('unhide', '', PARAM_INT
)) and confirm_sesskey()) {
884 question_require_capability_on($unhide, 'edit');
885 $DB->set_field('question', 'hidden', 0, array('id' => $unhide));
887 // Purge these questions from the cache.
888 \question_bank
::notify_question_edited($unhide);
890 redirect($this->baseurl
);
894 public function process_actions_needing_ui() {
896 if (optional_param('deleteselected', false, PARAM_BOOL
)) {
897 // Make a list of all the questions that are selected.
898 $rawquestions = $_REQUEST; // This code is called by both POST forms and GET links, so cannot use data_submitted.
899 $questionlist = ''; // comma separated list of ids of questions to be deleted
900 $questionnames = ''; // string with names of questions separated by <br /> with
901 // an asterix in front of those that are in use
902 $inuse = false; // set to true if at least one of the questions is in use
903 foreach ($rawquestions as $key => $value) { // Parse input for question ids.
904 if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
906 $questionlist .= $key.',';
907 question_require_capability_on($key, 'edit');
908 if (questions_in_use(array($key))) {
909 $questionnames .= '* ';
912 $questionnames .= $DB->get_field('question', 'name', array('id' => $key)) . '<br />';
915 if (!$questionlist) { // No questions were selected.
916 redirect($this->baseurl
);
918 $questionlist = rtrim($questionlist, ',');
920 // Add an explanation about questions in use.
922 $questionnames .= '<br />'.get_string('questionsinuse', 'question');
924 $baseurl = new \
moodle_url('edit.php', $this->baseurl
->params());
925 $deleteurl = new \
moodle_url($baseurl, array('deleteselected' => $questionlist, 'confirm' => md5($questionlist),
926 'sesskey' => sesskey()));
928 $continue = new \
single_button($deleteurl, get_string('delete'), 'post');
929 echo $OUTPUT->confirm(get_string('deletequestionscheck', 'question', $questionnames), $continue, $baseurl);
936 * Add another search control to this view.
937 * @param \core_question\bank\search\condition $searchcondition the condition to add.
939 public function add_searchcondition($searchcondition) {
940 $this->searchconditions
[] = $searchcondition;