PageRenderTime 62ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/coursecatlib.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 3055 lines | 1697 code | 229 blank | 1129 comment | 321 complexity | 3fe7fd973ca596da4507648cfdb9ca9c MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-3.0

Large files files are truncated, but you can click here to view the full file

  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. * Contains class coursecat reponsible for course category operations
  18. *
  19. * @package core
  20. * @subpackage course
  21. * @copyright 2013 Marina Glancy
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. /**
  26. * Class to store, cache, render and manage course category
  27. *
  28. * @property-read int $id
  29. * @property-read string $name
  30. * @property-read string $idnumber
  31. * @property-read string $description
  32. * @property-read int $descriptionformat
  33. * @property-read int $parent
  34. * @property-read int $sortorder
  35. * @property-read int $coursecount
  36. * @property-read int $visible
  37. * @property-read int $visibleold
  38. * @property-read int $timemodified
  39. * @property-read int $depth
  40. * @property-read string $path
  41. * @property-read string $theme
  42. *
  43. * @package core
  44. * @subpackage course
  45. * @copyright 2013 Marina Glancy
  46. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47. */
  48. class coursecat implements renderable, cacheable_object, IteratorAggregate {
  49. /** @var coursecat stores pseudo category with id=0. Use coursecat::get(0) to retrieve */
  50. protected static $coursecat0;
  51. /** Do not fetch course contacts more often than once per hour. */
  52. const CACHE_COURSE_CONTACTS_TTL = 3600;
  53. /** @var array list of all fields and their short name and default value for caching */
  54. protected static $coursecatfields = array(
  55. 'id' => array('id', 0),
  56. 'name' => array('na', ''),
  57. 'idnumber' => array('in', null),
  58. 'description' => null, // Not cached.
  59. 'descriptionformat' => null, // Not cached.
  60. 'parent' => array('pa', 0),
  61. 'sortorder' => array('so', 0),
  62. 'coursecount' => array('cc', 0),
  63. 'visible' => array('vi', 1),
  64. 'visibleold' => null, // Not cached.
  65. 'timemodified' => null, // Not cached.
  66. 'depth' => array('dh', 1),
  67. 'path' => array('ph', null),
  68. 'theme' => null, // Not cached.
  69. );
  70. /** @var int */
  71. protected $id;
  72. /** @var string */
  73. protected $name = '';
  74. /** @var string */
  75. protected $idnumber = null;
  76. /** @var string */
  77. protected $description = false;
  78. /** @var int */
  79. protected $descriptionformat = false;
  80. /** @var int */
  81. protected $parent = 0;
  82. /** @var int */
  83. protected $sortorder = 0;
  84. /** @var int */
  85. protected $coursecount = false;
  86. /** @var int */
  87. protected $visible = 1;
  88. /** @var int */
  89. protected $visibleold = false;
  90. /** @var int */
  91. protected $timemodified = false;
  92. /** @var int */
  93. protected $depth = 0;
  94. /** @var string */
  95. protected $path = '';
  96. /** @var string */
  97. protected $theme = false;
  98. /** @var bool */
  99. protected $fromcache;
  100. /** @var bool */
  101. protected $hasmanagecapability = null;
  102. /**
  103. * Magic setter method, we do not want anybody to modify properties from the outside
  104. *
  105. * @param string $name
  106. * @param mixed $value
  107. */
  108. public function __set($name, $value) {
  109. debugging('Can not change coursecat instance properties!', DEBUG_DEVELOPER);
  110. }
  111. /**
  112. * Magic method getter, redirects to read only values. Queries from DB the fields that were not cached
  113. *
  114. * @param string $name
  115. * @return mixed
  116. */
  117. public function __get($name) {
  118. global $DB;
  119. if (array_key_exists($name, self::$coursecatfields)) {
  120. if ($this->$name === false) {
  121. // Property was not retrieved from DB, retrieve all not retrieved fields.
  122. $notretrievedfields = array_diff_key(self::$coursecatfields, array_filter(self::$coursecatfields));
  123. $record = $DB->get_record('course_categories', array('id' => $this->id),
  124. join(',', array_keys($notretrievedfields)), MUST_EXIST);
  125. foreach ($record as $key => $value) {
  126. $this->$key = $value;
  127. }
  128. }
  129. return $this->$name;
  130. }
  131. debugging('Invalid coursecat property accessed! '.$name, DEBUG_DEVELOPER);
  132. return null;
  133. }
  134. /**
  135. * Full support for isset on our magic read only properties.
  136. *
  137. * @param string $name
  138. * @return bool
  139. */
  140. public function __isset($name) {
  141. if (array_key_exists($name, self::$coursecatfields)) {
  142. return isset($this->$name);
  143. }
  144. return false;
  145. }
  146. /**
  147. * All properties are read only, sorry.
  148. *
  149. * @param string $name
  150. */
  151. public function __unset($name) {
  152. debugging('Can not unset coursecat instance properties!', DEBUG_DEVELOPER);
  153. }
  154. /**
  155. * Create an iterator because magic vars can't be seen by 'foreach'.
  156. *
  157. * implementing method from interface IteratorAggregate
  158. *
  159. * @return ArrayIterator
  160. */
  161. public function getIterator() {
  162. $ret = array();
  163. foreach (self::$coursecatfields as $property => $unused) {
  164. if ($this->$property !== false) {
  165. $ret[$property] = $this->$property;
  166. }
  167. }
  168. return new ArrayIterator($ret);
  169. }
  170. /**
  171. * Constructor
  172. *
  173. * Constructor is protected, use coursecat::get($id) to retrieve category
  174. *
  175. * @param stdClass $record record from DB (may not contain all fields)
  176. * @param bool $fromcache whether it is being restored from cache
  177. */
  178. protected function __construct(stdClass $record, $fromcache = false) {
  179. context_helper::preload_from_record($record);
  180. foreach ($record as $key => $val) {
  181. if (array_key_exists($key, self::$coursecatfields)) {
  182. $this->$key = $val;
  183. }
  184. }
  185. $this->fromcache = $fromcache;
  186. }
  187. /**
  188. * Returns coursecat object for requested category
  189. *
  190. * If category is not visible to user it is treated as non existing
  191. * unless $alwaysreturnhidden is set to true
  192. *
  193. * If id is 0, the pseudo object for root category is returned (convenient
  194. * for calling other functions such as get_children())
  195. *
  196. * @param int $id category id
  197. * @param int $strictness whether to throw an exception (MUST_EXIST) or
  198. * return null (IGNORE_MISSING) in case the category is not found or
  199. * not visible to current user
  200. * @param bool $alwaysreturnhidden set to true if you want an object to be
  201. * returned even if this category is not visible to the current user
  202. * (category is hidden and user does not have
  203. * 'moodle/category:viewhiddencategories' capability). Use with care!
  204. * @return null|coursecat
  205. * @throws moodle_exception
  206. */
  207. public static function get($id, $strictness = MUST_EXIST, $alwaysreturnhidden = false) {
  208. if (!$id) {
  209. if (!isset(self::$coursecat0)) {
  210. $record = new stdClass();
  211. $record->id = 0;
  212. $record->visible = 1;
  213. $record->depth = 0;
  214. $record->path = '';
  215. self::$coursecat0 = new coursecat($record);
  216. }
  217. return self::$coursecat0;
  218. }
  219. $coursecatrecordcache = cache::make('core', 'coursecatrecords');
  220. $coursecat = $coursecatrecordcache->get($id);
  221. if ($coursecat === false) {
  222. if ($records = self::get_records('cc.id = :id', array('id' => $id))) {
  223. $record = reset($records);
  224. $coursecat = new coursecat($record);
  225. // Store in cache.
  226. $coursecatrecordcache->set($id, $coursecat);
  227. }
  228. }
  229. if ($coursecat && ($alwaysreturnhidden || $coursecat->is_uservisible())) {
  230. return $coursecat;
  231. } else {
  232. if ($strictness == MUST_EXIST) {
  233. throw new moodle_exception('unknowcategory');
  234. }
  235. }
  236. return null;
  237. }
  238. /**
  239. * Load many coursecat objects.
  240. *
  241. * @global moodle_database $DB
  242. * @param array $ids An array of category ID's to load.
  243. * @return coursecat[]
  244. */
  245. public static function get_many(array $ids) {
  246. global $DB;
  247. $coursecatrecordcache = cache::make('core', 'coursecatrecords');
  248. $categories = $coursecatrecordcache->get_many($ids);
  249. $toload = array();
  250. foreach ($categories as $id => $result) {
  251. if ($result === false) {
  252. $toload[] = $id;
  253. }
  254. }
  255. if (!empty($toload)) {
  256. list($where, $params) = $DB->get_in_or_equal($toload, SQL_PARAMS_NAMED);
  257. $records = self::get_records('cc.id '.$where, $params);
  258. $toset = array();
  259. foreach ($records as $record) {
  260. $categories[$record->id] = new coursecat($record);
  261. $toset[$record->id] = $categories[$record->id];
  262. }
  263. $coursecatrecordcache->set_many($toset);
  264. }
  265. return $categories;
  266. }
  267. /**
  268. * Returns the first found category
  269. *
  270. * Note that if there are no categories visible to the current user on the first level,
  271. * the invisible category may be returned
  272. *
  273. * @return coursecat
  274. */
  275. public static function get_default() {
  276. if ($visiblechildren = self::get(0)->get_children()) {
  277. $defcategory = reset($visiblechildren);
  278. } else {
  279. $toplevelcategories = self::get_tree(0);
  280. $defcategoryid = $toplevelcategories[0];
  281. $defcategory = self::get($defcategoryid, MUST_EXIST, true);
  282. }
  283. return $defcategory;
  284. }
  285. /**
  286. * Restores the object after it has been externally modified in DB for example
  287. * during {@link fix_course_sortorder()}
  288. */
  289. protected function restore() {
  290. // Update all fields in the current object.
  291. $newrecord = self::get($this->id, MUST_EXIST, true);
  292. foreach (self::$coursecatfields as $key => $unused) {
  293. $this->$key = $newrecord->$key;
  294. }
  295. }
  296. /**
  297. * Creates a new category either from form data or from raw data
  298. *
  299. * Please note that this function does not verify access control.
  300. *
  301. * Exception is thrown if name is missing or idnumber is duplicating another one in the system.
  302. *
  303. * Category visibility is inherited from parent unless $data->visible = 0 is specified
  304. *
  305. * @param array|stdClass $data
  306. * @param array $editoroptions if specified, the data is considered to be
  307. * form data and file_postupdate_standard_editor() is being called to
  308. * process images in description.
  309. * @return coursecat
  310. * @throws moodle_exception
  311. */
  312. public static function create($data, $editoroptions = null) {
  313. global $DB, $CFG;
  314. $data = (object)$data;
  315. $newcategory = new stdClass();
  316. $newcategory->descriptionformat = FORMAT_MOODLE;
  317. $newcategory->description = '';
  318. // Copy all description* fields regardless of whether this is form data or direct field update.
  319. foreach ($data as $key => $value) {
  320. if (preg_match("/^description/", $key)) {
  321. $newcategory->$key = $value;
  322. }
  323. }
  324. if (empty($data->name)) {
  325. throw new moodle_exception('categorynamerequired');
  326. }
  327. if (core_text::strlen($data->name) > 255) {
  328. throw new moodle_exception('categorytoolong');
  329. }
  330. $newcategory->name = $data->name;
  331. // Validate and set idnumber.
  332. if (!empty($data->idnumber)) {
  333. if (core_text::strlen($data->idnumber) > 100) {
  334. throw new moodle_exception('idnumbertoolong');
  335. }
  336. if ($DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
  337. throw new moodle_exception('categoryidnumbertaken');
  338. }
  339. }
  340. if (isset($data->idnumber)) {
  341. $newcategory->idnumber = $data->idnumber;
  342. }
  343. if (isset($data->theme) && !empty($CFG->allowcategorythemes)) {
  344. $newcategory->theme = $data->theme;
  345. }
  346. if (empty($data->parent)) {
  347. $parent = self::get(0);
  348. } else {
  349. $parent = self::get($data->parent, MUST_EXIST, true);
  350. }
  351. $newcategory->parent = $parent->id;
  352. $newcategory->depth = $parent->depth + 1;
  353. // By default category is visible, unless visible = 0 is specified or parent category is hidden.
  354. if (isset($data->visible) && !$data->visible) {
  355. // Create a hidden category.
  356. $newcategory->visible = $newcategory->visibleold = 0;
  357. } else {
  358. // Create a category that inherits visibility from parent.
  359. $newcategory->visible = $parent->visible;
  360. // In case parent is hidden, when it changes visibility this new subcategory will automatically become visible too.
  361. $newcategory->visibleold = 1;
  362. }
  363. $newcategory->sortorder = 0;
  364. $newcategory->timemodified = time();
  365. $newcategory->id = $DB->insert_record('course_categories', $newcategory);
  366. // Update path (only possible after we know the category id.
  367. $path = $parent->path . '/' . $newcategory->id;
  368. $DB->set_field('course_categories', 'path', $path, array('id' => $newcategory->id));
  369. // We should mark the context as dirty.
  370. context_coursecat::instance($newcategory->id)->mark_dirty();
  371. fix_course_sortorder();
  372. // If this is data from form results, save embedded files and update description.
  373. $categorycontext = context_coursecat::instance($newcategory->id);
  374. if ($editoroptions) {
  375. $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext,
  376. 'coursecat', 'description', 0);
  377. // Update only fields description and descriptionformat.
  378. $updatedata = new stdClass();
  379. $updatedata->id = $newcategory->id;
  380. $updatedata->description = $newcategory->description;
  381. $updatedata->descriptionformat = $newcategory->descriptionformat;
  382. $DB->update_record('course_categories', $updatedata);
  383. }
  384. add_to_log(SITEID, "category", 'add', "editcategory.php?id=$newcategory->id", $newcategory->id);
  385. cache_helper::purge_by_event('changesincoursecat');
  386. return self::get($newcategory->id, MUST_EXIST, true);
  387. }
  388. /**
  389. * Updates the record with either form data or raw data
  390. *
  391. * Please note that this function does not verify access control.
  392. *
  393. * This function calls coursecat::change_parent_raw if field 'parent' is updated.
  394. * It also calls coursecat::hide_raw or coursecat::show_raw if 'visible' is updated.
  395. * Visibility is changed first and then parent is changed. This means that
  396. * if parent category is hidden, the current category will become hidden
  397. * too and it may overwrite whatever was set in field 'visible'.
  398. *
  399. * Note that fields 'path' and 'depth' can not be updated manually
  400. * Also coursecat::update() can not directly update the field 'sortoder'
  401. *
  402. * @param array|stdClass $data
  403. * @param array $editoroptions if specified, the data is considered to be
  404. * form data and file_postupdate_standard_editor() is being called to
  405. * process images in description.
  406. * @throws moodle_exception
  407. */
  408. public function update($data, $editoroptions = null) {
  409. global $DB, $CFG;
  410. if (!$this->id) {
  411. // There is no actual DB record associated with root category.
  412. return;
  413. }
  414. $data = (object)$data;
  415. $newcategory = new stdClass();
  416. $newcategory->id = $this->id;
  417. // Copy all description* fields regardless of whether this is form data or direct field update.
  418. foreach ($data as $key => $value) {
  419. if (preg_match("/^description/", $key)) {
  420. $newcategory->$key = $value;
  421. }
  422. }
  423. if (isset($data->name) && empty($data->name)) {
  424. throw new moodle_exception('categorynamerequired');
  425. }
  426. if (!empty($data->name) && $data->name !== $this->name) {
  427. if (core_text::strlen($data->name) > 255) {
  428. throw new moodle_exception('categorytoolong');
  429. }
  430. $newcategory->name = $data->name;
  431. }
  432. if (isset($data->idnumber) && $data->idnumber != $this->idnumber) {
  433. if (core_text::strlen($data->idnumber) > 100) {
  434. throw new moodle_exception('idnumbertoolong');
  435. }
  436. if ($DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
  437. throw new moodle_exception('categoryidnumbertaken');
  438. }
  439. $newcategory->idnumber = $data->idnumber;
  440. }
  441. if (isset($data->theme) && !empty($CFG->allowcategorythemes)) {
  442. $newcategory->theme = $data->theme;
  443. }
  444. $changes = false;
  445. if (isset($data->visible)) {
  446. if ($data->visible) {
  447. $changes = $this->show_raw();
  448. } else {
  449. $changes = $this->hide_raw(0);
  450. }
  451. }
  452. if (isset($data->parent) && $data->parent != $this->parent) {
  453. if ($changes) {
  454. cache_helper::purge_by_event('changesincoursecat');
  455. }
  456. $parentcat = self::get($data->parent, MUST_EXIST, true);
  457. $this->change_parent_raw($parentcat);
  458. fix_course_sortorder();
  459. }
  460. $newcategory->timemodified = time();
  461. if ($editoroptions) {
  462. $categorycontext = $this->get_context();
  463. $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext,
  464. 'coursecat', 'description', 0);
  465. }
  466. $DB->update_record('course_categories', $newcategory);
  467. add_to_log(SITEID, "category", 'update', "editcategory.php?id=$this->id", $this->id);
  468. fix_course_sortorder();
  469. // Purge cache even if fix_course_sortorder() did not do it.
  470. cache_helper::purge_by_event('changesincoursecat');
  471. // Update all fields in the current object.
  472. $this->restore();
  473. }
  474. /**
  475. * Checks if this course category is visible to current user
  476. *
  477. * Please note that methods coursecat::get (without 3rd argumet),
  478. * coursecat::get_children(), etc. return only visible categories so it is
  479. * usually not needed to call this function outside of this class
  480. *
  481. * @return bool
  482. */
  483. public function is_uservisible() {
  484. return !$this->id || $this->visible ||
  485. has_capability('moodle/category:viewhiddencategories', $this->get_context());
  486. }
  487. /**
  488. * Returns all categories visible to the current user
  489. *
  490. * This is a generic function that returns an array of
  491. * (category id => coursecat object) sorted by sortorder
  492. *
  493. * @see coursecat::get_children()
  494. * @see coursecat::get_all_parents()
  495. *
  496. * @return cacheable_object_array array of coursecat objects
  497. */
  498. public static function get_all_visible() {
  499. global $USER;
  500. $coursecatcache = cache::make('core', 'coursecat');
  501. $ids = $coursecatcache->get('user'. $USER->id);
  502. if ($ids === false) {
  503. $all = self::get_all_ids();
  504. $parentvisible = $all[0];
  505. $rv = array();
  506. foreach ($all as $id => $children) {
  507. if ($id && in_array($id, $parentvisible) &&
  508. ($coursecat = self::get($id, IGNORE_MISSING)) &&
  509. (!$coursecat->parent || isset($rv[$coursecat->parent]))) {
  510. $rv[$id] = $coursecat;
  511. $parentvisible += $children;
  512. }
  513. }
  514. $coursecatcache->set('user'. $USER->id, array_keys($rv));
  515. } else {
  516. $rv = array();
  517. foreach ($ids as $id) {
  518. if ($coursecat = self::get($id, IGNORE_MISSING)) {
  519. $rv[$id] = $coursecat;
  520. }
  521. }
  522. }
  523. return $rv;
  524. }
  525. /**
  526. * Returns the complete corresponding record from DB table course_categories
  527. *
  528. * Mostly used in deprecated functions
  529. *
  530. * @return stdClass
  531. */
  532. public function get_db_record() {
  533. global $DB;
  534. if ($record = $DB->get_record('course_categories', array('id' => $this->id))) {
  535. return $record;
  536. } else {
  537. return (object)convert_to_array($this);
  538. }
  539. }
  540. /**
  541. * Returns the entry from categories tree and makes sure the application-level tree cache is built
  542. *
  543. * The following keys can be requested:
  544. *
  545. * 'countall' - total number of categories in the system (always present)
  546. * 0 - array of ids of top-level categories (always present)
  547. * '0i' - array of ids of top-level categories that have visible=0 (always present but may be empty array)
  548. * $id (int) - array of ids of categories that are direct children of category with id $id. If
  549. * category with id $id does not exist returns false. If category has no children returns empty array
  550. * $id.'i' - array of ids of children categories that have visible=0
  551. *
  552. * @param int|string $id
  553. * @return mixed
  554. */
  555. protected static function get_tree($id) {
  556. global $DB;
  557. $coursecattreecache = cache::make('core', 'coursecattree');
  558. $rv = $coursecattreecache->get($id);
  559. if ($rv !== false) {
  560. return $rv;
  561. }
  562. // Re-build the tree.
  563. $sql = "SELECT cc.id, cc.parent, cc.visible
  564. FROM {course_categories} cc
  565. ORDER BY cc.sortorder";
  566. $rs = $DB->get_recordset_sql($sql, array());
  567. $all = array(0 => array(), '0i' => array());
  568. $count = 0;
  569. foreach ($rs as $record) {
  570. $all[$record->id] = array();
  571. $all[$record->id. 'i'] = array();
  572. if (array_key_exists($record->parent, $all)) {
  573. $all[$record->parent][] = $record->id;
  574. if (!$record->visible) {
  575. $all[$record->parent. 'i'][] = $record->id;
  576. }
  577. } else {
  578. // Parent not found. This is data consistency error but next fix_course_sortorder() should fix it.
  579. $all[0][] = $record->id;
  580. if (!$record->visible) {
  581. $all['0i'][] = $record->id;
  582. }
  583. }
  584. $count++;
  585. }
  586. $rs->close();
  587. if (!$count) {
  588. // No categories found.
  589. // This may happen after upgrade of a very old moodle version.
  590. // In new versions the default category is created on install.
  591. $defcoursecat = self::create(array('name' => get_string('miscellaneous')));
  592. set_config('defaultrequestcategory', $defcoursecat->id);
  593. $all[0] = array($defcoursecat->id);
  594. $all[$defcoursecat->id] = array();
  595. $count++;
  596. }
  597. // We must add countall to all in case it was the requested ID.
  598. $all['countall'] = $count;
  599. foreach ($all as $key => $children) {
  600. $coursecattreecache->set($key, $children);
  601. }
  602. if (array_key_exists($id, $all)) {
  603. return $all[$id];
  604. }
  605. // Requested non-existing category.
  606. return array();
  607. }
  608. /**
  609. * Returns number of ALL categories in the system regardless if
  610. * they are visible to current user or not
  611. *
  612. * @return int
  613. */
  614. public static function count_all() {
  615. return self::get_tree('countall');
  616. }
  617. /**
  618. * Retrieves number of records from course_categories table
  619. *
  620. * Only cached fields are retrieved. Records are ready for preloading context
  621. *
  622. * @param string $whereclause
  623. * @param array $params
  624. * @return array array of stdClass objects
  625. */
  626. protected static function get_records($whereclause, $params) {
  627. global $DB;
  628. // Retrieve from DB only the fields that need to be stored in cache.
  629. $fields = array_keys(array_filter(self::$coursecatfields));
  630. $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
  631. $sql = "SELECT cc.". join(',cc.', $fields). ", $ctxselect
  632. FROM {course_categories} cc
  633. JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat
  634. WHERE ". $whereclause." ORDER BY cc.sortorder";
  635. return $DB->get_records_sql($sql,
  636. array('contextcoursecat' => CONTEXT_COURSECAT) + $params);
  637. }
  638. /**
  639. * Given list of DB records from table course populates each record with list of users with course contact roles
  640. *
  641. * This function fills the courses with raw information as {@link get_role_users()} would do.
  642. * See also {@link course_in_list::get_course_contacts()} for more readable return
  643. *
  644. * $courses[$i]->managers = array(
  645. * $roleassignmentid => $roleuser,
  646. * ...
  647. * );
  648. *
  649. * where $roleuser is an stdClass with the following properties:
  650. *
  651. * $roleuser->raid - role assignment id
  652. * $roleuser->id - user id
  653. * $roleuser->username
  654. * $roleuser->firstname
  655. * $roleuser->lastname
  656. * $roleuser->rolecoursealias
  657. * $roleuser->rolename
  658. * $roleuser->sortorder - role sortorder
  659. * $roleuser->roleid
  660. * $roleuser->roleshortname
  661. *
  662. * @todo MDL-38596 minimize number of queries to preload contacts for the list of courses
  663. *
  664. * @param array $courses
  665. */
  666. public static function preload_course_contacts(&$courses) {
  667. global $CFG, $DB;
  668. if (empty($courses) || empty($CFG->coursecontact)) {
  669. return;
  670. }
  671. $managerroles = explode(',', $CFG->coursecontact);
  672. $cache = cache::make('core', 'coursecontacts');
  673. $cacheddata = $cache->get_many(array_merge(array('basic'), array_keys($courses)));
  674. // Check if cache was set for the current course contacts and it is not yet expired.
  675. if (empty($cacheddata['basic']) || $cacheddata['basic']['roles'] !== $CFG->coursecontact ||
  676. $cacheddata['basic']['lastreset'] < time() - self::CACHE_COURSE_CONTACTS_TTL) {
  677. // Reset cache.
  678. $cache->purge();
  679. $cache->set('basic', array('roles' => $CFG->coursecontact, 'lastreset' => time()));
  680. $cacheddata = $cache->get_many(array_merge(array('basic'), array_keys($courses)));
  681. }
  682. $courseids = array();
  683. foreach (array_keys($courses) as $id) {
  684. if ($cacheddata[$id] !== false) {
  685. $courses[$id]->managers = $cacheddata[$id];
  686. } else {
  687. $courseids[] = $id;
  688. }
  689. }
  690. // Array $courseids now stores list of ids of courses for which we still need to retrieve contacts.
  691. if (empty($courseids)) {
  692. return;
  693. }
  694. // First build the array of all context ids of the courses and their categories.
  695. $allcontexts = array();
  696. foreach ($courseids as $id) {
  697. $context = context_course::instance($id);
  698. $courses[$id]->managers = array();
  699. foreach (preg_split('|/|', $context->path, 0, PREG_SPLIT_NO_EMPTY) as $ctxid) {
  700. if (!isset($allcontexts[$ctxid])) {
  701. $allcontexts[$ctxid] = array();
  702. }
  703. $allcontexts[$ctxid][] = $id;
  704. }
  705. }
  706. // Fetch list of all users with course contact roles in any of the courses contexts or parent contexts.
  707. list($sql1, $params1) = $DB->get_in_or_equal(array_keys($allcontexts), SQL_PARAMS_NAMED, 'ctxid');
  708. list($sql2, $params2) = $DB->get_in_or_equal($managerroles, SQL_PARAMS_NAMED, 'rid');
  709. list($sort, $sortparams) = users_order_by_sql('u');
  710. $notdeleted = array('notdeleted'=>0);
  711. $allnames = get_all_user_name_fields(true, 'u');
  712. $sql = "SELECT ra.contextid, ra.id AS raid,
  713. r.id AS roleid, r.name AS rolename, r.shortname AS roleshortname,
  714. rn.name AS rolecoursealias, u.id, u.username, $allnames
  715. FROM {role_assignments} ra
  716. JOIN {user} u ON ra.userid = u.id
  717. JOIN {role} r ON ra.roleid = r.id
  718. LEFT JOIN {role_names} rn ON (rn.contextid = ra.contextid AND rn.roleid = r.id)
  719. WHERE ra.contextid ". $sql1." AND ra.roleid ". $sql2." AND u.deleted = :notdeleted
  720. ORDER BY r.sortorder, $sort";
  721. $rs = $DB->get_recordset_sql($sql, $params1 + $params2 + $notdeleted + $sortparams);
  722. $checkenrolments = array();
  723. foreach ($rs as $ra) {
  724. foreach ($allcontexts[$ra->contextid] as $id) {
  725. $courses[$id]->managers[$ra->raid] = $ra;
  726. if (!isset($checkenrolments[$id])) {
  727. $checkenrolments[$id] = array();
  728. }
  729. $checkenrolments[$id][] = $ra->id;
  730. }
  731. }
  732. $rs->close();
  733. // Remove from course contacts users who are not enrolled in the course.
  734. $enrolleduserids = self::ensure_users_enrolled($checkenrolments);
  735. foreach ($checkenrolments as $id => $userids) {
  736. if (empty($enrolleduserids[$id])) {
  737. $courses[$id]->managers = array();
  738. } else if ($notenrolled = array_diff($userids, $enrolleduserids[$id])) {
  739. foreach ($courses[$id]->managers as $raid => $ra) {
  740. if (in_array($ra->id, $notenrolled)) {
  741. unset($courses[$id]->managers[$raid]);
  742. }
  743. }
  744. }
  745. }
  746. // Set the cache.
  747. $values = array();
  748. foreach ($courseids as $id) {
  749. $values[$id] = $courses[$id]->managers;
  750. }
  751. $cache->set_many($values);
  752. }
  753. /**
  754. * Verify user enrollments for multiple course-user combinations
  755. *
  756. * @param array $courseusers array where keys are course ids and values are array
  757. * of users in this course whose enrolment we wish to verify
  758. * @return array same structure as input array but values list only users from input
  759. * who are enrolled in the course
  760. */
  761. protected static function ensure_users_enrolled($courseusers) {
  762. global $DB;
  763. // If the input array is too big, split it into chunks.
  764. $maxcoursesinquery = 20;
  765. if (count($courseusers) > $maxcoursesinquery) {
  766. $rv = array();
  767. for ($offset = 0; $offset < count($courseusers); $offset += $maxcoursesinquery) {
  768. $chunk = array_slice($courseusers, $offset, $maxcoursesinquery, true);
  769. $rv = $rv + self::ensure_users_enrolled($chunk);
  770. }
  771. return $rv;
  772. }
  773. // Create a query verifying valid user enrolments for the number of courses.
  774. $sql = "SELECT DISTINCT e.courseid, ue.userid
  775. FROM {user_enrolments} ue
  776. JOIN {enrol} e ON e.id = ue.enrolid
  777. WHERE ue.status = :active
  778. AND e.status = :enabled
  779. AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
  780. $now = round(time(), -2); // Rounding helps caching in DB.
  781. $params = array('enabled' => ENROL_INSTANCE_ENABLED,
  782. 'active' => ENROL_USER_ACTIVE,
  783. 'now1' => $now, 'now2' => $now);
  784. $cnt = 0;
  785. $subsqls = array();
  786. $enrolled = array();
  787. foreach ($courseusers as $id => $userids) {
  788. $enrolled[$id] = array();
  789. if (count($userids)) {
  790. list($sql2, $params2) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'userid'.$cnt.'_');
  791. $subsqls[] = "(e.courseid = :courseid$cnt AND ue.userid ".$sql2.")";
  792. $params = $params + array('courseid'.$cnt => $id) + $params2;
  793. $cnt++;
  794. }
  795. }
  796. if (count($subsqls)) {
  797. $sql .= "AND (". join(' OR ', $subsqls).")";
  798. $rs = $DB->get_recordset_sql($sql, $params);
  799. foreach ($rs as $record) {
  800. $enrolled[$record->courseid][] = $record->userid;
  801. }
  802. $rs->close();
  803. }
  804. return $enrolled;
  805. }
  806. /**
  807. * Retrieves number of records from course table
  808. *
  809. * Not all fields are retrieved. Records are ready for preloading context
  810. *
  811. * @param string $whereclause
  812. * @param array $params
  813. * @param array $options may indicate that summary and/or coursecontacts need to be retrieved
  814. * @param bool $checkvisibility if true, capability 'moodle/course:viewhiddencourses' will be checked
  815. * on not visible courses
  816. * @return array array of stdClass objects
  817. */
  818. protected static function get_course_records($whereclause, $params, $options, $checkvisibility = false) {
  819. global $DB;
  820. $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
  821. $fields = array('c.id', 'c.category', 'c.sortorder',
  822. 'c.shortname', 'c.fullname', 'c.idnumber',
  823. 'c.startdate', 'c.visible', 'c.cacherev');
  824. if (!empty($options['summary'])) {
  825. $fields[] = 'c.summary';
  826. $fields[] = 'c.summaryformat';
  827. } else {
  828. $fields[] = $DB->sql_substr('c.summary', 1, 1). ' as hassummary';
  829. }
  830. $sql = "SELECT ". join(',', $fields). ", $ctxselect
  831. FROM {course} c
  832. JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextcourse
  833. WHERE ". $whereclause." ORDER BY c.sortorder";
  834. $list = $DB->get_records_sql($sql,
  835. array('contextcourse' => CONTEXT_COURSE) + $params);
  836. if ($checkvisibility) {
  837. // Loop through all records and make sure we only return the courses accessible by user.
  838. foreach ($list as $course) {
  839. if (isset($list[$course->id]->hassummary)) {
  840. $list[$course->id]->hassummary = strlen($list[$course->id]->hassummary) > 0;
  841. }
  842. if (empty($course->visible)) {
  843. // Load context only if we need to check capability.
  844. context_helper::preload_from_record($course);
  845. if (!has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
  846. unset($list[$course->id]);
  847. }
  848. }
  849. }
  850. }
  851. // Preload course contacts if necessary.
  852. if (!empty($options['coursecontacts'])) {
  853. self::preload_course_contacts($list);
  854. }
  855. return $list;
  856. }
  857. /**
  858. * Returns array of ids of children categories that current user can not see
  859. *
  860. * This data is cached in user session cache
  861. *
  862. * @return array
  863. */
  864. protected function get_not_visible_children_ids() {
  865. global $DB;
  866. $coursecatcache = cache::make('core', 'coursecat');
  867. if (($invisibleids = $coursecatcache->get('ic'. $this->id)) === false) {
  868. // We never checked visible children before.
  869. $hidden = self::get_tree($this->id.'i');
  870. $invisibleids = array();
  871. if ($hidden) {
  872. // Preload categories contexts.
  873. list($sql, $params) = $DB->get_in_or_equal($hidden, SQL_PARAMS_NAMED, 'id');
  874. $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
  875. $contexts = $DB->get_records_sql("SELECT $ctxselect FROM {context} ctx
  876. WHERE ctx.contextlevel = :contextcoursecat AND ctx.instanceid ".$sql,
  877. array('contextcoursecat' => CONTEXT_COURSECAT) + $params);
  878. foreach ($contexts as $record) {
  879. context_helper::preload_from_record($record);
  880. }
  881. // Check that user has 'viewhiddencategories' capability for each hidden category.
  882. foreach ($hidden as $id) {
  883. if (!has_capability('moodle/category:viewhiddencategories', context_coursecat::instance($id))) {
  884. $invisibleids[] = $id;
  885. }
  886. }
  887. }
  888. $coursecatcache->set('ic'. $this->id, $invisibleids);
  889. }
  890. return $invisibleids;
  891. }
  892. /**
  893. * Sorts list of records by several fields
  894. *
  895. * @param array $records array of stdClass objects
  896. * @param array $sortfields assoc array where key is the field to sort and value is 1 for asc or -1 for desc
  897. * @return int
  898. */
  899. protected static function sort_records(&$records, $sortfields) {
  900. if (empty($records)) {
  901. return;
  902. }
  903. // If sorting by course display name, calculate it (it may be fullname or shortname+fullname).
  904. if (array_key_exists('displayname', $sortfields)) {
  905. foreach ($records as $key => $record) {
  906. if (!isset($record->displayname)) {
  907. $records[$key]->displayname = get_course_display_name_for_list($record);
  908. }
  909. }
  910. }
  911. // Sorting by one field - use core_collator.
  912. if (count($sortfields) == 1) {
  913. $property = key($sortfields);
  914. if (in_array($property, array('sortorder', 'id', 'visible', 'parent', 'depth'))) {
  915. $sortflag = core_collator::SORT_NUMERIC;
  916. } else if (in_array($property, array('idnumber', 'displayname', 'name', 'shortname', 'fullname'))) {
  917. $sortflag = core_collator::SORT_STRING;
  918. } else {
  919. $sortflag = core_collator::SORT_REGULAR;
  920. }
  921. core_collator::asort_objects_by_property($records, $property, $sortflag);
  922. if ($sortfields[$property] < 0) {
  923. $records = array_reverse($records, true);
  924. }
  925. return;
  926. }
  927. $records = coursecat_sortable_records::sort($records, $sortfields);
  928. }
  929. /**
  930. * Returns array of children categories visible to the current user
  931. *
  932. * @param array $options options for retrieving children
  933. * - sort - list of fields to sort. Example
  934. * array('idnumber' => 1, 'name' => 1, 'id' => -1)
  935. * will sort by idnumber asc, name asc and id desc.
  936. * Default: array('sortorder' => 1)
  937. * Only cached fields may be used for sorting!
  938. * - offset
  939. * - limit - maximum number of children to return, 0 or null for no limit
  940. * @return coursecat[] Array of coursecat objects indexed by category id
  941. */
  942. public function get_children($options = array()) {
  943. global $DB;
  944. $coursecatcache = cache::make('core', 'coursecat');
  945. // Get default values for options.
  946. if (!empty($options['sort']) && is_array($options['sort'])) {
  947. $sortfields = $options['sort'];
  948. } else {
  949. $sortfields = array('sortorder' => 1);
  950. }
  951. $limit = null;
  952. if (!empty($options['limit']) && (int)$options['limit']) {
  953. $limit = (int)$options['limit'];
  954. }
  955. $offset = 0;
  956. if (!empty($options['offset']) && (int)$options['offset']) {
  957. $offset = (int)$options['offset'];
  958. }
  959. // First retrieve list of user-visible and sorted children ids from cache.
  960. $sortedids = $coursecatcache->get('c'. $this->id. ':'. serialize($sortfields));
  961. if ($sortedids === false) {
  962. $sortfieldskeys = array_keys($sortfields);
  963. if ($sortfieldskeys[0] === 'sortorder') {
  964. // No DB requests required to build the list of ids sorted by sortorder.
  965. // We can easily ignore other sort fields because sortorder is always different.
  966. $sortedids = self::get_tree($this->id);
  967. if ($sortedids && ($invisibleids = $this->get_not_visible_children_ids())) {
  968. $sortedids = array_diff($sortedids, $invisibleids);
  969. if ($sortfields['sortorder'] == -1) {
  970. $sortedids = array_reverse($sortedids, true);
  971. }
  972. }
  973. } else {
  974. // We need to retrieve and sort all children. Good thing that it is done only on first request.
  975. if ($invisibleids = $this->get_not_visible_children_ids()) {
  976. list($sql, $params) = $DB->get_in_or_equal($invisibleids, SQL_PARAMS_NAMED, 'id', false);
  977. $records = self::get_records('cc.parent = :parent AND cc.id '. $sql,
  978. array('parent' => $this->id) + $params);
  979. } else {
  980. $records = self::get_records('cc.parent = :parent', array('parent' => $this->id));
  981. }
  982. self::sort_records($records, $sortfields);
  983. $sortedids = array_keys($records);
  984. }
  985. $coursecatcache->set('c'. $this->id. ':'.serialize($sortfields), $sortedids);
  986. }
  987. if (empty($sortedids)) {
  988. return array();
  989. }
  990. // Now retrieive and return categories.
  991. if ($offset || $limit) {
  992. $sortedids = array_slice($sortedids, $offset, $limit);
  993. }
  994. if (isset($records)) {
  995. // Easy, we have already retrieved records.
  996. if ($offset || $limit) {
  997. $records = array_slice($records, $offset, $limit, true);
  998. }
  999. } else {
  1000. list($sql, $params) = $DB->get_in_or_equal($sortedids, SQL_PARAMS_NAMED, 'id');
  1001. $records = self::get_records('cc.id '. $sql, array('parent' => $this->id) + $params);
  1002. }
  1003. $rv = array();
  1004. foreach ($sortedids as $id) {
  1005. if (isset($records[$id])) {
  1006. $rv[$id] = new coursecat($records[$id]);
  1007. }
  1008. }
  1009. return $rv;
  1010. }
  1011. /**
  1012. * Returns true if the user has the manage capability on any category.
  1013. *
  1014. * This method uses the coursecat cache and an entry `has_manage_capability` to speed up
  1015. * calls to this method.
  1016. *
  1017. * @return bool
  1018. */
  1019. public static function has_manage_capability_on_any() {
  1020. return self::has_capability_on_any('moodle/category:manage');
  1021. }
  1022. /**
  1023. * Checks if the user has at least one of the given capabilities on any category.
  1024. *
  1025. * @param array|string $capabilities One or more capabilities to check. Check made is an OR.
  1026. * @return bool
  1027. */
  1028. public static function has_capability_on_any($capabilities) {
  1029. global $DB;
  1030. if (!isloggedin() || isguestuser()) {
  1031. return false;
  1032. }
  1033. if (!is_array($capabilities)) {
  1034. $capabilities = array($capabilities);
  1035. }
  1036. $keys = array();
  1037. foreach ($capabilities as $capability) {
  1038. $keys[$capability] = sha1($capability);
  1039. }
  1040. /* @var cache_session $cache */
  1041. $cache = cache::make('core', 'coursecat');
  1042. $hascapability = $cache->get_many($keys);
  1043. $needtoload = false;
  1044. foreach ($hascapability as $capability) {
  1045. if ($capability === '1') {
  1046. return true;
  1047. } else if ($capability === false) {
  1048. $needtoload = true;
  1049. }
  1050. }
  1051. if ($needtoload === false) {
  1052. // All capabilities were retrieved and the user didn't have any.
  1053. return false;
  1054. }
  1055. $haskey = null;
  1056. $fields = context_helper::get_preload_record_columns_sql('ctx');
  1057. $sql = "SELECT ctx.instanceid AS categoryid, $fields
  1058. FROM {context} ctx
  1059. WHERE contextlevel = :contextlevel
  1060. ORDER BY depth ASC";
  1061. $params = array('contextlevel' => CONTEXT_COURSECAT);
  1062. $recordset = $DB->get_recordset_sql($sql, $params);
  1063. foreach ($recordset as $context) {
  1064. context_helper::preload_from_record($context);
  1065. $context = context_coursecat::instance($context->categoryid);
  1066. foreach ($capabilities as $capability) {
  1067. if (has_capability($capability, $context)) {
  1068. $haskey = $capability;
  1069. break 2;
  1070. }
  1071. }
  1072. }
  1073. $recordset->close();
  1074. if ($haskey === null) {
  1075. $data = array();
  1076. foreach ($keys as $key) {
  1077. $data[$key] = '0';
  1078. }
  1079. $cache->set_many($data);
  1080. return false;
  1081. } else {
  1082. $cache->set($haskey, '1');
  1083. return true;
  1084. }
  1085. }
  1086. /**
  1087. * Returns true if the user can resort any category.
  1088. * @return bool
  1089. */
  1090. public static function can_resort_any() {
  1091. return self::has_manage_capability_on_any();
  1092. }
  1093. /**
  1094. * Returns true if the user can change the parent of any category.
  1095. * @return bool
  1096. */
  1097. public static function can_change_parent_any() {
  1098. return self::has_manage_capability_on_any();
  1099. }
  1100. /**
  1101. * Returns number of subcategories visible to the current user
  1102. *
  1103. * @return int
  1104. */
  1105. public function get_children_count() {
  1106. $sortedids = self::get_tree($this->id);
  1107. $invisibleids = $this->get_not_visible_children_ids();
  1108. return count($sortedids) - count($invisibleids);
  1109. }
  1110. /**
  1111. * Returns true if the category has ANY children, including those not visible to the user
  1112. *
  1113. * @return boolean
  1114. */
  1115. public function has_children() {
  1116. $allchildren = self::get_tree($this->id);
  1117. return !empty($allchildren);
  1118. }
  1119. /**
  1120. * Returns true if the category has courses in it (count does not include courses
  1121. * in child categories)
  1122. *
  1123. * @return bool
  1124. */
  1125. public function has_courses() {
  1126. global $DB;
  1127. return $DB->record_exists_sql("select 1 from {course} where category = ?",
  1128. array($this->id));
  1129. }
  1130. /**
  1131. * Searches courses
  1132. *
  1133. * List of found course ids is cached for 10 minutes. Cache may be purged prior
  1134. * to this when somebody edits courses or categories, however it is very
  1135. * difficult to keep track of all possible changes that may affect list of courses.
  1136. *
  1137. * @param array $search contains search criterias, such as:
  1138. * - search - search string
  1139. * - blocklist - id of block (if we are searching for courses containing specific block0
  1140. * - modulelist - name of module (if we are searching for courses containing specific module
  1141. * - tagid - id of tag
  1142. * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
  1143. * search is always category-independent
  1144. * @return course_in_list[]
  1145. */
  1146. public static function search_courses($search, $options = array()) {
  1147. global $DB;
  1148. $offset = !empty($options['offset']) ? $options['offset'] : 0;
  1149. $limit = !empty($options['limit']) ? $options['limit'] : null;
  1150. $sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1);
  1151. $coursecatcache = cache::make('core', 'coursecat');
  1152. $cachekey = 's-'. serialize($search + array('sort' => $sortfields));
  1153. $cntcachekey = 'scnt-'. serialize($search);
  1154. $ids = $coursecatcache->get($cachekey);
  1155. if ($ids !== false) {
  1156. // We already cached last search result.
  1157. $ids = array_slice($ids, $offset, $limit);
  1158. $courses = array();
  1159. if (!empty($ids)) {
  1160. list($sql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'id');
  1161. $records = self::get_course_records("c.id ". $sql, $params, $options);
  1162. // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
  1163. if (!empty($options['coursecontacts'])) {
  1164. self::preload_course_contacts($records);
  1165. }
  1166. // If option 'idonly' is specified no further action is needed, just return list of ids.
  1167. if (!empty($options['idonly'])) {
  1168. return array_keys($records);
  1169. }
  1170. // Prepare the list of course_in_list objects.
  1171. foreach ($ids as $id) {
  1172. $courses[$id] = new course_in_list($records[$id]);
  1173. }
  1174. }
  1175. return $courses;
  1176. }
  1177. $preloadcoursecontacts = !empty($options['coursecontacts']);
  1178. unset($options['coursecontacts']);
  1179. if (!empty($search['search'])) {
  1180. // Search courses that have specified words in their names/summaries.
  1181. $searchterms = preg_split('|\s+|', trim($search['search']), 0, PREG_SPLIT_NO_EMPTY);
  1182. $searchterms = array_filter($searchterms, create_function('$v', 'return strlen($v) > 1;'

Large files files are truncated, but you can click here to view the full file