diff --git a/admin/roles/classes/define_role_table_advanced.php b/admin/roles/classes/define_role_table_advanced.php index 354ba8cb8bd..d5f6c183508 100644 --- a/admin/roles/classes/define_role_table_advanced.php +++ b/admin/roles/classes/define_role_table_advanced.php @@ -434,7 +434,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi } public function save_changes() { - global $DB, $CFG; + global $DB; if (!$this->roleid) { // Creating role. @@ -448,8 +448,7 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi // the UI. It would be better to do this only when we know that fields affected are // updated. But thats getting into the weeds of the coursecat cache and role edits // should not be that frequent, so here is the ugly brutal approach. - require_once($CFG->libdir . '/coursecatlib.php'); - coursecat::role_assignment_changed($this->role->id, context_system::instance()); + core_course_category::role_assignment_changed($this->role->id, context_system::instance()); } // Assignable contexts. diff --git a/admin/tool/dataprivacy/classes/data_registry.php b/admin/tool/dataprivacy/classes/data_registry.php index dbc8f32e610..1fac2f5756c 100644 --- a/admin/tool/dataprivacy/classes/data_registry.php +++ b/admin/tool/dataprivacy/classes/data_registry.php @@ -35,8 +35,6 @@ use tool_dataprivacy\context_instance; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir . '/coursecatlib.php'); - /** * Data registry business logic methods. Mostly internal stuff. * @@ -113,17 +111,17 @@ class data_registry { /** * Returns all site categories that are visible to the current user. * - * @return \coursecat[] + * @return \core_course_category[] */ public static function get_site_categories() { global $DB; - if (method_exists('\coursecat', 'get_all')) { - $categories = \coursecat::get_all(['returnhidden' => true]); + if (method_exists('\core_course_category', 'get_all')) { + $categories = \core_course_category::get_all(['returnhidden' => true]); } else { // Fallback (to be removed once this gets integrated into master). $ids = $DB->get_fieldset_select('course_categories', 'id', ''); - $categories = \coursecat::get_many($ids); + $categories = \core_course_category::get_many($ids); } foreach ($categories as $key => $category) { diff --git a/admin/tool/dataprivacy/classes/output/data_registry_page.php b/admin/tool/dataprivacy/classes/output/data_registry_page.php index cfdad43d72a..8d5fd3503d4 100644 --- a/admin/tool/dataprivacy/classes/output/data_registry_page.php +++ b/admin/tool/dataprivacy/classes/output/data_registry_page.php @@ -30,7 +30,6 @@ use stdClass; use templatable; use tool_dataprivacy\data_registry; -require_once($CFG->libdir . '/coursecatlib.php'); require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php'); require_once($CFG->libdir . '/blocklib.php'); @@ -226,7 +225,7 @@ class data_registry_page implements renderable, templatable { throw new \coding_exception('A course category context should be provided'); } - $coursecat = \coursecat::get($catcontext->instanceid); + $coursecat = \core_course_category::get($catcontext->instanceid); $courses = $coursecat->get_courses(); $branches = []; diff --git a/admin/tool/recyclebin/tests/category_bin_test.php b/admin/tool/recyclebin/tests/category_bin_test.php index 22c7f216c35..bada3d63853 100644 --- a/admin/tool/recyclebin/tests/category_bin_test.php +++ b/admin/tool/recyclebin/tests/category_bin_test.php @@ -96,7 +96,7 @@ class tool_recyclebin_category_bin_tests extends advanced_testcase { $this->assertEquals(1, $DB->count_records('tool_recyclebin_category')); // Now let's delete the course category. - $category = coursecat::get($this->course->category); + $category = core_course_category::get($this->course->category); $category->delete_full(false); // Check that the course was deleted from the category recycle bin. diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index 370d13a8220..2e514a70f21 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -23,7 +23,6 @@ */ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir . '/coursecatlib.php'); require_once($CFG->dirroot . '/cache/lib.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); @@ -379,7 +378,7 @@ class tool_uploadcourse_helper { $catid = null; if (!empty($data['category'])) { - $category = coursecat::get((int) $data['category'], IGNORE_MISSING); + $category = core_course_category::get((int) $data['category'], IGNORE_MISSING); if (!empty($category) && !empty($category->id)) { $catid = $category->id; } else { diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php index 2e394637c33..89ee15d14ed 100644 --- a/admin/tool/uploadcourse/classes/step2_form.php +++ b/admin/tool/uploadcourse/classes/step2_form.php @@ -82,7 +82,7 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse')); $mform->setExpanded('defaultheader', true); - $displaylist = coursecat::make_categories_list('moodle/course:create'); + $displaylist = core_course_category::make_categories_list('moodle/course:create'); $mform->addElement('select', 'defaults[category]', get_string('coursecategory'), $displaylist); $mform->addHelpButton('defaults[category]', 'coursecategory'); diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php index 75c39d8680f..1e9394de24d 100644 --- a/admin/tool/uploadcourse/cli/uploadcourse.php +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -26,7 +26,6 @@ define('CLI_SCRIPT', true); require(__DIR__ . '/../../../../config.php'); require_once($CFG->libdir . '/clilib.php'); -require_once($CFG->libdir . '/coursecatlib.php'); require_once($CFG->libdir . '/csvlib.class.php'); $courseconfig = get_config('moodlecourse'); @@ -46,7 +45,7 @@ list($options, $unrecognized) = cli_get_params(array( 'allowrenames' => false, 'allowresets' => false, 'reset' => false, - 'category' => coursecat::get_default()->id, + 'category' => core_course_category::get_default()->id, ), array( 'h' => 'help', diff --git a/admin/tool/uploadcourse/index.php b/admin/tool/uploadcourse/index.php index efeebf98215..69f4410f94c 100644 --- a/admin/tool/uploadcourse/index.php +++ b/admin/tool/uploadcourse/index.php @@ -24,7 +24,6 @@ require(__DIR__ . '/../../../config.php'); require_once($CFG->libdir . '/adminlib.php'); -require_once($CFG->libdir . '/coursecatlib.php'); require_once($CFG->libdir . '/csvlib.class.php'); admin_externalpage_setup('tooluploadcourse'); diff --git a/admin/tool/usertours/classes/local/filter/category.php b/admin/tool/usertours/classes/local/filter/category.php index 5f7ffc5a24c..0b1f119853d 100644 --- a/admin/tool/usertours/classes/local/filter/category.php +++ b/admin/tool/usertours/classes/local/filter/category.php @@ -52,7 +52,7 @@ class category extends base { * And whose values are the values to display */ public static function get_filter_options() { - $options = \coursecat::make_categories_list(); + $options = \core_course_category::make_categories_list(); return $options; } diff --git a/badges/criteria/award_criteria_courseset.php b/badges/criteria/award_criteria_courseset.php index 52b62b03df5..f60715c9e56 100644 --- a/badges/criteria/award_criteria_courseset.php +++ b/badges/criteria/award_criteria_courseset.php @@ -81,8 +81,7 @@ class award_criteria_courseset extends award_criteria { // Get courses with enabled completion. $courses = $DB->get_records('course', array('enablecompletion' => COMPLETION_ENABLED)); if (!empty($courses)) { - require_once($CFG->libdir . '/coursecatlib.php'); - $list = coursecat::make_categories_list(); + $list = core_course_category::make_categories_list(); $select = array(); $selected = array(); diff --git a/blocks/course_list/block_course_list.php b/blocks/course_list/block_course_list.php index 10cbf23d5e7..9d09dcbd18d 100644 --- a/blocks/course_list/block_course_list.php +++ b/blocks/course_list/block_course_list.php @@ -23,7 +23,6 @@ */ include_once($CFG->dirroot . '/course/lib.php'); -include_once($CFG->libdir . '/coursecatlib.php'); class block_course_list extends block_list { function init() { @@ -76,7 +75,7 @@ class block_course_list extends block_list { } } - $categories = coursecat::get(0)->get_children(); // Parent = 0 ie top-level categories only + $categories = core_course_category::get(0)->get_children(); // Parent = 0 ie top-level categories only if ($categories) { //Check we have categories if (count($categories) > 1 || (count($categories) == 1 && $DB->count_records('course') > 200)) { // Just print top level category links foreach ($categories as $category) { diff --git a/blocks/myoverview/classes/output/courses_view.php b/blocks/myoverview/classes/output/courses_view.php index b00741d0979..2c6027e0436 100644 --- a/blocks/myoverview/classes/output/courses_view.php +++ b/blocks/myoverview/classes/output/courses_view.php @@ -65,7 +65,6 @@ class courses_view implements renderable, templatable { public function export_for_template(renderer_base $output) { global $CFG; require_once($CFG->dirroot.'/course/lib.php'); - require_once($CFG->dirroot.'/lib/coursecatlib.php'); // Build courses view data structure. $coursesview = [ @@ -84,7 +83,7 @@ class courses_view implements renderable, templatable { // Convert summary to plain text. $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat); - $course = new \course_in_list($course); + $course = new \core_course_list_element($course); foreach ($course->get_course_overviewfiles() as $file) { $isimage = $file->is_valid_image(); if ($isimage) { diff --git a/calendar/classes/local/event/container.php b/calendar/classes/local/event/container.php index 0afc5940281..4b7a850f40a 100644 --- a/calendar/classes/local/event/container.php +++ b/calendar/classes/local/event/container.php @@ -126,7 +126,7 @@ class container { if (!empty($dbrow->categoryid)) { // This is a category event. Check that the category is visible to this user. - $category = \coursecat::get($dbrow->categoryid, IGNORE_MISSING, true, $requestinguserid); + $category = \core_course_category::get($dbrow->categoryid, IGNORE_MISSING, true, $requestinguserid); if (empty($category) || !$category->is_uservisible($requestinguserid)) { return true; diff --git a/calendar/classes/local/event/data_access/event_vault.php b/calendar/classes/local/event/data_access/event_vault.php index 6c42d3c2e53..121a113c1bb 100644 --- a/calendar/classes/local/event/data_access/event_vault.php +++ b/calendar/classes/local/event/data_access/event_vault.php @@ -33,8 +33,6 @@ use core_calendar\local\event\factories\action_factory_interface; use core_calendar\local\event\factories\event_factory_interface; use core_calendar\local\event\strategies\raw_event_retrieval_strategy_interface; -require_once($CFG->libdir . '/coursecatlib.php'); - /** * Event vault class. * diff --git a/calendar/classes/local/event/factories/event_abstract_factory.php b/calendar/classes/local/event/factories/event_abstract_factory.php index 807ecadf61b..ff697dd5817 100644 --- a/calendar/classes/local/event/factories/event_abstract_factory.php +++ b/calendar/classes/local/event/factories/event_abstract_factory.php @@ -36,8 +36,6 @@ use core_calendar\local\event\value_objects\event_description; use core_calendar\local\event\value_objects\event_times; use core_calendar\local\event\entities\event_interface; -require_once($CFG->libdir . '/coursecatlib.php'); - /** * Abstract factory for creating calendar events. * diff --git a/calendar/classes/local/event/forms/eventtype.php b/calendar/classes/local/event/forms/eventtype.php index f8486875da7..b3d582694f1 100644 --- a/calendar/classes/local/event/forms/eventtype.php +++ b/calendar/classes/local/event/forms/eventtype.php @@ -90,7 +90,7 @@ trait eventtype { if (!empty($eventtypes['category'])) { $categoryoptions = []; - foreach (\coursecat::make_categories_list('moodle/category:manage') as $id => $category) { + foreach (\core_course_category::make_categories_list('moodle/category:manage') as $id => $category) { $categoryoptions[$id] = $category; } diff --git a/calendar/classes/local/event/proxies/coursecat_proxy.php b/calendar/classes/local/event/proxies/coursecat_proxy.php index c585f3a107a..8b0d4568979 100644 --- a/calendar/classes/local/event/proxies/coursecat_proxy.php +++ b/calendar/classes/local/event/proxies/coursecat_proxy.php @@ -26,8 +26,6 @@ namespace core_calendar\local\event\proxies; defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir . '/coursecatlib.php'); - /** * Course category proxy. * @@ -48,7 +46,7 @@ class coursecat_proxy implements proxy_interface { protected $base; /** - * @var \coursecat $category The proxied instance. + * @var \core_course_category $category The proxied instance. */ protected $category; @@ -81,11 +79,11 @@ class coursecat_proxy implements proxy_interface { /** * Get the full instance of the proxied class. * - * @return \coursecat + * @return \core_course_category */ - public function get_proxied_instance() : \coursecat { + public function get_proxied_instance() : \core_course_category { if (!$this->category) { - $this->category = \coursecat::get($this->id, IGNORE_MISSING, true); + $this->category = \core_course_category::get($this->id, IGNORE_MISSING, true); } return $this->category; } diff --git a/calendar/externallib.php b/calendar/externallib.php index 80e60cd8667..a7d784000c6 100644 --- a/calendar/externallib.php +++ b/calendar/externallib.php @@ -242,7 +242,8 @@ class core_calendar_external extends external_api { $categories = []; if (!empty($params['events']['categoryids'])) { - $catobjs = \coursecat::get_many(array_merge($params['events']['categoryids'], array_keys($coursecategories))); + $catobjs = \core_course_category::get_many( + array_merge($params['events']['categoryids'], array_keys($coursecategories))); foreach ($catobjs as $catobj) { if (isset($coursecategories[$catobj->id]) || has_capability('moodle/category:manage', $catobj->get_context())) { @@ -267,7 +268,7 @@ class core_calendar_external extends external_api { $categories = array_flip($categories); } else { $categories = []; - foreach (\coursecat::get_all() as $category) { + foreach (\core_course_category::get_all() as $category) { if (isset($coursecategories[$category->id]) || has_capability('moodle/category:manage', $category->get_context(), $USER, false)) { // If the user has access to a course in this category or can manage the category, diff --git a/calendar/lib.php b/calendar/lib.php index c3c7aac009a..3d8be5841a8 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -27,8 +27,6 @@ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } -require_once($CFG->libdir . '/coursecatlib.php'); - /** * These are read by the administration component to provide default values */ @@ -805,7 +803,7 @@ class calendar_event { $this->editoroptions['maxbytes'] = $course->maxbytes; } else if ($properties->eventtype === 'category') { // First check the course is valid. - \coursecat::get($properties->categoryid, MUST_EXIST, true); + \core_course_category::get($properties->categoryid, MUST_EXIST, true); // Course context. $this->editorcontext = $this->get_context(); } else { @@ -1067,14 +1065,14 @@ class calendar_information { } $courses = [$course->id => $course]; - $category = (\coursecat::get($course->category, MUST_EXIST, true))->get_db_record(); + $category = (\core_course_category::get($course->category, MUST_EXIST, true))->get_db_record(); } else if (!empty($categoryid)) { $course = get_site(); $courses = calendar_get_default_courses(null, 'id, category, groupmode, groupmodeforce'); // Filter available courses to those within this category or it's children. $ids = [$categoryid]; - $category = \coursecat::get($categoryid); + $category = \core_course_category::get($categoryid); $ids = array_merge($ids, array_keys($category->get_children())); $courses = array_filter($courses, function($course) use ($ids) { return array_search($course->category, $ids) !== false; @@ -1158,7 +1156,7 @@ class calendar_information { // A specific course was requested. // Fetch the category that this course is in, along with all parents. // Do not include child categories of this category, as the user many not have enrolments in those siblings or children. - $category = \coursecat::get($course->category, MUST_EXIST, true); + $category = \core_course_category::get($course->category, MUST_EXIST, true); $this->categoryid = $category->id; $this->categories = $category->get_parents(); @@ -1166,7 +1164,7 @@ class calendar_information { } else if (null !== $category && $category->id > 0) { // A specific category was requested. // Fetch all parents of this category, along with all children too. - $category = \coursecat::get($category->id); + $category = \core_course_category::get($category->id); $this->categoryid = $category->id; // Build the category list. @@ -1188,7 +1186,7 @@ class calendar_information { if ($this->categories === false) { // Use the category id as the key in the following array. That way we do not have to remove duplicates. $categories = []; - foreach (\coursecat::get_all() as $category) { + foreach (\core_course_category::get_all() as $category) { if (isset($coursecategories[$category->id]) || has_capability('moodle/category:manage', $category->get_context(), $USER, false)) { // If the user has access to a course in this category or can manage the category, @@ -2177,7 +2175,7 @@ function calendar_view_event_allowed(calendar_event $event) { return isset($mycourses[$courseid]); } else if ($event->categoryid) { // If this is a category we need to be able to see the category. - $cat = \coursecat::get($event->categoryid, IGNORE_MISSING); + $cat = \core_course_category::get($event->categoryid, IGNORE_MISSING); if (!$cat) { return false; } @@ -3147,7 +3145,7 @@ function calendar_can_edit_subscription($subscriptionorid) { $category = null; if (!empty($categoryid)) { - $category = \coursecat::get($categoryid); + $category = \core_course_category::get($categoryid); } calendar_get_allowed_types($allowed, $courseid, null, $category); switch ($subscription->eventtype) { @@ -3693,7 +3691,7 @@ function calendar_get_allowed_event_types(int $courseid = null) { if (has_capability('moodle/calendar:manageownentries', \context_system::instance())) { $types['user'] = true; } - if (coursecat::has_manage_capability_on_any()) { + if (core_course_category::has_manage_capability_on_any()) { $types['category'] = true; } diff --git a/calendar/managesubscriptions.php b/calendar/managesubscriptions.php index 87289a6c9be..3bf03dfe19e 100644 --- a/calendar/managesubscriptions.php +++ b/calendar/managesubscriptions.php @@ -154,7 +154,7 @@ if ($usedefaultfilters) { if (!empty($types['category'])) { list($categoryinsql, $categoryparams) = $DB->get_in_or_equal( - array_keys(\coursecat::make_categories_list('moodle/category:manage')), SQL_PARAMS_NAMED, 'category'); + array_keys(\core_course_category::make_categories_list('moodle/category:manage')), SQL_PARAMS_NAMED, 'category'); $searches[] = "(eventtype = 'category' AND categoryid {$categoryinsql})"; $params += $categoryparams; } diff --git a/calendar/tests/calendar_information_test.php b/calendar/tests/calendar_information_test.php index 3b325558efa..dccdda48798 100644 --- a/calendar/tests/calendar_information_test.php +++ b/calendar/tests/calendar_information_test.php @@ -293,7 +293,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $course->id); @@ -329,7 +329,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $course->id); @@ -378,7 +378,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $generator->enrol_user($user->id, $course->id); $this->setUser($user); @@ -419,7 +419,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $generator->enrol_user($user->id, $course->id); $this->setUser($user); @@ -503,7 +503,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $user = $generator->create_user(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $course->id); @@ -540,7 +540,7 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; - $category = \coursecat::get($coursea->category); + $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -576,7 +576,7 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; - $category = \coursecat::get($coursea->category); + $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -622,7 +622,7 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; - $category = \coursecat::get($coursea->category); + $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -669,7 +669,7 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; - $category = \coursecat::get($coursea->category); + $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -705,7 +705,7 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['A.2']; - $category = \coursecat::get($coursea->category); + $category = \core_course_category::get($coursea->category); $wrongcategory = $categories['B1']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -739,8 +739,8 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; - $categorya = \coursecat::get($coursea->category); - $categoryb = \coursecat::get($courseb->category); + $categorya = \core_course_category::get($coursea->category); + $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -776,8 +776,8 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; - $categorya = \coursecat::get($coursea->category); - $categoryb = \coursecat::get($courseb->category); + $categorya = \core_course_category::get($coursea->category); + $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -823,8 +823,8 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; - $categorya = \coursecat::get($coursea->category); - $categoryb = \coursecat::get($courseb->category); + $categorya = \core_course_category::get($coursea->category); + $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -871,8 +871,8 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; - $categorya = \coursecat::get($coursea->category); - $categoryb = \coursecat::get($courseb->category); + $categorya = \core_course_category::get($coursea->category); + $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -912,8 +912,8 @@ class calendar_information_testcase extends advanced_testcase { $user = $generator->create_user(); $coursea = $courses['A.1']; $courseb = $courses['B.1']; - $categorya = \coursecat::get($coursea->category); - $categoryb = \coursecat::get($courseb->category); + $categorya = \core_course_category::get($coursea->category); + $categoryb = \core_course_category::get($courseb->category); $wrongcategory = $categories['C']; $generator->enrol_user($user->id, $coursea->id); $generator->enrol_user($user->id, $courseb->id); @@ -971,7 +971,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $this->setAdminUser(); $generator->enrol_user($USER->id, $course->id); @@ -1002,7 +1002,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $this->setAdminUser(); $generator->enrol_user($USER->id, $course->id); @@ -1049,7 +1049,7 @@ class calendar_information_testcase extends advanced_testcase { $generator = $this->getDataGenerator(); $course = $courses['A1.1']; - $category = \coursecat::get($course->category); + $category = \core_course_category::get($course->category); $wrongcategory = $categories['B1']; $this->setAdminUser(); $generator->enrol_user($USER->id, $course->id); diff --git a/calendar/tests/coursecat_proxy_test.php b/calendar/tests/coursecat_proxy_test.php index c00fe47d389..346662970c9 100644 --- a/calendar/tests/coursecat_proxy_test.php +++ b/calendar/tests/coursecat_proxy_test.php @@ -59,6 +59,6 @@ class core_calendar_coursecat_proxy_testcase extends advanced_testcase { $this->assertEquals($name, $proxy->get('name')); $this->assertEquals(1, $DB->perf_get_reads() - $startreads); - $this->assertInstanceOf('coursecat', $proxy->get_proxied_instance()); + $this->assertInstanceOf('core_course_category', $proxy->get_proxied_instance()); } } diff --git a/cohort/edit_form.php b/cohort/edit_form.php index 83df3a7273c..7dbe18bc73a 100644 --- a/cohort/edit_form.php +++ b/cohort/edit_form.php @@ -100,9 +100,7 @@ class cohort_edit_form extends moodleform { } protected function get_category_options($currentcontextid) { - global $CFG; - require_once($CFG->libdir. '/coursecatlib.php'); - $displaylist = coursecat::make_categories_list('moodle/cohort:manage'); + $displaylist = core_course_category::make_categories_list('moodle/cohort:manage'); $options = array(); $syscontext = context_system::instance(); if (has_capability('moodle/cohort:manage', $syscontext)) { diff --git a/cohort/lib.php b/cohort/lib.php index 09d17dcc6b9..a76246ed6db 100644 --- a/cohort/lib.php +++ b/cohort/lib.php @@ -138,7 +138,7 @@ function cohort_delete_cohort($cohort) { * Somehow deal with cohorts when deleting course category, * we can not just delete them because they might be used in enrol * plugins or referenced in external systems. - * @param stdClass|coursecat $category + * @param stdClass|core_course_category $category * @return void */ function cohort_delete_category($category) { diff --git a/cohort/upload_form.php b/cohort/upload_form.php index dddb26ecda2..0b7ee248cae 100644 --- a/cohort/upload_form.php +++ b/cohort/upload_form.php @@ -138,11 +138,9 @@ class cohort_upload_form extends moodleform { * @return array */ protected function get_context_options() { - global $CFG; - require_once($CFG->libdir. '/coursecatlib.php'); if ($this->contextoptions === null) { $this->contextoptions = array(); - $displaylist = coursecat::make_categories_list('moodle/cohort:manage'); + $displaylist = core_course_category::make_categories_list('moodle/cohort:manage'); // We need to index the options array by context id instead of category id and add option for system context. $syscontext = context_system::instance(); if (has_capability('moodle/cohort:manage', $syscontext)) { diff --git a/course/ajax/management.php b/course/ajax/management.php index d6f79c30008..7df273ba2a4 100644 --- a/course/ajax/management.php +++ b/course/ajax/management.php @@ -27,7 +27,6 @@ define('AJAX_SCRIPT', true); require_once('../../config.php'); -require_once($CFG->dirroot.'/lib/coursecatlib.php'); require_once($CFG->dirroot.'/course/lib.php'); $action = required_param('action', PARAM_ALPHA); @@ -55,8 +54,8 @@ switch ($action) { $courseid = required_param('courseid', PARAM_INT); $categoryid = required_param('categoryid', PARAM_INT); $course = get_course($courseid); - $oldcategory = coursecat::get($course->category); - $category = coursecat::get($categoryid); + $oldcategory = core_course_category::get($course->category); + $category = core_course_category::get($categoryid); $outcome->outcome = \core_course\management\helper::move_courses_into_category($category, $courseid); $perpage = (int)get_user_preferences('coursecat_management_perpage', $CFG->coursesperpage); $totalcourses = $oldcategory->get_courses_count(); @@ -126,13 +125,13 @@ switch ($action) { break; case 'expandcategory': $categoryid = required_param('categoryid', PARAM_INT); - $coursecat = coursecat::get($categoryid); + $coursecat = core_course_category::get($categoryid); \core_course\management\helper::record_expanded_category($coursecat); $outcome->outcome = true; break; case 'collapsecategory': $categoryid = required_param('categoryid', PARAM_INT); - $coursecat = coursecat::get($categoryid); + $coursecat = core_course_category::get($categoryid); \core_course\management\helper::record_expanded_category($coursecat, false); $outcome->outcome = true; break; @@ -142,7 +141,7 @@ switch ($action) { $renderer = $PAGE->get_renderer('core_course', 'management'); $outcome->html = html_writer::start_tag('ul', array('class' => 'ml', 'role' => 'group', 'id' => 'subcategoriesof'.$categoryid)); - $coursecat = coursecat::get($categoryid); + $coursecat = core_course_category::get($categoryid); foreach ($coursecat->get_children() as $subcat) { $outcome->html .= $renderer->category_listitem($subcat, array(), $subcat->get_children_count()); } diff --git a/course/classes/category.php b/course/classes/category.php new file mode 100644 index 00000000000..a0345e604d2 --- /dev/null +++ b/course/classes/category.php @@ -0,0 +1,2853 @@ +. + +/** + * Contains class core_course_category responsible for course category operations + * + * @package core + * @subpackage course + * @copyright 2013 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class to store, cache, render and manage course category + * + * @property-read int $id + * @property-read string $name + * @property-read string $idnumber + * @property-read string $description + * @property-read int $descriptionformat + * @property-read int $parent + * @property-read int $sortorder + * @property-read int $coursecount + * @property-read int $visible + * @property-read int $visibleold + * @property-read int $timemodified + * @property-read int $depth + * @property-read string $path + * @property-read string $theme + * + * @package core + * @subpackage course + * @copyright 2013 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_course_category implements renderable, cacheable_object, IteratorAggregate { + /** @var core_course_category stores pseudo category with id=0. Use core_course_category::get(0) to retrieve */ + protected static $coursecat0; + + /** @var array list of all fields and their short name and default value for caching */ + protected static $coursecatfields = array( + 'id' => array('id', 0), + 'name' => array('na', ''), + 'idnumber' => array('in', null), + 'description' => null, // Not cached. + 'descriptionformat' => null, // Not cached. + 'parent' => array('pa', 0), + 'sortorder' => array('so', 0), + 'coursecount' => array('cc', 0), + 'visible' => array('vi', 1), + 'visibleold' => null, // Not cached. + 'timemodified' => null, // Not cached. + 'depth' => array('dh', 1), + 'path' => array('ph', null), + 'theme' => null, // Not cached. + ); + + /** @var int */ + protected $id; + + /** @var string */ + protected $name = ''; + + /** @var string */ + protected $idnumber = null; + + /** @var string */ + protected $description = false; + + /** @var int */ + protected $descriptionformat = false; + + /** @var int */ + protected $parent = 0; + + /** @var int */ + protected $sortorder = 0; + + /** @var int */ + protected $coursecount = false; + + /** @var int */ + protected $visible = 1; + + /** @var int */ + protected $visibleold = false; + + /** @var int */ + protected $timemodified = false; + + /** @var int */ + protected $depth = 0; + + /** @var string */ + protected $path = ''; + + /** @var string */ + protected $theme = false; + + /** @var bool */ + protected $fromcache; + + /** @var bool */ + protected $hasmanagecapability = null; + + /** + * Magic setter method, we do not want anybody to modify properties from the outside + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) { + debugging('Can not change core_course_category instance properties!', DEBUG_DEVELOPER); + } + + /** + * Magic method getter, redirects to read only values. Queries from DB the fields that were not cached + * + * @param string $name + * @return mixed + */ + public function __get($name) { + global $DB; + if (array_key_exists($name, self::$coursecatfields)) { + if ($this->$name === false) { + // Property was not retrieved from DB, retrieve all not retrieved fields. + $notretrievedfields = array_diff_key(self::$coursecatfields, array_filter(self::$coursecatfields)); + $record = $DB->get_record('course_categories', array('id' => $this->id), + join(',', array_keys($notretrievedfields)), MUST_EXIST); + foreach ($record as $key => $value) { + $this->$key = $value; + } + } + return $this->$name; + } + debugging('Invalid core_course_category property accessed! '.$name, DEBUG_DEVELOPER); + return null; + } + + /** + * Full support for isset on our magic read only properties. + * + * @param string $name + * @return bool + */ + public function __isset($name) { + if (array_key_exists($name, self::$coursecatfields)) { + return isset($this->$name); + } + return false; + } + + /** + * All properties are read only, sorry. + * + * @param string $name + */ + public function __unset($name) { + debugging('Can not unset core_course_category instance properties!', DEBUG_DEVELOPER); + } + + /** + * Create an iterator because magic vars can't be seen by 'foreach'. + * + * implementing method from interface IteratorAggregate + * + * @return ArrayIterator + */ + public function getIterator() { + $ret = array(); + foreach (self::$coursecatfields as $property => $unused) { + if ($this->$property !== false) { + $ret[$property] = $this->$property; + } + } + return new ArrayIterator($ret); + } + + /** + * Constructor + * + * Constructor is protected, use core_course_category::get($id) to retrieve category + * + * @param stdClass $record record from DB (may not contain all fields) + * @param bool $fromcache whether it is being restored from cache + */ + protected function __construct(stdClass $record, $fromcache = false) { + context_helper::preload_from_record($record); + foreach ($record as $key => $val) { + if (array_key_exists($key, self::$coursecatfields)) { + $this->$key = $val; + } + } + $this->fromcache = $fromcache; + } + + /** + * Returns coursecat object for requested category + * + * If category is not visible to the given user, it is treated as non existing + * unless $alwaysreturnhidden is set to true + * + * If id is 0, the pseudo object for root category is returned (convenient + * for calling other functions such as get_children()) + * + * @param int $id category id + * @param int $strictness whether to throw an exception (MUST_EXIST) or + * return null (IGNORE_MISSING) in case the category is not found or + * not visible to current user + * @param bool $alwaysreturnhidden set to true if you want an object to be + * returned even if this category is not visible to the current user + * (category is hidden and user does not have + * 'moodle/category:viewhiddencategories' capability). Use with care! + * @param int|stdClass $user The user id or object. By default (null) checks the visibility to the current user. + * @return null|self + * @throws moodle_exception + */ + public static function get($id, $strictness = MUST_EXIST, $alwaysreturnhidden = false, $user = null) { + if (!$id) { + if (!isset(self::$coursecat0)) { + $record = new stdClass(); + $record->id = 0; + $record->visible = 1; + $record->depth = 0; + $record->path = ''; + self::$coursecat0 = new self($record); + } + return self::$coursecat0; + } + $coursecatrecordcache = cache::make('core', 'coursecatrecords'); + $coursecat = $coursecatrecordcache->get($id); + if ($coursecat === false) { + if ($records = self::get_records('cc.id = :id', array('id' => $id))) { + $record = reset($records); + $coursecat = new self($record); + // Store in cache. + $coursecatrecordcache->set($id, $coursecat); + } + } + if ($coursecat && ($alwaysreturnhidden || $coursecat->is_uservisible($user))) { + return $coursecat; + } else { + if ($strictness == MUST_EXIST) { + throw new moodle_exception('unknowncategory'); + } + } + return null; + } + + /** + * Load many core_course_category objects. + * + * @param array $ids An array of category ID's to load. + * @return core_course_category[] + */ + public static function get_many(array $ids) { + global $DB; + $coursecatrecordcache = cache::make('core', 'coursecatrecords'); + $categories = $coursecatrecordcache->get_many($ids); + $toload = array(); + foreach ($categories as $id => $result) { + if ($result === false) { + $toload[] = $id; + } + } + if (!empty($toload)) { + list($where, $params) = $DB->get_in_or_equal($toload, SQL_PARAMS_NAMED); + $records = self::get_records('cc.id '.$where, $params); + $toset = array(); + foreach ($records as $record) { + $categories[$record->id] = new self($record); + $toset[$record->id] = $categories[$record->id]; + } + $coursecatrecordcache->set_many($toset); + } + return $categories; + } + + /** + * Load all core_course_category objects. + * + * @param array $options Options: + * - returnhidden Return categories even if they are hidden + * @return core_course_category[] + */ + public static function get_all($options = []) { + global $DB; + + $coursecatrecordcache = cache::make('core', 'coursecatrecords'); + + $catcontextsql = \context_helper::get_preload_record_columns_sql('ctx'); + $catsql = "SELECT cc.*, {$catcontextsql} + FROM {course_categories} cc + JOIN {context} ctx ON cc.id = ctx.instanceid"; + $catsqlwhere = "WHERE ctx.contextlevel = :contextlevel"; + $catsqlorder = "ORDER BY cc.depth ASC, cc.sortorder ASC"; + + $catrs = $DB->get_recordset_sql("{$catsql} {$catsqlwhere} {$catsqlorder}", [ + 'contextlevel' => CONTEXT_COURSECAT, + ]); + + $types['categories'] = []; + $categories = []; + $toset = []; + foreach ($catrs as $record) { + $category = new self($record); + $toset[$category->id] = $category; + + if (!empty($options['returnhidden']) || $category->is_uservisible()) { + $categories[$record->id] = $category; + } + } + $catrs->close(); + + $coursecatrecordcache->set_many($toset); + + return $categories; + + } + + /** + * Returns the first found category + * + * Note that if there are no categories visible to the current user on the first level, + * the invisible category may be returned + * + * @return core_course_category + */ + public static function get_default() { + if ($visiblechildren = self::get(0)->get_children()) { + $defcategory = reset($visiblechildren); + } else { + $toplevelcategories = self::get_tree(0); + $defcategoryid = $toplevelcategories[0]; + $defcategory = self::get($defcategoryid, MUST_EXIST, true); + } + return $defcategory; + } + + /** + * Restores the object after it has been externally modified in DB for example + * during {@link fix_course_sortorder()} + */ + protected function restore() { + // Update all fields in the current object. + $newrecord = self::get($this->id, MUST_EXIST, true); + foreach (self::$coursecatfields as $key => $unused) { + $this->$key = $newrecord->$key; + } + } + + /** + * Creates a new category either from form data or from raw data + * + * Please note that this function does not verify access control. + * + * Exception is thrown if name is missing or idnumber is duplicating another one in the system. + * + * Category visibility is inherited from parent unless $data->visible = 0 is specified + * + * @param array|stdClass $data + * @param array $editoroptions if specified, the data is considered to be + * form data and file_postupdate_standard_editor() is being called to + * process images in description. + * @return core_course_category + * @throws moodle_exception + */ + public static function create($data, $editoroptions = null) { + global $DB, $CFG; + $data = (object)$data; + $newcategory = new stdClass(); + + $newcategory->descriptionformat = FORMAT_MOODLE; + $newcategory->description = ''; + // Copy all description* fields regardless of whether this is form data or direct field update. + foreach ($data as $key => $value) { + if (preg_match("/^description/", $key)) { + $newcategory->$key = $value; + } + } + + if (empty($data->name)) { + throw new moodle_exception('categorynamerequired'); + } + if (core_text::strlen($data->name) > 255) { + throw new moodle_exception('categorytoolong'); + } + $newcategory->name = $data->name; + + // Validate and set idnumber. + if (isset($data->idnumber)) { + if (core_text::strlen($data->idnumber) > 100) { + throw new moodle_exception('idnumbertoolong'); + } + if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) { + throw new moodle_exception('categoryidnumbertaken'); + } + $newcategory->idnumber = $data->idnumber; + } + + if (isset($data->theme) && !empty($CFG->allowcategorythemes)) { + $newcategory->theme = $data->theme; + } + + if (empty($data->parent)) { + $parent = self::get(0); + } else { + $parent = self::get($data->parent, MUST_EXIST, true); + } + $newcategory->parent = $parent->id; + $newcategory->depth = $parent->depth + 1; + + // By default category is visible, unless visible = 0 is specified or parent category is hidden. + if (isset($data->visible) && !$data->visible) { + // Create a hidden category. + $newcategory->visible = $newcategory->visibleold = 0; + } else { + // Create a category that inherits visibility from parent. + $newcategory->visible = $parent->visible; + // In case parent is hidden, when it changes visibility this new subcategory will automatically become visible too. + $newcategory->visibleold = 1; + } + + $newcategory->sortorder = 0; + $newcategory->timemodified = time(); + + $newcategory->id = $DB->insert_record('course_categories', $newcategory); + + // Update path (only possible after we know the category id. + $path = $parent->path . '/' . $newcategory->id; + $DB->set_field('course_categories', 'path', $path, array('id' => $newcategory->id)); + + // We should mark the context as dirty. + context_coursecat::instance($newcategory->id)->mark_dirty(); + + fix_course_sortorder(); + + // If this is data from form results, save embedded files and update description. + $categorycontext = context_coursecat::instance($newcategory->id); + if ($editoroptions) { + $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext, + 'coursecat', 'description', 0); + + // Update only fields description and descriptionformat. + $updatedata = new stdClass(); + $updatedata->id = $newcategory->id; + $updatedata->description = $newcategory->description; + $updatedata->descriptionformat = $newcategory->descriptionformat; + $DB->update_record('course_categories', $updatedata); + } + + $event = \core\event\course_category_created::create(array( + 'objectid' => $newcategory->id, + 'context' => $categorycontext + )); + $event->trigger(); + + cache_helper::purge_by_event('changesincoursecat'); + + return self::get($newcategory->id, MUST_EXIST, true); + } + + /** + * Updates the record with either form data or raw data + * + * Please note that this function does not verify access control. + * + * This function calls core_course_category::change_parent_raw if field 'parent' is updated. + * It also calls core_course_category::hide_raw or core_course_category::show_raw if 'visible' is updated. + * Visibility is changed first and then parent is changed. This means that + * if parent category is hidden, the current category will become hidden + * too and it may overwrite whatever was set in field 'visible'. + * + * Note that fields 'path' and 'depth' can not be updated manually + * Also core_course_category::update() can not directly update the field 'sortoder' + * + * @param array|stdClass $data + * @param array $editoroptions if specified, the data is considered to be + * form data and file_postupdate_standard_editor() is being called to + * process images in description. + * @throws moodle_exception + */ + public function update($data, $editoroptions = null) { + global $DB, $CFG; + if (!$this->id) { + // There is no actual DB record associated with root category. + return; + } + + $data = (object)$data; + $newcategory = new stdClass(); + $newcategory->id = $this->id; + + // Copy all description* fields regardless of whether this is form data or direct field update. + foreach ($data as $key => $value) { + if (preg_match("/^description/", $key)) { + $newcategory->$key = $value; + } + } + + if (isset($data->name) && empty($data->name)) { + throw new moodle_exception('categorynamerequired'); + } + + if (!empty($data->name) && $data->name !== $this->name) { + if (core_text::strlen($data->name) > 255) { + throw new moodle_exception('categorytoolong'); + } + $newcategory->name = $data->name; + } + + if (isset($data->idnumber) && $data->idnumber !== $this->idnumber) { + if (core_text::strlen($data->idnumber) > 100) { + throw new moodle_exception('idnumbertoolong'); + } + if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) { + throw new moodle_exception('categoryidnumbertaken'); + } + $newcategory->idnumber = $data->idnumber; + } + + if (isset($data->theme) && !empty($CFG->allowcategorythemes)) { + $newcategory->theme = $data->theme; + } + + $changes = false; + if (isset($data->visible)) { + if ($data->visible) { + $changes = $this->show_raw(); + } else { + $changes = $this->hide_raw(0); + } + } + + if (isset($data->parent) && $data->parent != $this->parent) { + if ($changes) { + cache_helper::purge_by_event('changesincoursecat'); + } + $parentcat = self::get($data->parent, MUST_EXIST, true); + $this->change_parent_raw($parentcat); + fix_course_sortorder(); + } + + $newcategory->timemodified = time(); + + $categorycontext = $this->get_context(); + if ($editoroptions) { + $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext, + 'coursecat', 'description', 0); + } + $DB->update_record('course_categories', $newcategory); + + $event = \core\event\course_category_updated::create(array( + 'objectid' => $newcategory->id, + 'context' => $categorycontext + )); + $event->trigger(); + + fix_course_sortorder(); + // Purge cache even if fix_course_sortorder() did not do it. + cache_helper::purge_by_event('changesincoursecat'); + + // Update all fields in the current object. + $this->restore(); + } + + + /** + * Checks if this course category is visible to a user. + * + * Please note that methods core_course_category::get (without 3rd argumet), + * core_course_category::get_children(), etc. return only visible categories so it is + * usually not needed to call this function outside of this class + * + * @param int|stdClass $user The user id or object. By default (null) checks the visibility to the current user. + * @return bool + */ + public function is_uservisible($user = null) { + return !$this->id || $this->visible || + has_capability('moodle/category:viewhiddencategories', $this->get_context(), $user); + } + + /** + * Returns the complete corresponding record from DB table course_categories + * + * Mostly used in deprecated functions + * + * @return stdClass + */ + public function get_db_record() { + global $DB; + if ($record = $DB->get_record('course_categories', array('id' => $this->id))) { + return $record; + } else { + return (object)convert_to_array($this); + } + } + + /** + * Returns the entry from categories tree and makes sure the application-level tree cache is built + * + * The following keys can be requested: + * + * 'countall' - total number of categories in the system (always present) + * 0 - array of ids of top-level categories (always present) + * '0i' - array of ids of top-level categories that have visible=0 (always present but may be empty array) + * $id (int) - array of ids of categories that are direct children of category with id $id. If + * category with id $id does not exist returns false. If category has no children returns empty array + * $id.'i' - array of ids of children categories that have visible=0 + * + * @param int|string $id + * @return mixed + */ + protected static function get_tree($id) { + global $DB; + $coursecattreecache = cache::make('core', 'coursecattree'); + $rv = $coursecattreecache->get($id); + if ($rv !== false) { + return $rv; + } + // Re-build the tree. + $sql = "SELECT cc.id, cc.parent, cc.visible + FROM {course_categories} cc + ORDER BY cc.sortorder"; + $rs = $DB->get_recordset_sql($sql, array()); + $all = array(0 => array(), '0i' => array()); + $count = 0; + foreach ($rs as $record) { + $all[$record->id] = array(); + $all[$record->id. 'i'] = array(); + if (array_key_exists($record->parent, $all)) { + $all[$record->parent][] = $record->id; + if (!$record->visible) { + $all[$record->parent. 'i'][] = $record->id; + } + } else { + // Parent not found. This is data consistency error but next fix_course_sortorder() should fix it. + $all[0][] = $record->id; + if (!$record->visible) { + $all['0i'][] = $record->id; + } + } + $count++; + } + $rs->close(); + if (!$count) { + // No categories found. + // This may happen after upgrade of a very old moodle version. + // In new versions the default category is created on install. + $defcoursecat = self::create(array('name' => get_string('miscellaneous'))); + set_config('defaultrequestcategory', $defcoursecat->id); + $all[0] = array($defcoursecat->id); + $all[$defcoursecat->id] = array(); + $count++; + } + // We must add countall to all in case it was the requested ID. + $all['countall'] = $count; + $coursecattreecache->set_many($all); + if (array_key_exists($id, $all)) { + return $all[$id]; + } + // Requested non-existing category. + return array(); + } + + /** + * Returns number of ALL categories in the system regardless if + * they are visible to current user or not + * + * @return int + */ + public static function count_all() { + return self::get_tree('countall'); + } + + /** + * Retrieves number of records from course_categories table + * + * Only cached fields are retrieved. Records are ready for preloading context + * + * @param string $whereclause + * @param array $params + * @return array array of stdClass objects + */ + protected static function get_records($whereclause, $params) { + global $DB; + // Retrieve from DB only the fields that need to be stored in cache. + $fields = array_keys(array_filter(self::$coursecatfields)); + $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); + $sql = "SELECT cc.". join(',cc.', $fields). ", $ctxselect + FROM {course_categories} cc + JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat + WHERE ". $whereclause." ORDER BY cc.sortorder"; + return $DB->get_records_sql($sql, + array('contextcoursecat' => CONTEXT_COURSECAT) + $params); + } + + /** + * Resets course contact caches when role assignments were changed + * + * @param int $roleid role id that was given or taken away + * @param context $context context where role assignment has been changed + */ + public static function role_assignment_changed($roleid, $context) { + global $CFG, $DB; + + if ($context->contextlevel > CONTEXT_COURSE) { + // No changes to course contacts if role was assigned on the module/block level. + return; + } + + // Trigger a purge for all caches listening for changes to category enrolment. + cache_helper::purge_by_event('changesincategoryenrolment'); + + if (!$CFG->coursecontact || !in_array($roleid, explode(',', $CFG->coursecontact))) { + // The role is not one of course contact roles. + return; + } + + // Remove from cache course contacts of all affected courses. + $cache = cache::make('core', 'coursecontacts'); + if ($context->contextlevel == CONTEXT_COURSE) { + $cache->delete($context->instanceid); + } else if ($context->contextlevel == CONTEXT_SYSTEM) { + $cache->purge(); + } else { + $sql = "SELECT ctx.instanceid + FROM {context} ctx + WHERE ctx.path LIKE ? AND ctx.contextlevel = ?"; + $params = array($context->path . '/%', CONTEXT_COURSE); + if ($courses = $DB->get_fieldset_sql($sql, $params)) { + $cache->delete_many($courses); + } + } + } + + /** + * Executed when user enrolment was changed to check if course + * contacts cache needs to be cleared + * + * @param int $courseid course id + * @param int $userid user id + * @param int $status new enrolment status (0 - active, 1 - suspended) + * @param int $timestart new enrolment time start + * @param int $timeend new enrolment time end + */ + public static function user_enrolment_changed($courseid, $userid, + $status, $timestart = null, $timeend = null) { + $cache = cache::make('core', 'coursecontacts'); + $contacts = $cache->get($courseid); + if ($contacts === false) { + // The contacts for the affected course were not cached anyway. + return; + } + $enrolmentactive = ($status == 0) && + (!$timestart || $timestart < time()) && + (!$timeend || $timeend > time()); + if (!$enrolmentactive) { + $isincontacts = false; + foreach ($contacts as $contact) { + if ($contact->id == $userid) { + $isincontacts = true; + } + } + if (!$isincontacts) { + // Changed user's enrolment does not exist or is not active, + // and he is not in cached course contacts, no changes to be made. + return; + } + } + // Either enrolment of manager was deleted/suspended + // or user enrolment was added or activated. + // In order to see if the course contacts for this course need + // changing we would need to make additional queries, they will + // slow down bulk enrolment changes. It is better just to remove + // course contacts cache for this course. + $cache->delete($courseid); + } + + /** + * Given list of DB records from table course populates each record with list of users with course contact roles + * + * This function fills the courses with raw information as {@link get_role_users()} would do. + * See also {@link core_course_list_element::get_course_contacts()} for more readable return + * + * $courses[$i]->managers = array( + * $roleassignmentid => $roleuser, + * ... + * ); + * + * where $roleuser is an stdClass with the following properties: + * + * $roleuser->raid - role assignment id + * $roleuser->id - user id + * $roleuser->username + * $roleuser->firstname + * $roleuser->lastname + * $roleuser->rolecoursealias + * $roleuser->rolename + * $roleuser->sortorder - role sortorder + * $roleuser->roleid + * $roleuser->roleshortname + * + * @todo MDL-38596 minimize number of queries to preload contacts for the list of courses + * + * @param array $courses + */ + public static function preload_course_contacts(&$courses) { + global $CFG, $DB; + if (empty($courses) || empty($CFG->coursecontact)) { + return; + } + $managerroles = explode(',', $CFG->coursecontact); + $cache = cache::make('core', 'coursecontacts'); + $cacheddata = $cache->get_many(array_keys($courses)); + $courseids = array(); + foreach (array_keys($courses) as $id) { + if ($cacheddata[$id] !== false) { + $courses[$id]->managers = $cacheddata[$id]; + } else { + $courseids[] = $id; + } + } + + // Array $courseids now stores list of ids of courses for which we still need to retrieve contacts. + if (empty($courseids)) { + return; + } + + // First build the array of all context ids of the courses and their categories. + $allcontexts = array(); + foreach ($courseids as $id) { + $context = context_course::instance($id); + $courses[$id]->managers = array(); + foreach (preg_split('|/|', $context->path, 0, PREG_SPLIT_NO_EMPTY) as $ctxid) { + if (!isset($allcontexts[$ctxid])) { + $allcontexts[$ctxid] = array(); + } + $allcontexts[$ctxid][] = $id; + } + } + + // Fetch list of all users with course contact roles in any of the courses contexts or parent contexts. + list($sql1, $params1) = $DB->get_in_or_equal(array_keys($allcontexts), SQL_PARAMS_NAMED, 'ctxid'); + list($sql2, $params2) = $DB->get_in_or_equal($managerroles, SQL_PARAMS_NAMED, 'rid'); + list($sort, $sortparams) = users_order_by_sql('u'); + $notdeleted = array('notdeleted' => 0); + $allnames = get_all_user_name_fields(true, 'u'); + $sql = "SELECT ra.contextid, ra.id AS raid, + r.id AS roleid, r.name AS rolename, r.shortname AS roleshortname, + rn.name AS rolecoursealias, u.id, u.username, $allnames + FROM {role_assignments} ra + JOIN {user} u ON ra.userid = u.id + JOIN {role} r ON ra.roleid = r.id + LEFT JOIN {role_names} rn ON (rn.contextid = ra.contextid AND rn.roleid = r.id) + WHERE ra.contextid ". $sql1." AND ra.roleid ". $sql2." AND u.deleted = :notdeleted + ORDER BY r.sortorder, $sort"; + $rs = $DB->get_recordset_sql($sql, $params1 + $params2 + $notdeleted + $sortparams); + $checkenrolments = array(); + foreach ($rs as $ra) { + foreach ($allcontexts[$ra->contextid] as $id) { + $courses[$id]->managers[$ra->raid] = $ra; + if (!isset($checkenrolments[$id])) { + $checkenrolments[$id] = array(); + } + $checkenrolments[$id][] = $ra->id; + } + } + $rs->close(); + + // Remove from course contacts users who are not enrolled in the course. + $enrolleduserids = self::ensure_users_enrolled($checkenrolments); + foreach ($checkenrolments as $id => $userids) { + if (empty($enrolleduserids[$id])) { + $courses[$id]->managers = array(); + } else if ($notenrolled = array_diff($userids, $enrolleduserids[$id])) { + foreach ($courses[$id]->managers as $raid => $ra) { + if (in_array($ra->id, $notenrolled)) { + unset($courses[$id]->managers[$raid]); + } + } + } + } + + // Set the cache. + $values = array(); + foreach ($courseids as $id) { + $values[$id] = $courses[$id]->managers; + } + $cache->set_many($values); + } + + /** + * Verify user enrollments for multiple course-user combinations + * + * @param array $courseusers array where keys are course ids and values are array + * of users in this course whose enrolment we wish to verify + * @return array same structure as input array but values list only users from input + * who are enrolled in the course + */ + protected static function ensure_users_enrolled($courseusers) { + global $DB; + // If the input array is too big, split it into chunks. + $maxcoursesinquery = 20; + if (count($courseusers) > $maxcoursesinquery) { + $rv = array(); + for ($offset = 0; $offset < count($courseusers); $offset += $maxcoursesinquery) { + $chunk = array_slice($courseusers, $offset, $maxcoursesinquery, true); + $rv = $rv + self::ensure_users_enrolled($chunk); + } + return $rv; + } + + // Create a query verifying valid user enrolments for the number of courses. + $sql = "SELECT DISTINCT e.courseid, ue.userid + FROM {user_enrolments} ue + JOIN {enrol} e ON e.id = ue.enrolid + WHERE ue.status = :active + AND e.status = :enabled + AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)"; + $now = round(time(), -2); // Rounding helps caching in DB. + $params = array('enabled' => ENROL_INSTANCE_ENABLED, + 'active' => ENROL_USER_ACTIVE, + 'now1' => $now, 'now2' => $now); + $cnt = 0; + $subsqls = array(); + $enrolled = array(); + foreach ($courseusers as $id => $userids) { + $enrolled[$id] = array(); + if (count($userids)) { + list($sql2, $params2) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'userid'.$cnt.'_'); + $subsqls[] = "(e.courseid = :courseid$cnt AND ue.userid ".$sql2.")"; + $params = $params + array('courseid'.$cnt => $id) + $params2; + $cnt++; + } + } + if (count($subsqls)) { + $sql .= "AND (". join(' OR ', $subsqls).")"; + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $record) { + $enrolled[$record->courseid][] = $record->userid; + } + $rs->close(); + } + return $enrolled; + } + + /** + * Retrieves number of records from course table + * + * Not all fields are retrieved. Records are ready for preloading context + * + * @param string $whereclause + * @param array $params + * @param array $options may indicate that summary and/or coursecontacts need to be retrieved + * @param bool $checkvisibility if true, capability 'moodle/course:viewhiddencourses' will be checked + * on not visible courses + * @return array array of stdClass objects + */ + protected static function get_course_records($whereclause, $params, $options, $checkvisibility = false) { + global $DB; + $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); + $fields = array('c.id', 'c.category', 'c.sortorder', + 'c.shortname', 'c.fullname', 'c.idnumber', + 'c.startdate', 'c.enddate', 'c.visible', 'c.cacherev'); + if (!empty($options['summary'])) { + $fields[] = 'c.summary'; + $fields[] = 'c.summaryformat'; + } else { + $fields[] = $DB->sql_substr('c.summary', 1, 1). ' as hassummary'; + } + $sql = "SELECT ". join(',', $fields). ", $ctxselect + FROM {course} c + JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = :contextcourse + WHERE ". $whereclause." ORDER BY c.sortorder"; + $list = $DB->get_records_sql($sql, + array('contextcourse' => CONTEXT_COURSE) + $params); + + if ($checkvisibility) { + // Loop through all records and make sure we only return the courses accessible by user. + foreach ($list as $course) { + if (isset($list[$course->id]->hassummary)) { + $list[$course->id]->hassummary = strlen($list[$course->id]->hassummary) > 0; + } + if (empty($course->visible)) { + // Load context only if we need to check capability. + context_helper::preload_from_record($course); + if (!has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) { + unset($list[$course->id]); + } + } + } + } + + // Preload course contacts if necessary. + if (!empty($options['coursecontacts'])) { + self::preload_course_contacts($list); + } + return $list; + } + + /** + * Returns array of ids of children categories that current user can not see + * + * This data is cached in user session cache + * + * @return array + */ + protected function get_not_visible_children_ids() { + global $DB; + $coursecatcache = cache::make('core', 'coursecat'); + if (($invisibleids = $coursecatcache->get('ic'. $this->id)) === false) { + // We never checked visible children before. + $hidden = self::get_tree($this->id.'i'); + $invisibleids = array(); + if ($hidden) { + // Preload categories contexts. + list($sql, $params) = $DB->get_in_or_equal($hidden, SQL_PARAMS_NAMED, 'id'); + $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); + $contexts = $DB->get_records_sql("SELECT $ctxselect FROM {context} ctx + WHERE ctx.contextlevel = :contextcoursecat AND ctx.instanceid ".$sql, + array('contextcoursecat' => CONTEXT_COURSECAT) + $params); + foreach ($contexts as $record) { + context_helper::preload_from_record($record); + } + // Check that user has 'viewhiddencategories' capability for each hidden category. + foreach ($hidden as $id) { + if (!has_capability('moodle/category:viewhiddencategories', context_coursecat::instance($id))) { + $invisibleids[] = $id; + } + } + } + $coursecatcache->set('ic'. $this->id, $invisibleids); + } + return $invisibleids; + } + + /** + * Sorts list of records by several fields + * + * @param array $records array of stdClass objects + * @param array $sortfields assoc array where key is the field to sort and value is 1 for asc or -1 for desc + * @return int + */ + protected static function sort_records(&$records, $sortfields) { + if (empty($records)) { + return; + } + // If sorting by course display name, calculate it (it may be fullname or shortname+fullname). + if (array_key_exists('displayname', $sortfields)) { + foreach ($records as $key => $record) { + if (!isset($record->displayname)) { + $records[$key]->displayname = get_course_display_name_for_list($record); + } + } + } + // Sorting by one field - use core_collator. + if (count($sortfields) == 1) { + $property = key($sortfields); + if (in_array($property, array('sortorder', 'id', 'visible', 'parent', 'depth'))) { + $sortflag = core_collator::SORT_NUMERIC; + } else if (in_array($property, array('idnumber', 'displayname', 'name', 'shortname', 'fullname'))) { + $sortflag = core_collator::SORT_STRING; + } else { + $sortflag = core_collator::SORT_REGULAR; + } + core_collator::asort_objects_by_property($records, $property, $sortflag); + if ($sortfields[$property] < 0) { + $records = array_reverse($records, true); + } + return; + } + + // Sort by multiple fields - use custom sorting. + uasort($records, function($a, $b) use ($sortfields) { + foreach ($sortfields as $field => $mult) { + // Nulls first. + if (is_null($a->$field) && !is_null($b->$field)) { + return -$mult; + } + if (is_null($b->$field) && !is_null($a->$field)) { + return $mult; + } + + if (is_string($a->$field) || is_string($b->$field)) { + // String fields. + if ($cmp = strcoll($a->$field, $b->$field)) { + return $mult * $cmp; + } + } else { + // Int fields. + if ($a->$field > $b->$field) { + return $mult; + } + if ($a->$field < $b->$field) { + return -$mult; + } + } + } + return 0; + }); + } + + /** + * Returns array of children categories visible to the current user + * + * @param array $options options for retrieving children + * - sort - list of fields to sort. Example + * array('idnumber' => 1, 'name' => 1, 'id' => -1) + * will sort by idnumber asc, name asc and id desc. + * Default: array('sortorder' => 1) + * Only cached fields may be used for sorting! + * - offset + * - limit - maximum number of children to return, 0 or null for no limit + * @return core_course_category[] Array of core_course_category objects indexed by category id + */ + public function get_children($options = array()) { + global $DB; + $coursecatcache = cache::make('core', 'coursecat'); + + // Get default values for options. + if (!empty($options['sort']) && is_array($options['sort'])) { + $sortfields = $options['sort']; + } else { + $sortfields = array('sortorder' => 1); + } + $limit = null; + if (!empty($options['limit']) && (int)$options['limit']) { + $limit = (int)$options['limit']; + } + $offset = 0; + if (!empty($options['offset']) && (int)$options['offset']) { + $offset = (int)$options['offset']; + } + + // First retrieve list of user-visible and sorted children ids from cache. + $sortedids = $coursecatcache->get('c'. $this->id. ':'. serialize($sortfields)); + if ($sortedids === false) { + $sortfieldskeys = array_keys($sortfields); + if ($sortfieldskeys[0] === 'sortorder') { + // No DB requests required to build the list of ids sorted by sortorder. + // We can easily ignore other sort fields because sortorder is always different. + $sortedids = self::get_tree($this->id); + if ($sortedids && ($invisibleids = $this->get_not_visible_children_ids())) { + $sortedids = array_diff($sortedids, $invisibleids); + if ($sortfields['sortorder'] == -1) { + $sortedids = array_reverse($sortedids, true); + } + } + } else { + // We need to retrieve and sort all children. Good thing that it is done only on first request. + if ($invisibleids = $this->get_not_visible_children_ids()) { + list($sql, $params) = $DB->get_in_or_equal($invisibleids, SQL_PARAMS_NAMED, 'id', false); + $records = self::get_records('cc.parent = :parent AND cc.id '. $sql, + array('parent' => $this->id) + $params); + } else { + $records = self::get_records('cc.parent = :parent', array('parent' => $this->id)); + } + self::sort_records($records, $sortfields); + $sortedids = array_keys($records); + } + $coursecatcache->set('c'. $this->id. ':'.serialize($sortfields), $sortedids); + } + + if (empty($sortedids)) { + return array(); + } + + // Now retrieive and return categories. + if ($offset || $limit) { + $sortedids = array_slice($sortedids, $offset, $limit); + } + if (isset($records)) { + // Easy, we have already retrieved records. + if ($offset || $limit) { + $records = array_slice($records, $offset, $limit, true); + } + } else { + list($sql, $params) = $DB->get_in_or_equal($sortedids, SQL_PARAMS_NAMED, 'id'); + $records = self::get_records('cc.id '. $sql, array('parent' => $this->id) + $params); + } + + $rv = array(); + foreach ($sortedids as $id) { + if (isset($records[$id])) { + $rv[$id] = new self($records[$id]); + } + } + return $rv; + } + + /** + * Returns an array of ids of categories that are (direct and indirect) children + * of this category. + * + * @return int[] + */ + public function get_all_children_ids() { + $children = []; + $walk = [$this->id]; + while (count($walk) > 0) { + $catid = array_pop($walk); + $directchildren = self::get_tree($catid); + if ($directchildren !== false && count($directchildren) > 0) { + $walk = array_merge($walk, $directchildren); + $children = array_merge($children, $directchildren); + } + } + + return $children; + } + + /** + * Returns true if the user has the manage capability on any category. + * + * This method uses the coursecat cache and an entry `has_manage_capability` to speed up + * calls to this method. + * + * @return bool + */ + public static function has_manage_capability_on_any() { + return self::has_capability_on_any('moodle/category:manage'); + } + + /** + * Checks if the user has at least one of the given capabilities on any category. + * + * @param array|string $capabilities One or more capabilities to check. Check made is an OR. + * @return bool + */ + public static function has_capability_on_any($capabilities) { + global $DB; + if (!isloggedin() || isguestuser()) { + return false; + } + + if (!is_array($capabilities)) { + $capabilities = array($capabilities); + } + $keys = array(); + foreach ($capabilities as $capability) { + $keys[$capability] = sha1($capability); + } + + /** @var cache_session $cache */ + $cache = cache::make('core', 'coursecat'); + $hascapability = $cache->get_many($keys); + $needtoload = false; + foreach ($hascapability as $capability) { + if ($capability === '1') { + return true; + } else if ($capability === false) { + $needtoload = true; + } + } + if ($needtoload === false) { + // All capabilities were retrieved and the user didn't have any. + return false; + } + + $haskey = null; + $fields = context_helper::get_preload_record_columns_sql('ctx'); + $sql = "SELECT ctx.instanceid AS categoryid, $fields + FROM {context} ctx + WHERE contextlevel = :contextlevel + ORDER BY depth ASC"; + $params = array('contextlevel' => CONTEXT_COURSECAT); + $recordset = $DB->get_recordset_sql($sql, $params); + foreach ($recordset as $context) { + context_helper::preload_from_record($context); + $context = context_coursecat::instance($context->categoryid); + foreach ($capabilities as $capability) { + if (has_capability($capability, $context)) { + $haskey = $capability; + break 2; + } + } + } + $recordset->close(); + if ($haskey === null) { + $data = array(); + foreach ($keys as $key) { + $data[$key] = '0'; + } + $cache->set_many($data); + return false; + } else { + $cache->set($haskey, '1'); + return true; + } + } + + /** + * Returns true if the user can resort any category. + * @return bool + */ + public static function can_resort_any() { + return self::has_manage_capability_on_any(); + } + + /** + * Returns true if the user can change the parent of any category. + * @return bool + */ + public static function can_change_parent_any() { + return self::has_manage_capability_on_any(); + } + + /** + * Returns number of subcategories visible to the current user + * + * @return int + */ + public function get_children_count() { + $sortedids = self::get_tree($this->id); + $invisibleids = $this->get_not_visible_children_ids(); + return count($sortedids) - count($invisibleids); + } + + /** + * Returns true if the category has ANY children, including those not visible to the user + * + * @return boolean + */ + public function has_children() { + $allchildren = self::get_tree($this->id); + return !empty($allchildren); + } + + /** + * Returns true if the category has courses in it (count does not include courses + * in child categories) + * + * @return bool + */ + public function has_courses() { + global $DB; + return $DB->record_exists_sql("select 1 from {course} where category = ?", + array($this->id)); + } + + /** + * Get the link used to view this course category. + * + * @return \moodle_url + */ + public function get_view_link() { + return new \moodle_url('/course/index.php', [ + 'categoryid' => $this->id, + ]); + } + + /** + * Searches courses + * + * List of found course ids is cached for 10 minutes. Cache may be purged prior + * to this when somebody edits courses or categories, however it is very + * difficult to keep track of all possible changes that may affect list of courses. + * + * @param array $search contains search criterias, such as: + * - search - search string + * - blocklist - id of block (if we are searching for courses containing specific block0 + * - modulelist - name of module (if we are searching for courses containing specific module + * - tagid - id of tag + * @param array $options display options, same as in get_courses() except 'recursive' is ignored - + * search is always category-independent + * @param array $requiredcapabilities List of capabilities required to see return course. + * @return core_course_list_element[] + */ + public static function search_courses($search, $options = array(), $requiredcapabilities = array()) { + global $DB; + $offset = !empty($options['offset']) ? $options['offset'] : 0; + $limit = !empty($options['limit']) ? $options['limit'] : null; + $sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1); + + $coursecatcache = cache::make('core', 'coursecat'); + $cachekey = 's-'. serialize( + $search + array('sort' => $sortfields) + array('requiredcapabilities' => $requiredcapabilities) + ); + $cntcachekey = 'scnt-'. serialize($search); + + $ids = $coursecatcache->get($cachekey); + if ($ids !== false) { + // We already cached last search result. + $ids = array_slice($ids, $offset, $limit); + $courses = array(); + if (!empty($ids)) { + list($sql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'id'); + $records = self::get_course_records("c.id ". $sql, $params, $options); + // Preload course contacts if necessary - saves DB queries later to do it for each course separately. + if (!empty($options['coursecontacts'])) { + self::preload_course_contacts($records); + } + // If option 'idonly' is specified no further action is needed, just return list of ids. + if (!empty($options['idonly'])) { + return array_keys($records); + } + // Prepare the list of core_course_list_element objects. + foreach ($ids as $id) { + $courses[$id] = new core_course_list_element($records[$id]); + } + } + return $courses; + } + + $preloadcoursecontacts = !empty($options['coursecontacts']); + unset($options['coursecontacts']); + + // Empty search string will return all results. + if (!isset($search['search'])) { + $search['search'] = ''; + } + + if (empty($search['blocklist']) && empty($search['modulelist']) && empty($search['tagid'])) { + // Search courses that have specified words in their names/summaries. + $searchterms = preg_split('|\s+|', trim($search['search']), 0, PREG_SPLIT_NO_EMPTY); + + $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount, $requiredcapabilities); + self::sort_records($courselist, $sortfields); + $coursecatcache->set($cachekey, array_keys($courselist)); + $coursecatcache->set($cntcachekey, $totalcount); + $records = array_slice($courselist, $offset, $limit, true); + } else { + if (!empty($search['blocklist'])) { + // Search courses that have block with specified id. + $blockname = $DB->get_field('block', 'name', array('id' => $search['blocklist'])); + $where = 'ctx.id in (SELECT distinct bi.parentcontextid FROM {block_instances} bi + WHERE bi.blockname = :blockname)'; + $params = array('blockname' => $blockname); + } else if (!empty($search['modulelist'])) { + // Search courses that have module with specified name. + $where = "c.id IN (SELECT DISTINCT module.course ". + "FROM {".$search['modulelist']."} module)"; + $params = array(); + } else if (!empty($search['tagid'])) { + // Search courses that are tagged with the specified tag. + $where = "c.id IN (SELECT t.itemid ". + "FROM {tag_instance} t WHERE t.tagid = :tagid AND t.itemtype = :itemtype AND t.component = :component)"; + $params = array('tagid' => $search['tagid'], 'itemtype' => 'course', 'component' => 'core'); + if (!empty($search['ctx'])) { + $rec = isset($search['rec']) ? $search['rec'] : true; + $parentcontext = context::instance_by_id($search['ctx']); + if ($parentcontext->contextlevel == CONTEXT_SYSTEM && $rec) { + // Parent context is system context and recursive is set to yes. + // Nothing to filter - all courses fall into this condition. + } else if ($rec) { + // Filter all courses in the parent context at any level. + $where .= ' AND ctx.path LIKE :contextpath'; + $params['contextpath'] = $parentcontext->path . '%'; + } else if ($parentcontext->contextlevel == CONTEXT_COURSECAT) { + // All courses in the given course category. + $where .= ' AND c.category = :category'; + $params['category'] = $parentcontext->instanceid; + } else { + // No courses will satisfy the context criterion, do not bother searching. + $where = '1=0'; + } + } + } else { + debugging('No criteria is specified while searching courses', DEBUG_DEVELOPER); + return array(); + } + $courselist = self::get_course_records($where, $params, $options, true); + if (!empty($requiredcapabilities)) { + foreach ($courselist as $key => $course) { + context_helper::preload_from_record($course); + $coursecontext = context_course::instance($course->id); + if (!has_all_capabilities($requiredcapabilities, $coursecontext)) { + unset($courselist[$key]); + } + } + } + self::sort_records($courselist, $sortfields); + $coursecatcache->set($cachekey, array_keys($courselist)); + $coursecatcache->set($cntcachekey, count($courselist)); + $records = array_slice($courselist, $offset, $limit, true); + } + + // Preload course contacts if necessary - saves DB queries later to do it for each course separately. + if (!empty($preloadcoursecontacts)) { + self::preload_course_contacts($records); + } + // If option 'idonly' is specified no further action is needed, just return list of ids. + if (!empty($options['idonly'])) { + return array_keys($records); + } + // Prepare the list of core_course_list_element objects. + $courses = array(); + foreach ($records as $record) { + $courses[$record->id] = new core_course_list_element($record); + } + return $courses; + } + + /** + * Returns number of courses in the search results + * + * It is recommended to call this function after {@link core_course_category::search_courses()} + * and not before because only course ids are cached. Otherwise search_courses() may + * perform extra DB queries. + * + * @param array $search search criteria, see method search_courses() for more details + * @param array $options display options. They do not affect the result but + * the 'sort' property is used in cache key for storing list of course ids + * @param array $requiredcapabilities List of capabilities required to see return course. + * @return int + */ + public static function search_courses_count($search, $options = array(), $requiredcapabilities = array()) { + $coursecatcache = cache::make('core', 'coursecat'); + $cntcachekey = 'scnt-'. serialize($search) . serialize($requiredcapabilities); + if (($cnt = $coursecatcache->get($cntcachekey)) === false) { + // Cached value not found. Retrieve ALL courses and return their count. + unset($options['offset']); + unset($options['limit']); + unset($options['summary']); + unset($options['coursecontacts']); + $options['idonly'] = true; + $courses = self::search_courses($search, $options, $requiredcapabilities); + $cnt = count($courses); + } + return $cnt; + } + + /** + * Retrieves the list of courses accessible by user + * + * Not all information is cached, try to avoid calling this method + * twice in the same request. + * + * The following fields are always retrieved: + * - id, visible, fullname, shortname, idnumber, category, sortorder + * + * If you plan to use properties/methods core_course_list_element::$summary and/or + * core_course_list_element::get_course_contacts() + * you can preload this information using appropriate 'options'. Otherwise + * they will be retrieved from DB on demand and it may end with bigger DB load. + * + * Note that method core_course_list_element::has_summary() will not perform additional + * DB queries even if $options['summary'] is not specified + * + * List of found course ids is cached for 10 minutes. Cache may be purged prior + * to this when somebody edits courses or categories, however it is very + * difficult to keep track of all possible changes that may affect list of courses. + * + * @param array $options options for retrieving children + * - recursive - return courses from subcategories as well. Use with care, + * this may be a huge list! + * - summary - preloads fields 'summary' and 'summaryformat' + * - coursecontacts - preloads course contacts + * - sort - list of fields to sort. Example + * array('idnumber' => 1, 'shortname' => 1, 'id' => -1) + * will sort by idnumber asc, shortname asc and id desc. + * Default: array('sortorder' => 1) + * Only cached fields may be used for sorting! + * - offset + * - limit - maximum number of children to return, 0 or null for no limit + * - idonly - returns the array or course ids instead of array of objects + * used only in get_courses_count() + * @return core_course_list_element[] + */ + public function get_courses($options = array()) { + global $DB; + $recursive = !empty($options['recursive']); + $offset = !empty($options['offset']) ? $options['offset'] : 0; + $limit = !empty($options['limit']) ? $options['limit'] : null; + $sortfields = !empty($options['sort']) ? $options['sort'] : array('sortorder' => 1); + + // Check if this category is hidden. + // Also 0-category never has courses unless this is recursive call. + if (!$this->is_uservisible() || (!$this->id && !$recursive)) { + return array(); + } + + $coursecatcache = cache::make('core', 'coursecat'); + $cachekey = 'l-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : ''). + '-'. serialize($sortfields); + $cntcachekey = 'lcnt-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : ''); + + // Check if we have already cached results. + $ids = $coursecatcache->get($cachekey); + if ($ids !== false) { + // We already cached last search result and it did not expire yet. + $ids = array_slice($ids, $offset, $limit); + $courses = array(); + if (!empty($ids)) { + list($sql, $params) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'id'); + $records = self::get_course_records("c.id ". $sql, $params, $options); + // Preload course contacts if necessary - saves DB queries later to do it for each course separately. + if (!empty($options['coursecontacts'])) { + self::preload_course_contacts($records); + } + // If option 'idonly' is specified no further action is needed, just return list of ids. + if (!empty($options['idonly'])) { + return array_keys($records); + } + // Prepare the list of core_course_list_element objects. + foreach ($ids as $id) { + $courses[$id] = new core_course_list_element($records[$id]); + } + } + return $courses; + } + + // Retrieve list of courses in category. + $where = 'c.id <> :siteid'; + $params = array('siteid' => SITEID); + if ($recursive) { + if ($this->id) { + $context = context_coursecat::instance($this->id); + $where .= ' AND ctx.path like :path'; + $params['path'] = $context->path. '/%'; + } + } else { + $where .= ' AND c.category = :categoryid'; + $params['categoryid'] = $this->id; + } + // Get list of courses without preloaded coursecontacts because we don't need them for every course. + $list = $this->get_course_records($where, $params, array_diff_key($options, array('coursecontacts' => 1)), true); + + // Sort and cache list. + self::sort_records($list, $sortfields); + $coursecatcache->set($cachekey, array_keys($list)); + $coursecatcache->set($cntcachekey, count($list)); + + // Apply offset/limit, convert to core_course_list_element and return. + $courses = array(); + if (isset($list)) { + if ($offset || $limit) { + $list = array_slice($list, $offset, $limit, true); + } + // Preload course contacts if necessary - saves DB queries later to do it for each course separately. + if (!empty($options['coursecontacts'])) { + self::preload_course_contacts($list); + } + // If option 'idonly' is specified no further action is needed, just return list of ids. + if (!empty($options['idonly'])) { + return array_keys($list); + } + // Prepare the list of core_course_list_element objects. + foreach ($list as $record) { + $courses[$record->id] = new core_course_list_element($record); + } + } + return $courses; + } + + /** + * Returns number of courses visible to the user + * + * @param array $options similar to get_courses() except some options do not affect + * number of courses (i.e. sort, summary, offset, limit etc.) + * @return int + */ + public function get_courses_count($options = array()) { + $cntcachekey = 'lcnt-'. $this->id. '-'. (!empty($options['recursive']) ? 'r' : ''); + $coursecatcache = cache::make('core', 'coursecat'); + if (($cnt = $coursecatcache->get($cntcachekey)) === false) { + // Cached value not found. Retrieve ALL courses and return their count. + unset($options['offset']); + unset($options['limit']); + unset($options['summary']); + unset($options['coursecontacts']); + $options['idonly'] = true; + $courses = $this->get_courses($options); + $cnt = count($courses); + } + return $cnt; + } + + /** + * Returns true if the user is able to delete this category. + * + * Note if this category contains any courses this isn't a full check, it will need to be accompanied by a call to either + * {@link core_course_category::can_delete_full()} or {@link core_course_category::can_move_content_to()} + * depending upon what the user wished to do. + * + * @return boolean + */ + public function can_delete() { + if (!$this->has_manage_capability()) { + return false; + } + return $this->parent_has_manage_capability(); + } + + /** + * Returns true if user can delete current category and all its contents + * + * To be able to delete course category the user must have permission + * 'moodle/category:manage' in ALL child course categories AND + * be able to delete all courses + * + * @return bool + */ + public function can_delete_full() { + global $DB; + if (!$this->id) { + // Fool-proof. + return false; + } + + $context = $this->get_context(); + if (!$this->is_uservisible() || + !has_capability('moodle/category:manage', $context)) { + return false; + } + + // Check all child categories (not only direct children). + $sql = context_helper::get_preload_record_columns_sql('ctx'); + $childcategories = $DB->get_records_sql('SELECT c.id, c.visible, '. $sql. + ' FROM {context} ctx '. + ' JOIN {course_categories} c ON c.id = ctx.instanceid'. + ' WHERE ctx.path like ? AND ctx.contextlevel = ?', + array($context->path. '/%', CONTEXT_COURSECAT)); + foreach ($childcategories as $childcat) { + context_helper::preload_from_record($childcat); + $childcontext = context_coursecat::instance($childcat->id); + if ((!$childcat->visible && !has_capability('moodle/category:viewhiddencategories', $childcontext)) || + !has_capability('moodle/category:manage', $childcontext)) { + return false; + } + } + + // Check courses. + $sql = context_helper::get_preload_record_columns_sql('ctx'); + $coursescontexts = $DB->get_records_sql('SELECT ctx.instanceid AS courseid, '. + $sql. ' FROM {context} ctx '. + 'WHERE ctx.path like :pathmask and ctx.contextlevel = :courselevel', + array('pathmask' => $context->path. '/%', + 'courselevel' => CONTEXT_COURSE)); + foreach ($coursescontexts as $ctxrecord) { + context_helper::preload_from_record($ctxrecord); + if (!can_delete_course($ctxrecord->courseid)) { + return false; + } + } + + return true; + } + + /** + * Recursively delete category including all subcategories and courses + * + * Function {@link core_course_category::can_delete_full()} MUST be called prior + * to calling this function because there is no capability check + * inside this function + * + * @param boolean $showfeedback display some notices + * @return array return deleted courses + * @throws moodle_exception + */ + public function delete_full($showfeedback = true) { + global $CFG, $DB; + + require_once($CFG->libdir.'/gradelib.php'); + require_once($CFG->libdir.'/questionlib.php'); + require_once($CFG->dirroot.'/cohort/lib.php'); + + // Make sure we won't timeout when deleting a lot of courses. + $settimeout = core_php_time_limit::raise(); + + // Allow plugins to use this category before we completely delete it. + if ($pluginsfunction = get_plugins_with_function('pre_course_category_delete')) { + $category = $this->get_db_record(); + foreach ($pluginsfunction as $plugintype => $plugins) { + foreach ($plugins as $pluginfunction) { + $pluginfunction($category); + } + } + } + + $deletedcourses = array(); + + // Get children. Note, we don't want to use cache here because it would be rebuilt too often. + $children = $DB->get_records('course_categories', array('parent' => $this->id), 'sortorder ASC'); + foreach ($children as $record) { + $coursecat = new self($record); + $deletedcourses += $coursecat->delete_full($showfeedback); + } + + if ($courses = $DB->get_records('course', array('category' => $this->id), 'sortorder ASC')) { + foreach ($courses as $course) { + if (!delete_course($course, false)) { + throw new moodle_exception('cannotdeletecategorycourse', '', '', $course->shortname); + } + $deletedcourses[] = $course; + } + } + + // Move or delete cohorts in this context. + cohort_delete_category($this); + + // Now delete anything that may depend on course category context. + grade_course_category_delete($this->id, 0, $showfeedback); + if (!question_delete_course_category($this, 0, $showfeedback)) { + throw new moodle_exception('cannotdeletecategoryquestions', '', '', $this->get_formatted_name()); + } + + // Delete all events in the category. + $DB->delete_records('event', array('categoryid' => $this->id)); + + // Finally delete the category and it's context. + $DB->delete_records('course_categories', array('id' => $this->id)); + + $coursecatcontext = context_coursecat::instance($this->id); + $coursecatcontext->delete(); + + cache_helper::purge_by_event('changesincoursecat'); + + // Trigger a course category deleted event. + /** @var \core\event\course_category_deleted $event */ + $event = \core\event\course_category_deleted::create(array( + 'objectid' => $this->id, + 'context' => $coursecatcontext, + 'other' => array('name' => $this->name) + )); + $event->set_coursecat($this); + $event->trigger(); + + // If we deleted $CFG->defaultrequestcategory, make it point somewhere else. + if ($this->id == $CFG->defaultrequestcategory) { + set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent' => 0))); + } + return $deletedcourses; + } + + /** + * Checks if user can delete this category and move content (courses, subcategories and questions) + * to another category. If yes returns the array of possible target categories names + * + * If user can not manage this category or it is completely empty - empty array will be returned + * + * @return array + */ + public function move_content_targets_list() { + global $CFG; + require_once($CFG->libdir . '/questionlib.php'); + $context = $this->get_context(); + if (!$this->is_uservisible() || + !has_capability('moodle/category:manage', $context)) { + // User is not able to manage current category, he is not able to delete it. + // No possible target categories. + return array(); + } + + $testcaps = array(); + // If this category has courses in it, user must have 'course:create' capability in target category. + if ($this->has_courses()) { + $testcaps[] = 'moodle/course:create'; + } + // If this category has subcategories or questions, user must have 'category:manage' capability in target category. + if ($this->has_children() || question_context_has_any_questions($context)) { + $testcaps[] = 'moodle/category:manage'; + } + if (!empty($testcaps)) { + // Return list of categories excluding this one and it's children. + return self::make_categories_list($testcaps, $this->id); + } + + // Category is completely empty, no need in target for contents. + return array(); + } + + /** + * Checks if user has capability to move all category content to the new parent before + * removing this category + * + * @param int $newcatid + * @return bool + */ + public function can_move_content_to($newcatid) { + global $CFG; + require_once($CFG->libdir . '/questionlib.php'); + $context = $this->get_context(); + if (!$this->is_uservisible() || + !has_capability('moodle/category:manage', $context)) { + return false; + } + $testcaps = array(); + // If this category has courses in it, user must have 'course:create' capability in target category. + if ($this->has_courses()) { + $testcaps[] = 'moodle/course:create'; + } + // If this category has subcategories or questions, user must have 'category:manage' capability in target category. + if ($this->has_children() || question_context_has_any_questions($context)) { + $testcaps[] = 'moodle/category:manage'; + } + if (!empty($testcaps)) { + return has_all_capabilities($testcaps, context_coursecat::instance($newcatid)); + } + + // There is no content but still return true. + return true; + } + + /** + * Deletes a category and moves all content (children, courses and questions) to the new parent + * + * Note that this function does not check capabilities, {@link core_course_category::can_move_content_to()} + * must be called prior + * + * @param int $newparentid + * @param bool $showfeedback + * @return bool + */ + public function delete_move($newparentid, $showfeedback = false) { + global $CFG, $DB, $OUTPUT; + + require_once($CFG->libdir.'/gradelib.php'); + require_once($CFG->libdir.'/questionlib.php'); + require_once($CFG->dirroot.'/cohort/lib.php'); + + // Get all objects and lists because later the caches will be reset so. + // We don't need to make extra queries. + $newparentcat = self::get($newparentid, MUST_EXIST, true); + $catname = $this->get_formatted_name(); + $children = $this->get_children(); + $params = array('category' => $this->id); + $coursesids = $DB->get_fieldset_select('course', 'id', 'category = :category ORDER BY sortorder ASC', $params); + $context = $this->get_context(); + + if ($children) { + foreach ($children as $childcat) { + $childcat->change_parent_raw($newparentcat); + // Log action. + $event = \core\event\course_category_updated::create(array( + 'objectid' => $childcat->id, + 'context' => $childcat->get_context() + )); + $event->set_legacy_logdata(array(SITEID, 'category', 'move', 'editcategory.php?id=' . $childcat->id, + $childcat->id)); + $event->trigger(); + } + fix_course_sortorder(); + } + + if ($coursesids) { + require_once($CFG->dirroot.'/course/lib.php'); + if (!move_courses($coursesids, $newparentid)) { + if ($showfeedback) { + echo $OUTPUT->notification("Error moving courses"); + } + return false; + } + if ($showfeedback) { + echo $OUTPUT->notification(get_string('coursesmovedout', '', $catname), 'notifysuccess'); + } + } + + // Move or delete cohorts in this context. + cohort_delete_category($this); + + // Now delete anything that may depend on course category context. + grade_course_category_delete($this->id, $newparentid, $showfeedback); + if (!question_delete_course_category($this, $newparentcat, $showfeedback)) { + if ($showfeedback) { + echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $catname), 'notifysuccess'); + } + return false; + } + + // Finally delete the category and it's context. + $DB->delete_records('course_categories', array('id' => $this->id)); + $context->delete(); + + // Trigger a course category deleted event. + /** @var \core\event\course_category_deleted $event */ + $event = \core\event\course_category_deleted::create(array( + 'objectid' => $this->id, + 'context' => $context, + 'other' => array('name' => $this->name) + )); + $event->set_coursecat($this); + $event->trigger(); + + cache_helper::purge_by_event('changesincoursecat'); + + if ($showfeedback) { + echo $OUTPUT->notification(get_string('coursecategorydeleted', '', $catname), 'notifysuccess'); + } + + // If we deleted $CFG->defaultrequestcategory, make it point somewhere else. + if ($this->id == $CFG->defaultrequestcategory) { + set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent' => 0))); + } + return true; + } + + /** + * Checks if user can move current category to the new parent + * + * This checks if new parent category exists, user has manage cap there + * and new parent is not a child of this category + * + * @param int|stdClass|core_course_category $newparentcat + * @return bool + */ + public function can_change_parent($newparentcat) { + if (!has_capability('moodle/category:manage', $this->get_context())) { + return false; + } + if (is_object($newparentcat)) { + $newparentcat = self::get($newparentcat->id, IGNORE_MISSING); + } else { + $newparentcat = self::get((int)$newparentcat, IGNORE_MISSING); + } + if (!$newparentcat) { + return false; + } + if ($newparentcat->id == $this->id || in_array($this->id, $newparentcat->get_parents())) { + // Can not move to itself or it's own child. + return false; + } + if ($newparentcat->id) { + return has_capability('moodle/category:manage', context_coursecat::instance($newparentcat->id)); + } else { + return has_capability('moodle/category:manage', context_system::instance()); + } + } + + /** + * Moves the category under another parent category. All associated contexts are moved as well + * + * This is protected function, use change_parent() or update() from outside of this class + * + * @see core_course_category::change_parent() + * @see core_course_category::update() + * + * @param core_course_category $newparentcat + * @throws moodle_exception + */ + protected function change_parent_raw(core_course_category $newparentcat) { + global $DB; + + $context = $this->get_context(); + + $hidecat = false; + if (empty($newparentcat->id)) { + $DB->set_field('course_categories', 'parent', 0, array('id' => $this->id)); + $newparent = context_system::instance(); + } else { + if ($newparentcat->id == $this->id || in_array($this->id, $newparentcat->get_parents())) { + // Can not move to itself or it's own child. + throw new moodle_exception('cannotmovecategory'); + } + $DB->set_field('course_categories', 'parent', $newparentcat->id, array('id' => $this->id)); + $newparent = context_coursecat::instance($newparentcat->id); + + if (!$newparentcat->visible and $this->visible) { + // Better hide category when moving into hidden category, teachers may unhide afterwards and the hidden children + // will be restored properly. + $hidecat = true; + } + } + $this->parent = $newparentcat->id; + + $context->update_moved($newparent); + + // Now make it last in new category. + $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES, ['id' => $this->id]); + + if ($hidecat) { + fix_course_sortorder(); + $this->restore(); + // Hide object but store 1 in visibleold, because when parent category visibility changes this category must + // become visible again. + $this->hide_raw(1); + } + } + + /** + * Efficiently moves a category - NOTE that this can have + * a huge impact access-control-wise... + * + * Note that this function does not check capabilities. + * + * Example of usage: + * $coursecat = core_course_category::get($categoryid); + * if ($coursecat->can_change_parent($newparentcatid)) { + * $coursecat->change_parent($newparentcatid); + * } + * + * This function does not update field course_categories.timemodified + * If you want to update timemodified, use + * $coursecat->update(array('parent' => $newparentcat)); + * + * @param int|stdClass|core_course_category $newparentcat + */ + public function change_parent($newparentcat) { + // Make sure parent category exists but do not check capabilities here that it is visible to current user. + if (is_object($newparentcat)) { + $newparentcat = self::get($newparentcat->id, MUST_EXIST, true); + } else { + $newparentcat = self::get((int)$newparentcat, MUST_EXIST, true); + } + if ($newparentcat->id != $this->parent) { + $this->change_parent_raw($newparentcat); + fix_course_sortorder(); + cache_helper::purge_by_event('changesincoursecat'); + $this->restore(); + + $event = \core\event\course_category_updated::create(array( + 'objectid' => $this->id, + 'context' => $this->get_context() + )); + $event->set_legacy_logdata(array(SITEID, 'category', 'move', 'editcategory.php?id=' . $this->id, $this->id)); + $event->trigger(); + } + } + + /** + * Hide course category and child course and subcategories + * + * If this category has changed the parent and is moved under hidden + * category we will want to store it's current visibility state in + * the field 'visibleold'. If admin clicked 'hide' for this particular + * category, the field 'visibleold' should become 0. + * + * All subcategories and courses will have their current visibility in the field visibleold + * + * This is protected function, use hide() or update() from outside of this class + * + * @see core_course_category::hide() + * @see core_course_category::update() + * + * @param int $visibleold value to set in field $visibleold for this category + * @return bool whether changes have been made and caches need to be purged afterwards + */ + protected function hide_raw($visibleold = 0) { + global $DB; + $changes = false; + + // Note that field 'visibleold' is not cached so we must retrieve it from DB if it is missing. + if ($this->id && $this->__get('visibleold') != $visibleold) { + $this->visibleold = $visibleold; + $DB->set_field('course_categories', 'visibleold', $visibleold, array('id' => $this->id)); + $changes = true; + } + if (!$this->visible || !$this->id) { + // Already hidden or can not be hidden. + return $changes; + } + + $this->visible = 0; + $DB->set_field('course_categories', 'visible', 0, array('id' => $this->id)); + // Store visible flag so that we can return to it if we immediately unhide. + $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($this->id)); + $DB->set_field('course', 'visible', 0, array('category' => $this->id)); + // Get all child categories and hide too. + if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$this->path/%"), 'id, visible')) { + foreach ($subcats as $cat) { + $DB->set_field('course_categories', 'visibleold', $cat->visible, array('id' => $cat->id)); + $DB->set_field('course_categories', 'visible', 0, array('id' => $cat->id)); + $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($cat->id)); + $DB->set_field('course', 'visible', 0, array('category' => $cat->id)); + } + } + return true; + } + + /** + * Hide course category and child course and subcategories + * + * Note that there is no capability check inside this function + * + * This function does not update field course_categories.timemodified + * If you want to update timemodified, use + * $coursecat->update(array('visible' => 0)); + */ + public function hide() { + if ($this->hide_raw(0)) { + cache_helper::purge_by_event('changesincoursecat'); + + $event = \core\event\course_category_updated::create(array( + 'objectid' => $this->id, + 'context' => $this->get_context() + )); + $event->set_legacy_logdata(array(SITEID, 'category', 'hide', 'editcategory.php?id=' . $this->id, $this->id)); + $event->trigger(); + } + } + + /** + * Show course category and restores visibility for child course and subcategories + * + * Note that there is no capability check inside this function + * + * This is protected function, use show() or update() from outside of this class + * + * @see core_course_category::show() + * @see core_course_category::update() + * + * @return bool whether changes have been made and caches need to be purged afterwards + */ + protected function show_raw() { + global $DB; + + if ($this->visible) { + // Already visible. + return false; + } + + $this->visible = 1; + $this->visibleold = 1; + $DB->set_field('course_categories', 'visible', 1, array('id' => $this->id)); + $DB->set_field('course_categories', 'visibleold', 1, array('id' => $this->id)); + $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($this->id)); + // Get all child categories and unhide too. + if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$this->path/%"), 'id, visibleold')) { + foreach ($subcats as $cat) { + if ($cat->visibleold) { + $DB->set_field('course_categories', 'visible', 1, array('id' => $cat->id)); + } + $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($cat->id)); + } + } + return true; + } + + /** + * Show course category and restores visibility for child course and subcategories + * + * Note that there is no capability check inside this function + * + * This function does not update field course_categories.timemodified + * If you want to update timemodified, use + * $coursecat->update(array('visible' => 1)); + */ + public function show() { + if ($this->show_raw()) { + cache_helper::purge_by_event('changesincoursecat'); + + $event = \core\event\course_category_updated::create(array( + 'objectid' => $this->id, + 'context' => $this->get_context() + )); + $event->set_legacy_logdata(array(SITEID, 'category', 'show', 'editcategory.php?id=' . $this->id, $this->id)); + $event->trigger(); + } + } + + /** + * Returns name of the category formatted as a string + * + * @param array $options formatting options other than context + * @return string + */ + public function get_formatted_name($options = array()) { + if ($this->id) { + $context = $this->get_context(); + return format_string($this->name, true, array('context' => $context) + $options); + } else { + return get_string('top'); + } + } + + /** + * Get the nested name of this category, with all of it's parents. + * + * @param bool $includelinks Whether to wrap each name in the view link for that category. + * @param string $separator The string between each name. + * @param array $options Formatting options. + * @return string + */ + public function get_nested_name($includelinks = true, $separator = ' / ', $options = []) { + // Get the name of hierarchical name of this category. + $parents = $this->get_parents(); + $categories = static::get_many($parents); + $categories[] = $this; + + $names = array_map(function($category) use ($options, $includelinks) { + if ($includelinks) { + return html_writer::link($category->get_view_link(), $category->get_formatted_name($options)); + } else { + return $category->get_formatted_name($options); + } + + }, $categories); + + return implode($separator, $names); + } + + /** + * Returns ids of all parents of the category. Last element in the return array is the direct parent + * + * For example, if you have a tree of categories like: + * Miscellaneous (id = 1) + * Subcategory (id = 2) + * Sub-subcategory (id = 4) + * Other category (id = 3) + * + * core_course_category::get(1)->get_parents() == array() + * core_course_category::get(2)->get_parents() == array(1) + * core_course_category::get(4)->get_parents() == array(1, 2); + * + * Note that this method does not check if all parents are accessible by current user + * + * @return array of category ids + */ + public function get_parents() { + $parents = preg_split('|/|', $this->path, 0, PREG_SPLIT_NO_EMPTY); + array_pop($parents); + return $parents; + } + + /** + * This function returns a nice list representing category tree + * for display or to use in a form element - * - * List is cached for 10 minutes - * - * For example, if you have a tree of categories like: - * Miscellaneous (id = 1) - * Subcategory (id = 2) - * Sub-subcategory (id = 4) - * Other category (id = 3) - * Then after calling this function you will have - * array(1 => 'Miscellaneous', - * 2 => 'Miscellaneous / Subcategory', - * 4 => 'Miscellaneous / Subcategory / Sub-subcategory', - * 3 => 'Other category'); - * - * If you specify $requiredcapability, then only categories where the current - * user has that capability will be added to $list. - * If you only have $requiredcapability in a child category, not the parent, - * then the child catgegory will still be included. - * - * If you specify the option $excludeid, then that category, and all its children, - * are omitted from the tree. This is useful when you are doing something like - * moving categories, where you do not want to allow people to move a category - * to be the child of itself. - * - * See also {@link make_categories_options()} - * - * @param string/array $requiredcapability if given, only categories where the current - * user has this capability will be returned. Can also be an array of capabilities, - * in which case they are all required. - * @param integer $excludeid Exclude this category and its children from the lists built. - * @param string $separator string to use as a separator between parent and child category. Default ' / ' - * @return array of strings - */ - public static function make_categories_list($requiredcapability = '', $excludeid = 0, $separator = ' / ') { - global $DB; - $coursecatcache = cache::make('core', 'coursecat'); - - // Check if we cached the complete list of user-accessible category names ($baselist) or list of ids - // with requried cap ($thislist). - $currentlang = current_language(); - $basecachekey = $currentlang . '_catlist'; - $baselist = $coursecatcache->get($basecachekey); - $thislist = false; - $thiscachekey = null; - if (!empty($requiredcapability)) { - $requiredcapability = (array)$requiredcapability; - $thiscachekey = 'catlist:'. serialize($requiredcapability); - if ($baselist !== false && ($thislist = $coursecatcache->get($thiscachekey)) !== false) { - $thislist = preg_split('|,|', $thislist, -1, PREG_SPLIT_NO_EMPTY); - } - } else if ($baselist !== false) { - $thislist = array_keys($baselist); - } - - if ($baselist === false) { - // We don't have $baselist cached, retrieve it. Retrieve $thislist again in any case. - $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); - $sql = "SELECT cc.id, cc.sortorder, cc.name, cc.visible, cc.parent, cc.path, $ctxselect - FROM {course_categories} cc - JOIN {context} ctx ON cc.id = ctx.instanceid AND ctx.contextlevel = :contextcoursecat - ORDER BY cc.sortorder"; - $rs = $DB->get_recordset_sql($sql, array('contextcoursecat' => CONTEXT_COURSECAT)); - $baselist = array(); - $thislist = array(); - foreach ($rs as $record) { - // If the category's parent is not visible to the user, it is not visible as well. - if (!$record->parent || isset($baselist[$record->parent])) { - context_helper::preload_from_record($record); - $context = context_coursecat::instance($record->id); - if (!$record->visible && !has_capability('moodle/category:viewhiddencategories', $context)) { - // No cap to view category, added to neither $baselist nor $thislist. - continue; - } - $baselist[$record->id] = array( - 'name' => format_string($record->name, true, array('context' => $context)), - 'path' => $record->path - ); - if (!empty($requiredcapability) && !has_all_capabilities($requiredcapability, $context)) { - // No required capability, added to $baselist but not to $thislist. - continue; - } - $thislist[] = $record->id; - } - } - $rs->close(); - $coursecatcache->set($basecachekey, $baselist); - if (!empty($requiredcapability)) { - $coursecatcache->set($thiscachekey, join(',', $thislist)); - } - } else if ($thislist === false) { - // We have $baselist cached but not $thislist. Simplier query is used to retrieve. - $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); - $sql = "SELECT ctx.instanceid AS id, $ctxselect - FROM {context} ctx WHERE ctx.contextlevel = :contextcoursecat"; - $contexts = $DB->get_records_sql($sql, array('contextcoursecat' => CONTEXT_COURSECAT)); - $thislist = array(); - foreach (array_keys($baselist) as $id) { - context_helper::preload_from_record($contexts[$id]); - if (has_all_capabilities($requiredcapability, context_coursecat::instance($id))) { - $thislist[] = $id; - } - } - $coursecatcache->set($thiscachekey, join(',', $thislist)); - } - - // Now build the array of strings to return, mind $separator and $excludeid. - $names = array(); - foreach ($thislist as $id) { - $path = preg_split('|/|', $baselist[$id]['path'], -1, PREG_SPLIT_NO_EMPTY); - if (!$excludeid || !in_array($excludeid, $path)) { - $namechunks = array(); - foreach ($path as $parentid) { - $namechunks[] = $baselist[$parentid]['name']; - } - $names[$id] = join($separator, $namechunks); - } - } - return $names; - } - - /** - * Prepares the object for caching. Works like the __sleep method. - * - * implementing method from interface cacheable_object - * - * @return array ready to be cached - */ - public function prepare_to_cache() { - $a = array(); - foreach (self::$coursecatfields as $property => $cachedirectives) { - if ($cachedirectives !== null) { - list($shortname, $defaultvalue) = $cachedirectives; - if ($this->$property !== $defaultvalue) { - $a[$shortname] = $this->$property; - } - } - } - $context = $this->get_context(); - $a['xi'] = $context->id; - $a['xp'] = $context->path; - return $a; - } - - /** - * Takes the data provided by prepare_to_cache and reinitialises an instance of the associated from it. - * - * implementing method from interface cacheable_object - * - * @param array $a - * @return coursecat - */ - public static function wake_from_cache($a) { - $record = new stdClass; - foreach (self::$coursecatfields as $property => $cachedirectives) { - if ($cachedirectives !== null) { - list($shortname, $defaultvalue) = $cachedirectives; - if (array_key_exists($shortname, $a)) { - $record->$property = $a[$shortname]; - } else { - $record->$property = $defaultvalue; - } - } - } - $record->ctxid = $a['xi']; - $record->ctxpath = $a['xp']; - $record->ctxdepth = $record->depth + 1; - $record->ctxlevel = CONTEXT_COURSECAT; - $record->ctxinstance = $record->id; - return new coursecat($record, true); - } - - /** - * Returns true if the user is able to create a top level category. - * @return bool - */ - public static function can_create_top_level_category() { - return has_capability('moodle/category:manage', context_system::instance()); - } - - /** - * Returns the category context. - * @return context_coursecat - */ - public function get_context() { - if ($this->id === 0) { - // This is the special top level category object. - return context_system::instance(); - } else { - return context_coursecat::instance($this->id); - } - } - - /** - * Returns true if the user is able to manage this category. - * @return bool - */ - public function has_manage_capability() { - if ($this->hasmanagecapability === null) { - $this->hasmanagecapability = has_capability('moodle/category:manage', $this->get_context()); - } - return $this->hasmanagecapability; - } - - /** - * Returns true if the user has the manage capability on the parent category. - * @return bool - */ - public function parent_has_manage_capability() { - return has_capability('moodle/category:manage', get_category_or_system_context($this->parent)); - } - - /** - * Returns true if the current user can create subcategories of this category. - * @return bool - */ - public function can_create_subcategory() { - return $this->has_manage_capability(); - } - - /** - * Returns true if the user can resort this categories sub categories and courses. - * Must have manage capability and be able to see all subcategories. - * @return bool - */ - public function can_resort_subcategories() { - return $this->has_manage_capability() && !$this->get_not_visible_children_ids(); - } - - /** - * Returns true if the user can resort the courses within this category. - * Must have manage capability and be able to see all courses. - * @return bool - */ - public function can_resort_courses() { - return $this->has_manage_capability() && $this->coursecount == $this->get_courses_count(); - } - - /** - * Returns true of the user can change the sortorder of this category (resort in the parent category) - * @return bool - */ - public function can_change_sortorder() { - return $this->id && $this->get_parent_coursecat()->can_resort_subcategories(); - } - - /** - * Returns true if the current user can create a course within this category. - * @return bool - */ - public function can_create_course() { - return has_capability('moodle/course:create', $this->get_context()); - } - - /** - * Returns true if the current user can edit this categories settings. - * @return bool - */ - public function can_edit() { - return $this->has_manage_capability(); - } - - /** - * Returns true if the current user can review role assignments for this category. - * @return bool - */ - public function can_review_roles() { - return has_capability('moodle/role:assign', $this->get_context()); - } - - /** - * Returns true if the current user can review permissions for this category. - * @return bool - */ - public function can_review_permissions() { - return has_any_capability(array( - 'moodle/role:assign', - 'moodle/role:safeoverride', - 'moodle/role:override', - 'moodle/role:assign' - ), $this->get_context()); - } - - /** - * Returns true if the current user can review cohorts for this category. - * @return bool - */ - public function can_review_cohorts() { - return has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $this->get_context()); - } - - /** - * Returns true if the current user can review filter settings for this category. - * @return bool - */ - public function can_review_filters() { - return has_capability('moodle/filter:manage', $this->get_context()) && - count(filter_get_available_in_context($this->get_context()))>0; - } - - /** - * Returns true if the current user is able to change the visbility of this category. - * @return bool - */ - public function can_change_visibility() { - return $this->parent_has_manage_capability(); - } - - /** - * Returns true if the user can move courses out of this category. - * @return bool - */ - public function can_move_courses_out_of() { - return $this->has_manage_capability(); - } - - /** - * Returns true if the user can move courses into this category. - * @return bool - */ - public function can_move_courses_into() { - return $this->has_manage_capability(); - } - - /** - * Returns true if the user is able to restore a course into this category as a new course. - * @return bool - */ - public function can_restore_courses_into() { - return has_capability('moodle/restore:restorecourse', $this->get_context()); - } - - /** - * Resorts the sub categories of this category by the given field. - * - * @param string $field One of name, idnumber or descending values of each (appended desc) - * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later. - * @return bool True on success. - * @throws coding_exception - */ - public function resort_subcategories($field, $cleanup = true) { - global $DB; - $desc = false; - if (substr($field, -4) === "desc") { - $desc = true; - $field = substr($field, 0, -4); // Remove "desc" from field name. - } - if ($field !== 'name' && $field !== 'idnumber') { - throw new coding_exception('Invalid field requested'); - } - $children = $this->get_children(); - core_collator::asort_objects_by_property($children, $field, core_collator::SORT_NATURAL); - if (!empty($desc)) { - $children = array_reverse($children); - } - $i = 1; - foreach ($children as $cat) { - $i++; - $DB->set_field('course_categories', 'sortorder', $i, array('id' => $cat->id)); - $i += $cat->coursecount; - } - if ($cleanup) { - self::resort_categories_cleanup(); - } - return true; - } - - /** - * Cleans things up after categories have been resorted. - * @param bool $includecourses If set to true we know courses have been resorted as well. - */ - public static function resort_categories_cleanup($includecourses = false) { - // This should not be needed but we do it just to be safe. - fix_course_sortorder(); - cache_helper::purge_by_event('changesincoursecat'); - if ($includecourses) { - cache_helper::purge_by_event('changesincourse'); - } - } - - /** - * Resort the courses within this category by the given field. - * - * @param string $field One of fullname, shortname, idnumber or descending values of each (appended desc) - * @param bool $cleanup - * @return bool True for success. - * @throws coding_exception - */ - public function resort_courses($field, $cleanup = true) { - global $DB; - $desc = false; - if (substr($field, -4) === "desc") { - $desc = true; - $field = substr($field, 0, -4); // Remove "desc" from field name. - } - if ($field !== 'fullname' && $field !== 'shortname' && $field !== 'idnumber' && $field !== 'timecreated') { - // This is ultra important as we use $field in an SQL statement below this. - throw new coding_exception('Invalid field requested'); - } - $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); - $sql = "SELECT c.id, c.sortorder, c.{$field}, $ctxfields - FROM {course} c - LEFT JOIN {context} ctx ON ctx.instanceid = c.id - WHERE ctx.contextlevel = :ctxlevel AND - c.category = :categoryid"; - $params = array( - 'ctxlevel' => CONTEXT_COURSE, - 'categoryid' => $this->id - ); - $courses = $DB->get_records_sql($sql, $params); - if (count($courses) > 0) { - foreach ($courses as $courseid => $course) { - context_helper::preload_from_record($course); - if ($field === 'idnumber') { - $course->sortby = $course->idnumber; - } else { - // It'll require formatting. - $options = array( - 'context' => context_course::instance($course->id) - ); - // We format the string first so that it appears as the user would see it. - // This ensures the sorting makes sense to them. However it won't necessarily make - // sense to everyone if things like multilang filters are enabled. - // We then strip any tags as we don't want things such as image tags skewing the - // sort results. - $course->sortby = strip_tags(format_string($course->$field, true, $options)); - } - // We set it back here rather than using references as there is a bug with using - // references in a foreach before passing as an arg by reference. - $courses[$courseid] = $course; - } - // Sort the courses. - core_collator::asort_objects_by_property($courses, 'sortby', core_collator::SORT_NATURAL); - if (!empty($desc)) { - $courses = array_reverse($courses); - } - $i = 1; - foreach ($courses as $course) { - $DB->set_field('course', 'sortorder', $this->sortorder + $i, array('id' => $course->id)); - $i++; - } - if ($cleanup) { - // This should not be needed but we do it just to be safe. - fix_course_sortorder(); - cache_helper::purge_by_event('changesincourse'); - } - } - return true; - } - - /** - * Changes the sort order of this categories parent shifting this category up or down one. - * - * @global \moodle_database $DB - * @param bool $up If set to true the category is shifted up one spot, else its moved down. - * @return bool True on success, false otherwise. - */ - public function change_sortorder_by_one($up) { - global $DB; - $params = array($this->sortorder, $this->parent); - if ($up) { - $select = 'sortorder < ? AND parent = ?'; - $sort = 'sortorder DESC'; - } else { - $select = 'sortorder > ? AND parent = ?'; - $sort = 'sortorder ASC'; - } - fix_course_sortorder(); - $swapcategory = $DB->get_records_select('course_categories', $select, $params, $sort, '*', 0, 1); - $swapcategory = reset($swapcategory); - if ($swapcategory) { - $DB->set_field('course_categories', 'sortorder', $swapcategory->sortorder, array('id' => $this->id)); - $DB->set_field('course_categories', 'sortorder', $this->sortorder, array('id' => $swapcategory->id)); - $this->sortorder = $swapcategory->sortorder; - - $event = \core\event\course_category_updated::create(array( - 'objectid' => $this->id, - 'context' => $this->get_context() - )); - $event->set_legacy_logdata(array(SITEID, 'category', 'move', 'management.php?categoryid=' . $this->id, - $this->id)); - $event->trigger(); - - // Finally reorder courses. - fix_course_sortorder(); - cache_helper::purge_by_event('changesincoursecat'); - return true; - } - return false; - } - - /** - * Returns the parent coursecat object for this category. - * - * @return coursecat - */ - public function get_parent_coursecat() { - return self::get($this->parent); - } - - - /** - * Returns true if the user is able to request a new course be created. - * @return bool - */ - public function can_request_course() { - global $CFG; - if (empty($CFG->enablecourserequests) || $this->id != $CFG->defaultrequestcategory) { - return false; - } - return !$this->can_create_course() && has_capability('moodle/course:request', $this->get_context()); - } - - /** - * Returns true if the user can approve course requests. - * @return bool - */ - public static function can_approve_course_requests() { - global $CFG, $DB; - if (empty($CFG->enablecourserequests)) { - return false; - } - $context = context_system::instance(); - if (!has_capability('moodle/site:approvecourse', $context)) { - return false; - } - if (!$DB->record_exists('course_request', array())) { - return false; - } - return true; - } -} - -/** - * Class to store information about one course in a list of courses - * - * Not all information may be retrieved when object is created but - * it will be retrieved on demand when appropriate property or method is - * called. - * - * Instances of this class are usually returned by functions - * {@link coursecat::search_courses()} - * and - * {@link coursecat::get_courses()} - * - * @property-read int $id - * @property-read int $category Category ID - * @property-read int $sortorder - * @property-read string $fullname - * @property-read string $shortname - * @property-read string $idnumber - * @property-read string $summary Course summary. Field is present if coursecat::get_courses() - * was called with option 'summary'. Otherwise will be retrieved from DB on first request - * @property-read int $summaryformat Summary format. Field is present if coursecat::get_courses() - * was called with option 'summary'. Otherwise will be retrieved from DB on first request - * @property-read string $format Course format. Retrieved from DB on first request - * @property-read int $showgrades Retrieved from DB on first request - * @property-read int $newsitems Retrieved from DB on first request - * @property-read int $startdate - * @property-read int $enddate - * @property-read int $marker Retrieved from DB on first request - * @property-read int $maxbytes Retrieved from DB on first request - * @property-read int $legacyfiles Retrieved from DB on first request - * @property-read int $showreports Retrieved from DB on first request - * @property-read int $visible - * @property-read int $visibleold Retrieved from DB on first request - * @property-read int $groupmode Retrieved from DB on first request - * @property-read int $groupmodeforce Retrieved from DB on first request - * @property-read int $defaultgroupingid Retrieved from DB on first request - * @property-read string $lang Retrieved from DB on first request - * @property-read string $theme Retrieved from DB on first request - * @property-read int $timecreated Retrieved from DB on first request - * @property-read int $timemodified Retrieved from DB on first request - * @property-read int $requested Retrieved from DB on first request - * @property-read int $enablecompletion Retrieved from DB on first request - * @property-read int $completionnotify Retrieved from DB on first request - * @property-read int $cacherev - * - * @package core - * @subpackage course - * @copyright 2013 Marina Glancy - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class course_in_list implements IteratorAggregate { - - /** @var stdClass record retrieved from DB, may have additional calculated property such as managers and hassummary */ - protected $record; - - /** @var array array of course contacts - stores result of call to get_course_contacts() */ - protected $coursecontacts; - - /** @var bool true if the current user can access the course, false otherwise. */ - protected $canaccess = null; - - /** - * Creates an instance of the class from record - * - * @param stdClass $record except fields from course table it may contain - * field hassummary indicating that summary field is not empty. - * Also it is recommended to have context fields here ready for - * context preloading - */ - public function __construct(stdClass $record) { - context_helper::preload_from_record($record); - $this->record = new stdClass(); - foreach ($record as $key => $value) { - $this->record->$key = $value; - } - } - - /** - * Indicates if the course has non-empty summary field - * - * @return bool - */ - public function has_summary() { - if (isset($this->record->hassummary)) { - return !empty($this->record->hassummary); - } - if (!isset($this->record->summary)) { - // We need to retrieve summary. - $this->__get('summary'); - } - return !empty($this->record->summary); - } - - /** - * Indicates if the course have course contacts to display - * - * @return bool - */ - public function has_course_contacts() { - if (!isset($this->record->managers)) { - $courses = array($this->id => &$this->record); - coursecat::preload_course_contacts($courses); - } - return !empty($this->record->managers); - } - - /** - * Returns list of course contacts (usually teachers) to display in course link - * - * Roles to display are set up in $CFG->coursecontact - * - * The result is the list of users where user id is the key and the value - * is an array with elements: - * - 'user' - object containing basic user information - * - 'role' - object containing basic role information (id, name, shortname, coursealias) - * - 'rolename' => role_get_name($role, $context, ROLENAME_ALIAS) - * - 'username' => fullname($user, $canviewfullnames) - * - * @return array - */ - public function get_course_contacts() { - global $CFG; - if (empty($CFG->coursecontact)) { - // No roles are configured to be displayed as course contacts. - return array(); - } - if ($this->coursecontacts === null) { - $this->coursecontacts = array(); - $context = context_course::instance($this->id); - - if (!isset($this->record->managers)) { - // Preload course contacts from DB. - $courses = array($this->id => &$this->record); - coursecat::preload_course_contacts($courses); - } - - // Build return array with full roles names (for this course context) and users names. - $canviewfullnames = has_capability('moodle/site:viewfullnames', $context); - foreach ($this->record->managers as $ruser) { - if (isset($this->coursecontacts[$ruser->id])) { - // Only display a user once with the highest sortorder role. - continue; - } - $user = new stdClass(); - $user = username_load_fields_from_object($user, $ruser, null, array('id', 'username')); - $role = new stdClass(); - $role->id = $ruser->roleid; - $role->name = $ruser->rolename; - $role->shortname = $ruser->roleshortname; - $role->coursealias = $ruser->rolecoursealias; - - $this->coursecontacts[$user->id] = array( - 'user' => $user, - 'role' => $role, - 'rolename' => role_get_name($role, $context, ROLENAME_ALIAS), - 'username' => fullname($user, $canviewfullnames) - ); - } - } - return $this->coursecontacts; - } - - /** - * Checks if course has any associated overview files - * - * @return bool - */ - public function has_course_overviewfiles() { - global $CFG; - if (empty($CFG->courseoverviewfileslimit)) { - return false; - } - $fs = get_file_storage(); - $context = context_course::instance($this->id); - return !$fs->is_area_empty($context->id, 'course', 'overviewfiles'); - } - - /** - * Returns all course overview files - * - * @return array array of stored_file objects - */ - public function get_course_overviewfiles() { - global $CFG; - if (empty($CFG->courseoverviewfileslimit)) { - return array(); - } - require_once($CFG->libdir. '/filestorage/file_storage.php'); - require_once($CFG->dirroot. '/course/lib.php'); - $fs = get_file_storage(); - $context = context_course::instance($this->id); - $files = $fs->get_area_files($context->id, 'course', 'overviewfiles', false, 'filename', false); - if (count($files)) { - $overviewfilesoptions = course_overviewfiles_options($this->id); - $acceptedtypes = $overviewfilesoptions['accepted_types']; - if ($acceptedtypes !== '*') { - // Filter only files with allowed extensions. - require_once($CFG->libdir. '/filelib.php'); - foreach ($files as $key => $file) { - if (!file_extension_in_typegroup($file->get_filename(), $acceptedtypes)) { - unset($files[$key]); - } - } - } - if (count($files) > $CFG->courseoverviewfileslimit) { - // Return no more than $CFG->courseoverviewfileslimit files. - $files = array_slice($files, 0, $CFG->courseoverviewfileslimit, true); - } - } - return $files; - } - - /** - * Magic method to check if property is set - * - * @param string $name - * @return bool - */ - public function __isset($name) { - return isset($this->record->$name); - } - - /** - * Magic method to get a course property - * - * Returns any field from table course (retrieves it from DB if it was not retrieved before) - * - * @param string $name - * @return mixed - */ - public function __get($name) { - global $DB; - if (property_exists($this->record, $name)) { - return $this->record->$name; - } else if ($name === 'summary' || $name === 'summaryformat') { - // Retrieve fields summary and summaryformat together because they are most likely to be used together. - $record = $DB->get_record('course', array('id' => $this->record->id), 'summary, summaryformat', MUST_EXIST); - $this->record->summary = $record->summary; - $this->record->summaryformat = $record->summaryformat; - return $this->record->$name; - } else if (array_key_exists($name, $DB->get_columns('course'))) { - // Another field from table 'course' that was not retrieved. - $this->record->$name = $DB->get_field('course', $name, array('id' => $this->record->id), MUST_EXIST); - return $this->record->$name; - } - debugging('Invalid course property accessed! '.$name); - return null; - } - - /** - * All properties are read only, sorry. - * - * @param string $name - */ - public function __unset($name) { - debugging('Can not unset '.get_class($this).' instance properties!'); - } - - /** - * Magic setter method, we do not want anybody to modify properties from the outside - * - * @param string $name - * @param mixed $value - */ - public function __set($name, $value) { - debugging('Can not change '.get_class($this).' instance properties!'); - } - - /** - * Create an iterator because magic vars can't be seen by 'foreach'. - * Exclude context fields - * - * Implementing method from interface IteratorAggregate - * - * @return ArrayIterator - */ - public function getIterator() { - $ret = array('id' => $this->record->id); - foreach ($this->record as $property => $value) { - $ret[$property] = $value; - } - return new ArrayIterator($ret); - } - - /** - * Returns the name of this course as it should be displayed within a list. - * @return string - */ - public function get_formatted_name() { - return format_string(get_course_display_name_for_list($this), true, $this->get_context()); - } - - /** - * Returns the formatted fullname for this course. - * @return string - */ - public function get_formatted_fullname() { - return format_string($this->__get('fullname'), true, $this->get_context()); - } - - /** - * Returns the formatted shortname for this course. - * @return string - */ - public function get_formatted_shortname() { - return format_string($this->__get('shortname'), true, $this->get_context()); - } - - /** - * Returns true if the current user can access this course. - * @return bool - */ - public function can_access() { - if ($this->canaccess === null) { - $this->canaccess = can_access_course($this->record); - } - return $this->canaccess; - } - - /** - * Returns true if the user can edit this courses settings. - * - * Note: this function does not check that the current user can access the course. - * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()} - * - * @return bool - */ - public function can_edit() { - return has_capability('moodle/course:update', $this->get_context()); - } - - /** - * Returns true if the user can change the visibility of this course. - * - * Note: this function does not check that the current user can access the course. - * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()} - * - * @return bool - */ - public function can_change_visibility() { - // You must be able to both hide a course and view the hidden course. - return has_all_capabilities(array('moodle/course:visibility', 'moodle/course:viewhiddencourses'), $this->get_context()); - } - - /** - * Returns the context for this course. - * @return context_course - */ - public function get_context() { - return context_course::instance($this->__get('id')); - } - - /** - * Returns true if this course is visible to the current user. - * @return bool - */ - public function is_uservisible() { - return $this->visible || has_capability('moodle/course:viewhiddencourses', $this->get_context()); - } - - /** - * Returns true if the current user can review enrolments for this course. - * - * Note: this function does not check that the current user can access the course. - * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()} - * - * @return bool - */ - public function can_review_enrolments() { - return has_capability('moodle/course:enrolreview', $this->get_context()); - } - - /** - * Returns true if the current user can delete this course. - * - * Note: this function does not check that the current user can access the course. - * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()} - * - * @return bool - */ - public function can_delete() { - return can_delete_course($this->id); - } - - /** - * Returns true if the current user can backup this course. - * - * Note: this function does not check that the current user can access the course. - * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()} - * - * @return bool - */ - public function can_backup() { - return has_capability('moodle/backup:backupcourse', $this->get_context()); - } - - /** - * Returns true if the current user can restore this course. - * - * Note: this function does not check that the current user can access the course. - * To do that please call require_login with the course, or if not possible call {@see course_in_list::can_access()} - * - * @return bool - */ - public function can_restore() { - return has_capability('moodle/restore:restorecourse', $this->get_context()); - } -} +debugging('Class coursecat is now alias to autoloaded class core_course_category, ' . + 'course_in_list is an alias to core_course_list_element. '. + 'Class coursecat_sortable_records is deprecated without replacement. Do not include coursecatlib.php', + DEBUG_DEVELOPER); /** * An array of records that is sortable by many fields. diff --git a/lib/db/renamedclasses.php b/lib/db/renamedclasses.php index 9ccba774025..6e020632614 100644 --- a/lib/db/renamedclasses.php +++ b/lib/db/renamedclasses.php @@ -36,4 +36,6 @@ defined('MOODLE_INTERNAL') || die(); // The old class name is the key, the new class name is the value. // The array must be called $renamedclasses. $renamedclasses = array( + 'course_in_list' => 'core_course_list_element', + 'coursecat' => 'core_course_category', ); diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index 1298165e489..ca248e71620 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -1090,71 +1090,77 @@ function update_category_button() { */ function make_categories_list() { throw new coding_exception('Global function make_categories_list() is removed. Please use '. - 'coursecat::make_categories_list() and coursecat::get_parents()'); + 'core_course_category::make_categories_list() and core_course_category::get_parents()'); } /** * @deprecated since 2.5 */ function category_delete_move() { - throw new coding_exception('Function category_delete_move() is removed. Please use coursecat::delete_move() instead.'); + throw new coding_exception('Function category_delete_move() is removed. Please use ' . + 'core_course_category::delete_move() instead.'); } /** * @deprecated since 2.5 */ function category_delete_full() { - throw new coding_exception('Function category_delete_full() is removed. Please use coursecat::delete_full() instead.'); + throw new coding_exception('Function category_delete_full() is removed. Please use ' . + 'core_course_category::delete_full() instead.'); } /** * @deprecated since 2.5 */ function move_category() { - throw new coding_exception('Function move_category() is removed. Please use coursecat::change_parent() instead.'); + throw new coding_exception('Function move_category() is removed. Please use core_course_category::change_parent() instead.'); } /** * @deprecated since 2.5 */ function course_category_hide() { - throw new coding_exception('Function course_category_hide() is removed. Please use coursecat::hide() instead.'); + throw new coding_exception('Function course_category_hide() is removed. Please use core_course_category::hide() instead.'); } /** * @deprecated since 2.5 */ function course_category_show() { - throw new coding_exception('Function course_category_show() is removed. Please use coursecat::show() instead.'); + throw new coding_exception('Function course_category_show() is removed. Please use core_course_category::show() instead.'); } /** - * @deprecated since 2.5. Please use coursecat::get($catid, IGNORE_MISSING) or coursecat::get($catid, MUST_EXIST). + * @deprecated since 2.5. Please use core_course_category::get($catid, IGNORE_MISSING) or + * core_course_category::get($catid, MUST_EXIST). */ function get_course_category() { - throw new coding_exception('Function get_course_category() is removed. Please use coursecat::get(), see phpdocs for more details'); + throw new coding_exception('Function get_course_category() is removed. Please use core_course_category::get(), ' . + 'see phpdocs for more details'); } /** * @deprecated since 2.5 */ function create_course_category() { - throw new coding_exception('Function create_course_category() is removed. Please use coursecat::create()'); + throw new coding_exception('Function create_course_category() is removed. Please use core_course_category::create(), ' . + 'see phpdocs for more details'); } /** - * @deprecated since 2.5. Please use coursecat::get() and coursecat::get_children() + * @deprecated since 2.5. Please use core_course_category::get() and core_course_category::get_children() */ function get_all_subcategories() { - throw new coding_exception('Function get_all_subcategories() is removed. Please use appropriate methods() ' . - 'of coursecat class.'); + throw new coding_exception('Function get_all_subcategories() is removed. Please use appropriate methods() '. + 'of core_course_category class. See phpdocs for more details'); } /** - * @deprecated since 2.5. Please use coursecat::get($parentid)->get_children(). + * @deprecated since 2.5. Please use core_course_category::get($parentid)->get_children(). */ function get_child_categories() { - throw new coding_exception('Function get_child_categories() is removed. Use coursecat::get_children().'); + throw new coding_exception('Function get_child_categories() is removed. Use core_course_category::get_children() or see ' . + 'phpdocs for more details.'); } /** @@ -1162,7 +1168,7 @@ function get_child_categories() { */ function get_categories() { throw new coding_exception('Function get_categories() is removed. Please use ' . - 'appropriate functions from class coursecat'); + 'appropriate functions from class core_course_category'); } /** @@ -1213,7 +1219,7 @@ function print_category_info() { */ function get_course_category_tree() { throw new coding_exception('Function get_course_category_tree() is removed, please use course ' . - 'renderer or coursecat class, see function phpdocs for more info'); + 'renderer or core_course_category class, see function phpdocs for more info'); } /** @@ -1234,14 +1240,16 @@ function print_course() { * @deprecated since 2.5 */ function get_category_courses_array() { - throw new coding_exception('Function get_category_courses_array() is removed, please use methods of coursecat class'); + throw new coding_exception('Function get_category_courses_array() is removed, please use methods of ' . + 'core_course_category class'); } /** * @deprecated since 2.5 */ function get_category_courses_array_recursively() { - throw new coding_exception('Function get_category_courses_array_recursively() is removed, please use methods of coursecat class', DEBUG_DEVELOPER); + throw new coding_exception('Function get_category_courses_array_recursively() is removed, please use ' . + 'methods of core_course_category class', DEBUG_DEVELOPER); } /** @@ -1255,7 +1263,8 @@ function blog_get_context_url() { * @deprecated since 2.5 */ function get_courses_wmanagers() { - throw new coding_exception('Function get_courses_wmanagers() is removed, please use coursecat::get_courses()'); + throw new coding_exception('Function get_courses_wmanagers() is removed, please use ' . + 'core_course_category::get_courses()'); } /** @@ -3101,8 +3110,8 @@ function calendar_get_all_allowed_types() { $types['site'] = true; } - if (coursecat::has_manage_capability_on_any()) { - $types['category'] = coursecat::make_categories_list('moodle/category:manage'); + if (core_course_category::has_manage_capability_on_any()) { + $types['category'] = core_course_category::make_categories_list('moodle/category:manage'); } // This function warms the context cache for the course so the calls diff --git a/lib/enrollib.php b/lib/enrollib.php index 1a73b2df51e..2df4d50d238 100644 --- a/lib/enrollib.php +++ b/lib/enrollib.php @@ -1909,8 +1909,7 @@ abstract class enrol_plugin { ); $event->trigger(); // Check if course contacts cache needs to be cleared. - require_once($CFG->libdir . '/coursecatlib.php'); - coursecat::user_enrolment_changed($courseid, $ue->userid, + core_course_category::user_enrolment_changed($courseid, $ue->userid, $ue->status, $ue->timestart, $ue->timeend); } @@ -2004,8 +2003,7 @@ abstract class enrol_plugin { ); $event->trigger(); - require_once($CFG->libdir . '/coursecatlib.php'); - coursecat::user_enrolment_changed($instance->courseid, $ue->userid, + core_course_category::user_enrolment_changed($instance->courseid, $ue->userid, $ue->status, $ue->timestart, $ue->timeend); } @@ -2089,8 +2087,7 @@ abstract class enrol_plugin { $context->mark_dirty(); // Check if courrse contacts cache needs to be cleared. - require_once($CFG->libdir . '/coursecatlib.php'); - coursecat::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED); + core_course_category::user_enrolment_changed($courseid, $ue->userid, ENROL_USER_SUSPENDED); // reset current user enrolment caching if ($userid == $USER->id) { diff --git a/lib/filebrowser/tests/file_browser_test.php b/lib/filebrowser/tests/file_browser_test.php index 99f3af2444c..2959646fc1f 100644 --- a/lib/filebrowser/tests/file_browser_test.php +++ b/lib/filebrowser/tests/file_browser_test.php @@ -131,7 +131,7 @@ class file_browser_testcase extends advanced_testcase { public function test_file_info_context_system_hidden() { // Hide the course category that contains our two courses. Teacher does not have cap to view hidden categories. - coursecat::get($this->course1->category)->update(['visible' => 0]); + core_course_category::get($this->course1->category)->update(['visible' => 0]); // We should have two non-empty children in system context (courses). $browser = get_file_browser(); diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 4962aa46499..57a0d0bf045 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -9841,7 +9841,7 @@ function get_home_page() { * Gets the name of a course to be displayed when showing a list of courses. * By default this is just $course->fullname but user can configure it. The * result of this function should be passed through print_string. - * @param stdClass|course_in_list $course Moodle course object + * @param stdClass|core_course_list_element $course Moodle course object * @return string Display name of course (either fullname or short + fullname) */ function get_course_display_name_for_list($course) { diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 4a1f7d79b75..398e56e0b8c 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -1497,8 +1497,7 @@ class global_navigation extends navigation_node { protected function show_my_categories() { global $CFG; if ($this->showmycategories === null) { - require_once('coursecatlib.php'); - $this->showmycategories = !empty($CFG->navshowmycoursecategories) && coursecat::count_all() > 1; + $this->showmycategories = !empty($CFG->navshowmycoursecategories) && core_course_category::count_all() > 1; } return $this->showmycategories; } @@ -2969,8 +2968,7 @@ class global_navigation extends navigation_node { // Array of category IDs that include the categories of the user's courses and the related course categories. $fullpathcategoryids = []; // Get the course categories for the enrolled courses' category IDs. - require_once('coursecatlib.php'); - $mycoursecategories = coursecat::get_many($categoryids); + $mycoursecategories = core_course_category::get_many($categoryids); // Loop over each of these categories and build the category tree using each category's path. foreach ($mycoursecategories as $mycoursecat) { $pathcategoryids = explode('/', $mycoursecat->path); @@ -2981,7 +2979,7 @@ class global_navigation extends navigation_node { } // Fetch all of the categories related to the user's courses. - $pathcategories = coursecat::get_many($fullpathcategoryids); + $pathcategories = core_course_category::get_many($fullpathcategoryids); // Loop over each of these categories and build the category tree. foreach ($pathcategories as $coursecat) { // No need to process categories that have already been added. @@ -3525,11 +3523,10 @@ class navbar extends navigation_node { private function get_course_categories() { global $CFG; require_once($CFG->dirroot.'/course/lib.php'); - require_once($CFG->libdir.'/coursecatlib.php'); $categories = array(); $cap = 'moodle/category:viewhiddencategories'; - $showcategories = coursecat::count_all() > 1; + $showcategories = core_course_category::count_all() > 1; if ($showcategories) { foreach ($this->page->categories as $category) { diff --git a/lib/questionlib.php b/lib/questionlib.php index 213a75fc836..268c066ed4b 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -257,7 +257,7 @@ function question_remove_stale_questions_from_category($categoryid) { * 2/ Any questions that can't be deleted are moved to a new category * NOTE: this function is called from lib/db/upgrade.php * - * @param object|coursecat $category course category object + * @param object|core_course_category $category course category object */ function question_category_delete_safe($category) { global $DB; @@ -438,8 +438,8 @@ function question_delete_course($course, $feedback=true) { * 1/ All question categories and their questions are deleted for this course category. * 2/ All questions are moved to new category * - * @param object|coursecat $category course category object - * @param object|coursecat $newcategory empty means everything deleted, otherwise id of + * @param object|core_course_category $category course category object + * @param object|core_course_category $newcategory empty means everything deleted, otherwise id of * category where content moved * @param boolean $feedback to specify if the process must output a summary of its work * @return boolean diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php index b52fa58da65..23b5042be9d 100644 --- a/lib/testing/generator/data_generator.php +++ b/lib/testing/generator/data_generator.php @@ -289,12 +289,9 @@ EOD; * Create a test course category * @param array|stdClass $record * @param array $options - * @return coursecat course category record + * @return core_course_category course category record */ public function create_category($record=null, array $options=null) { - global $DB, $CFG; - require_once("$CFG->libdir/coursecatlib.php"); - $this->categorycount++; $i = $this->categorycount; @@ -312,7 +309,7 @@ EOD; $record['idnumber'] = ''; } - return coursecat::create($record); + return core_course_category::create($record); } /** diff --git a/lib/tests/gradelib_test.php b/lib/tests/gradelib_test.php index fcc42adf80a..17878396999 100644 --- a/lib/tests/gradelib_test.php +++ b/lib/tests/gradelib_test.php @@ -90,7 +90,7 @@ class core_gradelib_testcase extends advanced_testcase { $this->resetAfterTest(); - $category = coursecat::create(array('name' => 'Cat1')); + $category = core_course_category::create(array('name' => 'Cat1')); // Add a grade letter to the category. $letter = new stdClass(); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 4244d98241d..2877dd097e2 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -101,6 +101,8 @@ information provided here is intended especially for developers. - file_merge_draft_areas() - file_replace_file_area_in_text() - extract_draft_file_urls_from_text() +* Class coursecat is now alias to autoloaded class core_course_category, course_in_list is an alias to + core_course_list_element, class coursecat_sortable_records is deprecated without replacement. === 3.5 === diff --git a/message/classes/api.php b/message/classes/api.php index 25246a0c7c0..51a067a7fc5 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -162,8 +162,6 @@ class api { public static function search_users($userid, $search, $limitnum = 0) { global $CFG, $DB; - require_once($CFG->dirroot . '/lib/coursecatlib.php'); - // Used to search for contacts. $fullname = $DB->sql_fullname(); $ufields = \user_picture::fields('u', array('lastaccess')); @@ -199,7 +197,7 @@ class api { // 'moodle/course:viewparticipants' or 'moodle/course:enrolreview', but since the search_courses function // only takes required parameters we can't. However, the chance of a user having 'moodle/course:enrolreview' but // *not* 'moodle/course:viewparticipants' are pretty much zero, so it is not worth addressing. - if ($arrcourses = \coursecat::search_courses(array('search' => $search), array('limit' => $limitnum), + if ($arrcourses = \core_course_category::search_courses(array('search' => $search), array('limit' => $limitnum), array('moodle/course:viewparticipants'))) { foreach ($arrcourses as $course) { if (isset($enrolledcourses[$course->id])) { diff --git a/theme/boost/classes/output/core_course/management/renderer.php b/theme/boost/classes/output/core_course/management/renderer.php index 476942b8ae4..443764c6cf5 100644 --- a/theme/boost/classes/output/core_course/management/renderer.php +++ b/theme/boost/classes/output/core_course/management/renderer.php @@ -28,9 +28,9 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . "/course/classes/management_renderer.php"); use html_writer; -use coursecat; +use core_course_category; use moodle_url; -use course_in_list; +use core_course_list_element; use lang_string; use context_system; use stdClass; @@ -103,10 +103,10 @@ class renderer extends \core_course_management_renderer { /** * Renderers detailed course information. * - * @param course_in_list $course The course to display details for. + * @param core_course_list_element $course The course to display details for. * @return string */ - public function course_detail(course_in_list $course) { + public function course_detail(core_course_list_element $course) { $details = \core_course\management\helper::get_course_detail_array($course); $fullname = $details['fullname']['value']; @@ -184,10 +184,10 @@ class renderer extends \core_course_management_renderer { /** * Presents a course category listing. * - * @param coursecat $category The currently selected category. Also the category to highlight in the listing. + * @param core_course_category $category The currently selected category. Also the category to highlight in the listing. * @return string */ - public function category_listing(coursecat $category = null) { + public function category_listing(core_course_category $category = null) { if ($category === null) { $selectedparents = array(); @@ -201,7 +201,7 @@ class renderer extends \core_course_management_renderer { $catatlevel[] = array_shift($selectedparents); $catatlevel = array_unique($catatlevel); - $listing = coursecat::get(0)->get_children(); + $listing = core_course_category::get(0)->get_children(); $attributes = array( 'class' => 'ml-1 list-unstyled', @@ -241,14 +241,14 @@ class renderer extends \core_course_management_renderer { * * This function gets called recursively to render sub categories. * - * @param coursecat $category The category to render as listitem. - * @param coursecat[] $subcategories The subcategories belonging to the category being rented. + * @param core_course_category $category The category to render as listitem. + * @param core_course_category[] $subcategories The subcategories belonging to the category being rented. * @param int $totalsubcategories The total number of sub categories. * @param int $selectedcategory The currently selected category * @param int[] $selectedcategories The path to the selected category and its ID. * @return string */ - public function category_listitem(coursecat $category, array $subcategories, $totalsubcategories, + public function category_listitem(core_course_category $category, array $subcategories, $totalsubcategories, $selectedcategory = null, $selectedcategories = array()) { $isexpandable = ($totalsubcategories > 0); @@ -383,23 +383,23 @@ class renderer extends \core_course_management_renderer { * These are not the actions associated with an individual category listing. * That happens through category_listitem_actions. * - * @param coursecat $category + * @param core_course_category $category * @return string */ - public function category_listing_actions(coursecat $category = null) { + public function category_listing_actions(core_course_category $category = null) { $actions = array(); $cancreatecategory = $category && $category->can_create_subcategory(); - $cancreatecategory = $cancreatecategory || coursecat::can_create_top_level_category(); + $cancreatecategory = $cancreatecategory || core_course_category::can_create_top_level_category(); if ($category === null) { - $category = coursecat::get(0); + $category = core_course_category::get(0); } if ($cancreatecategory) { $url = new moodle_url('/course/editcategory.php', array('parent' => $category->id)); $actions[] = html_writer::link($url, get_string('createnewcategory'), array('class' => 'btn btn-default')); } - if (coursecat::can_approve_course_requests()) { + if (core_course_category::can_approve_course_requests()) { $actions[] = html_writer::link(new moodle_url('/course/pending.php'), get_string('coursespending')); } if (count($actions) === 0) { @@ -411,14 +411,14 @@ class renderer extends \core_course_management_renderer { /** * Renders a course listing. * - * @param coursecat $category The currently selected category. This is what the listing is focused on. - * @param course_in_list $course The currently selected course. + * @param core_course_category $category The currently selected category. This is what the listing is focused on. + * @param core_course_list_element $course The currently selected course. * @param int $page The page being displayed. * @param int $perpage The number of courses to display per page. * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'. * @return string */ - public function course_listing(coursecat $category = null, course_in_list $course = null, + public function course_listing(core_course_category $category = null, core_course_list_element $course = null, $page = 0, $perpage = 20, $viewmode = 'default') { if ($category === null) { @@ -479,12 +479,12 @@ class renderer extends \core_course_management_renderer { * * This function will be called for every course being displayed by course_listing. * - * @param coursecat $category The currently selected category and the category the course belongs to. - * @param course_in_list $course The course to produce HTML for. + * @param core_course_category $category The currently selected category and the category the course belongs to. + * @param core_course_list_element $course The course to produce HTML for. * @param int $selectedcourse The id of the currently selected course. * @return string */ - public function course_listitem(coursecat $category, course_in_list $course, $selectedcourse) { + public function course_listitem(core_course_category $category, core_course_list_element $course, $selectedcourse) { $text = $course->get_formatted_name(); $attributes = array( @@ -537,12 +537,12 @@ class renderer extends \core_course_management_renderer { * * Not to be confused with course_listitem_actions which renderers the actions for individual courses. * - * @param coursecat $category - * @param course_in_list $course The currently selected course. + * @param core_course_category $category + * @param core_course_list_element $course The currently selected course. * @param int $perpage * @return string */ - public function course_listing_actions(coursecat $category, course_in_list $course = null, $perpage = 20) { + public function course_listing_actions(core_course_category $category, core_course_list_element $course = null, $perpage = 20) { $actions = array(); if ($category->can_create_course()) { $url = new moodle_url('/course/edit.php', array('category' => $category->id, 'returnto' => 'catmanage')); @@ -618,13 +618,13 @@ class renderer extends \core_course_management_renderer { * * @param array $courses The courses to display. * @param int $totalcourses The total number of courses to display. - * @param course_in_list $course The currently selected course if there is one. + * @param core_course_list_element $course The currently selected course if there is one. * @param int $page The current page, starting at 0. * @param int $perpage The number of courses to display per page. * @param string $search The string we are searching for. * @return string */ - public function search_listing(array $courses, $totalcourses, course_in_list $course = null, $page = 0, $perpage = 20, + public function search_listing(array $courses, $totalcourses, core_course_list_element $course = null, $page = 0, $perpage = 20, $search = '') { $page = max($page, 0); $perpage = max($perpage, 2); @@ -666,11 +666,11 @@ class renderer extends \core_course_management_renderer { * * This function will be called for every course being displayed by course_listing. * - * @param course_in_list $course The course to produce HTML for. + * @param core_course_list_element $course The course to produce HTML for. * @param int $selectedcourse The id of the currently selected course. * @return string */ - public function search_listitem(course_in_list $course, $selectedcourse) { + public function search_listitem(core_course_list_element $course, $selectedcourse) { $text = $course->get_formatted_name(); $attributes = array( @@ -680,7 +680,7 @@ class renderer extends \core_course_management_renderer { 'data-visible' => $course->visible ? '1' : '0' ); $bulkcourseinput = ''; - if (coursecat::get($course->category)->can_move_courses_out_of()) { + if (core_course_category::get($course->category)->can_move_courses_out_of()) { $bulkcourseinput = array( 'type' => 'checkbox', 'name' => 'bc[]', @@ -691,7 +691,7 @@ class renderer extends \core_course_management_renderer { ); } $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id)); - $categoryname = coursecat::get($course->category)->get_formatted_name(); + $categoryname = core_course_category::get($course->category)->get_formatted_name(); $html = html_writer::start_tag('li', $attributes); $html .= html_writer::start_div('clearfix'); @@ -730,10 +730,10 @@ class renderer extends \core_course_management_renderer { /** * A collection of actions for a course. * - * @param course_in_list $course The course to display actions for. + * @param core_course_list_element $course The course to display actions for. * @return string */ - public function course_detail_actions(course_in_list $course) { + public function course_detail_actions(core_course_list_element $course) { $actions = \core_course\management\helper::get_course_detail_actions($course); if (empty($actions)) { return ''; diff --git a/user/filters/courserole.php b/user/filters/courserole.php index b4c5ec984f8..7aa793de152 100644 --- a/user/filters/courserole.php +++ b/user/filters/courserole.php @@ -66,9 +66,7 @@ class user_filter_courserole extends user_filter_type { * @return array of course categories */ public function get_course_categories() { - global $CFG; - require_once($CFG->libdir.'/coursecatlib.php'); - return array(0 => get_string('anycategory', 'filters')) + coursecat::make_categories_list(); + return array(0 => get_string('anycategory', 'filters')) + core_course_category::make_categories_list(); } /**