category_class.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  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. /**
  58. * An item in a list of question categories.
  59. *
  60. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  61. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  62. */
  63. class question_category_list_item extends list_item {
  64. public function set_icon_html($first, $last, $lastitem){
  65. global $CFG;
  66. $category = $this->item;
  67. $url = new moodle_url('/question/category.php', ($this->parentlist->pageurl->params() + array('edit'=>$category->id)));
  68. $this->icons['edit']= $this->image_icon(get_string('editthiscategory', 'question'), $url, 'edit');
  69. parent::set_icon_html($first, $last, $lastitem);
  70. $toplevel = ($this->parentlist->parentitem === null);//this is a top level item
  71. if (($this->parentlist->nextlist !== null) && $last && $toplevel && (count($this->parentlist->items)>1)){
  72. $url = new moodle_url($this->parentlist->pageurl, array('movedowncontext'=>$this->id, 'tocontext'=>$this->parentlist->nextlist->context->id, 'sesskey'=>sesskey()));
  73. $this->icons['down'] = $this->image_icon(
  74. get_string('shareincontext', 'question', $this->parentlist->nextlist->context->get_context_name()), $url, 'down');
  75. }
  76. if (($this->parentlist->lastlist !== null) && $first && $toplevel && (count($this->parentlist->items)>1)){
  77. $url = new moodle_url($this->parentlist->pageurl, array('moveupcontext'=>$this->id, 'tocontext'=>$this->parentlist->lastlist->context->id, 'sesskey'=>sesskey()));
  78. $this->icons['up'] = $this->image_icon(
  79. get_string('shareincontext', 'question', $this->parentlist->lastlist->context->get_context_name()), $url, 'up');
  80. }
  81. }
  82. public function item_html($extraargs = array()){
  83. global $CFG, $OUTPUT;
  84. $str = $extraargs['str'];
  85. $category = $this->item;
  86. $editqestions = get_string('editquestions', 'question');
  87. // Each section adds html to be displayed as part of this list item.
  88. $questionbankurl = new moodle_url('/question/edit.php', $this->parentlist->pageurl->params());
  89. $questionbankurl->param('cat', $category->id . ',' . $category->contextid);
  90. $catediturl = new moodle_url($this->parentlist->pageurl, array('edit' => $this->id));
  91. $item = '';
  92. $item .= html_writer::tag('b', html_writer::link($catediturl,
  93. format_string($category->name, true, array('context' => $this->parentlist->context)),
  94. array('title' => $str->edit))) . ' ';
  95. $item .= html_writer::link($questionbankurl, '(' . $category->questioncount . ')',
  96. array('title' => $editqestions)) . ' ';
  97. $item .= format_text($category->info, $category->infoformat,
  98. array('context' => $this->parentlist->context, 'noclean' => true));
  99. // don't allow delete if this is the last category in this context.
  100. if (!question_is_only_toplevel_category_in_context($category->id)) {
  101. $deleteurl = new moodle_url($this->parentlist->pageurl, array('delete' => $this->id, 'sesskey' => sesskey()));
  102. $item .= html_writer::link($deleteurl,
  103. html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('t/delete'),
  104. 'class' => 'iconsmall', 'alt' => $str->delete)),
  105. array('title' => $str->delete));
  106. }
  107. return $item;
  108. }
  109. }
  110. /**
  111. * Class representing q question category
  112. *
  113. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  114. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  115. */
  116. class question_category_object {
  117. /**
  118. * @var array common language strings.
  119. */
  120. public $str;
  121. /**
  122. * @var array nested lists to display categories.
  123. */
  124. public $editlists = array();
  125. public $newtable;
  126. public $tab;
  127. public $tabsize = 3;
  128. /**
  129. * @var moodle_url Object representing url for this page
  130. */
  131. public $pageurl;
  132. /**
  133. * @var question_category_edit_form Object representing form for adding / editing categories.
  134. */
  135. public $catform;
  136. /**
  137. * Constructor
  138. *
  139. * Gets necessary strings and sets relevant path information
  140. */
  141. public function __construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
  142. global $CFG, $COURSE, $OUTPUT;
  143. $this->tab = str_repeat('&nbsp;', $this->tabsize);
  144. $this->str = new stdClass();
  145. $this->str->course = get_string('course');
  146. $this->str->category = get_string('category', 'question');
  147. $this->str->categoryinfo = get_string('categoryinfo', 'question');
  148. $this->str->questions = get_string('questions', 'question');
  149. $this->str->add = get_string('add');
  150. $this->str->delete = get_string('delete');
  151. $this->str->moveup = get_string('moveup');
  152. $this->str->movedown = get_string('movedown');
  153. $this->str->edit = get_string('editthiscategory', 'question');
  154. $this->str->hide = get_string('hide');
  155. $this->str->order = get_string('order');
  156. $this->str->parent = get_string('parent', 'question');
  157. $this->str->add = get_string('add');
  158. $this->str->action = get_string('action');
  159. $this->str->top = get_string('top');
  160. $this->str->addcategory = get_string('addcategory', 'question');
  161. $this->str->editcategory = get_string('editcategory', 'question');
  162. $this->str->cancel = get_string('cancel');
  163. $this->str->editcategories = get_string('editcategories', 'question');
  164. $this->str->page = get_string('page');
  165. $this->pageurl = $pageurl;
  166. $this->initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts);
  167. }
  168. /**
  169. * Old syntax of class constructor. Deprecated in PHP7.
  170. *
  171. * @deprecated since Moodle 3.1
  172. */
  173. public function question_category_object($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
  174. debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
  175. self::__construct($page, $pageurl, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts);
  176. }
  177. /**
  178. * Initializes this classes general category-related variables
  179. */
  180. public function initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts) {
  181. $lastlist = null;
  182. foreach ($contexts as $context){
  183. $this->editlists[$context->id] = new question_category_list('ul', '', true, $this->pageurl, $page, 'cpage', QUESTION_PAGE_LENGTH, $context);
  184. $this->editlists[$context->id]->lastlist =& $lastlist;
  185. if ($lastlist!== null){
  186. $lastlist->nextlist =& $this->editlists[$context->id];
  187. }
  188. $lastlist =& $this->editlists[$context->id];
  189. }
  190. $count = 1;
  191. $paged = false;
  192. foreach ($this->editlists as $key => $list){
  193. list($paged, $count) = $this->editlists[$key]->list_from_records($paged, $count);
  194. }
  195. $this->catform = new question_category_edit_form($this->pageurl, compact('contexts', 'currentcat'));
  196. if (!$currentcat){
  197. $this->catform->set_data(array('parent'=>$defaultcategory));
  198. }
  199. }
  200. /**
  201. * Displays the user interface
  202. *
  203. */
  204. public function display_user_interface() {
  205. /// Interface for editing existing categories
  206. $this->output_edit_lists();
  207. echo '<br />';
  208. /// Interface for adding a new category:
  209. $this->output_new_table();
  210. echo '<br />';
  211. }
  212. /**
  213. * Outputs a table to allow entry of a new category
  214. */
  215. public function output_new_table() {
  216. $this->catform->display();
  217. }
  218. /**
  219. * Outputs a list to allow editing/rearranging of existing categories
  220. *
  221. * $this->initialize() must have already been called
  222. *
  223. */
  224. public function output_edit_lists() {
  225. global $OUTPUT;
  226. echo $OUTPUT->heading_with_help(get_string('editcategories', 'question'), 'editcategories', 'question');
  227. foreach ($this->editlists as $context => $list){
  228. $listhtml = $list->to_html(0, array('str'=>$this->str));
  229. if ($listhtml){
  230. echo $OUTPUT->box_start('boxwidthwide boxaligncenter generalbox questioncategories contextlevel' . $list->context->contextlevel);
  231. $fullcontext = context::instance_by_id($context);
  232. echo $OUTPUT->heading(get_string('questioncatsfor', 'question', $fullcontext->get_context_name()), 3);
  233. echo $listhtml;
  234. echo $OUTPUT->box_end();
  235. }
  236. }
  237. echo $list->display_page_numbers();
  238. }
  239. /**
  240. * gets all the courseids for the given categories
  241. *
  242. * @param array categories contains category objects in a tree representation
  243. * @return array courseids flat array in form categoryid=>courseid
  244. */
  245. public function get_course_ids($categories) {
  246. $courseids = array();
  247. foreach ($categories as $key=>$cat) {
  248. $courseids[$key] = $cat->course;
  249. if (!empty($cat->children)) {
  250. $courseids = array_merge($courseids, $this->get_course_ids($cat->children));
  251. }
  252. }
  253. return $courseids;
  254. }
  255. public function edit_single_category($categoryid) {
  256. /// Interface for adding a new category
  257. global $COURSE, $DB;
  258. /// Interface for editing existing categories
  259. if ($category = $DB->get_record("question_categories", array("id" => $categoryid))) {
  260. $category->parent = "{$category->parent},{$category->contextid}";
  261. $category->submitbutton = get_string('savechanges');
  262. $category->categoryheader = $this->str->edit;
  263. $this->catform->set_data($category);
  264. $this->catform->display();
  265. } else {
  266. print_error('invalidcategory', '', '', $categoryid);
  267. }
  268. }
  269. /**
  270. * Sets the viable parents
  271. *
  272. * Viable parents are any except for the category itself, or any of it's descendants
  273. * The parentstrings parameter is passed by reference and changed by this function.
  274. *
  275. * @param array parentstrings a list of parentstrings
  276. * @param object category
  277. */
  278. public function set_viable_parents(&$parentstrings, $category) {
  279. unset($parentstrings[$category->id]);
  280. if (isset($category->children)) {
  281. foreach ($category->children as $child) {
  282. $this->set_viable_parents($parentstrings, $child);
  283. }
  284. }
  285. }
  286. /**
  287. * Gets question categories
  288. *
  289. * @param int parent - if given, restrict records to those with this parent id.
  290. * @param string sort - [[sortfield [,sortfield]] {ASC|DESC}]
  291. * @return array categories
  292. */
  293. public function get_question_categories($parent=null, $sort="sortorder ASC") {
  294. global $COURSE, $DB;
  295. if (is_null($parent)) {
  296. $categories = $DB->get_records('question_categories', array('course' => $COURSE->id), $sort);
  297. } else {
  298. $select = "parent = ? AND course = ?";
  299. $categories = $DB->get_records_select('question_categories', $select, array($parent, $COURSE->id), $sort);
  300. }
  301. return $categories;
  302. }
  303. /**
  304. * Deletes an existing question category
  305. *
  306. * @param int deletecat id of category to delete
  307. */
  308. public function delete_category($categoryid) {
  309. global $CFG, $DB;
  310. question_can_delete_cat($categoryid);
  311. if (!$category = $DB->get_record("question_categories", array("id" => $categoryid))) { // security
  312. print_error('unknowcategory');
  313. }
  314. /// Send the children categories to live with their grandparent
  315. $DB->set_field("question_categories", "parent", $category->parent, array("parent" => $category->id));
  316. /// Finally delete the category itself
  317. $DB->delete_records("question_categories", array("id" => $category->id));
  318. }
  319. public function move_questions_and_delete_category($oldcat, $newcat){
  320. question_can_delete_cat($oldcat);
  321. $this->move_questions($oldcat, $newcat);
  322. $this->delete_category($oldcat);
  323. }
  324. public function display_move_form($questionsincategory, $category){
  325. global $OUTPUT;
  326. $vars = new stdClass();
  327. $vars->name = $category->name;
  328. $vars->count = $questionsincategory;
  329. echo $OUTPUT->box(get_string('categorymove', 'question', $vars), 'generalbox boxaligncenter');
  330. $this->moveform->display();
  331. }
  332. public function move_questions($oldcat, $newcat){
  333. global $DB;
  334. $questionids = $DB->get_records_select_menu('question',
  335. 'category = ? AND (parent = 0 OR parent = id)', array($oldcat), '', 'id,1');
  336. question_move_questions_to_category(array_keys($questionids), $newcat);
  337. }
  338. /**
  339. * Creates a new category with given params
  340. */
  341. public function add_category($newparent, $newcategory, $newinfo, $return = false, $newinfoformat = FORMAT_HTML) {
  342. global $DB;
  343. if (empty($newcategory)) {
  344. print_error('categorynamecantbeblank', 'question');
  345. }
  346. list($parentid, $contextid) = explode(',', $newparent);
  347. //moodle_form makes sure select element output is legal no need for further cleaning
  348. require_capability('moodle/question:managecategory', context::instance_by_id($contextid));
  349. if ($parentid) {
  350. if(!($DB->get_field('question_categories', 'contextid', array('id' => $parentid)) == $contextid)) {
  351. print_error('cannotinsertquestioncatecontext', 'question', '', array('cat'=>$newcategory, 'ctx'=>$contextid));
  352. }
  353. }
  354. $cat = new stdClass();
  355. $cat->parent = $parentid;
  356. $cat->contextid = $contextid;
  357. $cat->name = $newcategory;
  358. $cat->info = $newinfo;
  359. $cat->infoformat = $newinfoformat;
  360. $cat->sortorder = 999;
  361. $cat->stamp = make_unique_id_code();
  362. $categoryid = $DB->insert_record("question_categories", $cat);
  363. // Log the creation of this category.
  364. $params = array(
  365. 'objectid' => $categoryid,
  366. 'contextid' => $contextid
  367. );
  368. $event = \core\event\question_category_created::create($params);
  369. $event->trigger();
  370. if ($return) {
  371. return $categoryid;
  372. } else {
  373. redirect($this->pageurl);//always redirect after successful action
  374. }
  375. }
  376. /**
  377. * Updates an existing category with given params
  378. */
  379. public function update_category($updateid, $newparent, $newname, $newinfo, $newinfoformat = FORMAT_HTML) {
  380. global $CFG, $DB;
  381. if (empty($newname)) {
  382. print_error('categorynamecantbeblank', 'question');
  383. }
  384. // Get the record we are updating.
  385. $oldcat = $DB->get_record('question_categories', array('id' => $updateid));
  386. $lastcategoryinthiscontext = question_is_only_toplevel_category_in_context($updateid);
  387. if (!empty($newparent) && !$lastcategoryinthiscontext) {
  388. list($parentid, $tocontextid) = explode(',', $newparent);
  389. } else {
  390. $parentid = $oldcat->parent;
  391. $tocontextid = $oldcat->contextid;
  392. }
  393. // Check permissions.
  394. $fromcontext = context::instance_by_id($oldcat->contextid);
  395. require_capability('moodle/question:managecategory', $fromcontext);
  396. // If moving to another context, check permissions some more, and confirm contextid,stamp uniqueness.
  397. $newstamprequired = false;
  398. if ($oldcat->contextid != $tocontextid) {
  399. $tocontext = context::instance_by_id($tocontextid);
  400. require_capability('moodle/question:managecategory', $tocontext);
  401. // Confirm stamp uniqueness in the new context. If the stamp already exists, generate a new one.
  402. if ($DB->record_exists('question_categories', array('contextid' => $tocontextid, 'stamp' => $oldcat->stamp))) {
  403. $newstamprequired = true;
  404. }
  405. }
  406. // Update the category record.
  407. $cat = new stdClass();
  408. $cat->id = $updateid;
  409. $cat->name = $newname;
  410. $cat->info = $newinfo;
  411. $cat->infoformat = $newinfoformat;
  412. $cat->parent = $parentid;
  413. $cat->contextid = $tocontextid;
  414. if ($newstamprequired) {
  415. $cat->stamp = make_unique_id_code();
  416. }
  417. $DB->update_record('question_categories', $cat);
  418. // If the category name has changed, rename any random questions in that category.
  419. if ($oldcat->name != $cat->name) {
  420. $where = "qtype = 'random' AND category = ? AND " . $DB->sql_compare_text('questiontext') . " = ?";
  421. $randomqtype = question_bank::get_qtype('random');
  422. $randomqname = $randomqtype->question_name($cat, false);
  423. $DB->set_field_select('question', 'name', $randomqname, $where, array($cat->id, '0'));
  424. $randomqname = $randomqtype->question_name($cat, true);
  425. $DB->set_field_select('question', 'name', $randomqname, $where, array($cat->id, '1'));
  426. }
  427. if ($oldcat->contextid != $tocontextid) {
  428. // Moving to a new context. Must move files belonging to questions.
  429. question_move_category_to_context($cat->id, $oldcat->contextid, $tocontextid);
  430. }
  431. // Cat param depends on the context id, so update it.
  432. $this->pageurl->param('cat', $updateid . ',' . $tocontextid);
  433. redirect($this->pageurl);
  434. }
  435. }