PageRenderTime 127ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/question/category_class.php

http://github.com/moodle/moodle
PHP | 609 lines | 348 code | 73 blank | 188 comment | 56 complexity | fab063632be2dbd662b1d5659855a29e MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  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. * A class for representing question categories.
  18. *
  19. * @package moodlecore
  20. * @subpackage questionbank
  21. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. // number of categories to display on page
  26. define('QUESTION_PAGE_LENGTH', 25);
  27. require_once($CFG->libdir . '/listlib.php');
  28. require_once($CFG->dirroot . '/question/category_form.php');
  29. require_once($CFG->dirroot . '/question/move_form.php');
  30. /**
  31. * Class representing a list of question categories
  32. *
  33. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. class question_category_list extends moodle_list {
  37. public $table = "question_categories";
  38. public $listitemclassname = 'question_category_list_item';
  39. /**
  40. * @var reference to list displayed below this one.
  41. */
  42. public $nextlist = null;
  43. /**
  44. * @var reference to list displayed above this one.
  45. */
  46. public $lastlist = null;
  47. public $context = null;
  48. public $sortby = 'parent, sortorder, name';
  49. public function __construct($type='ul', $attributes='', $editable = false, $pageurl=null, $page = 0, $pageparamname = 'page', $itemsperpage = 20, $context = null){
  50. parent::__construct('ul', '', $editable, $pageurl, $page, 'cpage', $itemsperpage);
  51. $this->context = $context;
  52. }
  53. public function get_records() {
  54. $this->records = get_categories_for_contexts($this->context->id, $this->sortby);
  55. }
  56. /**
  57. * Returns the highest category id that the $item can have as its parent.
  58. * Note: question categories cannot go higher than the TOP category.
  59. *
  60. * @param list_item $item The item which its top level parent is going to be returned.
  61. * @return int
  62. */
  63. public function get_top_level_parent_id($item) {
  64. // Put the item at the highest level it can go.
  65. $topcategory = question_get_top_category($item->item->contextid, true);
  66. return $topcategory->id;
  67. }
  68. /**
  69. * process any actions.
  70. *
  71. * @param integer $left id of item to move left
  72. * @param integer $right id of item to move right
  73. * @param integer $moveup id of item to move up
  74. * @param integer $movedown id of item to move down
  75. * @return void
  76. * @throws coding_exception
  77. */
  78. public function process_actions($left, $right, $moveup, $movedown) {
  79. $category = new stdClass();
  80. if (!empty($left)) {
  81. // Moved Left (In to another category).
  82. $category->id = $left;
  83. $category->contextid = $this->context->id;
  84. $event = \core\event\question_category_moved::create_from_question_category_instance($category);
  85. $event->trigger();
  86. } else if (!empty($right)) {
  87. // Moved Right (Out of the current category).
  88. $category->id = $right;
  89. $category->contextid = $this->context->id;
  90. $event = \core\event\question_category_moved::create_from_question_category_instance($category);
  91. $event->trigger();
  92. }
  93. parent::process_actions($left, $right, $moveup, $movedown);
  94. }
  95. }
  96. /**
  97. * An item in a list of question categories.
  98. *
  99. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  100. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  101. */
  102. class question_category_list_item extends list_item {
  103. public function set_icon_html($first, $last, $lastitem){
  104. global $CFG;
  105. $category = $this->item;
  106. $url = new moodle_url('/question/category.php', ($this->parentlist->pageurl->params() + array('edit'=>$category->id)));
  107. $this->icons['edit']= $this->image_icon(get_string('editthiscategory', 'question'), $url, 'edit');
  108. parent::set_icon_html($first, $last, $lastitem);
  109. $toplevel = ($this->parentlist->parentitem === null);//this is a top level item
  110. if (($this->parentlist->nextlist !== null) && $last && $toplevel && (count($this->parentlist->items)>1)){
  111. $url = new moodle_url($this->parentlist->pageurl, array('movedowncontext'=>$this->id, 'tocontext'=>$this->parentlist->nextlist->context->id, 'sesskey'=>sesskey()));
  112. $this->icons['down'] = $this->image_icon(
  113. get_string('shareincontext', 'question', $this->parentlist->nextlist->context->get_context_name()), $url, 'down');
  114. }
  115. if (($this->parentlist->lastlist !== null) && $first && $toplevel && (count($this->parentlist->items)>1)){
  116. $url = new moodle_url($this->parentlist->pageurl, array('moveupcontext'=>$this->id, 'tocontext'=>$this->parentlist->lastlist->context->id, 'sesskey'=>sesskey()));
  117. $this->icons['up'] = $this->image_icon(
  118. get_string('shareincontext', 'question', $this->parentlist->lastlist->context->get_context_name()), $url, 'up');
  119. }
  120. }
  121. public function item_html($extraargs = array()){
  122. global $CFG, $OUTPUT;
  123. $str = $extraargs['str'];
  124. $category = $this->item;
  125. $editqestions = get_string('editquestions', 'question');
  126. // Each section adds html to be displayed as part of this list item.
  127. $questionbankurl = new moodle_url('/question/edit.php', $this->parentlist->pageurl->params());
  128. $questionbankurl->param('cat', $category->id . ',' . $category->contextid);
  129. $item = '';
  130. $text = format_string($category->name, true, ['context' => $this->parentlist->context]);
  131. if ($category->idnumber !== null && $category->idnumber !== '') {
  132. $text .= ' ' . html_writer::span(
  133. html_writer::span(get_string('idnumber', 'question'), 'accesshide') .
  134. ' ' . $category->idnumber, 'badge badge-primary');
  135. }
  136. $text .= ' (' . $category->questioncount . ')';
  137. $item .= html_writer::tag('b', html_writer::link($questionbankurl, $text,
  138. ['title' => $editqestions]) . ' ');
  139. $item .= format_text($category->info, $category->infoformat,
  140. array('context' => $this->parentlist->context, 'noclean' => true));
  141. // Don't allow delete if this is the top category, or the last editable category in this context.
  142. if ($category->parent && !question_is_only_child_of_top_category_in_context($category->id)) {
  143. $deleteurl = new moodle_url($this->parentlist->pageurl, array('delete' => $this->id, 'sesskey' => sesskey()));
  144. $item .= html_writer::link($deleteurl,
  145. $OUTPUT->pix_icon('t/delete', $str->delete),
  146. array('title' => $str->delete));
  147. }
  148. return $item;
  149. }
  150. }
  151. /**
  152. * Class for performing operations on question categories.
  153. *
  154. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  155. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  156. */
  157. class question_category_object {
  158. /**
  159. * @var array common language strings.
  160. */
  161. public $str;
  162. /**
  163. * @var array nested lists to display categories.
  164. */
  165. public $editlists = array();
  166. public $tab;
  167. public $tabsize = 3;
  168. /**
  169. * @var moodle_url Object representing url for this page
  170. */
  171. public $pageurl;
  172. /**
  173. * @var question_category_edit_form Object representing form for adding / editing categories.
  174. */
  175. public $catform;
  176. /**
  177. * Constructor.
  178. *
  179. * @param int $page page number
  180. * @param moodle_url $pageurl base URL of the display categories page. Used for redirects.
  181. * @param context[] $contexts contexts where the current user can edit categories.
  182. * @param int $currentcat id of the category to be edited. 0 if none.
  183. * @param int|null $defaultcategory id of the current category. null if none.
  184. * @param int $todelete id of the category to delete. 0 if none.
  185. * @param context[] $addcontexts contexts where the current user can add questions.
  186. */
  187. public function __construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
  188. $this->tab = str_repeat('&nbsp;', $this->tabsize);
  189. $this->str = new stdClass();
  190. $this->str->course = get_string('course');
  191. $this->str->category = get_string('category', 'question');
  192. $this->str->categoryinfo = get_string('categoryinfo', 'question');
  193. $this->str->questions = get_string('questions', 'question');
  194. $this->str->add = get_string('add');
  195. $this->str->delete = get_string('delete');
  196. $this->str->moveup = get_string('moveup');
  197. $this->str->movedown = get_string('movedown');
  198. $this->str->edit = get_string('editthiscategory', 'question');
  199. $this->str->hide = get_string('hide');
  200. $this->str->order = get_string('order');
  201. $this->str->parent = get_string('parent', 'question');
  202. $this->str->add = get_string('add');
  203. $this->str->action = get_string('action');
  204. $this->str->top = get_string('top');
  205. $this->str->addcategory = get_string('addcategory', 'question');
  206. $this->str->editcategory = get_string('editcategory', 'question');
  207. $this->str->cancel = get_string('cancel');
  208. $this->str->editcategories = get_string('editcategories', 'question');
  209. $this->str->page = get_string('page');
  210. $this->pageurl = $pageurl;
  211. $this->initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts);
  212. }
  213. /**
  214. * Old syntax of class constructor. Deprecated in PHP7.
  215. *
  216. * @deprecated since Moodle 3.1
  217. */
  218. public function question_category_object($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
  219. debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  220. self::__construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts);
  221. }
  222. /**
  223. * Initializes this classes general category-related variables
  224. */
  225. public function initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
  226. $lastlist = null;
  227. foreach ($contexts as $context){
  228. $this->editlists[$context->id] = new question_category_list('ul', '', true, $this->pageurl, $page, 'cpage', QUESTION_PAGE_LENGTH, $context);
  229. $this->editlists[$context->id]->lastlist =& $lastlist;
  230. if ($lastlist!== null){
  231. $lastlist->nextlist =& $this->editlists[$context->id];
  232. }
  233. $lastlist =& $this->editlists[$context->id];
  234. }
  235. $count = 1;
  236. $paged = false;
  237. foreach ($this->editlists as $key => $list){
  238. list($paged, $count) = $this->editlists[$key]->list_from_records($paged, $count);
  239. }
  240. $this->catform = new question_category_edit_form($this->pageurl, compact('contexts', 'currentcat'));
  241. if (!$currentcat){
  242. $this->catform->set_data(array('parent'=>$defaultcategory));
  243. }
  244. }
  245. /**
  246. * Displays the user interface
  247. *
  248. */
  249. public function display_user_interface() {
  250. /// Interface for editing existing categories
  251. $this->output_edit_lists();
  252. echo '<br />';
  253. /// Interface for adding a new category:
  254. $this->output_new_table();
  255. echo '<br />';
  256. }
  257. /**
  258. * Outputs a table to allow entry of a new category
  259. */
  260. public function output_new_table() {
  261. $this->catform->display();
  262. }
  263. /**
  264. * Outputs a list to allow editing/rearranging of existing categories
  265. *
  266. * $this->initialize() must have already been called
  267. *
  268. */
  269. public function output_edit_lists() {
  270. global $OUTPUT;
  271. echo $OUTPUT->heading_with_help(get_string('editcategories', 'question'), 'editcategories', 'question');
  272. foreach ($this->editlists as $context => $list){
  273. $listhtml = $list->to_html(0, array('str'=>$this->str));
  274. if ($listhtml){
  275. echo $OUTPUT->box_start('boxwidthwide boxaligncenter generalbox questioncategories contextlevel' . $list->context->contextlevel);
  276. $fullcontext = context::instance_by_id($context);
  277. echo $OUTPUT->heading(get_string('questioncatsfor', 'question', $fullcontext->get_context_name()), 3);
  278. echo $listhtml;
  279. echo $OUTPUT->box_end();
  280. }
  281. }
  282. echo $list->display_page_numbers();
  283. }
  284. /**
  285. * gets all the courseids for the given categories
  286. *
  287. * @param array categories contains category objects in a tree representation
  288. * @return array courseids flat array in form categoryid=>courseid
  289. */
  290. public function get_course_ids($categories) {
  291. $courseids = array();
  292. foreach ($categories as $key=>$cat) {
  293. $courseids[$key] = $cat->course;
  294. if (!empty($cat->children)) {
  295. $courseids = array_merge($courseids, $this->get_course_ids($cat->children));
  296. }
  297. }
  298. return $courseids;
  299. }
  300. public function edit_single_category($categoryid) {
  301. /// Interface for adding a new category
  302. global $DB;
  303. /// Interface for editing existing categories
  304. $category = $DB->get_record("question_categories", array("id" => $categoryid));
  305. if (empty($category)) {
  306. print_error('invalidcategory', '', '', $categoryid);
  307. } else if ($category->parent == 0) {
  308. print_error('cannotedittopcat', 'question', '', $categoryid);
  309. } else {
  310. $category->parent = "{$category->parent},{$category->contextid}";
  311. $category->submitbutton = get_string('savechanges');
  312. $category->categoryheader = $this->str->edit;
  313. $this->catform->set_data($category);
  314. $this->catform->display();
  315. }
  316. }
  317. /**
  318. * Sets the viable parents
  319. *
  320. * Viable parents are any except for the category itself, or any of it's descendants
  321. * The parentstrings parameter is passed by reference and changed by this function.
  322. *
  323. * @param array parentstrings a list of parentstrings
  324. * @param object category
  325. */
  326. public function set_viable_parents(&$parentstrings, $category) {
  327. unset($parentstrings[$category->id]);
  328. if (isset($category->children)) {
  329. foreach ($category->children as $child) {
  330. $this->set_viable_parents($parentstrings, $child);
  331. }
  332. }
  333. }
  334. /**
  335. * Gets question categories
  336. *
  337. * @param int parent - if given, restrict records to those with this parent id.
  338. * @param string sort - [[sortfield [,sortfield]] {ASC|DESC}]
  339. * @return array categories
  340. */
  341. public function get_question_categories($parent=null, $sort="sortorder ASC") {
  342. global $COURSE, $DB;
  343. if (is_null($parent)) {
  344. $categories = $DB->get_records('question_categories', array('course' => $COURSE->id), $sort);
  345. } else {
  346. $select = "parent = ? AND course = ?";
  347. $categories = $DB->get_records_select('question_categories', $select, array($parent, $COURSE->id), $sort);
  348. }
  349. return $categories;
  350. }
  351. /**
  352. * Deletes an existing question category
  353. *
  354. * @param int deletecat id of category to delete
  355. */
  356. public function delete_category($categoryid) {
  357. global $CFG, $DB;
  358. question_can_delete_cat($categoryid);
  359. if (!$category = $DB->get_record("question_categories", array("id" => $categoryid))) { // security
  360. print_error('unknowcategory');
  361. }
  362. /// Send the children categories to live with their grandparent
  363. $DB->set_field("question_categories", "parent", $category->parent, array("parent" => $category->id));
  364. /// Finally delete the category itself
  365. $DB->delete_records("question_categories", array("id" => $category->id));
  366. // Log the deletion of this category.
  367. $event = \core\event\question_category_deleted::create_from_question_category_instance($category);
  368. $event->add_record_snapshot('question_categories', $category);
  369. $event->trigger();
  370. }
  371. public function move_questions_and_delete_category($oldcat, $newcat){
  372. question_can_delete_cat($oldcat);
  373. $this->move_questions($oldcat, $newcat);
  374. $this->delete_category($oldcat);
  375. }
  376. public function display_move_form($questionsincategory, $category){
  377. global $OUTPUT;
  378. $vars = new stdClass();
  379. $vars->name = $category->name;
  380. $vars->count = $questionsincategory;
  381. echo $OUTPUT->box(get_string('categorymove', 'question', $vars), 'generalbox boxaligncenter');
  382. $this->moveform->display();
  383. }
  384. public function move_questions($oldcat, $newcat){
  385. global $DB;
  386. $questionids = $DB->get_records_select_menu('question',
  387. 'category = ? AND (parent = 0 OR parent = id)', array($oldcat), '', 'id,1');
  388. question_move_questions_to_category(array_keys($questionids), $newcat);
  389. }
  390. /**
  391. * Create a new category.
  392. *
  393. * Data is expected to come from question_category_edit_form.
  394. *
  395. * By default redirects on success, unless $return is true.
  396. *
  397. * @param string $newparent 'categoryid,contextid' of the parent category.
  398. * @param string $newcategory the name.
  399. * @param string $newinfo the description.
  400. * @param bool $return if true, return rather than redirecting.
  401. * @param int|string $newinfoformat description format. One of the FORMAT_ constants.
  402. * @param null $idnumber the idnumber. '' is converted to null.
  403. * @return bool|int New category id if successful, else false.
  404. */
  405. public function add_category($newparent, $newcategory, $newinfo, $return = false, $newinfoformat = FORMAT_HTML,
  406. $idnumber = null) {
  407. global $DB;
  408. if (empty($newcategory)) {
  409. print_error('categorynamecantbeblank', 'question');
  410. }
  411. list($parentid, $contextid) = explode(',', $newparent);
  412. //moodle_form makes sure select element output is legal no need for further cleaning
  413. require_capability('moodle/question:managecategory', context::instance_by_id($contextid));
  414. if ($parentid) {
  415. if(!($DB->get_field('question_categories', 'contextid', array('id' => $parentid)) == $contextid)) {
  416. print_error('cannotinsertquestioncatecontext', 'question', '', array('cat'=>$newcategory, 'ctx'=>$contextid));
  417. }
  418. }
  419. if ((string) $idnumber === '') {
  420. $idnumber = null;
  421. } else if (!empty($contextid)) {
  422. // While this check already exists in the form validation, this is a backstop preventing unnecessary errors.
  423. if ($DB->record_exists('question_categories',
  424. ['idnumber' => $idnumber, 'contextid' => $contextid])) {
  425. $idnumber = null;
  426. }
  427. }
  428. $cat = new stdClass();
  429. $cat->parent = $parentid;
  430. $cat->contextid = $contextid;
  431. $cat->name = $newcategory;
  432. $cat->info = $newinfo;
  433. $cat->infoformat = $newinfoformat;
  434. $cat->sortorder = 999;
  435. $cat->stamp = make_unique_id_code();
  436. $cat->idnumber = $idnumber;
  437. $categoryid = $DB->insert_record("question_categories", $cat);
  438. // Log the creation of this category.
  439. $category = new stdClass();
  440. $category->id = $categoryid;
  441. $category->contextid = $contextid;
  442. $event = \core\event\question_category_created::create_from_question_category_instance($category);
  443. $event->trigger();
  444. if ($return) {
  445. return $categoryid;
  446. } else {
  447. redirect($this->pageurl);//always redirect after successful action
  448. }
  449. }
  450. /**
  451. * Updates an existing category with given params.
  452. *
  453. * Warning! parameter order and meaning confusingly different from add_category in some ways!
  454. *
  455. * @param int $updateid id of the category to update.
  456. * @param int $newparent 'categoryid,contextid' of the parent category to set.
  457. * @param string $newname category name.
  458. * @param string $newinfo category description.
  459. * @param int|string $newinfoformat description format. One of the FORMAT_ constants.
  460. * @param int $idnumber the idnumber. '' is converted to null.
  461. * @param bool $redirect if true, will redirect once the DB is updated (default).
  462. */
  463. public function update_category($updateid, $newparent, $newname, $newinfo, $newinfoformat = FORMAT_HTML,
  464. $idnumber = null, $redirect = true) {
  465. global $CFG, $DB;
  466. if (empty($newname)) {
  467. print_error('categorynamecantbeblank', 'question');
  468. }
  469. // Get the record we are updating.
  470. $oldcat = $DB->get_record('question_categories', array('id' => $updateid));
  471. $lastcategoryinthiscontext = question_is_only_child_of_top_category_in_context($updateid);
  472. if (!empty($newparent) && !$lastcategoryinthiscontext) {
  473. list($parentid, $tocontextid) = explode(',', $newparent);
  474. } else {
  475. $parentid = $oldcat->parent;
  476. $tocontextid = $oldcat->contextid;
  477. }
  478. // Check permissions.
  479. $fromcontext = context::instance_by_id($oldcat->contextid);
  480. require_capability('moodle/question:managecategory', $fromcontext);
  481. // If moving to another context, check permissions some more, and confirm contextid,stamp uniqueness.
  482. $newstamprequired = false;
  483. if ($oldcat->contextid != $tocontextid) {
  484. $tocontext = context::instance_by_id($tocontextid);
  485. require_capability('moodle/question:managecategory', $tocontext);
  486. // Confirm stamp uniqueness in the new context. If the stamp already exists, generate a new one.
  487. if ($DB->record_exists('question_categories', array('contextid' => $tocontextid, 'stamp' => $oldcat->stamp))) {
  488. $newstamprequired = true;
  489. }
  490. }
  491. if ((string) $idnumber === '') {
  492. $idnumber = null;
  493. } else if (!empty($tocontextid)) {
  494. // While this check already exists in the form validation, this is a backstop preventing unnecessary errors.
  495. if ($DB->record_exists_select('question_categories',
  496. 'idnumber = ? AND contextid = ? AND id <> ?',
  497. [$idnumber, $tocontextid, $updateid])) {
  498. $idnumber = null;
  499. }
  500. }
  501. // Update the category record.
  502. $cat = new stdClass();
  503. $cat->id = $updateid;
  504. $cat->name = $newname;
  505. $cat->info = $newinfo;
  506. $cat->infoformat = $newinfoformat;
  507. $cat->parent = $parentid;
  508. $cat->contextid = $tocontextid;
  509. $cat->idnumber = $idnumber;
  510. if ($newstamprequired) {
  511. $cat->stamp = make_unique_id_code();
  512. }
  513. $DB->update_record('question_categories', $cat);
  514. // Log the update of this category.
  515. $event = \core\event\question_category_updated::create_from_question_category_instance($cat);
  516. $event->trigger();
  517. // If the category name has changed, rename any random questions in that category.
  518. if ($oldcat->name != $cat->name) {
  519. $where = "qtype = 'random' AND category = ? AND " . $DB->sql_compare_text('questiontext') . " = ?";
  520. $randomqtype = question_bank::get_qtype('random');
  521. $randomqname = $randomqtype->question_name($cat, false);
  522. $DB->set_field_select('question', 'name', $randomqname, $where, array($cat->id, '0'));
  523. $randomqname = $randomqtype->question_name($cat, true);
  524. $DB->set_field_select('question', 'name', $randomqname, $where, array($cat->id, '1'));
  525. }
  526. if ($oldcat->contextid != $tocontextid) {
  527. // Moving to a new context. Must move files belonging to questions.
  528. question_move_category_to_context($cat->id, $oldcat->contextid, $tocontextid);
  529. }
  530. // Cat param depends on the context id, so update it.
  531. $this->pageurl->param('cat', $updateid . ',' . $tocontextid);
  532. if ($redirect) {
  533. redirect($this->pageurl); // Always redirect after successful action.
  534. }
  535. }
  536. }