PageRenderTime 68ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/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
  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;'));
  1183. $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount);
  1184. self::sort_records($courselist, $sortfields);
  1185. $coursecatcache->set($cachekey, array_keys($courselist));
  1186. $coursecatcache->set($cntcachekey, $totalcount);
  1187. $records = array_slice($courselist, $offset, $limit, true);
  1188. } else {
  1189. if (!empty($search['blocklist'])) {
  1190. // Search courses that have block with specified id.
  1191. $blockname = $DB->get_field('block', 'name', array('id' => $search['blocklist']));
  1192. $where = 'ctx.id in (SELECT distinct bi.parentcontextid FROM {block_instances} bi
  1193. WHERE bi.blockname = :blockname)';
  1194. $params = array('blockname' => $blockname);
  1195. } else if (!empty($search['modulelist'])) {
  1196. // Search courses that have module with specified name.
  1197. $where = "c.id IN (SELECT DISTINCT module.course ".
  1198. "FROM {".$search['modulelist']."} module)";
  1199. $params = array();
  1200. } else if (!empty($search['tagid'])) {
  1201. // Search courses that are tagged with the specified tag.
  1202. $where = "c.id IN (SELECT t.itemid ".
  1203. "FROM {tag_instance} t WHERE t.tagid = :tagid AND t.itemtype = :itemtype)";
  1204. $params = array('tagid' => $search['tagid'], 'itemtype' => 'course');
  1205. } else {
  1206. debugging('No criteria is specified while searching courses', DEBUG_DEVELOPER);
  1207. return array();
  1208. }
  1209. $courselist = self::get_course_records($where, $params, $options, true);
  1210. self::sort_records($courselist, $sortfields);
  1211. $coursecatcache->set($cachekey, array_keys($courselist));
  1212. $coursecatcache->set($cntcachekey, count($courselist));
  1213. $records = array_slice($courselist, $offset, $limit, true);
  1214. }
  1215. // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
  1216. if (!empty($preloadcoursecontacts)) {
  1217. self::preload_course_contacts($records);
  1218. }
  1219. // If option 'idonly' is specified no further action is needed, just return list of ids.
  1220. if (!empty($options['idonly'])) {
  1221. return array_keys($records);
  1222. }
  1223. // Prepare the list of course_in_list objects.
  1224. $courses = array();
  1225. foreach ($records as $record) {
  1226. $courses[$record->id] = new course_in_list($record);
  1227. }
  1228. return $courses;
  1229. }
  1230. /**
  1231. * Returns number of courses in the search results
  1232. *
  1233. * It is recommended to call this function after {@link coursecat::search_courses()}
  1234. * and not before because only course ids are cached. Otherwise search_courses() may
  1235. * perform extra DB queries.
  1236. *
  1237. * @param array $search search criteria, see method search_courses() for more details
  1238. * @param array $options display options. They do not affect the result but
  1239. * the 'sort' property is used in cache key for storing list of course ids
  1240. * @return int
  1241. */
  1242. public static function search_courses_count($search, $options = array()) {
  1243. $coursecatcache = cache::make('core', 'coursecat');
  1244. $cntcachekey = 'scnt-'. serialize($search);
  1245. if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
  1246. // Cached value not found. Retrieve ALL courses and return their count.
  1247. unset($options['offset']);
  1248. unset($options['limit']);
  1249. unset($options['summary']);
  1250. unset($options['coursecontacts']);
  1251. $options['idonly'] = true;
  1252. $courses = self::search_courses($search, $options);
  1253. $cnt = count($courses);
  1254. }
  1255. return $cnt;
  1256. }
  1257. /**
  1258. * Retrieves the list of courses accessible by user
  1259. *
  1260. * Not all information is cached, try to avoid calling this method
  1261. * twice in the same request.
  1262. *
  1263. * The following fields are always retrieved:
  1264. * - id, visible, fullname, shortname, idnumber, category, sortorder
  1265. *
  1266. * If you plan to use properties/methods course_in_list::$summary and/or
  1267. * course_in_list::get_course_contacts()
  1268. * you can preload this information using appropriate 'options'. Otherwise
  1269. * they will be retrieved from DB on demand and it may end with bigger DB load.
  1270. *
  1271. * Note that method course_in_list::has_summary() will not perform additional
  1272. * DB queries even if $options['summary'] is not specified
  1273. *
  1274. * List of found course ids is cached for 10 minutes. Cache may be purged prior
  1275. * to this when somebody edits courses or categories, however it is very
  1276. * difficult to keep track of all possible changes that may affect list of courses.
  1277. *
  1278. * @param array $options options for retrieving children
  1279. * - recursive - return courses from subcategories as well. Use with care,
  1280. * this may be a huge list!
  1281. * - summary - preloads fields 'summary' and 'summaryformat'
  1282. * - coursecontacts - preloads course contacts
  1283. * - sort - list of fields to sort. Example
  1284. * array('idnumber' => 1, 'shortname' => 1, 'id' => -1)
  1285. * will sort by idnumber asc, shortname asc and id desc.
  1286. * Default: array('sortorder' => 1)
  1287. * Only cached fields may be used for sorting!
  1288. * - offset
  1289. * - limit - maximum number of children to return, 0 or null for no limit
  1290. * - idonly - returns the array or course ids instead of array of objects
  1291. * used only in get_courses_count()
  1292. * @return course_in_list[]
  1293. */
  1294. public function get_courses($options = array()) {
  1295. global $DB;
  1296. $recursive = !empty($options['recursive']);
  1297. $offset = !empty($options['offset']) ? $options['offset'] : 0;
  1298. $limit = !empty($options['limit']) ? $options['limit'] : null;
  1299. $sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1);
  1300. // Check if this category is hidden.
  1301. // Also 0-category never has courses unless this is recursive call.
  1302. if (!$this->is_uservisible() || (!$this->id && !$recursive)) {
  1303. return array();
  1304. }
  1305. $coursecatcache = cache::make('core', 'coursecat');
  1306. $cachekey = 'l-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : '').
  1307. '-'. serialize($sortfields);
  1308. $cntcachekey = 'lcnt-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : '');
  1309. // Check if we have already cached results.
  1310. $ids = $coursecatcache->get($cachekey);
  1311. if ($ids !== false) {
  1312. // We already cached last search result and it did not expire yet.
  1313. $ids = array_slice($ids, $offset, $limit);
  1314. $courses = array();
  1315. if (!empty($ids)) {
  1316. list($sql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'id');
  1317. $records = self::get_course_records("c.id ". $sql, $params, $options);
  1318. // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
  1319. if (!empty($options['coursecontacts'])) {
  1320. self::preload_course_contacts($records);
  1321. }
  1322. // If option 'idonly' is specified no further action is needed, just return list of ids.
  1323. if (!empty($options['idonly'])) {
  1324. return array_keys($records);
  1325. }
  1326. // Prepare the list of course_in_list objects.
  1327. foreach ($ids as $id) {
  1328. $courses[$id] = new course_in_list($records[$id]);
  1329. }
  1330. }
  1331. return $courses;
  1332. }
  1333. // Retrieve list of courses in category.
  1334. $where = 'c.id <> :siteid';
  1335. $params = array('siteid' => SITEID);
  1336. if ($recursive) {
  1337. if ($this->id) {
  1338. $context = context_coursecat::instance($this->id);
  1339. $where .= ' AND ctx.path like :path';
  1340. $params['path'] = $context->path. '/%';
  1341. }
  1342. } else {
  1343. $where .= ' AND c.category = :categoryid';
  1344. $params['categoryid'] = $this->id;
  1345. }
  1346. // Get list of courses without preloaded coursecontacts because we don't need them for every course.
  1347. $list = $this->get_course_records($where, $params, array_diff_key($options, array('coursecontacts' => 1)), true);
  1348. // Sort and cache list.
  1349. self::sort_records($list, $sortfields);
  1350. $coursecatcache->set($cachekey, array_keys($list));
  1351. $coursecatcache->set($cntcachekey, count($list));
  1352. // Apply offset/limit, convert to course_in_list and return.
  1353. $courses = array();
  1354. if (isset($list)) {
  1355. if ($offset || $limit) {
  1356. $list = array_slice($list, $offset, $limit, true);
  1357. }
  1358. // Preload course contacts if necessary - saves DB queries later to do it for each course separately.
  1359. if (!empty($options['coursecontacts'])) {
  1360. self::preload_course_contacts($list);
  1361. }
  1362. // If option 'idonly' is specified no further action is needed, just return list of ids.
  1363. if (!empty($options['idonly'])) {
  1364. return array_keys($list);
  1365. }
  1366. // Prepare the list of course_in_list objects.
  1367. foreach ($list as $record) {
  1368. $courses[$record->id] = new course_in_list($record);
  1369. }
  1370. }
  1371. return $courses;
  1372. }
  1373. /**
  1374. * Returns number of courses visible to the user
  1375. *
  1376. * @param array $options similar to get_courses() except some options do not affect
  1377. * number of courses (i.e. sort, summary, offset, limit etc.)
  1378. * @return int
  1379. */
  1380. public function get_courses_count($options = array()) {
  1381. $cntcachekey = 'lcnt-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : '');
  1382. $coursecatcache = cache::make('core', 'coursecat');
  1383. if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
  1384. // Cached value not found. Retrieve ALL courses and return their count.
  1385. unset($options['offset']);
  1386. unset($options['limit']);
  1387. unset($options['summary']);
  1388. unset($options['coursecontacts']);
  1389. $options['idonly'] = true;
  1390. $courses = $this->get_courses($options);
  1391. $cnt = count($courses);
  1392. }
  1393. return $cnt;
  1394. }
  1395. /**
  1396. * Returns true if the user is able to delete this category.
  1397. *
  1398. * Note if this category contains any courses this isn't a full check, it will need to be accompanied by a call to either
  1399. * {@link coursecat::can_delete_full()} or {@link coursecat::can_move_content_to()} depending upon what the user wished to do.
  1400. *
  1401. * @return boolean
  1402. */
  1403. public function can_delete() {
  1404. if (!$this->has_manage_capability()) {
  1405. return false;
  1406. }
  1407. return $this->parent_has_manage_capability();
  1408. }
  1409. /**
  1410. * Returns true if user can delete current category and all its contents
  1411. *
  1412. * To be able to delete course category the user must have permission
  1413. * 'moodle/category:manage' in ALL child course categories AND
  1414. * be able to delete all courses
  1415. *
  1416. * @return bool
  1417. */
  1418. public function can_delete_full() {
  1419. global $DB;
  1420. if (!$this->id) {
  1421. // Fool-proof.
  1422. return false;
  1423. }
  1424. $context = $this->get_context();
  1425. if (!$this->is_uservisible() ||
  1426. !has_capability('moodle/category:manage', $context)) {
  1427. return false;
  1428. }
  1429. // Check all child categories (not only direct children).
  1430. $sql = context_helper::get_preload_record_columns_sql('ctx');
  1431. $childcategories = $DB->get_records_sql('SELECT c.id, c.visible, '. $sql.
  1432. ' FROM {context} ctx '.
  1433. ' JOIN {course_categories} c ON c.id = ctx.instanceid'.
  1434. ' WHERE ctx.path like ? AND ctx.contextlevel = ?',
  1435. array($context->path. '/%', CONTEXT_COURSECAT));
  1436. foreach ($childcategories as $childcat) {
  1437. context_helper::preload_from_record($childcat);
  1438. $childcontext = context_coursecat::instance($childcat->id);
  1439. if ((!$childcat->visible && !has_capability('moodle/category:viewhiddencategories', $childcontext)) ||
  1440. !has_capability('moodle/category:manage', $childcontext)) {
  1441. return false;
  1442. }
  1443. }
  1444. // Check courses.
  1445. $sql = context_helper::get_preload_record_columns_sql('ctx');
  1446. $coursescontexts = $DB->get_records_sql('SELECT ctx.instanceid AS courseid, '.
  1447. $sql. ' FROM {context} ctx '.
  1448. 'WHERE ctx.path like :pathmask and ctx.contextlevel = :courselevel',
  1449. array('pathmask' => $context->path. '/%',
  1450. 'courselevel' => CONTEXT_COURSE));
  1451. foreach ($coursescontexts as $ctxrecord) {
  1452. context_helper::preload_from_record($ctxrecord);
  1453. if (!can_delete_course($ctxrecord->courseid)) {
  1454. return false;
  1455. }
  1456. }
  1457. return true;
  1458. }
  1459. /**
  1460. * Recursively delete category including all subcategories and courses
  1461. *
  1462. * Function {@link coursecat::can_delete_full()} MUST be called prior
  1463. * to calling this function because there is no capability check
  1464. * inside this function
  1465. *
  1466. * @param boolean $showfeedback display some notices
  1467. * @return array return deleted courses
  1468. * @throws moodle_exception
  1469. */
  1470. public function delete_full($showfeedback = true) {
  1471. global $CFG, $DB;
  1472. require_once($CFG->libdir.'/gradelib.php');
  1473. require_once($CFG->libdir.'/questionlib.php');
  1474. require_once($CFG->dirroot.'/cohort/lib.php');
  1475. $deletedcourses = array();
  1476. // Get children. Note, we don't want to use cache here because it would be rebuilt too often.
  1477. $children = $DB->get_records('course_categories', array('parent' => $this->id), 'sortorder ASC');
  1478. foreach ($children as $record) {
  1479. $coursecat = new coursecat($record);
  1480. $deletedcourses += $coursecat->delete_full($showfeedback);
  1481. }
  1482. if ($courses = $DB->get_records('course', array('category' => $this->id), 'sortorder ASC')) {
  1483. foreach ($courses as $course) {
  1484. if (!delete_course($course, false)) {
  1485. throw new moodle_exception('cannotdeletecategorycourse', '', '', $course->shortname);
  1486. }
  1487. $deletedcourses[] = $course;
  1488. }
  1489. }
  1490. // Move or delete cohorts in this context.
  1491. cohort_delete_category($this);
  1492. // Now delete anything that may depend on course category context.
  1493. grade_course_category_delete($this->id, 0, $showfeedback);
  1494. if (!question_delete_course_category($this, 0, $showfeedback)) {
  1495. throw new moodle_exception('cannotdeletecategoryquestions', '', '', $this->get_formatted_name());
  1496. }
  1497. // Finally delete the category and it's context.
  1498. $DB->delete_records('course_categories', array('id' => $this->id));
  1499. $coursecatcontext = context_coursecat::instance($this->id);
  1500. $coursecatcontext->delete();
  1501. cache_helper::purge_by_event('changesincoursecat');
  1502. // Trigger a course category deleted event.
  1503. /* @var \core\event\course_category_deleted $event */
  1504. $event = \core\event\course_category_deleted::create(array(
  1505. 'objectid' => $this->id,
  1506. 'context' => $coursecatcontext,
  1507. 'other' => array('name' => $this->name)
  1508. ));
  1509. $event->set_coursecat($this);
  1510. $event->trigger();
  1511. // If we deleted $CFG->defaultrequestcategory, make it point somewhere else.
  1512. if ($this->id == $CFG->defaultrequestcategory) {
  1513. set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent' => 0)));
  1514. }
  1515. return $deletedcourses;
  1516. }
  1517. /**
  1518. * Checks if user can delete this category and move content (courses, subcategories and questions)
  1519. * to another category. If yes returns the array of possible target categories names
  1520. *
  1521. * If user can not manage this category or it is completely empty - empty array will be returned
  1522. *
  1523. * @return array
  1524. */
  1525. public function move_content_targets_list() {
  1526. global $CFG;
  1527. require_once($CFG->libdir . '/questionlib.php');
  1528. $context = $this->get_context();
  1529. if (!$this->is_uservisible() ||
  1530. !has_capability('moodle/category:manage', $context)) {
  1531. // User is not able to manage current category, he is not able to delete it.
  1532. // No possible target categories.
  1533. return array();
  1534. }
  1535. $testcaps = array();
  1536. // If this category has courses in it, user must have 'course:create' capability in target category.
  1537. if ($this->has_courses()) {
  1538. $testcaps[] = 'moodle/course:create';
  1539. }
  1540. // If this category has subcategories or questions, user must have 'category:manage' capability in target category.
  1541. if ($this->has_children() || question_context_has_any_questions($context)) {
  1542. $testcaps[] = 'moodle/category:manage';
  1543. }
  1544. if (!empty($testcaps)) {
  1545. // Return list of categories excluding this one and it's children.
  1546. return self::make_categories_list($testcaps, $this->id);
  1547. }
  1548. // Category is completely empty, no need in target for contents.
  1549. return array();
  1550. }
  1551. /**
  1552. * Checks if user has capability to move all category content to the new parent before
  1553. * removing this category
  1554. *
  1555. * @param int $newcatid
  1556. * @return bool
  1557. */
  1558. public function can_move_content_to($newcatid) {
  1559. global $CFG;
  1560. require_once($CFG->libdir . '/questionlib.php');
  1561. $context = $this->get_context();
  1562. if (!$this->is_uservisible() ||
  1563. !has_capability('moodle/category:manage', $context)) {
  1564. return false;
  1565. }
  1566. $testcaps = array();
  1567. // If this category has courses in it, user must have 'course:create' capability in target category.
  1568. if ($this->has_courses()) {
  1569. $testcaps[] = 'moodle/course:create';
  1570. }
  1571. // If this category has subcategories or questions, user must have 'category:manage' capability in target category.
  1572. if ($this->has_children() || question_context_has_any_questions($context)) {
  1573. $testcaps[] = 'moodle/category:manage';
  1574. }
  1575. if (!empty($testcaps)) {
  1576. return has_all_capabilities($testcaps, context_coursecat::instance($newcatid));
  1577. }
  1578. // There is no content but still return true.
  1579. return true;
  1580. }
  1581. /**
  1582. * Deletes a category and moves all content (children, courses and questions) to the new parent
  1583. *
  1584. * Note that this function does not check capabilities, {@link coursecat::can_move_content_to()}
  1585. * must be called prior
  1586. *
  1587. * @param int $newparentid
  1588. * @param bool $showfeedback
  1589. * @return bool
  1590. */
  1591. public function delete_move($newparentid, $showfeedback = false) {
  1592. global $CFG, $DB, $OUTPUT;
  1593. require_once($CFG->libdir.'/gradelib.php');
  1594. require_once($CFG->libdir.'/questionlib.php');
  1595. require_once($CFG->dirroot.'/cohort/lib.php');
  1596. // Get all objects and lists because later the caches will be reset so.
  1597. // We don't need to make extra queries.
  1598. $newparentcat = self::get($newparentid, MUST_EXIST, true);
  1599. $catname = $this->get_formatted_name();
  1600. $children = $this->get_children();
  1601. $params = array('category' => $this->id);
  1602. $coursesids = $DB->get_fieldset_select('course', 'id', 'category = :category ORDER BY sortorder ASC', $params);
  1603. $context = $this->get_context();
  1604. if ($children) {
  1605. foreach ($children as $childcat) {
  1606. $childcat->change_parent_raw($newparentcat);
  1607. // Log action.
  1608. add_to_log(SITEID, "category", "move", "editcategory.php?id=$childcat->id", $childcat->id);
  1609. }
  1610. fix_course_sortorder();
  1611. }
  1612. if ($coursesids) {
  1613. if (!move_courses($coursesids, $newparentid)) {
  1614. if ($showfeedback) {
  1615. echo $OUTPUT->notification("Error moving courses");
  1616. }
  1617. return false;
  1618. }
  1619. if ($showfeedback) {
  1620. echo $OUTPUT->notification(get_string('coursesmovedout', '', $catname), 'notifysuccess');
  1621. }
  1622. }
  1623. // Move or delete cohorts in this context.
  1624. cohort_delete_category($this);
  1625. // Now delete anything that may depend on course category context.
  1626. grade_course_category_delete($this->id, $newparentid, $showfeedback);
  1627. if (!question_delete_course_category($this, $newparentcat, $showfeedback)) {
  1628. if ($showfeedback) {
  1629. echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $catname), 'notifysuccess');
  1630. }
  1631. return false;
  1632. }
  1633. // Finally delete the category and it's context.
  1634. $DB->delete_records('course_categories', array('id' => $this->id));
  1635. $context->delete();
  1636. // Trigger a course category deleted event.
  1637. /* @var \core\event\course_category_deleted $event */
  1638. $event = \core\event\course_category_deleted::create(array(
  1639. 'objectid' => $this->id,
  1640. 'context' => $context,
  1641. 'other' => array('name' => $this->name)
  1642. ));
  1643. $event->set_coursecat($this);
  1644. $event->trigger();
  1645. cache_helper::purge_by_event('changesincoursecat');
  1646. if ($showfeedback) {
  1647. echo $OUTPUT->notification(get_string('coursecategorydeleted', '', $catname), 'notifysuccess');
  1648. }
  1649. // If we deleted $CFG->defaultrequestcategory, make it point somewhere else.
  1650. if ($this->id == $CFG->defaultrequestcategory) {
  1651. set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent' => 0)));
  1652. }
  1653. return true;
  1654. }
  1655. /**
  1656. * Checks if user can move current category to the new parent
  1657. *
  1658. * This checks if new parent category exists, user has manage cap there
  1659. * and new parent is not a child of this category
  1660. *
  1661. * @param int|stdClass|coursecat $newparentcat
  1662. * @return bool
  1663. */
  1664. public function can_change_parent($newparentcat) {
  1665. if (!has_capability('moodle/category:manage', $this->get_context())) {
  1666. return false;
  1667. }
  1668. if (is_object($newparentcat)) {
  1669. $newparentcat = self::get($newparentcat->id, IGNORE_MISSING);
  1670. } else {
  1671. $newparentcat = self::get((int)$newparentcat, IGNORE_MISSING);
  1672. }
  1673. if (!$newparentcat) {
  1674. return false;
  1675. }
  1676. if ($newparentcat->id == $this->id || in_array($this->id, $newparentcat->get_parents())) {
  1677. // Can not move to itself or it's own child.
  1678. return false;
  1679. }
  1680. if ($newparentcat->id) {
  1681. return has_capability('moodle/category:manage', context_coursecat::instance($newparentcat->id));
  1682. } else {
  1683. return has_capability('moodle/category:manage', context_system::instance());
  1684. }
  1685. }
  1686. /**
  1687. * Moves the category under another parent category. All associated contexts are moved as well
  1688. *
  1689. * This is protected function, use change_parent() or update() from outside of this class
  1690. *
  1691. * @see coursecat::change_parent()
  1692. * @see coursecat::update()
  1693. *
  1694. * @param coursecat $newparentcat
  1695. * @throws moodle_exception
  1696. */
  1697. protected function change_parent_raw(coursecat $newparentcat) {
  1698. global $DB;
  1699. $context = $this->get_context();
  1700. $hidecat = false;
  1701. if (empty($newparentcat->id)) {
  1702. $DB->set_field('course_categories', 'parent', 0, array('id' => $this->id));
  1703. $newparent = context_system::instance();
  1704. } else {
  1705. if ($newparentcat->id == $this->id || in_array($this->id, $newparentcat->get_parents())) {
  1706. // Can not move to itself or it's own child.
  1707. throw new moodle_exception('cannotmovecategory');
  1708. }
  1709. $DB->set_field('course_categories', 'parent', $newparentcat->id, array('id' => $this->id));
  1710. $newparent = context_coursecat::instance($newparentcat->id);
  1711. if (!$newparentcat->visible and $this->visible) {
  1712. // Better hide category when moving into hidden category, teachers may unhide afterwards and the hidden children
  1713. // will be restored properly.
  1714. $hidecat = true;
  1715. }
  1716. }
  1717. $this->parent = $newparentcat->id;
  1718. $context->update_moved($newparent);
  1719. // Now make it last in new category.
  1720. $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('id' => $this->id));
  1721. if ($hidecat) {
  1722. fix_course_sortorder();
  1723. $this->restore();
  1724. // Hide object but store 1 in visibleold, because when parent category visibility changes this category must
  1725. // become visible again.
  1726. $this->hide_raw(1);
  1727. }
  1728. }
  1729. /**
  1730. * Efficiently moves a category - NOTE that this can have
  1731. * a huge impact access-control-wise...
  1732. *
  1733. * Note that this function does not check capabilities.
  1734. *
  1735. * Example of usage:
  1736. * $coursecat = coursecat::get($categoryid);
  1737. * if ($coursecat->can_change_parent($newparentcatid)) {
  1738. * $coursecat->change_parent($newparentcatid);
  1739. * }
  1740. *
  1741. * This function does not update field course_categories.timemodified
  1742. * If you want to update timemodified, use
  1743. * $coursecat->update(array('parent' => $newparentcat));
  1744. *
  1745. * @param int|stdClass|coursecat $newparentcat
  1746. */
  1747. public function change_parent($newparentcat) {
  1748. // Make sure parent category exists but do not check capabilities here that it is visible to current user.
  1749. if (is_object($newparentcat)) {
  1750. $newparentcat = self::get($newparentcat->id, MUST_EXIST, true);
  1751. } else {
  1752. $newparentcat = self::get((int)$newparentcat, MUST_EXIST, true);
  1753. }
  1754. if ($newparentcat->id != $this->parent) {
  1755. $this->change_parent_raw($newparentcat);
  1756. fix_course_sortorder();
  1757. cache_helper::purge_by_event('changesincoursecat');
  1758. $this->restore();
  1759. add_to_log(SITEID, "category", "move", "editcategory.php?id=$this->id", $this->id);
  1760. }
  1761. }
  1762. /**
  1763. * Hide course category and child course and subcategories
  1764. *
  1765. * If this category has changed the parent and is moved under hidden
  1766. * category we will want to store it's current visibility state in
  1767. * the field 'visibleold'. If admin clicked 'hide' for this particular
  1768. * category, the field 'visibleold' should become 0.
  1769. *
  1770. * All subcategories and courses will have their current visibility in the field visibleold
  1771. *
  1772. * This is protected function, use hide() or update() from outside of this class
  1773. *
  1774. * @see coursecat::hide()
  1775. * @see coursecat::update()
  1776. *
  1777. * @param int $visibleold value to set in field $visibleold for this category
  1778. * @return bool whether changes have been made and caches need to be purged afterwards
  1779. */
  1780. protected function hide_raw($visibleold = 0) {
  1781. global $DB;
  1782. $changes = false;
  1783. // Note that field 'visibleold' is not cached so we must retrieve it from DB if it is missing.
  1784. if ($this->id && $this->__get('visibleold') != $visibleold) {
  1785. $this->visibleold = $visibleold;
  1786. $DB->set_field('course_categories', 'visibleold', $visibleold, array('id' => $this->id));
  1787. $changes = true;
  1788. }
  1789. if (!$this->visible || !$this->id) {
  1790. // Already hidden or can not be hidden.
  1791. return $changes;
  1792. }
  1793. $this->visible = 0;
  1794. $DB->set_field('course_categories', 'visible', 0, array('id'=>$this->id));
  1795. // Store visible flag so that we can return to it if we immediately unhide.
  1796. $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($this->id));
  1797. $DB->set_field('course', 'visible', 0, array('category' => $this->id));
  1798. // Get all child categories and hide too.
  1799. if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$this->path/%"), 'id, visible')) {
  1800. foreach ($subcats as $cat) {
  1801. $DB->set_field('course_categories', 'visibleold', $cat->visible, array('id' => $cat->id));
  1802. $DB->set_field('course_categories', 'visible', 0, array('id' => $cat->id));
  1803. $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($cat->id));
  1804. $DB->set_field('course', 'visible', 0, array('category' => $cat->id));
  1805. }
  1806. }
  1807. return true;
  1808. }
  1809. /**
  1810. * Hide course category and child course and subcategories
  1811. *
  1812. * Note that there is no capability check inside this function
  1813. *
  1814. * This function does not update field course_categories.timemodified
  1815. * If you want to update timemodified, use
  1816. * $coursecat->update(array('visible' => 0));
  1817. */
  1818. public function hide() {
  1819. if ($this->hide_raw(0)) {
  1820. cache_helper::purge_by_event('changesincoursecat');
  1821. add_to_log(SITEID, "category", "hide", "editcategory.php?id=$this->id", $this->id);
  1822. }
  1823. }
  1824. /**
  1825. * Show course category and restores visibility for child course and subcategories
  1826. *
  1827. * Note that there is no capability check inside this function
  1828. *
  1829. * This is protected function, use show() or update() from outside of this class
  1830. *
  1831. * @see coursecat::show()
  1832. * @see coursecat::update()
  1833. *
  1834. * @return bool whether changes have been made and caches need to be purged afterwards
  1835. */
  1836. protected function show_raw() {
  1837. global $DB;
  1838. if ($this->visible) {
  1839. // Already visible.
  1840. return false;
  1841. }
  1842. $this->visible = 1;
  1843. $this->visibleold = 1;
  1844. $DB->set_field('course_categories', 'visible', 1, array('id' => $this->id));
  1845. $DB->set_field('course_categories', 'visibleold', 1, array('id' => $this->id));
  1846. $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($this->id));
  1847. // Get all child categories and unhide too.
  1848. if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$this->path/%"), 'id, visibleold')) {
  1849. foreach ($subcats as $cat) {
  1850. if ($cat->visibleold) {
  1851. $DB->set_field('course_categories', 'visible', 1, array('id' => $cat->id));
  1852. }
  1853. $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($cat->id));
  1854. }
  1855. }
  1856. return true;
  1857. }
  1858. /**
  1859. * Show course category and restores visibility for child course and subcategories
  1860. *
  1861. * Note that there is no capability check inside this function
  1862. *
  1863. * This function does not update field course_categories.timemodified
  1864. * If you want to update timemodified, use
  1865. * $coursecat->update(array('visible' => 1));
  1866. */
  1867. public function show() {
  1868. if ($this->show_raw()) {
  1869. cache_helper::purge_by_event('changesincoursecat');
  1870. add_to_log(SITEID, "category", "show", "editcategory.php?id=$this->id", $this->id);
  1871. }
  1872. }
  1873. /**
  1874. * Returns name of the category formatted as a string
  1875. *
  1876. * @param array $options formatting options other than context
  1877. * @return string
  1878. */
  1879. public function get_formatted_name($options = array()) {
  1880. if ($this->id) {
  1881. $context = $this->get_context();
  1882. return format_string($this->name, true, array('context' => $context) + $options);
  1883. } else {
  1884. return get_string('top');
  1885. }
  1886. }
  1887. /**
  1888. * Returns ids of all parents of the category. Last element in the return array is the direct parent
  1889. *
  1890. * For example, if you have a tree of categories like:
  1891. * Miscellaneous (id = 1)
  1892. * Subcategory (id = 2)
  1893. * Sub-subcategory (id = 4)
  1894. * Other category (id = 3)
  1895. *
  1896. * coursecat::get(1)->get_parents() == array()
  1897. * coursecat::get(2)->get_parents() == array(1)
  1898. * coursecat::get(4)->get_parents() == array(1, 2);
  1899. *
  1900. * Note that this method does not check if all parents are accessible by current user
  1901. *
  1902. * @return array of category ids
  1903. */
  1904. public function get_parents() {
  1905. $parents = preg_split('|/|', $this->path, 0, PREG_SPLIT_NO_EMPTY);
  1906. array_pop($parents);
  1907. return $parents;
  1908. }
  1909. /**
  1910. * This function returns a nice list representing category tree
  1911. * for display or to use in a form <select> element
  1912. *
  1913. * List is cached for 10 minutes
  1914. *
  1915. * For example, if you have a tree of categories like:
  1916. * Miscellaneous (id = 1)
  1917. * Subcategory (id = 2)
  1918. * Sub-subcategory (id = 4)
  1919. * Other category (id = 3)
  1920. * Then after calling this function you will have
  1921. * array(1 => 'Miscellaneous',
  1922. * 2 => 'Miscellaneous / Subcategory',
  1923. * 4 => 'Miscellaneous / Subcategory / Sub-subcategory',
  1924. * 3 => 'Other category');
  1925. *
  1926. * If you specify $requiredcapability, then only categories where the current
  1927. * user has that capability will be added to $list.
  1928. * If you only have $requiredcapability in a child category, not the parent,
  1929. * then the child catgegory will still be included.
  1930. *
  1931. * If you specify the option $excludeid, then that category, and all its children,
  1932. * are omitted from the tree. This is useful when you are doing something like
  1933. * moving categories, where you do not want to allow people to move a category
  1934. * to be the child of itself.
  1935. *
  1936. * See also {@link make_categories_options()}
  1937. *
  1938. * @param string/array $requiredcapability if given, only categories where the current
  1939. * user has this capability will be returned. Can also be an array of capabilities,
  1940. * in which case they are all required.
  1941. * @param integer $excludeid Exclude this category and its children from the lists built.
  1942. * @param string $separator string to use as a separator between parent and child category. Default ' / '
  1943. * @return array of strings
  1944. */
  1945. public static function make_categories_list($requiredcapability = '', $excludeid = 0, $separator = ' / ') {
  1946. global $DB;
  1947. $coursecatcache = cache::make('core', 'coursecat');
  1948. // Check if we cached the complete list of user-accessible category names ($baselist) or list of ids
  1949. // with requried cap ($thislist).
  1950. $basecachekey = 'catlist';
  1951. $baselist = $coursecatcache->get($basecachekey);
  1952. $thislist = false;
  1953. $thiscachekey = null;
  1954. if (!empty($requiredcapability)) {
  1955. $requiredcapability = (array)$requiredcapability;
  1956. $thiscachekey = 'catlist:'. serialize($requiredcapability);
  1957. if ($baselist !== false && ($thislist = $coursecatcache->get($thiscachekey)) !== false) {
  1958. $thislist = preg_split('|,|', $thislist, -1, PREG_SPLIT_NO_EMPTY);
  1959. }
  1960. } else if ($baselist !== false) {
  1961. $thislist = array_keys($baselist);
  1962. }
  1963. if ($baselist === false) {
  1964. // We don't have $baselist cached, retrieve it. Retrieve $thislist again in any case.
  1965. $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
  1966. $sql = "SELECT cc.id, cc.sortorder, cc.name, cc.visible, cc.parent, cc.path, $ctxselect
  1967. FROM {course_categories} cc
  1968. JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat
  1969. ORDER BY cc.sortorder";
  1970. $rs = $DB->get_recordset_sql($sql, array('contextcoursecat' => CONTEXT_COURSECAT));
  1971. $baselist = array();
  1972. $thislist = array();
  1973. foreach ($rs as $record) {
  1974. // If the category's parent is not visible to the user, it is not visible as well.
  1975. if (!$record->parent || isset($baselist[$record->parent])) {
  1976. context_helper::preload_from_record($record);
  1977. $context = context_coursecat::instance($record->id);
  1978. if (!$record->visible && !has_capability('moodle/category:viewhiddencategories', $context)) {
  1979. // No cap to view category, added to neither $baselist nor $thislist.
  1980. continue;
  1981. }
  1982. $baselist[$record->id] = array(
  1983. 'name' => format_string($record->name, true, array('context' => $context)),
  1984. 'path' => $record->path
  1985. );
  1986. if (!empty($requiredcapability) && !has_all_capabilities($requiredcapability, $context)) {
  1987. // No required capability, added to $baselist but not to $thislist.
  1988. continue;
  1989. }
  1990. $thislist[] = $record->id;
  1991. }
  1992. }
  1993. $rs->close();
  1994. $coursecatcache->set($basecachekey, $baselist);
  1995. if (!empty($requiredcapability)) {
  1996. $coursecatcache->set($thiscachekey, join(',', $thislist));
  1997. }
  1998. } else if ($thislist === false) {
  1999. // We have $baselist cached but not $thislist. Simplier query is used to retrieve.
  2000. $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
  2001. $sql = "SELECT ctx.instanceid AS id, $ctxselect
  2002. FROM {context} ctx WHERE ctx.contextlevel = :contextcoursecat";
  2003. $contexts = $DB->get_records_sql($sql, array('contextcoursecat' => CONTEXT_COURSECAT));
  2004. $thislist = array();
  2005. foreach (array_keys($baselist) as $id) {
  2006. context_helper::preload_from_record($contexts[$id]);
  2007. if (has_all_capabilities($requiredcapability, context_coursecat::instance($id))) {
  2008. $thislist[] = $id;
  2009. }
  2010. }
  2011. $coursecatcache->set($thiscachekey, join(',', $thislist));
  2012. }
  2013. // Now build the array of strings to return, mind $separator and $excludeid.
  2014. $names = array();
  2015. foreach ($thislist as $id) {
  2016. $path = preg_split('|/|', $baselist[$id]['path'], -1, PREG_SPLIT_NO_EMPTY);
  2017. if (!$excludeid || !in_array($excludeid, $path)) {
  2018. $namechunks = array();
  2019. foreach ($path as $parentid) {
  2020. $namechunks[] = $baselist[$parentid]['name'];
  2021. }
  2022. $names[$id] = join($separator, $namechunks);
  2023. }
  2024. }
  2025. return $names;
  2026. }
  2027. /**
  2028. * Prepares the object for caching. Works like the __sleep method.
  2029. *
  2030. * implementing method from interface cacheable_object
  2031. *
  2032. * @return array ready to be cached
  2033. */
  2034. public function prepare_to_cache() {
  2035. $a = array();
  2036. foreach (self::$coursecatfields as $property => $cachedirectives) {
  2037. if ($cachedirectives !== null) {
  2038. list($shortname, $defaultvalue) = $cachedirectives;
  2039. if ($this->$property !== $defaultvalue) {
  2040. $a[$shortname] = $this->$property;
  2041. }
  2042. }
  2043. }
  2044. $context = $this->get_context();
  2045. $a['xi'] = $context->id;
  2046. $a['xp'] = $context->path;
  2047. return $a;
  2048. }
  2049. /**
  2050. * Takes the data provided by prepare_to_cache and reinitialises an instance of the associated from it.
  2051. *
  2052. * implementing method from interface cacheable_object
  2053. *
  2054. * @param array $a
  2055. * @return coursecat
  2056. */
  2057. public static function wake_from_cache($a) {
  2058. $record = new stdClass;
  2059. foreach (self::$coursecatfields as $property => $cachedirectives) {
  2060. if ($cachedirectives !== null) {
  2061. list($shortname, $defaultvalue) = $cachedirectives;
  2062. if (array_key_exists($shortname, $a)) {
  2063. $record->$property = $a[$shortname];
  2064. } else {
  2065. $record->$property = $defaultvalue;
  2066. }
  2067. }
  2068. }
  2069. $record->ctxid = $a['xi'];
  2070. $record->ctxpath = $a['xp'];
  2071. $record->ctxdepth = $record->depth + 1;
  2072. $record->ctxlevel = CONTEXT_COURSECAT;
  2073. $record->ctxinstance = $record->id;
  2074. return new coursecat($record, true);
  2075. }
  2076. /**
  2077. * Returns true if the user is able to create a top level category.
  2078. * @return bool
  2079. */
  2080. public static function can_create_top_level_category() {
  2081. return has_capability('moodle/category:manage', context_system::instance());
  2082. }
  2083. /**
  2084. * Returns the category context.
  2085. * @return context_coursecat
  2086. */
  2087. public function get_context() {
  2088. if ($this->id === 0) {
  2089. // This is the special top level category object.
  2090. return context_system::instance();
  2091. } else {
  2092. return context_coursecat::instance($this->id);
  2093. }
  2094. }
  2095. /**
  2096. * Returns true if the user is able to manage this category.
  2097. * @return bool
  2098. */
  2099. public function has_manage_capability() {
  2100. if ($this->hasmanagecapability === null) {
  2101. $this->hasmanagecapability = has_capability('moodle/category:manage', $this->get_context());
  2102. }
  2103. return $this->hasmanagecapability;
  2104. }
  2105. /**
  2106. * Returns true if the user has the manage capability on the parent category.
  2107. * @return bool
  2108. */
  2109. public function parent_has_manage_capability() {
  2110. return has_capability('moodle/category:manage', get_category_or_system_context($this->parent));
  2111. }
  2112. /**
  2113. * Returns true if the current user can create subcategories of this category.
  2114. * @return bool
  2115. */
  2116. public function can_create_subcategory() {
  2117. return $this->has_manage_capability();
  2118. }
  2119. /**
  2120. * Returns true if the user can resort this categories sub categories and courses.
  2121. * Must have manage capability and be able to see all subcategories.
  2122. * @return bool
  2123. */
  2124. public function can_resort_subcategories() {
  2125. return $this->has_manage_capability() && !$this->get_not_visible_children_ids();
  2126. }
  2127. /**
  2128. * Returns true if the user can resort the courses within this category.
  2129. * Must have manage capability and be able to see all courses.
  2130. * @return bool
  2131. */
  2132. public function can_resort_courses() {
  2133. return $this->has_manage_capability() && $this->coursecount == $this->get_courses_count();
  2134. }
  2135. /**
  2136. * Returns true of the user can change the sortorder of this category (resort in the parent category)
  2137. * @return bool
  2138. */
  2139. public function can_change_sortorder() {
  2140. return $this->id && $this->get_parent_coursecat()->can_resort_subcategories();
  2141. }
  2142. /**
  2143. * Returns true if the current user can create a course within this category.
  2144. * @return bool
  2145. */
  2146. public function can_create_course() {
  2147. return has_capability('moodle/course:create', $this->get_context());
  2148. }
  2149. /**
  2150. * Returns true if the current user can edit this categories settings.
  2151. * @return bool
  2152. */
  2153. public function can_edit() {
  2154. return $this->has_manage_capability();
  2155. }
  2156. /**
  2157. * Returns true if the current user can review role assignments for this category.
  2158. * @return bool
  2159. */
  2160. public function can_review_roles() {
  2161. return has_capability('moodle/role:assign', $this->get_context());
  2162. }
  2163. /**
  2164. * Returns true if the current user can review permissions for this category.
  2165. * @return bool
  2166. */
  2167. public function can_review_permissions() {
  2168. return has_any_capability(array(
  2169. 'moodle/role:assign',
  2170. 'moodle/role:safeoverride',
  2171. 'moodle/role:override',
  2172. 'moodle/role:assign'
  2173. ), $this->get_context());
  2174. }
  2175. /**
  2176. * Returns true if the current user can review cohorts for this category.
  2177. * @return bool
  2178. */
  2179. public function can_review_cohorts() {
  2180. return has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $this->get_context());
  2181. }
  2182. /**
  2183. * Returns true if the current user can review filter settings for this category.
  2184. * @return bool
  2185. */
  2186. public function can_review_filters() {
  2187. return has_capability('moodle/filter:manage', $this->get_context()) &&
  2188. count(filter_get_available_in_context($this->get_context()))>0;
  2189. }
  2190. /**
  2191. * Returns true if the current user is able to change the visbility of this category.
  2192. * @return bool
  2193. */
  2194. public function can_change_visibility() {
  2195. return $this->parent_has_manage_capability();
  2196. }
  2197. /**
  2198. * Returns true if the user can move courses out of this category.
  2199. * @return bool
  2200. */
  2201. public function can_move_courses_out_of() {
  2202. return $this->has_manage_capability();
  2203. }
  2204. /**
  2205. * Returns true if the user can move courses into this category.
  2206. * @return bool
  2207. */
  2208. public function can_move_courses_into() {
  2209. return $this->has_manage_capability();
  2210. }
  2211. /**
  2212. * Resorts the sub categories of this category by the given field.
  2213. *
  2214. * @param string $field
  2215. * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
  2216. * @return bool True on success.
  2217. * @throws coding_exception
  2218. */
  2219. public function resort_subcategories($field, $cleanup = true) {
  2220. global $DB;
  2221. if ($field !== 'name' && $field !== 'idnumber') {
  2222. throw new coding_exception('Invalid field requested');
  2223. }
  2224. $children = $this->get_children();
  2225. core_collator::asort_objects_by_property($children, $field, core_collator::SORT_NATURAL);
  2226. $i = 1;
  2227. foreach ($children as $cat) {
  2228. $i++;
  2229. $DB->set_field('course_categories', 'sortorder', $i, array('id' => $cat->id));
  2230. $i += $cat->coursecount;
  2231. }
  2232. if ($cleanup) {
  2233. self::resort_categories_cleanup();
  2234. }
  2235. return true;
  2236. }
  2237. /**
  2238. * Cleans things up after categories have been resorted.
  2239. * @param bool $includecourses If set to true we know courses have been resorted as well.
  2240. */
  2241. public static function resort_categories_cleanup($includecourses = false) {
  2242. // This should not be needed but we do it just to be safe.
  2243. fix_course_sortorder();
  2244. cache_helper::purge_by_event('changesincoursecat');
  2245. if ($includecourses) {
  2246. cache_helper::purge_by_event('changesincourse');
  2247. }
  2248. }
  2249. /**
  2250. * Resort the courses within this category by the given field.
  2251. *
  2252. * @param string $field One of fullname, shortname or idnumber
  2253. * @param bool $cleanup
  2254. * @return bool True for success.
  2255. * @throws coding_exception
  2256. */
  2257. public function resort_courses($field, $cleanup = true) {
  2258. global $DB;
  2259. if ($field !== 'fullname' && $field !== 'shortname' && $field !== 'idnumber') {
  2260. // This is ultra important as we use $field in an SQL statement below this.
  2261. throw new coding_exception('Invalid field requested');
  2262. }
  2263. $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
  2264. $sql = "SELECT c.id, c.sortorder, c.{$field}, $ctxfields
  2265. FROM {course} c
  2266. LEFT JOIN {context} ctx ON ctx.instanceid = c.id
  2267. WHERE ctx.contextlevel = :ctxlevel AND
  2268. c.category = :categoryid
  2269. ORDER BY c.{$field}, c.sortorder";
  2270. $params = array(
  2271. 'ctxlevel' => CONTEXT_COURSE,
  2272. 'categoryid' => $this->id
  2273. );
  2274. $courses = $DB->get_records_sql($sql, $params);
  2275. if (count($courses) > 0) {
  2276. foreach ($courses as $courseid => $course) {
  2277. context_helper::preload_from_record($course);
  2278. if ($field === 'idnumber') {
  2279. $course->sortby = $course->idnumber;
  2280. } else {
  2281. // It'll require formatting.
  2282. $options = array(
  2283. 'context' => context_course::instance($course->id)
  2284. );
  2285. // We format the string first so that it appears as the user would see it.
  2286. // This ensures the sorting makes sense to them. However it won't necessarily make
  2287. // sense to everyone if things like multilang filters are enabled.
  2288. // We then strip any tags as we don't want things such as image tags skewing the
  2289. // sort results.
  2290. $course->sortby = strip_tags(format_string($course->$field, true, $options));
  2291. }
  2292. // We set it back here rather than using references as there is a bug with using
  2293. // references in a foreach before passing as an arg by reference.
  2294. $courses[$courseid] = $course;
  2295. }
  2296. // Sort the courses.
  2297. core_collator::asort_objects_by_property($courses, 'sortby', core_collator::SORT_NATURAL);
  2298. $i = 1;
  2299. foreach ($courses as $course) {
  2300. $DB->set_field('course', 'sortorder', $this->sortorder + $i, array('id' => $course->id));
  2301. $i++;
  2302. }
  2303. if ($cleanup) {
  2304. // This should not be needed but we do it just to be safe.
  2305. fix_course_sortorder();
  2306. cache_helper::purge_by_event('changesincourse');
  2307. }
  2308. }
  2309. return true;
  2310. }
  2311. /**
  2312. * Changes the sort order of this categories parent shifting this category up or down one.
  2313. *
  2314. * @global \moodle_database $DB
  2315. * @param bool $up If set to true the category is shifted up one spot, else its moved down.
  2316. * @return bool True on success, false otherwise.
  2317. */
  2318. public function change_sortorder_by_one($up) {
  2319. global $DB;
  2320. $params = array($this->sortorder, $this->parent);
  2321. if ($up) {
  2322. $select = 'sortorder < ? AND parent = ?';
  2323. $sort = 'sortorder DESC';
  2324. } else {
  2325. $select = 'sortorder > ? AND parent = ?';
  2326. $sort = 'sortorder ASC';
  2327. }
  2328. fix_course_sortorder();
  2329. $swapcategory = $DB->get_records_select('course_categories', $select, $params, $sort, '*', 0, 1);
  2330. $swapcategory = reset($swapcategory);
  2331. if ($swapcategory) {
  2332. $DB->set_field('course_categories', 'sortorder', $swapcategory->sortorder, array('id' => $this->id));
  2333. $DB->set_field('course_categories', 'sortorder', $this->sortorder, array('id' => $swapcategory->id));
  2334. $this->sortorder = $swapcategory->sortorder;
  2335. add_to_log(SITEID, "category", "move", "management.php?categoryid={$this->id}", $this->id);
  2336. // Finally reorder courses.
  2337. fix_course_sortorder();
  2338. cache_helper::purge_by_event('changesincoursecat');
  2339. return true;
  2340. }
  2341. return false;
  2342. }
  2343. /**
  2344. * Returns the parent coursecat object for this category.
  2345. *
  2346. * @return coursecat
  2347. */
  2348. public function get_parent_coursecat() {
  2349. return self::get($this->parent);
  2350. }
  2351. /**
  2352. * Returns true if the user is able to request a new course be created.
  2353. * @return bool
  2354. */
  2355. public function can_request_course() {
  2356. global $CFG;
  2357. if (empty($CFG->enablecourserequests) || $this->id != $CFG->defaultrequestcategory) {
  2358. return false;
  2359. }
  2360. return !$this->can_create_course() && has_capability('moodle/course:request', $this->get_context());
  2361. }
  2362. /**
  2363. * Returns true if the user can approve course requests.
  2364. * @return bool
  2365. */
  2366. public static function can_approve_course_requests() {
  2367. global $CFG, $DB;
  2368. if (empty($CFG->enablecourserequests)) {
  2369. return false;
  2370. }
  2371. $context = context_system::instance();
  2372. if (!has_capability('moodle/site:approvecourse', $context)) {
  2373. return false;
  2374. }
  2375. if (!$DB->record_exists('course_request', array())) {
  2376. return false;
  2377. }
  2378. return true;
  2379. }
  2380. }
  2381. /**
  2382. * Class to store information about one course in a list of courses
  2383. *
  2384. * Not all information may be retrieved when object is created but
  2385. * it will be retrieved on demand when appropriate property or method is
  2386. * called.
  2387. *
  2388. * Instances of this class are usually returned by functions
  2389. * {@link coursecat::search_courses()}
  2390. * and
  2391. * {@link coursecat::get_courses()}
  2392. *
  2393. * @property-read int $id
  2394. * @property-read int $category Category ID
  2395. * @property-read int $sortorder
  2396. * @property-read string $fullname
  2397. * @property-read string $shortname
  2398. * @property-read string $idnumber
  2399. * @property-read string $summary Course summary. Field is present if coursecat::get_courses()
  2400. * was called with option 'summary'. Otherwise will be retrieved from DB on first request
  2401. * @property-read int $summaryformat Summary format. Field is present if coursecat::get_courses()
  2402. * was called with option 'summary'. Otherwise will be retrieved from DB on first request
  2403. * @property-read string $format Course format. Retrieved from DB on first request
  2404. * @property-read int $showgrades Retrieved from DB on first request
  2405. * @property-read int $newsitems Retrieved from DB on first request
  2406. * @property-read int $startdate
  2407. * @property-read int $marker Retrieved from DB on first request
  2408. * @property-read int $maxbytes Retrieved from DB on first request
  2409. * @property-read int $legacyfiles Retrieved from DB on first request
  2410. * @property-read int $showreports Retrieved from DB on first request
  2411. * @property-read int $visible
  2412. * @property-read int $visibleold Retrieved from DB on first request
  2413. * @property-read int $groupmode Retrieved from DB on first request
  2414. * @property-read int $groupmodeforce Retrieved from DB on first request
  2415. * @property-read int $defaultgroupingid Retrieved from DB on first request
  2416. * @property-read string $lang Retrieved from DB on first request
  2417. * @property-read string $theme Retrieved from DB on first request
  2418. * @property-read int $timecreated Retrieved from DB on first request
  2419. * @property-read int $timemodified Retrieved from DB on first request
  2420. * @property-read int $requested Retrieved from DB on first request
  2421. * @property-read int $enablecompletion Retrieved from DB on first request
  2422. * @property-read int $completionnotify Retrieved from DB on first request
  2423. * @property-read int $cacherev
  2424. *
  2425. * @package core
  2426. * @subpackage course
  2427. * @copyright 2013 Marina Glancy
  2428. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  2429. */
  2430. class course_in_list implements IteratorAggregate {
  2431. /** @var stdClass record retrieved from DB, may have additional calculated property such as managers and hassummary */
  2432. protected $record;
  2433. /** @var array array of course contacts - stores result of call to get_course_contacts() */
  2434. protected $coursecontacts;
  2435. /** @var bool true if the current user can access the course, false otherwise. */
  2436. protected $canaccess = null;
  2437. /**
  2438. * Creates an instance of the class from record
  2439. *
  2440. * @param stdClass $record except fields from course table it may contain
  2441. * field hassummary indicating that summary field is not empty.
  2442. * Also it is recommended to have context fields here ready for
  2443. * context preloading
  2444. */
  2445. public function __construct(stdClass $record) {
  2446. context_helper::preload_from_record($record);
  2447. $this->record = new stdClass();
  2448. foreach ($record as $key => $value) {
  2449. $this->record->$key = $value;
  2450. }
  2451. }
  2452. /**
  2453. * Indicates if the course has non-empty summary field
  2454. *
  2455. * @return bool
  2456. */
  2457. public function has_summary() {
  2458. if (isset($this->record->hassummary)) {
  2459. return !empty($this->record->hassummary);
  2460. }
  2461. if (!isset($this->record->summary)) {
  2462. // We need to retrieve summary.
  2463. $this->__get('summary');
  2464. }
  2465. return !empty($this->record->summary);
  2466. }
  2467. /**
  2468. * Indicates if the course have course contacts to display
  2469. *
  2470. * @return bool
  2471. */
  2472. public function has_course_contacts() {
  2473. if (!isset($this->record->managers)) {
  2474. $courses = array($this->id => &$this->record);
  2475. coursecat::preload_course_contacts($courses);
  2476. }
  2477. return !empty($this->record->managers);
  2478. }
  2479. /**
  2480. * Returns list of course contacts (usually teachers) to display in course link
  2481. *
  2482. * Roles to display are set up in $CFG->coursecontact
  2483. *
  2484. * The result is the list of users where user id is the key and the value
  2485. * is an array with elements:
  2486. * - 'user' - object containing basic user information
  2487. * - 'role' - object containing basic role information (id, name, shortname, coursealias)
  2488. * - 'rolename' => role_get_name($role, $context, ROLENAME_ALIAS)
  2489. * - 'username' => fullname($user, $canviewfullnames)
  2490. *
  2491. * @return array
  2492. */
  2493. public function get_course_contacts() {
  2494. global $CFG;
  2495. if (empty($CFG->coursecontact)) {
  2496. // No roles are configured to be displayed as course contacts.
  2497. return array();
  2498. }
  2499. if ($this->coursecontacts === null) {
  2500. $this->coursecontacts = array();
  2501. $context = context_course::instance($this->id);
  2502. if (!isset($this->record->managers)) {
  2503. // Preload course contacts from DB.
  2504. $courses = array($this->id => &$this->record);
  2505. coursecat::preload_course_contacts($courses);
  2506. }
  2507. // Build return array with full roles names (for this course context) and users names.
  2508. $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
  2509. foreach ($this->record->managers as $ruser) {
  2510. if (isset($this->coursecontacts[$ruser->id])) {
  2511. // Only display a user once with the highest sortorder role.
  2512. continue;
  2513. }
  2514. $user = new stdClass();
  2515. $user = username_load_fields_from_object($user, $ruser, null, array('id', 'username'));
  2516. $role = new stdClass();
  2517. $role->id = $ruser->roleid;
  2518. $role->name = $ruser->rolename;
  2519. $role->shortname = $ruser->roleshortname;
  2520. $role->coursealias = $ruser->rolecoursealias;
  2521. $this->coursecontacts[$user->id] = array(
  2522. 'user' => $user,
  2523. 'role' => $role,
  2524. 'rolename' => role_get_name($role, $context, ROLENAME_ALIAS),
  2525. 'username' => fullname($user, $canviewfullnames)
  2526. );
  2527. }
  2528. }
  2529. return $this->coursecontacts;
  2530. }
  2531. /**
  2532. * Checks if course has any associated overview files
  2533. *
  2534. * @return bool
  2535. */
  2536. public function has_course_overviewfiles() {
  2537. global $CFG;
  2538. if (empty($CFG->courseoverviewfileslimit)) {
  2539. return false;
  2540. }
  2541. $fs = get_file_storage();
  2542. $context = context_course::instance($this->id);
  2543. return !$fs->is_area_empty($context->id, 'course', 'overviewfiles');
  2544. }
  2545. /**
  2546. * Returns all course overview files
  2547. *
  2548. * @return array array of stored_file objects
  2549. */
  2550. public function get_course_overviewfiles() {
  2551. global $CFG;
  2552. if (empty($CFG->courseoverviewfileslimit)) {
  2553. return array();
  2554. }
  2555. require_once($CFG->libdir. '/filestorage/file_storage.php');
  2556. require_once($CFG->dirroot. '/course/lib.php');
  2557. $fs = get_file_storage();
  2558. $context = context_course::instance($this->id);
  2559. $files = $fs->get_area_files($context->id, 'course', 'overviewfiles', false, 'filename', false);
  2560. if (count($files)) {
  2561. $overviewfilesoptions = course_overviewfiles_options($this->id);
  2562. $acceptedtypes = $overviewfilesoptions['accepted_types'];
  2563. if ($acceptedtypes !== '*') {
  2564. // Filter only files with allowed extensions.
  2565. require_once($CFG->libdir. '/filelib.php');
  2566. foreach ($files as $key => $file) {
  2567. if (!file_extension_in_typegroup($file->get_filename(), $acceptedtypes)) {
  2568. unset($files[$key]);
  2569. }
  2570. }
  2571. }
  2572. if (count($files) > $CFG->courseoverviewfileslimit) {
  2573. // Return no more than $CFG->courseoverviewfileslimit files.
  2574. $files = array_slice($files, 0, $CFG->courseoverviewfileslimit, true);
  2575. }
  2576. }
  2577. return $files;
  2578. }
  2579. /**
  2580. * Magic method to check if property is set
  2581. *
  2582. * @param string $name
  2583. * @return bool
  2584. */
  2585. public function __isset($name) {
  2586. return isset($this->record->$name);
  2587. }
  2588. /**
  2589. * Magic method to get a course property
  2590. *
  2591. * Returns any field from table course (retrieves it from DB if it was not retrieved before)
  2592. *
  2593. * @param string $name
  2594. * @return mixed
  2595. */
  2596. public function __get($name) {
  2597. global $DB;
  2598. if (property_exists($this->record, $name)) {
  2599. return $this->record->$name;
  2600. } else if ($name === 'summary' || $name === 'summaryformat') {
  2601. // Retrieve fields summary and summaryformat together because they are most likely to be used together.
  2602. $record = $DB->get_record('course', array('id' => $this->record->id), 'summary, summaryformat', MUST_EXIST);
  2603. $this->record->summary = $record->summary;
  2604. $this->record->summaryformat = $record->summaryformat;
  2605. return $this->record->$name;
  2606. } else if (array_key_exists($name, $DB->get_columns('course'))) {
  2607. // Another field from table 'course' that was not retrieved.
  2608. $this->record->$name = $DB->get_field('course', $name, array('id' => $this->record->id), MUST_EXIST);
  2609. return $this->record->$name;
  2610. }
  2611. debugging('Invalid course property accessed! '.$name);
  2612. return null;
  2613. }
  2614. /**
  2615. * All properties are read only, sorry.
  2616. *
  2617. * @param string $name
  2618. */
  2619. public function __unset($name) {
  2620. debugging('Can not unset '.get_class($this).' instance properties!');
  2621. }
  2622. /**
  2623. * Magic setter method, we do not want anybody to modify properties from the outside
  2624. *
  2625. * @param string $name
  2626. * @param mixed $value
  2627. */
  2628. public function __set($name, $value) {
  2629. debugging('Can not change '.get_class($this).' instance properties!');
  2630. }
  2631. /**
  2632. * Create an iterator because magic vars can't be seen by 'foreach'.
  2633. * Exclude context fields
  2634. *
  2635. * Implementing method from interface IteratorAggregate
  2636. *
  2637. * @return ArrayIterator
  2638. */
  2639. public function getIterator() {
  2640. $ret = array('id' => $this->record->id);
  2641. foreach ($this->record as $property => $value) {
  2642. $ret[$property] = $value;
  2643. }
  2644. return new ArrayIterator($ret);
  2645. }
  2646. /**
  2647. * Returns the name of this course as it should be displayed within a list.
  2648. * @return string
  2649. */
  2650. public function get_formatted_name() {
  2651. return format_string(get_course_display_name_for_list($this), true, $this->get_context());
  2652. }
  2653. /**
  2654. * Returns the formatted fullname for this course.
  2655. * @return string
  2656. */
  2657. public function get_formatted_fullname() {
  2658. return format_string($this->__get('fullname'), true, $this->get_context());
  2659. }
  2660. /**
  2661. * Returns the formatted shortname for this course.
  2662. * @return string
  2663. */
  2664. public function get_formatted_shortname() {
  2665. return format_string($this->__get('shortname'), true, $this->get_context());
  2666. }
  2667. /**
  2668. * Returns true if the current user can access this course.
  2669. * @return bool
  2670. */
  2671. public function can_access() {
  2672. if ($this->canaccess === null) {
  2673. $this->canaccess = can_access_course($this->record);
  2674. }
  2675. return $this->canaccess;
  2676. }
  2677. /**
  2678. * Returns true if the user can edit this courses settings.
  2679. *
  2680. * Note: this function does not check that the current user can access the course.
  2681. * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()}
  2682. *
  2683. * @return bool
  2684. */
  2685. public function can_edit() {
  2686. return has_capability('moodle/course:update', $this->get_context());
  2687. }
  2688. /**
  2689. * Returns true if the user can change the visibility of this course.
  2690. *
  2691. * Note: this function does not check that the current user can access the course.
  2692. * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()}
  2693. *
  2694. * @return bool
  2695. */
  2696. public function can_change_visibility() {
  2697. // You must be able to both hide a course and view the hidden course.
  2698. return has_all_capabilities(array('moodle/course:visibility', 'moodle/course:viewhiddencourses'), $this->get_context());
  2699. }
  2700. /**
  2701. * Returns the context for this course.
  2702. * @return context_course
  2703. */
  2704. public function get_context() {
  2705. return context_course::instance($this->__get('id'));
  2706. }
  2707. /**
  2708. * Returns true if this course is visible to the current user.
  2709. * @return bool
  2710. */
  2711. public function is_uservisible() {
  2712. return $this->visible || has_capability('moodle/course:viewhiddencourses', $this->get_context());
  2713. }
  2714. /**
  2715. * Returns true if the current user can review enrolments for this course.
  2716. *
  2717. * Note: this function does not check that the current user can access the course.
  2718. * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()}
  2719. *
  2720. * @return bool
  2721. */
  2722. public function can_review_enrolments() {
  2723. return has_capability('moodle/course:enrolreview', $this->get_context());
  2724. }
  2725. /**
  2726. * Returns true if the current user can delete this course.
  2727. *
  2728. * Note: this function does not check that the current user can access the course.
  2729. * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()}
  2730. *
  2731. * @return bool
  2732. */
  2733. public function can_delete() {
  2734. return can_delete_course($this->id);
  2735. }
  2736. /**
  2737. * Returns true if the current user can backup this course.
  2738. *
  2739. * Note: this function does not check that the current user can access the course.
  2740. * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()}
  2741. *
  2742. * @return bool
  2743. */
  2744. public function can_backup() {
  2745. return has_capability('moodle/backup:backupcourse', $this->get_context());
  2746. }
  2747. /**
  2748. * Returns true if the current user can restore this course.
  2749. *
  2750. * Note: this function does not check that the current user can access the course.
  2751. * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()}
  2752. *
  2753. * @return bool
  2754. */
  2755. public function can_restore() {
  2756. return has_capability('moodle/restore:restorecourse', $this->get_context());
  2757. }
  2758. }
  2759. /**
  2760. * An array of records that is sortable by many fields.
  2761. *
  2762. * For more info on the ArrayObject class have a look at php.net.
  2763. *
  2764. * @package core
  2765. * @subpackage course
  2766. * @copyright 2013 Sam Hemelryk
  2767. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  2768. */
  2769. class coursecat_sortable_records extends ArrayObject {
  2770. /**
  2771. * An array of sortable fields.
  2772. * Gets set temporarily when sort is called.
  2773. * @var array
  2774. */
  2775. protected $sortfields = array();
  2776. /**
  2777. * Sorts this array using the given fields.
  2778. *
  2779. * @param array $records
  2780. * @param array $fields
  2781. * @return array
  2782. */
  2783. public static function sort(array $records, array $fields) {
  2784. $records = new coursecat_sortable_records($records);
  2785. $records->sortfields = $fields;
  2786. $records->uasort(array($records, 'sort_by_many_fields'));
  2787. return $records->getArrayCopy();
  2788. }
  2789. /**
  2790. * Sorts the two records based upon many fields.
  2791. *
  2792. * This method should not be called itself, please call $sort instead.
  2793. * It has been marked as access private as such.
  2794. *
  2795. * @access private
  2796. * @param stdClass $a
  2797. * @param stdClass $b
  2798. * @return int
  2799. */
  2800. public function sort_by_many_fields($a, $b) {
  2801. foreach ($this->sortfields as $field => $mult) {
  2802. // Nulls first.
  2803. if (is_null($a->$field) && !is_null($b->$field)) {
  2804. return -$mult;
  2805. }
  2806. if (is_null($b->$field) && !is_null($a->$field)) {
  2807. return $mult;
  2808. }
  2809. if (is_string($a->$field) || is_string($b->$field)) {
  2810. // String fields.
  2811. if ($cmp = strcoll($a->$field, $b->$field)) {
  2812. return $mult * $cmp;
  2813. }
  2814. } else {
  2815. // Int fields.
  2816. if ($a->$field > $b->$field) {
  2817. return $mult;
  2818. }
  2819. if ($a->$field < $b->$field) {
  2820. return -$mult;
  2821. }
  2822. }
  2823. }
  2824. return 0;
  2825. }
  2826. }